From ed7e5ff0ea8b0d8afd0f08f56062e5994dd2b15a Mon Sep 17 00:00:00 2001 From: DimuthuMadushan Date: Mon, 21 Apr 2025 14:35:37 +0530 Subject: [PATCH 1/2] Add support to custom duration calculation --- ballerina/Ballerina.toml | 6 +- ballerina/Dependencies.toml | 2 +- ballerina/{time_errors.bal => errors.bal} | 0 ballerina/tests/{time_test.bal => test.bal} | 126 +++++++++++++++++- ballerina/{time_apis.bal => time.bal} | 51 +++++-- ballerina/{time_types.bal => types.bal} | 65 ++++++++- changelog.md | 4 + docs/spec/spec.md | 16 ++- gradle.properties | 2 +- .../stdlib/time/nativeimpl/Civil.java | 11 ++ .../time/nativeimpl/CustomDuration.java | 36 +++++ .../stdlib/time/nativeimpl/ExternMethods.java | 21 +++ .../time/nativeimpl/TimeZoneExternUtils.java | 18 +++ .../stdlib/time/nativeimpl/Zone.java | 6 + .../stdlib/time/util/TimeValueHandler.java | 1 + .../io/ballerina/stdlib/time/util/Utils.java | 6 + 16 files changed, 346 insertions(+), 25 deletions(-) rename ballerina/{time_errors.bal => errors.bal} (100%) rename ballerina/tests/{time_test.bal => test.bal} (74%) rename ballerina/{time_apis.bal => time.bal} (84%) rename ballerina/{time_types.bal => types.bal} (77%) create mode 100644 native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 84649336..3a0d4def 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "time" -version = "2.7.0" +version = "2.8.0" authors = ["Ballerina"] keywords = ["time", "utc", "epoch", "civil"] repository = "https://github.com/ballerina-platform/module-ballerina-time" @@ -15,5 +15,5 @@ graalvmCompatible = true [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "time-native" -version = "2.7.0" -path = "../native/build/libs/time-native-2.7.0.jar" +version = "2.8.0" +path = "../native/build/libs/time-native-2.8.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 853c976d..425168b3 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -67,7 +67,7 @@ modules = [ [[package]] org = "ballerina" name = "time" -version = "2.7.0" +version = "2.8.0" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "test"} diff --git a/ballerina/time_errors.bal b/ballerina/errors.bal similarity index 100% rename from ballerina/time_errors.bal rename to ballerina/errors.bal diff --git a/ballerina/tests/time_test.bal b/ballerina/tests/test.bal similarity index 74% rename from ballerina/tests/time_test.bal rename to ballerina/tests/test.bal index 33ad1ca7..3fb7c736 100644 --- a/ballerina/tests/time_test.bal +++ b/ballerina/tests/test.bal @@ -56,7 +56,7 @@ isolated function testUtcFromStringWithInvalidFormat() { Utc|Error err = utcFromString("2007-12-0310:15:30.00Z"); test:assertTrue(err is Error); test:assertEquals((err).message(), - "The provided string '2007-12-0310:15:30.00Z' does not adhere to the expected RFC 3339 format 'YYYY-MM-DDTHH:MM:SS.SSZ'. "); + "The provided string '2007-12-0310:15:30.00Z' does not adhere to the expected RFC 3339 format 'YYYY-MM-DDTHH:MM:SS.SSZ'. "); } @test:Config {} @@ -668,7 +668,7 @@ isolated function testGmtToEmailStringConversion() returns Error? { Utc utc = check utcFromString("2007-12-03T10:15:30.00Z"); Utc utc2 = check utcFromString("2007-12-03T10:15:30.00+05:30"); Civil civil = check civilFromString("2007-12-03T10:15:30.00+00:00"); - + test:assertEquals(utcToEmailString(utc, "Z"), "Mon, 3 Dec 2007 10:15:30 Z"); test:assertEquals(utcToEmailString(utc2, "0"), "Mon, 3 Dec 2007 04:45:30 +0000"); test:assertEquals(utcToEmailString(utc), "Mon, 3 Dec 2007 10:15:30 +0000"); @@ -709,7 +709,6 @@ isolated function testCivilToStringWithEmptyTimeOffset() returns Error? { @test:Config {enable: true} isolated function testUtcFromCivilWithEmptyTimeOffsetNegative() returns Error? { - Utc expectedUtc = check utcFromString("2021-04-12T23:20:50.520Z"); Civil civil = { year: 2021, month: 4, @@ -728,7 +727,6 @@ isolated function testUtcFromCivilWithEmptyTimeOffsetNegative() returns Error? { } isolated function testUtcFromCivilWithEmptyTimeOffsetAndAbbreviation() returns Error? { - Utc expectedUtc = check utcFromString("2021-04-12T23:20:50.520Z"); Civil civil = { year: 2021, month: 4, @@ -760,7 +758,7 @@ isolated function testCivilToStringWithEmptyTimeOffsetAndAbbreviation() returns test:assertEquals(civilString.message(), "the civil value should have either `utcOffset` or `timeAbbrev`"); } else { test:assertFail("civilString should be error"); - } + } } @test:Config {enable: true} @@ -769,4 +767,120 @@ isolated function testRepeatedUtcToCivilConversion() returns Error? { Civil civil = utcToCivil(utc); Utc utc2 = check utcFromCivil(civil); test:assertEquals(utc, utc2); -} +} + +@test:Config { + groups: ["duration"], + dataProvider: dataProviderCivilAddDuration +} +isolated function testCivilAddDuration(string civilString, Duration duration, string expectedResult) returns Error? { + Civil actualResult = check civilAddDuration(check civilFromString(civilString), duration); + test:assertEquals(civilToString(actualResult), expectedResult); +} + +isolated function dataProviderCivilAddDuration() returns [string, Duration, string][] { + return [ + ["2025-06-02T10:30:00Z", {years: -1, months: -8, days: -5, hours: -3, minutes: -5, seconds: -6}, "2023-09-27T07:24:54Z"], + ["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"], + ["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"], + ["2025-05-22T08:30:04.67Z", {years: -1, months: 4, days: 0, hours: -23, minutes: 5, seconds: -1}, "2024-09-21T09:35:03.670Z"] + ]; +} + +@test:Config { + groups: ["duration", "zone"], + dataProvider: dataProviderZoneDateTimeCivilAddDuration +} +isolated function testZoneDataTimeCivilAddDuration(string zone, string civilString, Duration duration, string expectedResult) returns Error? { + Zone? systemZone = getZone(zone); + test:assertTrue(systemZone is Zone); + Civil civil = check (systemZone).civilAddDuration(check civilFromString(civilString), duration); + test:assertEquals(civilToString(civil), expectedResult); +} + +isolated function dataProviderZoneDateTimeCivilAddDuration() returns [string, string, Duration, string][] { + return [ + ["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]"], + ["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]"], + ["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]"], + ["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]"], + ["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]"], + ["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]"] + ]; +} + +@test:Config { + groups: ["duration"], + dataProvider: dataProviderCivilRecordAddDuration +} +isolated function testCivilRecordAddDuration(Civil civilString, Duration duration, string expectedResult) returns Error? { + Civil actualResult = check civilAddDuration(civilString, duration); + test:assertEquals(civilToString(actualResult), expectedResult); +} + +isolated function dataProviderCivilRecordAddDuration() returns [Civil, Duration, string][] { + return [ + [{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"], + [{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]"], + [{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"], + [{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"] + ]; +} + +@test:Config { + groups: ["duration", "zone"], + dataProvider: dataProviderZoneDateTimeCivilRecordAddDuration +} +isolated function testZoneDataTimeCivilRecordAddDuration(string zone, Civil civil, Duration duration, string expectedResult) returns Error? { + Zone? systemZone = getZone(zone); + test:assertTrue(systemZone is Zone); + Civil result = check (systemZone).civilAddDuration(civil, duration); + test:assertEquals(civilToString(result), expectedResult); +} + +isolated function dataProviderZoneDateTimeCivilRecordAddDuration() returns [string, Civil, Duration, string][] { + return [ + ["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]"], + ["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"], + ["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]"], + ["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]"] + ]; +} + +@test:Config { + groups: ["duration"], + dataProvider: dataProviderInvalidCivilAddDuration +} +isolated function testInvalidCivilAddDuration(Civil civil, Duration duration, string errorMsg) returns Error? { + Civil|Error actualResult = civilAddDuration(civil, duration); + test:assertTrue(actualResult is Error); + test:assertEquals((actualResult).message(), errorMsg); +} + +isolated function dataProviderInvalidCivilAddDuration() returns [Civil, Duration, string][] { + return [ + [{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`"], + [{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"], + [{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"] + ]; +} + +@test:Config { + groups: ["duration", "zone"], + dataProvider: dataProviderInvalidZoneDateTimeCivilAddDuration +} +isolated function testInvalidZoneDataTimeCivilAddDuration(string zone, Civil civil, Duration duration, string errorMsg) returns Error? { + Zone? systemZone = getZone(zone); + test:assertTrue(systemZone is Zone); + Civil|Error result = (systemZone).civilAddDuration(civil, duration); + test:assertTrue(result is Error); + test:assertEquals((result).message(), errorMsg); +} + +isolated function dataProviderInvalidZoneDateTimeCivilAddDuration() returns [string, Civil, Duration, string][] { + return [ + ["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"], + ["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`"], + ["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"] + ]; +} diff --git a/ballerina/time_apis.bal b/ballerina/time.bal similarity index 84% rename from ballerina/time_apis.bal rename to ballerina/time.bal index 5eb89222..2974a07a 100644 --- a/ballerina/time_apis.bal +++ b/ballerina/time.bal @@ -148,7 +148,7 @@ public isolated function utcFromCivil(Civil civilTime) returns Utc|Error { decimal utcOffsetSeconds = (utcOffsetSecField is decimal) ? utcOffsetSecField : 0.0; return externUtcFromCivil(civilTime.year, civilTime.month, civilTime.day, civilTime.hour, civilTime.minute, - civilTimeSeconds, utcOffset.hours, utcOffset.minutes, utcOffsetSeconds); + civilTimeSeconds, utcOffset.hours, utcOffset.minutes, utcOffsetSeconds); } # 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 { decimal utcOffsetSeconds = utcOffset?.seconds ?: 0.0; decimal civilTimeSeconds = civil?.second ?: 0.0; - return externCivilToString(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds, - utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev ?: "", zoneHandling); + return externCivilToString(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds, + utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev ?: "", zoneHandling); } # Converts a given UTC to an email formatted string (e.g `Mon, 3 Dec 2007 10:15:30 GMT`). @@ -244,9 +244,34 @@ public isolated function civilToEmailString(Civil civil, HeaderZoneHandling zone string timeAbbrev = (timeAbbrevField is string) ? timeAbbrevField : ""; return externCivilToEmailString(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds, - utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev, zoneHandling); + utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev, zoneHandling); } +# Adds the given time duration to the specified civil date-time. This is a time zone-agnostic operation and assumes that +# all days have exactly 86,400 seconds. +# +# + civil - The civil time to which the duration should be added +# + duration - The time duration to be added +# + return - The civil time after adding the duration +public isolated function civilAddDuration(Civil civil, Duration duration) returns Civil|Error { + ZoneOffset? utcOffset = civil?.utcOffset; + string? timeAbbrev = civil?.timeAbbrev; + HeaderZoneHandling zoneHandling = PREFER_ZONE_OFFSET; + if utcOffset is () && timeAbbrev is () { + return error FormatError("The civil value should have either `utcOffset` or `timeAbbrev`"); + } else if utcOffset is () && timeAbbrev is string { + zoneHandling = PREFER_TIME_ABBREV; + } + int utcOffsetHours = utcOffset?.hours ?: 0; + int utcOffsetMinutes = utcOffset?.minutes ?: 0; + decimal utcOffsetSeconds = utcOffset?.seconds ?: 0.0; + decimal civilTimeSeconds = civil?.second ?: 0.0; + + return externCivilAddDuration(civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds, + utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev ?: "", zoneHandling, duration.years, + duration.months, duration.days, duration.hours, duration.minutes, duration.seconds); +}; + isolated function externUtcNow(int precision) returns Utc = @java:Method { name: "externUtcNow", 'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods" @@ -288,7 +313,7 @@ isolated function externUtcToCivil(Utc utc) returns Civil = @java:Method { } external; isolated function externUtcFromCivil(int year, int month, int day, int hour, int minute, decimal second, int zoneHour, - int zoneMinute, decimal zoneSecond) returns Utc|Error = @java:Method { + int zoneMinute, decimal zoneSecond) returns Utc|Error = @java:Method { name: "externUtcFromCivil", 'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods" } external; @@ -298,9 +323,9 @@ isolated function externCivilFromString(string dateTimeString) returns Civil|Err 'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods" } external; -isolated function externCivilToString(int year, int month, int day, int hour, int minute, decimal second, - int zoneHour, int zoneMinute, decimal zoneSecond, - string timeAbber, HeaderZoneHandling zoneHandling) returns string|Error = @java:Method { +isolated function externCivilToString(int year, int month, int day, int hour, int minute, decimal second, + int zoneHour, int zoneMinute, decimal zoneSecond, + string timeAbber, HeaderZoneHandling zoneHandling) returns string|Error = @java:Method { name: "externCivilToString", 'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods" } external; @@ -316,8 +341,14 @@ isolated function externCivilFromEmailString(string dateTimeString) returns Civi } external; isolated function externCivilToEmailString(int year, int month, int day, int hour, int minute, decimal second, - int zoneHour, int zoneMinute, decimal zoneSecond, string timeAbber, - HeaderZoneHandling zoneHandling) returns string|Error = @java:Method { + int zoneHour, int zoneMinute, decimal zoneSecond, string timeAbber, HeaderZoneHandling zoneHandling) + returns string|Error = @java:Method { name: "externCivilToEmailString", 'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods" } external; + +isolated function externCivilAddDuration(int year, int month, int day, int hour, int minute, decimal second, + int zoneHour, int zoneMinute, decimal zoneSecond, string timeAbbrev, HeaderZoneHandling zoneHandling, + int duYear, int duMonth, int duDay, int duHour, int duMinute, decimal duSecond) returns Civil|Error = @java:Method { + 'class: "io.ballerina.stdlib.time.nativeimpl.ExternMethods" +} external; diff --git a/ballerina/time_types.bal b/ballerina/types.bal similarity index 77% rename from ballerina/time_types.bal rename to ballerina/types.bal index 4711c982..cc0ac27e 100644 --- a/ballerina/time_types.bal +++ b/ballerina/types.bal @@ -172,9 +172,29 @@ public type Civil record { DayOfWeek dayOfWeek?; }; -# Defualt zone value represation in different formats. +# Default zone value representation in different formats. public type UtcZoneHandling "0"|"GMT"|"UT"|"Z"; +# Represents the time duration used to adjust a civil date-time value by a specified amount. +# The duration can be added to or subtracted from the civil time. +# Fields in the record can be negative, in which case the duration is subtracted. +public type Duration record {| + # The duration in years. + int years = 0; + # The duration in months. + int months = 0; + # The duration in weeks. + int weeks = 0; + # The duration in days. + int days = 0; + # The duration in hours. + int hours = 0; + # The duration in minutes. + int minutes = 0; + # The duration in seconds. + Seconds seconds = 0.0; +|}; + # Indicate how to handle both `zoneOffset` and `timeAbbrev`. public enum HeaderZoneHandling { PREFER_TIME_ABBREV, @@ -201,6 +221,15 @@ public type Zone readonly & object { # + utc - The `time:Utc` timestamp value to be converted # + return - The corresponding `time:Civil` value public isolated function utcToCivil(Utc utc) returns Civil; + + # Adds the given time duration to the specified civil date-time based on the time zone. + # The operation assumes that all days have exactly 86,400 seconds. + # + # + civil - The civil time to which the duration should be added + # + duration - The date-time duration to be added + # + return - The civil time after adding the duration + public isolated function civilAddDuration(Civil civil, Duration duration) returns Civil|Error; + }; # Localized time zone implementation to handle time zones. @@ -248,6 +277,31 @@ public readonly class TimeZone { public isolated function utcToCivil(Utc utc) returns Civil { return externTimeZoneUtcToCivil(self, utc); } + + # Adds the given time duration to the specified civil date-time based on the time zone. + # The operation assumes that all days have exactly 86,400 seconds. + # + # + civil - The civil time to which the duration should be added + # + duration - The date-time duration to be added + # + return - The civil time after adding the duration + public isolated function civilAddDuration(Civil civil, Duration duration) returns Civil|Error { + ZoneOffset? utcOffset = civil?.utcOffset; + string? timeAbbrev = civil?.timeAbbrev; + HeaderZoneHandling zoneHandling = PREFER_ZONE_OFFSET; + if utcOffset is () && timeAbbrev is () { + return error FormatError("The civil value should have either `utcOffset` or `timeAbbrev`"); + } else if utcOffset is () && timeAbbrev is string { + zoneHandling = PREFER_TIME_ABBREV; + } + int utcOffsetHours = utcOffset?.hours ?: 0; + int utcOffsetMinutes = utcOffset?.minutes ?: 0; + decimal utcOffsetSeconds = utcOffset?.seconds ?: 0.0; + decimal civilTimeSeconds = civil?.second ?: 0.0; + + return externTimeZoneCivilAddDuration(self, civil.year, civil.month, civil.day, civil.hour, civil.minute, + civilTimeSeconds, utcOffsetHours, utcOffsetMinutes, utcOffsetSeconds, timeAbbrev ?: "", zoneHandling, + duration.years, duration.months, duration.days, duration.hours, duration.minutes, duration.seconds); + } } # Load the default time zone of the system. @@ -290,7 +344,14 @@ isolated function externTimeZoneUtcToCivil(TimeZone timeZone, Utc utc) returns C } external; isolated function externTimeZoneUtcFromCivil(TimeZone timeZone, int year, int month, int day, -int hour, int minute, decimal second, string timeAbber, HeaderZoneHandling zoneHandling) + int hour, int minute, decimal second, string timeAbber, HeaderZoneHandling zoneHandling) returns Utc|Error = @java:Method { 'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternUtils" } external; + +isolated function externTimeZoneCivilAddDuration(TimeZone timeZone, int year, int month, int day, int hour, int minute, + decimal second, int zoneHour, int zoneMinute, decimal zoneSecond, string timeAbbrev, + HeaderZoneHandling zoneHandling, int duYear, int duMonth, int duDay, int duHour, int duMinute, + decimal duSecond) returns Civil|Error = @java:Method { + 'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternUtils" +} external; diff --git a/changelog.md b/changelog.md index 7b9f9438..6a1a3ec2 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [unreleased] +### Added + +- [Add support for custom time duration calculations](https://github.com/ballerina-platform/ballerina-library/issues/6840) + ## [2.5.0] - 2024-09-12 ### Fixed - [When converting a `time:Civil` with time-zone information to a string using `time:civilToString` API error is thrown](https://github.com/ballerina-platform/ballerina-library/issues/6986) diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 29e89e9d..2ff0534d 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -3,7 +3,7 @@ _Owners_: @daneshk @BuddhiWathsala _Reviewers_: @daneshk _Created_: 2021/12/04 -_Updated_: 2022/02/17 +_Updated_: 2025/04/23 _Edition_: Swan Lake ## Introduction @@ -59,7 +59,7 @@ The `Civil` record represents time within some region relative to a time scale s The time library contains two APIs to get the systematic time values. -The following API returns the current instant of the system clock in seconds from the epoch of `1970-01-01T00:00:00` with a given precision. The precision specifies the number of zeros after the decimal point (e.g. 3) would give the millisecond precision, and nil means native precision (nanosecond precision 9) of the clock). +The following API returns the current instant of the system clock in seconds from the epoch of `1970-01-01T00:00:00` with a given precision. The precision specifies the number of zeros after the decimal point (e.g. 3) would give the millisecond precision, and nil means native precision (nanosecond precision 9) of the clock. ```ballerina public isolated function utcNow(int? precision = ()) returns Utc; @@ -99,6 +99,12 @@ The following API returns the day of week value (e.g. Sunday, Monday etc.) of a public isolated function dayOfWeek(Date date) returns DayOfWeek; ``` +The following API adds or subtracts a given time duration from a Civil value in a time zone-agnostic way. + +```ballerina +public isolated function civilAddDuration(Civil civil, Duration duration) returns Civil|Error; +``` + ## 5. Time conversions The time library contains several conversion APIs to convert UTC to civil. The time library also has APIs to generate several string representations using UTC and Civil. @@ -162,3 +168,9 @@ On the other hand, the following API in the Zone object converts a given `Utc` t ```ballerina public isolated function utcToCivil(Utc utc) returns Civil; ``` + +The following API in the zone object adds or subtracts a given time duration from a Civil value based on the time zone. + +```ballerina +public isolated function civilAddDuration(Civil civil, Duration duration) returns Civil|Error; +``` diff --git a/gradle.properties b/gradle.properties index 5467a05b..df429afc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=2.7.1-SNAPSHOT +version=2.8.0-SNAPSHOT ballerinaLangVersion=2201.12.0 spotbugsPluginVersion=6.0.18 diff --git a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Civil.java b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Civil.java index 69847e86..d77b883d 100644 --- a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Civil.java +++ b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Civil.java @@ -27,6 +27,8 @@ import java.math.BigDecimal; import java.math.MathContext; +import java.time.Period; +import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.Map; @@ -128,4 +130,13 @@ public BMap createZoneOffsetFromZonedDateTime(ZonedDateTime zon return Utils.createZoneOffsetFromZoneInfoMap(zoneInfo); } + public Civil addDuration(ZoneId zoneId, CustomDuration duration) { + ZonedDateTime zoneDateTime = this.zonedDateTime.withZoneSameInstant(zoneId); + Period period = Period.of(duration.years(), duration.months(), duration.days()); + zoneDateTime = zoneDateTime.plus(period); + zoneDateTime = zoneDateTime.plus(Utils.createTimeDuration(duration.hours(), duration.minutes(), + duration.seconds(), duration.nanoSeconds())); + return new Civil(zoneDateTime); + } + } diff --git a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java new file mode 100644 index 00000000..2652e169 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java @@ -0,0 +1,36 @@ +package io.ballerina.stdlib.time.nativeimpl; + +import io.ballerina.runtime.api.values.BDecimal; +import io.ballerina.stdlib.time.util.Constants; + +import java.math.BigDecimal; +import java.math.RoundingMode; + +public record CustomDuration(int years, int months, int days, int hours, int minutes, int seconds, int nanoSeconds) { + + public CustomDuration(int years, int months, int days, int hours, int minutes, BDecimal seconds) { + this( + years, + months, + days, + hours, + minutes, + getSeconds(seconds), + getNanoSeconds(seconds) + ); + } + + private static int getSeconds(BDecimal seconds) { + BigDecimal decimal = seconds.decimalValue(); + return decimal.setScale(0, RoundingMode.FLOOR).intValue(); + } + + private static int getNanoSeconds(BDecimal seconds) { + BigDecimal decimal = seconds.decimalValue(); + BigDecimal fractional = decimal.subtract(new BigDecimal(decimal.setScale(0, RoundingMode.FLOOR).intValue())); + return fractional + .multiply(Constants.ANALOG_GIGA) + .setScale(0, RoundingMode.HALF_UP) + .intValue(); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/ExternMethods.java b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/ExternMethods.java index 8809ffd6..4b85e5c0 100644 --- a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/ExternMethods.java +++ b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/ExternMethods.java @@ -33,6 +33,7 @@ import java.time.DateTimeException; import java.time.Instant; import java.time.LocalDate; +import java.time.Period; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -199,4 +200,24 @@ public static Object externCivilToEmailString(long year, long month, long day, l } } + public static Object externCivilAddDuration(int year, int month, int day, int hour, int minute, + BDecimal second, int zoneHour, int zoneMinute, + BDecimal zoneSecond, BString zoneAbbrev, + BString zoneHandling, int duYears, int duMonths, + int duDays, int duHours, int duMinutes, + BDecimal duSeconds) { + try { + ZonedDateTime zonedDateTime = TimeValueHandler.createZoneDateTimeFromCivilValues(year, month, day, hour, + minute, second, zoneHour, zoneMinute, zoneSecond, zoneAbbrev, zoneHandling.getValue()); + CustomDuration duration = new CustomDuration(duYears, duMonths, duDays, duHours, duMinutes, duSeconds); + Period period = Period.of(duration.years(), duration.months(), duration.days()); + zonedDateTime = zonedDateTime.plus(period); + zonedDateTime = zonedDateTime.plus(Utils.createTimeDuration(duration.hours(), duration.minutes(), + duration.seconds(), duration.nanoSeconds())); + return TimeValueHandler.createCivilFromZoneDateTime(zonedDateTime); + } catch (DateTimeException e) { + return Utils.createError(Errors.FormatError, e.getMessage()); + } + } + } diff --git a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/TimeZoneExternUtils.java b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/TimeZoneExternUtils.java index d8b0befd..a39efe25 100644 --- a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/TimeZoneExternUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/TimeZoneExternUtils.java @@ -80,4 +80,22 @@ public static BMap externTimeZoneUtcToCivil(BObject timeZoneObj, BArray utc) { return zone.utcToCivil(new Utc(utc)).build(); } + public static Object externTimeZoneCivilAddDuration(BObject timeZoneObj, int year, int month, + int day, int hour, int minute, BDecimal second, + int zoneHour, int zoneMinute, BDecimal zoneSecond, + BString zoneAbbrev, BString zoneHandling, int duYears, + int duMonths, int duDays, int duHours, int duMinutes, + BDecimal duSeconds) { + try { + Zone zone = (Zone) timeZoneObj.getNativeData(ZONE_ID_ENTRY); + ZonedDateTime zonedDateTime = Utils.createZoneDateTimeFromCivilValues(year, month, day, hour, minute, + second, zoneHour, zoneMinute, zoneSecond, zoneAbbrev, zoneHandling.getValue()); + CustomDuration customDuration = new CustomDuration(duYears, duMonths, duDays, duHours, duMinutes, + duSeconds); + return zone.civilAddDuration(new Civil(zonedDateTime), customDuration); + } catch (DateTimeException e) { + return Utils.createError(Errors.FormatError, e.getMessage()); + } + } + } diff --git a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Zone.java b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Zone.java index a472d405..06024382 100644 --- a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Zone.java +++ b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Zone.java @@ -17,6 +17,8 @@ */ package io.ballerina.stdlib.time.nativeimpl; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BString; import io.ballerina.stdlib.time.util.Utils; import java.time.DateTimeException; @@ -62,4 +64,8 @@ public Civil utcToCivil(Utc utc) { return new Civil(utc.generateInstant().atZone(zoneId)); } + + public BMap civilAddDuration(Civil civil, CustomDuration customDuration) { + return civil.addDuration(this.zoneId, customDuration).build(); + } } diff --git a/native/src/main/java/io/ballerina/stdlib/time/util/TimeValueHandler.java b/native/src/main/java/io/ballerina/stdlib/time/util/TimeValueHandler.java index 5672762f..262c6b53 100644 --- a/native/src/main/java/io/ballerina/stdlib/time/util/TimeValueHandler.java +++ b/native/src/main/java/io/ballerina/stdlib/time/util/TimeValueHandler.java @@ -84,4 +84,5 @@ public static ZonedDateTime createZonedDateTimeFromUtc(BArray utc) { return new Utc(utc).generateZonedDateAtZ(); } + } diff --git a/native/src/main/java/io/ballerina/stdlib/time/util/Utils.java b/native/src/main/java/io/ballerina/stdlib/time/util/Utils.java index 39ccc309..0bac4a5d 100644 --- a/native/src/main/java/io/ballerina/stdlib/time/util/Utils.java +++ b/native/src/main/java/io/ballerina/stdlib/time/util/Utils.java @@ -27,6 +27,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.Duration; import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; @@ -127,4 +128,9 @@ public static BError createError(Errors errorType, String errorMsg) { return ErrorCreator.createError(ModuleUtils.getModule(), errorType.name(), StringUtils.fromString(errorMsg), null, null); } + + public static Duration createTimeDuration(int hours, int minutes, int seconds, int nanoSeconds) { + return Duration.ofHours(hours).plusMinutes(minutes).plusSeconds(seconds).plusNanos(nanoSeconds); + } + } From 70ce602377b1bfa07e5b6b25ba18f45c8e4995be Mon Sep 17 00:00:00 2001 From: DimuthuMadushan Date: Fri, 25 Apr 2025 10:20:39 +0530 Subject: [PATCH 2/2] Apply suggestions from code review --- ballerina/time.bal | 5 +++- ballerina/types.bal | 6 +++- .../time/nativeimpl/CustomDuration.java | 29 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/ballerina/time.bal b/ballerina/time.bal index 2974a07a..9e5dcdab 100644 --- a/ballerina/time.bal +++ b/ballerina/time.bal @@ -249,7 +249,10 @@ public isolated function civilToEmailString(Civil civil, HeaderZoneHandling zone # Adds the given time duration to the specified civil date-time. This is a time zone-agnostic operation and assumes that # all days have exactly 86,400 seconds. -# +# ```ballerina +# time:Civil civil = check time:civilFromString("2025-04-25T10:15:30.00Z"); +# time:Civil|time:Error updatedCivil = time:civilAddDuration(civil, {years: 1, days: 3, hours: 4, seconds: 6}); +# ``` # + civil - The civil time to which the duration should be added # + duration - The time duration to be added # + return - The civil time after adding the duration diff --git a/ballerina/types.bal b/ballerina/types.bal index cc0ac27e..c1fdd6ad 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -280,7 +280,11 @@ public readonly class TimeZone { # Adds the given time duration to the specified civil date-time based on the time zone. # The operation assumes that all days have exactly 86,400 seconds. - # + # ```ballerina + # time:TimeZone timeZone = check new("Asia/Colombo"); + # time:Civil civil = check time:civilFromString("2025-04-25T10:15:30.00Z"); + # time:Civil|time:Error updatedCivil = timeZone.civilAddDuration(civil, {years: 1, days: 3, hours: 4}); + # ``` # + civil - The civil time to which the duration should be added # + duration - The date-time duration to be added # + return - The civil time after adding the duration diff --git a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java index 2652e169..91598936 100644 --- a/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java +++ b/native/src/main/java/io/ballerina/stdlib/time/nativeimpl/CustomDuration.java @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2025, WSO2 LLC. (http://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ package io.ballerina.stdlib.time.nativeimpl; import io.ballerina.runtime.api.values.BDecimal; @@ -6,6 +23,18 @@ import java.math.BigDecimal; import java.math.RoundingMode; +/** + * This {@link CustomDuration} record represents a custom duration with various time components. + * @param years - The number of years. + * @param months - The number of months. + * @param days - The number of days. + * @param hours - The number of hours. + * @param minutes - The number of minutes. + * @param seconds - The number of seconds. + * @param nanoSeconds - The number of nanoseconds. + * + * @since 2.8.0 + */ public record CustomDuration(int years, int months, int days, int hours, int minutes, int seconds, int nanoSeconds) { public CustomDuration(int years, int months, int days, int hours, int minutes, BDecimal seconds) {