Skip to content

Commit

Permalink
Merge pull request #337 from ballerina-platform/bdev
Browse files Browse the repository at this point in the history
Add time zone handling APIs
  • Loading branch information
BuddhiWathsala authored Nov 30, 2021
2 parents fe51b83 + 851aca6 commit 407301d
Show file tree
Hide file tree
Showing 11 changed files with 632 additions and 175 deletions.
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.TimeZoneExternUtils"
} external;

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

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

isolated function externTimeZoneUtcToCivil(TimeZone timeZone, Utc utc) returns Civil = @java:Method {
'class: "io.ballerina.stdlib.time.nativeimpl.TimeZoneExternUtils"
} 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.TimeZoneExternUtils"
} 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

0 comments on commit 407301d

Please sign in to comment.