Skip to content

Commit 8cb0128

Browse files
authored
Merge pull request #521 from DimuthuMadushan/dur-calc
Add Support for Custom Time Duration Calculation
2 parents 016ed68 + 70ce602 commit 8cb0128

File tree

16 files changed

+382
-25
lines changed

16 files changed

+382
-25
lines changed

ballerina/Ballerina.toml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
org = "ballerina"
33
name = "time"
4-
version = "2.7.0"
4+
version = "2.8.0"
55
authors = ["Ballerina"]
66
keywords = ["time", "utc", "epoch", "civil"]
77
repository = "https://github.com/ballerina-platform/module-ballerina-time"
@@ -15,5 +15,5 @@ graalvmCompatible = true
1515
[[platform.java21.dependency]]
1616
groupId = "io.ballerina.stdlib"
1717
artifactId = "time-native"
18-
version = "2.7.0"
19-
path = "../native/build/libs/time-native-2.7.0.jar"
18+
version = "2.8.0"
19+
path = "../native/build/libs/time-native-2.8.0-SNAPSHOT.jar"

ballerina/Dependencies.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ modules = [
6767
[[package]]
6868
org = "ballerina"
6969
name = "time"
70-
version = "2.7.0"
70+
version = "2.8.0"
7171
dependencies = [
7272
{org = "ballerina", name = "jballerina.java"},
7373
{org = "ballerina", name = "test"}
File renamed without changes.

ballerina/tests/time_test.bal renamed to ballerina/tests/test.bal

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ isolated function testUtcFromStringWithInvalidFormat() {
5656
Utc|Error err = utcFromString("2007-12-0310:15:30.00Z");
5757
test:assertTrue(err is Error);
5858
test:assertEquals((<Error>err).message(),
59-
"The provided string '2007-12-0310:15:30.00Z' does not adhere to the expected RFC 3339 format 'YYYY-MM-DDTHH:MM:SS.SSZ'. ");
59+
"The provided string '2007-12-0310:15:30.00Z' does not adhere to the expected RFC 3339 format 'YYYY-MM-DDTHH:MM:SS.SSZ'. ");
6060
}
6161

6262
@test:Config {}
@@ -668,7 +668,7 @@ isolated function testGmtToEmailStringConversion() returns Error? {
668668
Utc utc = check utcFromString("2007-12-03T10:15:30.00Z");
669669
Utc utc2 = check utcFromString("2007-12-03T10:15:30.00+05:30");
670670
Civil civil = check civilFromString("2007-12-03T10:15:30.00+00:00");
671-
671+
672672
test:assertEquals(utcToEmailString(utc, "Z"), "Mon, 3 Dec 2007 10:15:30 Z");
673673
test:assertEquals(utcToEmailString(utc2, "0"), "Mon, 3 Dec 2007 04:45:30 +0000");
674674
test:assertEquals(utcToEmailString(utc), "Mon, 3 Dec 2007 10:15:30 +0000");
@@ -709,7 +709,6 @@ isolated function testCivilToStringWithEmptyTimeOffset() returns Error? {
709709

710710
@test:Config {enable: true}
711711
isolated function testUtcFromCivilWithEmptyTimeOffsetNegative() returns Error? {
712-
Utc expectedUtc = check utcFromString("2021-04-12T23:20:50.520Z");
713712
Civil civil = {
714713
year: 2021,
715714
month: 4,
@@ -728,7 +727,6 @@ isolated function testUtcFromCivilWithEmptyTimeOffsetNegative() returns Error? {
728727
}
729728

730729
isolated function testUtcFromCivilWithEmptyTimeOffsetAndAbbreviation() returns Error? {
731-
Utc expectedUtc = check utcFromString("2021-04-12T23:20:50.520Z");
732730
Civil civil = {
733731
year: 2021,
734732
month: 4,
@@ -760,7 +758,7 @@ isolated function testCivilToStringWithEmptyTimeOffsetAndAbbreviation() returns
760758
test:assertEquals(civilString.message(), "the civil value should have either `utcOffset` or `timeAbbrev`");
761759
} else {
762760
test:assertFail("civilString should be error");
763-
}
761+
}
764762
}
765763

766764
@test:Config {enable: true}
@@ -769,4 +767,120 @@ isolated function testRepeatedUtcToCivilConversion() returns Error? {
769767
Civil civil = utcToCivil(utc);
770768
Utc utc2 = check utcFromCivil(civil);
771769
test:assertEquals(utc, utc2);
772-
}
770+
}
771+
772+
@test:Config {
773+
groups: ["duration"],
774+
dataProvider: dataProviderCivilAddDuration
775+
}
776+
isolated function testCivilAddDuration(string civilString, Duration duration, string expectedResult) returns Error? {
777+
Civil actualResult = check civilAddDuration(check civilFromString(civilString), duration);
778+
test:assertEquals(civilToString(actualResult), expectedResult);
779+
}
780+
781+
isolated function dataProviderCivilAddDuration() returns [string, Duration, string][] {
782+
return [
783+
["2025-06-02T10:30:00Z", {years: -1, months: -8, days: -5, hours: -3, minutes: -5, seconds: -6}, "2023-09-27T07:24:54Z"],
784+
["2024-02-27T22:30:30.00+02:00", {years: 0, months: 0, days: 3, hours: 1, minutes: 29, seconds: 30}, "2024-03-02T00:00+02:00"],
785+
["1972-12-31T23:59:59+05:30", {years: 0, months: 15, days: 30, hours: 0, minutes: 0, seconds: 1}, "1974-05-01T00:00+05:30"],
786+
["2025-05-22T08:30:04.67Z", {years: -1, months: 4, days: 0, hours: -23, minutes: 5, seconds: -1}, "2024-09-21T09:35:03.670Z"]
787+
];
788+
}
789+
790+
@test:Config {
791+
groups: ["duration", "zone"],
792+
dataProvider: dataProviderZoneDateTimeCivilAddDuration
793+
}
794+
isolated function testZoneDataTimeCivilAddDuration(string zone, string civilString, Duration duration, string expectedResult) returns Error? {
795+
Zone? systemZone = getZone(zone);
796+
test:assertTrue(systemZone is Zone);
797+
Civil civil = check (<Zone>systemZone).civilAddDuration(check civilFromString(civilString), duration);
798+
test:assertEquals(civilToString(civil), expectedResult);
799+
}
800+
801+
isolated function dataProviderZoneDateTimeCivilAddDuration() returns [string, string, Duration, string][] {
802+
return [
803+
["Asia/Colombo", "2025-06-02T10:30:00+05:30", {years: -1, months: -8, days: -5, hours: -3, minutes: -5, seconds: -6}, "2023-09-27T07:24:54+05:30[Asia/Colombo]"],
804+
["Greenwich", "2025-06-02T10:30:00+05:30", {years: -1, months: -8, days: -5, hours: -3, minutes: -5, seconds: -6}, "2023-09-27T01:54:54Z[Greenwich]"],
805+
["Etc/GMT-9", "2025-06-02T10:30:00+05:30", {years: -1, months: -8, days: -5, hours: -3, minutes: -5, seconds: -6}, "2023-09-27T10:54:54+09:00[Etc/GMT-9]"],
806+
["Asia/Colombo", "2024-02-27T22:30:30.00+02:00", {years: 0, months: 0, days: 3, hours: 1, minutes: 29, seconds: 30}, "2024-03-02T03:30+05:30[Asia/Colombo]"],
807+
["Asia/Tokyo", "2025-05-22T08:30:04.67Z", {years: 1, months: -4, days: 3, hours: 25, minutes: -5, seconds: 111}, "2026-01-26T18:26:55.670+09:00[Asia/Tokyo]"],
808+
["Asia/Colombo", "2025-05-22T08:30:04.67Z", {years: 1, months: -12, days: 1, hours: -24, minutes: 1, seconds: -60}, "2025-05-22T14:00:04.670+05:30[Asia/Colombo]"]
809+
];
810+
}
811+
812+
@test:Config {
813+
groups: ["duration"],
814+
dataProvider: dataProviderCivilRecordAddDuration
815+
}
816+
isolated function testCivilRecordAddDuration(Civil civilString, Duration duration, string expectedResult) returns Error? {
817+
Civil actualResult = check civilAddDuration(civilString, duration);
818+
test:assertEquals(civilToString(actualResult), expectedResult);
819+
}
820+
821+
isolated function dataProviderCivilRecordAddDuration() returns [Civil, Duration, string][] {
822+
return [
823+
[{year: 2021, month: 4, day: 12, hour: 23, minute: 20, second: 50.52, timeAbbrev: "Z"}, {years: -100, months: -8, days: -5, hours: 22, minutes: 5, seconds: -6}, "1920-08-08T21:25:44.520Z"],
824+
[{year: 2025, month: 4, day: 23, hour: 0, minute: 20, second: 1.2, timeAbbrev: "Asia/Colombo"}, {years: 5, months: 0, days: 0, hours: 3, minutes: 8, seconds: 34}, "2030-04-23T03:28:35.200+05:30[Asia/Colombo]"],
825+
[{year: 2025, month: 4, day: 23, hour: 0, minute: 20, second: 1.2, utcOffset: {hours: 8, minutes: 0}}, {years: 0, months: 10, days: 5, hours: 0, minutes: 0, seconds: 0}, "2026-02-28T00:20:01.200+08:00"],
826+
[{year: 2025, month: 4, day: 23, hour: 0, minute: 20, second: 1.2, timeAbbrev: "America/Los_Angeles", utcOffset: {hours: 8, minutes: 0}}, {years: 0, months: 10, days: 5, hours: 0, minutes: 0, seconds: 0}, "2026-02-28T00:20:01.200+08:00"]
827+
];
828+
}
829+
830+
@test:Config {
831+
groups: ["duration", "zone"],
832+
dataProvider: dataProviderZoneDateTimeCivilRecordAddDuration
833+
}
834+
isolated function testZoneDataTimeCivilRecordAddDuration(string zone, Civil civil, Duration duration, string expectedResult) returns Error? {
835+
Zone? systemZone = getZone(zone);
836+
test:assertTrue(systemZone is Zone);
837+
Civil result = check (<Zone>systemZone).civilAddDuration(civil, duration);
838+
test:assertEquals(civilToString(result), expectedResult);
839+
}
840+
841+
isolated function dataProviderZoneDateTimeCivilRecordAddDuration() returns [string, Civil, Duration, string][] {
842+
return [
843+
["Asia/Colombo", {year: 2021, month: 4, day: 12, hour: 23, minute: 20, second: 50.52, timeAbbrev: "Z"}, {years: -11, months: -8, days: -30, hours: 22, minutes: 5, seconds: -6}, "2009-07-15T02:55:44.520+05:30[Asia/Colombo]"],
844+
["Z", {year: 2025, month: 4, day: 23, hour: 0, minute: 20, second: 1.2, timeAbbrev: "Asia/Colombo"}, {years: 4, months: 7, days: 9, hours: 3, minutes: 8, seconds: 34}, "2029-12-01T21:58:35.200Z"],
845+
["America/Los_Angeles", {year: 2030, month: 4, day: 23, hour: 0, minute: 20, second: 1.2, utcOffset: {hours: 8, minutes: 0}}, {years: 3, months: 10, days: 5, hours: 0, minutes: 0, seconds: 55.5}, "2034-02-27T09:20:56.700-08:00[America/Los_Angeles]"],
846+
["America/Los_Angeles", {year: 2011, month: 4, day: 23, hour: 0, minute: 20, second: 1.2, timeAbbrev: "America/Los_Angeles", utcOffset: {hours: 8, minutes: 0}}, {years: 0, months: 10, days: 6, hours: 0, minutes: 0, seconds: 9.34}, "2012-02-28T09:20:10.540-08:00[America/Los_Angeles]"]
847+
];
848+
}
849+
850+
@test:Config {
851+
groups: ["duration"],
852+
dataProvider: dataProviderInvalidCivilAddDuration
853+
}
854+
isolated function testInvalidCivilAddDuration(Civil civil, Duration duration, string errorMsg) returns Error? {
855+
Civil|Error actualResult = civilAddDuration(civil, duration);
856+
test:assertTrue(actualResult is Error);
857+
test:assertEquals((<Error>actualResult).message(), errorMsg);
858+
}
859+
860+
isolated function dataProviderInvalidCivilAddDuration() returns [Civil, Duration, string][] {
861+
return [
862+
[{year: 2021, month: 4, day: 12, hour: 23, minute: 20, second: 50.52}, {years: -25, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0}, "The civil value should have either `utcOffset` or `timeAbbrev`"],
863+
[{year: 2021, month: 4, day: 12, hour: 23, minute: 20, second: 50.52, timeAbbrev: "Colombo"}, {years: -25, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0}, "Unknown time-zone ID: Colombo"],
864+
[{year: 2025, month: 13, day: 23, hour: 0, minute: 20, second: 1.2, timeAbbrev: "America/Los_Angeles", utcOffset: {hours: 8, minutes: 0}}, {years: 0, months: 10, days: 5, hours: 0, minutes: 0, seconds: 0}, "Invalid value for MonthOfYear (valid values 1 - 12): 13"]
865+
];
866+
}
867+
868+
@test:Config {
869+
groups: ["duration", "zone"],
870+
dataProvider: dataProviderInvalidZoneDateTimeCivilAddDuration
871+
}
872+
isolated function testInvalidZoneDataTimeCivilAddDuration(string zone, Civil civil, Duration duration, string errorMsg) returns Error? {
873+
Zone? systemZone = getZone(zone);
874+
test:assertTrue(systemZone is Zone);
875+
Civil|Error result = (<Zone>systemZone).civilAddDuration(civil, duration);
876+
test:assertTrue(result is Error);
877+
test:assertEquals((<Error>result).message(), errorMsg);
878+
}
879+
880+
isolated function dataProviderInvalidZoneDateTimeCivilAddDuration() returns [string, Civil, Duration, string][] {
881+
return [
882+
["Asia/Colombo", {year: 2021, month: 4, day: 12, hour: 23, minute: 20, second: 50.52, timeAbbrev: "Colombo"}, {years: -1, months: -8, days: -5, hours: -3, minutes: -5, seconds: -6}, "Unknown time-zone ID: Colombo"],
883+
["Asia/Colombo", {year: 2021, month: 4, day: 12, hour: 23, minute: 20, second: 50.52}, {years: -25, months: 0, days: 0, hours: 0, minutes: 0, seconds: 0}, "The civil value should have either `utcOffset` or `timeAbbrev`"],
884+
["Asia/Colombo", {year: 2025, month: 13, day: 23, hour: 0, minute: 20, second: 1.2, timeAbbrev: "America/Los_Angeles", utcOffset: {hours: 8, minutes: 0}}, {years: 0, months: 10, days: 5, hours: 0, minutes: 0, seconds: 0}, "Invalid value for MonthOfYear (valid values 1 - 12): 13"]
885+
];
886+
}

ballerina/time_apis.bal renamed to ballerina/time.bal

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ public isolated function utcFromCivil(Civil civilTime) returns Utc|Error {
148148
decimal utcOffsetSeconds = (utcOffsetSecField is decimal) ? utcOffsetSecField : 0.0;
149149

150150
return externUtcFromCivil(civilTime.year, civilTime.month, civilTime.day, civilTime.hour, civilTime.minute,
151-
civilTimeSeconds, utcOffset.hours, utcOffset.minutes, utcOffsetSeconds);
151+
civilTimeSeconds, utcOffset.hours, utcOffset.minutes, utcOffsetSeconds);
152152
}
153153

154154
# Converts a given RFC 3339 timestamp(e.g., `2007-12-03T10:15:30.00Z`) to `time:Civil`.
@@ -185,8 +185,8 @@ public isolated function civilToString(Civil civil) returns string|Error {
185185
decimal utcOffsetSeconds = utcOffset?.seconds ?: 0.0;
186186
decimal civilTimeSeconds = civil?.second ?: 0.0;
187187

188-
return externCivilToString(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds,
189-
utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev ?: "", zoneHandling);
188+
return externCivilToString(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds,
189+
utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev ?: "", zoneHandling);
190190
}
191191

192192
# Converts a given UTC to an email formatted string (e.g `Mon, 3 Dec 2007 10:15:30 GMT`).
@@ -244,9 +244,37 @@ public isolated function civilToEmailString(Civil civil, HeaderZoneHandling zone
244244
string timeAbbrev = (timeAbbrevField is string) ? timeAbbrevField : "";
245245

246246
return externCivilToEmailString(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds,
247-
utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev, zoneHandling);
247+
utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev, zoneHandling);
248248
}
249249

250+
# Adds the given time duration to the specified civil date-time. This is a time zone-agnostic operation and assumes that
251+
# all days have exactly 86,400 seconds.
252+
# ```ballerina
253+
# time:Civil civil = check time:civilFromString("2025-04-25T10:15:30.00Z");
254+
# time:Civil|time:Error updatedCivil = time:civilAddDuration(civil, {years: 1, days: 3, hours: 4, seconds: 6});
255+
# ```
256+
# + civil - The civil time to which the duration should be added
257+
# + duration - The time duration to be added
258+
# + return - The civil time after adding the duration
259+
public isolated function civilAddDuration(Civil civil, Duration duration) returns Civil|Error {
260+
ZoneOffset? utcOffset = civil?.utcOffset;
261+
string? timeAbbrev = civil?.timeAbbrev;
262+
HeaderZoneHandling zoneHandling = PREFER_ZONE_OFFSET;
263+
if utcOffset is () && timeAbbrev is () {
264+
return error FormatError("The civil value should have either `utcOffset` or `timeAbbrev`");
265+
} else if utcOffset is () && timeAbbrev is string {
266+
zoneHandling = PREFER_TIME_ABBREV;
267+
}
268+
int utcOffsetHours = utcOffset?.hours ?: 0;
269+
int utcOffsetMinutes = utcOffset?.minutes ?: 0;
270+
decimal utcOffsetSeconds = utcOffset?.seconds ?: 0.0;
271+
decimal civilTimeSeconds = civil?.second ?: 0.0;
272+
273+
return externCivilAddDuration(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds,
274+
utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev ?: "", zoneHandling, duration.years,
275+
duration.months, duration.days, duration.hours, duration.minutes, duration.seconds);
276+
};
277+
250278
isolated function externUtcNow(int precision) returns Utc = @java:Method {
251279
name: "externUtcNow",
252280
'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods"
@@ -288,7 +316,7 @@ isolated function externUtcToCivil(Utc utc) returns Civil = @java:Method {
288316
} external;
289317

290318
isolated function externUtcFromCivil(int year, int month, int day, int hour, int minute, decimal second, int zoneHour,
291-
int zoneMinute, decimal zoneSecond) returns Utc|Error = @java:Method {
319+
int zoneMinute, decimal zoneSecond) returns Utc|Error = @java:Method {
292320
name: "externUtcFromCivil",
293321
'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods"
294322
} external;
@@ -298,9 +326,9 @@ isolated function externCivilFromString(string dateTimeString) returns Civil|Err
298326
'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods"
299327
} external;
300328

301-
isolated function externCivilToString(int year, int month, int day, int hour, int minute, decimal second,
302-
int zoneHour, int zoneMinute, decimal zoneSecond,
303-
string timeAbber, HeaderZoneHandling zoneHandling) returns string|Error = @java:Method {
329+
isolated function externCivilToString(int year, int month, int day, int hour, int minute, decimal second,
330+
int zoneHour, int zoneMinute, decimal zoneSecond,
331+
string timeAbber, HeaderZoneHandling zoneHandling) returns string|Error = @java:Method {
304332
name: "externCivilToString",
305333
'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods"
306334
} external;
@@ -316,8 +344,14 @@ isolated function externCivilFromEmailString(string dateTimeString) returns Civi
316344
} external;
317345

318346
isolated function externCivilToEmailString(int year, int month, int day, int hour, int minute, decimal second,
319-
int zoneHour, int zoneMinute, decimal zoneSecond, string timeAbber,
320-
HeaderZoneHandling zoneHandling) returns string|Error = @java:Method {
347+
int zoneHour, int zoneMinute, decimal zoneSecond, string timeAbber, HeaderZoneHandling zoneHandling)
348+
returns string|Error = @java:Method {
321349
name: "externCivilToEmailString",
322350
'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods"
323351
} external;
352+
353+
isolated function externCivilAddDuration(int year, int month, int day, int hour, int minute, decimal second,
354+
int zoneHour, int zoneMinute, decimal zoneSecond, string timeAbbrev, HeaderZoneHandling zoneHandling,
355+
int duYear, int duMonth, int duDay, int duHour, int duMinute, decimal duSecond) returns Civil|Error = @java:Method {
356+
'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods"
357+
} external;

0 commit comments

Comments
 (0)