Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add time zone handling APIs #337

Merged
merged 21 commits into from
Nov 30, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
84ca978
Add a generic set of utils to the time package
BuddhiWathsala Nov 25, 2021
5792b77
Add zone class to handle time zones
BuddhiWathsala Nov 25, 2021
eb66645
Add native extern time zone APIs for Ballerina
BuddhiWathsala Nov 25, 2021
f33a216
Improve the time value handler APIs by using generic utils
BuddhiWathsala Nov 25, 2021
f0b42e0
Improve the extern APIs by using generic utils
BuddhiWathsala Nov 25, 2021
da5e0a0
Introduce enumerations for civil to string conversion types
BuddhiWathsala Nov 25, 2021
72c7644
Simplify the Civil APIs using generic util APIs
BuddhiWathsala Nov 25, 2021
6b68db3
Introduce Ballerina-level time zone handling APIs
BuddhiWathsala Nov 25, 2021
d4ffccd
Introduce Ballerina time zone handling tests
BuddhiWathsala Nov 25, 2021
a69b853
Ignore unnecessary VSCode files using gitignore
BuddhiWathsala Nov 25, 2021
d8ab382
Ignore the load time zone check since it may fail in different enviro…
BuddhiWathsala Nov 25, 2021
7063285
Add tests to check time zone errors and specific conversions
BuddhiWathsala Nov 26, 2021
8db9958
Remove unnecessary parentheses in if statements in tests
BuddhiWathsala Nov 26, 2021
b14fa78
Remove unnecessary parentheses in if statements in APIs
BuddhiWathsala Nov 26, 2021
71095fe
Replace the deprecated error creation API
BuddhiWathsala Nov 26, 2021
6adc2e7
Remove unnecessary Civil constructor
BuddhiWathsala Nov 26, 2021
b70b7ea
Add private constructors
BuddhiWathsala Nov 26, 2021
1ea4304
Add private constructor to TimeValueHandler
BuddhiWathsala Nov 26, 2021
5508ec4
Add API docs for time zone APIs
BuddhiWathsala Nov 26, 2021
c4a8810
Fix the error message and remove the unnecessary timeAbbrev variable
BuddhiWathsala Nov 30, 2021
851aca6
Rename the TimeZoneExternMethods->TimeZoneExternUtils
BuddhiWathsala Nov 30, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,5 @@ velocity.log*

# VSCode
.vscode
.project
.settings/
225 changes: 175 additions & 50 deletions ballerina/tests/time_test.bal

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions ballerina/time_apis.bal
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public isolated function utcAddSeconds(Utc utc, Seconds seconds) returns Utc {
[int, decimal] [secondsFromEpoch, lastSecondFraction] = utc;
secondsFromEpoch = secondsFromEpoch + <int>seconds.floor();
lastSecondFraction = lastSecondFraction + (seconds - seconds.floor());
if (lastSecondFraction >= 1.0d) {
if lastSecondFraction >= 1.0d {
secondsFromEpoch = secondsFromEpoch + <int>lastSecondFraction.floor();
lastSecondFraction = lastSecondFraction - lastSecondFraction.floor();
}
Expand Down Expand Up @@ -132,7 +132,7 @@ public isolated function utcToCivil(Utc utc) returns Civil {
# + civilTime - `Civil` time
# + return - The corresponding `Utc` value or an error if `civilTime.utcOffset` is missing
public isolated function utcFromCivil(Civil civilTime) returns Utc|Error {
if (civilTime?.utcOffset is ()) {
if civilTime?.utcOffset is () {
return error FormatError("civilTime.utcOffset must not be null");
}
ZoneOffset utcOffset = <ZoneOffset>civilTime?.utcOffset;
Expand Down Expand Up @@ -164,7 +164,7 @@ public isolated function civilFromString(string dateTimeString) returns Civil|Er
# + civil - `time:Civil` that needs to be converted
# + return - The corresponding string value or an error if the specified `time:Civil` contains invalid parameters(e.g. `month` > 12)
public isolated function civilToString(Civil civil) returns string|Error {
if (civil?.utcOffset is ()) {
if civil?.utcOffset is () {
return error FormatError("civil.utcOffset must not be null");
}
ZoneOffset utcOffset = <ZoneOffset>civil?.utcOffset;
Expand Down
110 changes: 110 additions & 0 deletions ballerina/time_types.bal
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
// specific language governing permissions and limitations
// under the License.

import ballerina/jballerina.java;

# Holds the seconds as a decimal value.
public type Seconds decimal;

Expand Down Expand Up @@ -172,3 +174,111 @@ public enum HeaderZoneHandling {
PREFER_ZONE_OFFSET,
ZONE_OFFSET_WITH_TIME_ABBREV_COMMENT
}

# Abstract object representation to handle time zones.
public type Zone readonly & object {

# If always at a fixed offset from Utc, then this function returns it; otherwise nil.
#
# + return - The fixed zone offset or nil
public isolated function fixedOffset() returns ZoneOffset?;

# Converts a given `Civil` value to an `Utc` timestamp based on the time zone value.
#
# + civil - `Civil` time
# + return - The corresponding `Utc` value or an error if `civil.timeAbbrev` is missing
public isolated function utcFromCivil(Civil civil) returns Utc|Error;

# Converts a given `Utc` timestamp to a `Civil` value based on the time zone value.
#
# + utc - `Utc` timestamp
# + return - The corresponding `Civil` value
public isolated function utcToCivil(Utc utc) returns Civil;
};

# Localized time zone implementation to handle time zones.
public readonly class TimeZone {
*Zone;

# Initialize a TimeZone class using a zone ID.
#
# + zoneId - Zone ID as a string or nil to initialize a TimeZone object with the system default time zone
# + return - An error or nil
public isolated function init(string? zoneId = ()) returns Error? {
if zoneId is string {
externTimeZoneInitWithId(self, zoneId);
} else {
check externTimeZoneInitWithSystemZone(self);
}
}

# If always at a fixed offset from Utc, then this function returns it; otherwise nil.
#
# + return - The fixed zone offset or nil
public isolated function fixedOffset() returns ZoneOffset? {
return externTimeZoneFixedOffset(self);
}

# Converts a given `Civil` value to an `Utc` timestamp based on the time zone value.
#
# + civil - `Civil` time
# + return - The corresponding `Utc` value or an error if `civil.timeAbbrev` is missing
public isolated function utcFromCivil(Civil civil) returns Utc|Error {
string? timeAbbrev = civil?.timeAbbrev;
if timeAbbrev is () {
return error FormatError("Abbreviation for the local time is required for the conversion");
}
decimal? civilTimeSecField = civil?.second;
decimal civilTimeSeconds = (civilTimeSecField is Seconds) ? civilTimeSecField : 0.0;

return externTimeZoneUtcFromCivil(self, civil.year, civil.month, civil.day, civil.hour, civil.minute, civilTimeSeconds, timeAbbrev, PREFER_TIME_ABBREV);
}

# Converts a given `Utc` timestamp to a `Civil` value based on the time zone value.
#
# + utc - `Utc` timestamp
# + return - The corresponding `Civil` value
public isolated function utcToCivil(Utc utc) returns Civil {
return externTimeZoneUtcToCivil(self, utc);
}
}

# Load the default time zone of the system.
# + return - Zone value or error when the zone ID of the system is in invalid format.
public isolated function loadSystemZone() returns Zone|Error {
return check new TimeZone();
}

# Return the time zone object of a given zone ID.
#
# + id - Time zone ID (e.g. "Continent/City")
# + return - Corresponding ime zone object or null
public isolated function getZone(string id) returns Zone? {
TimeZone|Error timeZone = new TimeZone(id);
if timeZone is TimeZone {
return timeZone;
}
return;
}

isolated function externTimeZoneInitWithSystemZone(TimeZone timeZone) returns Error? = @java:Method {
'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternMethods"
} external;

isolated function externTimeZoneInitWithId(TimeZone timeZone, string zoneId) = @java:Method {
'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternMethods"
} external;

isolated function externTimeZoneFixedOffset(TimeZone timeZone) returns ZoneOffset? = @java:Method {
'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternMethods"
} external;

isolated function externTimeZoneUtcToCivil(TimeZone timeZone, Utc utc) returns Civil = @java:Method {
'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternMethods"
} external;

isolated function externTimeZoneUtcFromCivil(TimeZone timeZone, int year, int month, int day,
int hour, int minute, decimal second, string timeAbber, HeaderZoneHandling zoneHandling)
returns Utc|Error = @java:Method {
'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternMethods"
} external;
115 changes: 35 additions & 80 deletions native/src/main/java/io/ballerina/stdlib/time/nativeimpl/Civil.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,20 @@
* specific language governing permissions and limitations
* under the License.
*/
package io.ballerina.stdlib.time.nativeimpl;
package io.ballerina.stdlib.time.nativeimpl;

import io.ballerina.runtime.api.creators.ValueCreator;
import io.ballerina.runtime.api.utils.StringUtils;
import io.ballerina.runtime.api.values.BMap;
import io.ballerina.runtime.api.values.BString;
import io.ballerina.stdlib.time.util.Constants;
import io.ballerina.stdlib.time.util.ModuleUtils;
import io.ballerina.stdlib.time.util.Utils;

import java.math.BigDecimal;
import java.math.MathContext;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

Expand All @@ -41,13 +41,38 @@
*/
public class Civil {

private ZonedDateTime zonedDateTime = ZonedDateTime.now();
private BMap<BString, Object> civilMap = ValueCreator.createRecordValue(ModuleUtils.getModule(),
private final ZonedDateTime zonedDateTime;
private boolean isSecondExists = false;
private boolean isLocalTimeZoneExists = false;
private final BMap<BString, Object> civilMap = ValueCreator.createRecordValue(ModuleUtils.getModule(),
Constants.CIVIL_RECORD);

public BMap<BString, Object> buildFromZonedDateTime(ZonedDateTime zonedDateTime) {
public Civil(ZonedDateTime zonedDateTime) {

this.zonedDateTime = zonedDateTime;
}

public Civil(String zonedDateTimeString, Constants.CivilInputStringTypes inputStringTypes) {

if (Constants.CivilInputStringTypes.EMAIL_STRING.toString().equals(inputStringTypes.toString())) {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Constants.EMAIL_DATE_TIME_FORMAT);
this.zonedDateTime = ZonedDateTime.parse(zonedDateTimeString, dateTimeFormatter);
this.isSecondExists = true;
this.isLocalTimeZoneExists = true;
} else {
this.zonedDateTime = ZonedDateTime.parse(zonedDateTimeString);
this.isSecondExists = isSecondExists(zonedDateTimeString);
this.isLocalTimeZoneExists = isLocalTimeZoneExists(zonedDateTimeString);
}
}

public ZonedDateTime getZonedDateTime() {

return zonedDateTime;
}

public BMap<BString, Object> build() {

setCommonCivilFields();
BigDecimal second = new BigDecimal(zonedDateTime.getSecond());
second = second.add(new BigDecimal(zonedDateTime.getNano()).divide(ANALOG_GIGA, MathContext.DECIMAL128));
Expand All @@ -57,18 +82,16 @@ public BMap<BString, Object> buildFromZonedDateTime(ZonedDateTime zonedDateTime)

}

public BMap<BString, Object> buildFromZonedDateTimeString(String zonedDateTimeString) {
public BMap<BString, Object> buildWithZone() {

ZonedDateTime zonedDateTime = ZonedDateTime.parse(zonedDateTimeString);
this.zonedDateTime = zonedDateTime;
setCommonCivilFields();
BigDecimal second = new BigDecimal(zonedDateTime.getSecond());
second = second.add(new BigDecimal(zonedDateTime.getNano()).divide(ANALOG_GIGA, MathContext.DECIMAL128));

if (isSecondExists(zonedDateTimeString)) {
if (this.isSecondExists) {
civilMap.put(Constants.TIME_OF_DAY_RECORD_SECOND_BSTRING, ValueCreator.createDecimalValue(second));
}
if (isLocalTimeZoneExists(zonedDateTimeString)) {
if (this.isLocalTimeZoneExists) {
civilMap.put(Constants.CIVIL_RECORD_UTC_OFFSET_BSTRING,
createZoneOffsetFromZonedDateTime(zonedDateTime));
}
Expand All @@ -77,22 +100,6 @@ public BMap<BString, Object> buildFromZonedDateTimeString(String zonedDateTimeSt

}

public BMap<BString, Object> buildFromEmailString(String zonedDateTimeString) {

DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(Constants.EMAIL_DATE_TIME_FORMAT);
ZonedDateTime zonedDateTime = ZonedDateTime.parse(zonedDateTimeString, dateTimeFormatter);
this.zonedDateTime = zonedDateTime;
setCommonCivilFields();
BigDecimal second = new BigDecimal(zonedDateTime.getSecond());
second = second.add(new BigDecimal(zonedDateTime.getNano()).divide(ANALOG_GIGA, MathContext.DECIMAL128));
civilMap.put(Constants.TIME_OF_DAY_RECORD_SECOND_BSTRING, ValueCreator.createDecimalValue(second));
civilMap.put(Constants.CIVIL_RECORD_UTC_OFFSET_BSTRING,
createZoneOffsetFromZonedDateTime(zonedDateTime));

return civilMap;

}

private void setCommonCivilFields() {

civilMap.put(Constants.DATE_RECORD_YEAR_BSTRING, zonedDateTime.getYear());
Expand All @@ -119,60 +126,8 @@ private boolean isSecondExists(String time) {

public BMap<BString, Object> createZoneOffsetFromZonedDateTime(ZonedDateTime zonedDateTime) {

BMap<BString, Object> civilMap = ValueCreator.createRecordValue(ModuleUtils.getModule(),
Constants.READABLE_ZONE_OFFSET_RECORD);
Map<String, Integer> zoneInfo = zoneOffsetMapFromString(zonedDateTime.getOffset().toString());
if (zoneInfo.get(Constants.ZONE_OFFSET_RECORD_HOUR) != null) {
civilMap.put(Constants.ZONE_OFFSET_RECORD_HOUR_BSTRING,
zoneInfo.get(Constants.ZONE_OFFSET_RECORD_HOUR).longValue());
} else {
civilMap.put(Constants.ZONE_OFFSET_RECORD_HOUR_BSTRING, 0);
}

if (zoneInfo.get(Constants.ZONE_OFFSET_RECORD_MINUTE) != null) {
civilMap.put(Constants.ZONE_OFFSET_RECORD_MINUTE_BSTRING,
zoneInfo.get(Constants.ZONE_OFFSET_RECORD_MINUTE).longValue());
} else {
civilMap.put(Constants.ZONE_OFFSET_RECORD_MINUTE_BSTRING, 0);
}

if (zoneInfo.get(Constants.ZONE_OFFSET_RECORD_SECOND) != null) {
civilMap.put(Constants.ZONE_OFFSET_RECORD_SECOND_BSTRING,
zoneInfo.get(Constants.ZONE_OFFSET_RECORD_SECOND).longValue());
}
civilMap.freezeDirect();
return civilMap;
}

public Map<String, Integer> zoneOffsetMapFromString(String dateTime) {

Map<String, Integer> zone = new HashMap<>();
if (dateTime.strip().startsWith("+")) {
dateTime = dateTime.replaceFirst("\\+", "");
String[] zoneInfo = dateTime.split(":");
if (zoneInfo.length > 0 && zoneInfo[0] != null) {
zone.put(Constants.ZONE_OFFSET_RECORD_HOUR, Integer.parseInt(zoneInfo[0]));
}
if (zoneInfo.length > 1 && zoneInfo[1] != null) {
zone.put(Constants.ZONE_OFFSET_RECORD_MINUTE, Integer.parseInt(zoneInfo[1]));
}
if (zoneInfo.length > 2 && zoneInfo[2] != null) {
zone.put(Constants.ZONE_OFFSET_RECORD_SECOND, Integer.parseInt(zoneInfo[2]));
}
} else if (dateTime.strip().startsWith("-")) {
dateTime = dateTime.replaceFirst("\\-", "");
String[] zoneInfo = dateTime.split(":");
if (zoneInfo.length > 0 && zoneInfo[0] != null) {
zone.put(Constants.ZONE_OFFSET_RECORD_HOUR, Integer.parseInt(zoneInfo[0]) * -1);
}
if (zoneInfo.length > 1 && zoneInfo[1] != null) {
zone.put(Constants.ZONE_OFFSET_RECORD_MINUTE, Integer.parseInt(zoneInfo[1]) * -1);
}
if (zoneInfo.length > 2 && zoneInfo[2] != null) {
zone.put(Constants.ZONE_OFFSET_RECORD_SECOND, Integer.parseInt(zoneInfo[2]) * -1);
}
}
return zone;
Map<String, Integer> zoneInfo = Utils.zoneOffsetMapFromString(zonedDateTime.getOffset().toString());
return Utils.createZoneOffsetFromZoneInfoMap(zoneInfo);
}

}
Loading