From d87a32d0195b51a2526f95d32fbbbb0757b3d22b Mon Sep 17 00:00:00 2001 From: Hubert Plociniczak Date: Wed, 24 Aug 2022 12:31:29 +0200 Subject: [PATCH] Builtin Date_Time, Time_Of_Day, Zone (#3658) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Builtin Date_Time, Time_Of_Day, Zone Improved polyglot support for Date_Time (formerly Time), Time_Of_Day and Zone. This follows the pattern introduced for Enso Date. Minor caveat - in tests for Date, had to bend a lot for JS Date to pass. This is because JS Date is not really only a Date, but also a Time and Timezone, previously we just didn't consider the latter. Also, JS Date does not deal well with setting timezones so the trick I used is to first call foreign function returning a polyglot JS Date, which is converted to ZonedDateTime and only then set the correct timezone. That way none of the existing tests had to be changes or special cased. Additionally, JS deals with milliseconds rather than nanoseconds so there is loss in precision, as noted in Time_Spec. * Add tests for Java's LocalTime * changelog * Make date formatters in table happy * PR review, add more tests for zone * More tests and fixed a bug in column reader Column reader didn't take into account timezone but that was a mistake since then it wouldn't map to Enso's Date_Time. Added tests that check it now. * remove redundant conversion * Update distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time.enso Co-authored-by: Radosław Waśko * First round of addressing PR review * don't leak java exceptions in Zone * Move Date_Time to top-level module * PR review Co-authored-by: Radosław Waśko Co-authored-by: Jaroslav Tulach --- CHANGELOG.md | 3 + .../Base/0.0.0-dev/src/Data/Time.enso | 474 +---------------- .../Base/0.0.0-dev/src/Data/Time/Date.enso | 15 +- .../0.0.0-dev/src/Data/Time/Date_Time.enso | 483 ++++++++++++++++++ .../0.0.0-dev/src/Data/Time/Duration.enso | 14 +- .../0.0.0-dev/src/Data/Time/Time_Of_Day.enso | 53 +- .../Base/0.0.0-dev/src/Data/Time/Zone.enso | 20 +- .../Base/0.0.0-dev/src/System/File.enso | 6 +- .../Standard/Examples/0.0.0-dev/src/Main.enso | 4 +- .../src/Data_Science/Date_And_Time.enso | 8 +- .../0.0.0-dev/src/Data/Data_Formatter.enso | 19 +- .../lib/Standard/Test/0.0.0-dev/src/Main.enso | 6 +- .../epb/runtime/PolyglotProxy.java | 53 ++ .../node/callable/InvokeMethodNode.java | 116 ++++- .../callable/resolver/HostMethodCallNode.java | 44 +- .../interpreter/runtime/builtin/Builtins.java | 39 ++ .../interpreter/runtime/data/EnsoDate.java | 19 +- .../runtime/data/EnsoDateTime.java | 284 ++++++++++ .../interpreter/runtime/data/EnsoFile.java | 10 +- .../runtime/data/EnsoTimeOfDay.java | 163 ++++++ .../interpreter/runtime/data/EnsoZone.java | 112 ++++ .../enso/interpreter/runtime/type/Types.java | 9 +- .../interpreter/test/semantic/DateTest.scala | 17 - .../dsl/builtins/TypeWithKind.java | 3 + .../main/java/org/enso/base/Time_Utils.java | 92 +++- .../org/enso/table/data/table/Column.java | 23 +- .../enso/table/formatting/DateFormatter.java | 2 +- .../table/formatting/DateTimeFormatter.java | 14 +- .../enso/table/formatting/TimeFormatter.java | 8 +- test/Table_Tests/data/datetime_sample.csv | 7 + test/Table_Tests/data/time_of_day_sample.csv | 7 + test/Table_Tests/src/Data_Formatter_Spec.enso | 15 +- .../Table_Tests/src/Delimited_Write_Spec.enso | 2 +- .../src/Table_Time_Of_Day_Spec.enso | 53 ++ test/Table_Tests/src/Table_Time_Spec.enso | 52 ++ test/Tests/src/Data/Time/Date_Spec.enso | 12 +- test/Tests/src/Data/Time/Duration_Spec.enso | 8 +- .../Tests/src/Data/Time/Time_Of_Day_Spec.enso | 73 ++- test/Tests/src/Data/Time/Time_Spec.enso | 143 ++++-- test/Tests/src/Data/Time/Zone_Spec.enso | 37 ++ .../Tests/src/Semantic/Java_Interop_Spec.enso | 4 +- 41 files changed, 1854 insertions(+), 672 deletions(-) create mode 100644 distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDateTime.java create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeOfDay.java create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoZone.java create mode 100644 test/Table_Tests/data/datetime_sample.csv create mode 100644 test/Table_Tests/data/time_of_day_sample.csv create mode 100644 test/Table_Tests/src/Table_Time_Of_Day_Spec.enso create mode 100644 test/Table_Tests/src/Table_Time_Spec.enso diff --git a/CHANGELOG.md b/CHANGELOG.md index 57b3e1c6c5..a70a001f5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -327,6 +327,8 @@ - [Support importing module methods][3633] - [Support Autosave for open buffers][3637] - [Support pattern matching on constants][3641] +- [Builtin Date_Time, Time_Of_Day and Zone types for better polyglot + support][3658] [3227]: https://github.com/enso-org/enso/pull/3227 [3248]: https://github.com/enso-org/enso/pull/3248 @@ -369,6 +371,7 @@ [3633]: https://github.com/enso-org/enso/pull/3633 [3637]: https://github.com/enso-org/enso/pull/3637 [3633]: https://github.com/enso-org/enso/pull/3641 +[3658]: https://github.com/enso-org/enso/pull/3658 # Enso 2.0.0-alpha.18 (2021-10-12) diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time.enso index c4caf18bdd..4b911e44be 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time.enso @@ -1,481 +1,9 @@ -from Standard.Base import all - -import Standard.Base.Data.Time.Duration -import Standard.Base.Data.Time.Time_Of_Day -import Standard.Base.Data.Time.Zone - -polyglot java import java.time.format.DateTimeFormatter -polyglot java import java.time.ZonedDateTime -polyglot java import org.enso.base.Time_Utils - -## ALIAS Current Time - - Obtains the current date-time from the system clock in the system timezone. - - > Example - Get the current time - - import Standard.Base.Data.Time - - example_now = Time.now -now : Time -now = Time ZonedDateTime.now - -## Obtains an instance of `Time` from a year, month, day, hour, minute, - second, nanosecond and timezone. - - Arguments: - - month: the month-of-year to represent, from 1 (January) to 12 (December) - - day: the day-of-month to represent, from 1 to 31 and must be valid for the - year and month - - hour: the hour-of-day to represent, from 0 to 23 - - minute: the minute-of-hour to represent, from 0 to 59 - - second: the second-of-minute to represent, from 0 to 59 - - nanosecond: the nano-of-second to represent, from 0 to 999,999,999 - - zone: the timezone - - Returns a `Time_Error` if the provided time cannot be represented. - - > Example - Create a new zoned date time at Unix epoch. - - import Standard.Base.Data.Time - import Standard.Base.Data.Time.Zone - - example_new = Time.new 1970 (zone = Zone.utc) - - > Example - Get the 5 August 1986 at midnight. - - import Standard.Base.Data.Time - import Standard.Base.Data.Time.Zone - - example_new = Time.new 1986 8 5 -new : Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Zone -> Time ! Time_Error -new year (month = 1) (day = 1) (hour = 0) (minute = 0) (second = 0) (nanosecond = 0) (zone = Zone.system) = - Panic.catch_java Any (Time (ZonedDateTime.of year month day hour minute second nanosecond zone.internal_zone_id)) java_exception-> - Error.throw (Time_Error java_exception.getMessage) - -## ALIAS Time from Text - - Obtains an instance of `Time` from a text such as - "2007-12-03T10:15:30+01:00 Europe/Paris". - - Arguments: - - text: The text representing the time to be parsed. - - pattern: The pattern to use for parsing the input text. - - locale: The locale in which the pattern should be interpreted. - - ? Pattern Syntax - For the list of accepted symbols in pattern refer to `Time.format` doc. - - ? Default Time Format - The text must represent a valid date-time and is parsed using the ISO-8601 - extended offset date-time format to add the timezone. The section in square - brackets is not part of the ISO-8601 standard. The format consists of: - - - The ISO offset date time. - - If the zone ID is not available or is a zone offset then the format is - complete. - - An open square bracket '['. - - The zone ID. This is not part of the ISO-8601 standard. Parsing is case - sensitive. - - A close square bracket ']'. - - This method will return a `Time_Error` if the provided time cannot be parsed - using the above format. - - > Example - Parse UTC time. - - import Standard.Base.Data.Time - - example_parse = Time.parse "2020-10-01T04:11:12Z" - - > Example - Parse UTC-04:00 time. - - import Standard.Base.Data.Time - - example_parse = Time.parse "2020-10-01T04:11:12-04:00" - - > Example - Parse UTC-04:00 time specifying New York timezone. - - import Standard.Base.Data.Time - - example_parse = Time.parse "2020-10-01T04:11:12-04:00[America/New_York]" - - > Example - Parse UTC-04:00 time with nanoseconds. - - import Standard.Base.Data.Time - - example_parse = Time.parse "2020-10-01T04:11:12.177528-04:00" - - > Example - Recover from the parse error. - - import Standard.Base.Data.Time - - example_parse = Time.parse "2020-10-01" . catch Time_Error (_->Time.now) - - > Example - Parse "2020-05-06 04:30:20" as Time - - import Standard.Base.Data.Time - - example_parse = Date.parse "2020-05-06 04:30:20" "yyyy-MM-dd HH:mm:ss" - - > Example - Parse "06 of May 2020 at 04:30AM" as Time - - import Standard.Base.Data.Time - - example_parse = - Date.parse "06 of May 2020 at 04:30AM" "dd 'of' MMMM yyyy 'at' hh:mma" -parse : Text -> Text | Nothing -> Locale -> Time ! Time_Error -parse text pattern=Nothing locale=Locale.default = - result = Panic.recover Any <| case pattern of - Nothing -> Time_Utils.parse_time text - Text -> Time_Utils.parse_time_format text pattern locale.java_locale - _ -> Panic.throw (Time_Error "An invalid pattern was provided.") - Time result . map_error <| case _ of - Polyglot_Error err -> Time_Error err.getMessage - x -> x - -type Time - - ## PRIVATE - - A date-time with a timezone in the ISO-8601 calendar system, such as - "2007-12-03T10:15:30+01:00 Europe/Paris". - - Arguments: - - internal_zoned_date_time: The internal repreentation of the time. - - Time is a representation of a date-time with a timezone. This class - stores all date and time fields, to a precision of nanoseconds, and a - timezone, with a zone offset used to handle ambiguous local - date-times. - - For example, the value "2nd October 2007 at 13:45.30.123456789 +02:00 in - the Europe/Paris timezone" can be stored as `Time`. - type Time internal_zoned_date_time - - ## Get the year portion of the time. - - > Example - Get the current year. - - import Standard.Base.Data.Time - - example_year = Time.now.year - year : Integer - year self = self . internal_zoned_date_time . getYear - - ## Get the month portion of the time as a number from 1 to 12. - - > Example - Get the current month. - - import Standard.Base.Data.Time - - example_month = Time.now.month - month : Integer - month self = self . internal_zoned_date_time . getMonthValue - - ## Get the day portion of the time. - - > Example - Get the current day. - - import Standard.Base.Data.Time - - example_day = Time.now.day - day : Integer - day self = self . internal_zoned_date_time . getDayOfMonth - - ## Get the hour portion of the time. - - > Example - Get the current hour. - - import Standard.Base.Data.Time - - example_hour = Time.now.hour - hour : Integer - hour self = self . internal_zoned_date_time . getHour - - ## Get the minute portion of the time. - - > Example - Get the current minute. - - import Standard.Base.Data.Time - - example_minute = Time.now.minute - minute : Integer - minute self = self . internal_zoned_date_time . getMinute - - ## Get the second portion of the time. - - > Example - Get the current second. - - import Standard.Base.Data.Time - - example_second = Time.now.second - second : Integer - second self = self . internal_zoned_date_time . getSecond - - ## Get the nanosecond portion of the time. - - > Example - Get the current nanosecond. - - import Standard.Base.Data.Time - - example_nanosecond = Time.now.nanosecond - nanosecond : Integer - nanosecond self = self . internal_zoned_date_time . getNano - - ## Get the timezone for the time. - - > Example - Get the current timezone. - - import Standard.Base.Data.Time - - example_zone = Time.now.zone - zone : Zone - zone self = Zone.Zone self.internal_zoned_date_time.getZone - - ## Return the number of seconds from the Unix epoch. - - > Example - Get the current number of seconds from the Unix epoch. - - import Standard.Base.Data.Time - - example_epoch = Time.now.to_epoch_seconds - to_epoch_seconds : Integer - to_epoch_seconds self = self . internal_zoned_date_time . toEpochSecond - - ## Return the number of milliseconds from the Unix epoch. - - > Example - Get the current number of milliseconds from the unix epoch. - - import Standard.Base.Data.Time - - example_epoch = Time.now.to_epoch_milliseconds - to_epoch_milliseconds : Integer - to_epoch_milliseconds self = self . internal_zoned_date_time . toInstant . toEpochMilli - - ## Convert this point in time to time of day, discarding the time zone - information. - - > Example - Convert the current time to a time of day. - - import Standard.Base.Data.Time - - example_time_of_day = Time.now.time_of_day - time_of_day : Time_Of_Day - time_of_day self = Time_Of_Day.Time_Of_Day self.internal_zoned_date_time.toLocalTime - - ## ALIAS Time to Date - - Convert this point in time to date, discarding the time of day - information. - - > Example - Convert the current time to a date. - - import Standard.Base.Data.Time - - example_date = Time.now.date - date : Date - date self = self.internal_zoned_date_time.toLocalDate - - ## ALIAS Change Time Zone - - Convert the time instant to the same instant in the provided time zone. - - Arguments: - - zone: The time-zone to convert the time instant into. - - > Example - Convert time instance to -04:00 timezone. - - import Standard.Base.Data.Time - import Standard.Base.Data.Time.Zone - - exaomple_at_zone = Time.new 2020 . at_zone (Zone.new -4) - at_zone : Zone -> Time - at_zone self zone = Time (self.internal_zoned_date_time . withZoneSameInstant zone.internal_zone_id) - - ## Add the specified amount of time to this instant to produce a new instant. - - Arguments: - - amount: The amount of time to add to this instant. - - > Example - Add 15 years and 3 hours to a zoned date time. - - import Standard.Base.Data.Time - import Standard.Base.Data.Time.Duration - - example_plus = Time.new 2020 + 15.years + 3.hours - + : Duration -> Time - + self amount = Time (self . internal_zoned_date_time . plus amount.internal_period . plus amount.internal_duration) - - ## Subtract the specified amount of time from this instant to get a new - instant. - - Arguments: - - amount: The amount of time to subtract from this instant. - - > Example - Subtract 1 year and 9 months from a zoned date time. - - import Standard.Base.Data.Time - import Standard.Base.Data.Time.Duration - - example_minus = Time.new 2020 - 1.year - 9.months - - : Duration -> Time - - self amount = Time (self . internal_zoned_date_time . minus amount.internal_period . minus amount.internal_duration) - - ## Convert this time to text using the default formatter. - - > Example - Convert the current time to text. - - import Standard.Base.Data.Time - - example_to_text = Time.now.to_text - to_text : Text - to_text self = Time_Utils.default_time_formatter . format self.internal_zoned_date_time - - ## Convert the time to JSON. - - > Example - Convert the current time to JSON. - - import Standard.Base.Data.Time - - example_to_json = Time.now.to_json - to_json : Json.Object - to_json self = Json.from_pairs [["type", "Time"], ["year", self.year], ["month", self.month], ["day", self.day], ["hour", self.hour], ["minute", self.minute], ["second", self.second], ["nanosecond", self.nanosecond], ["zone", self.zone]] - - ## Format this time as text using the specified format specifier. - - Arguments: - - pattern: The pattern that specifies how to format the time. - - ? Pattern Syntax - Patterns are based on a simple sequence of letters and symbols. For - example, "d MMM uuuu" will format "2011-12-03" as "3 Dec 2011". - - The list of accepted symbols with examples: - - - 'G', era, "AD; Anno Domini" - - 'u', year, "2004; 04" - - 'y', year-of-era, "2004; 04" - - 'D', day-of-year, "189" - - 'M/L', month-of-year, "7; 07; Jul; July; J" - - 'd', day-of-month, "10" - - 'g', modified-julian-day, "2451334" - - 'Q/q', quarter-of-year, "3; 03; Q3; 3rd quarter" - - 'Y', week-based-year, "1996; 96" - - 'w', week-of-week-based-year, "27" - - 'W', week-of-month, "4" - - 'E', day-of-week, "Tue; Tuesday; T" - - 'e/c', localized day-of-week, "2; 02; Tue; Tuesday; T" - - 'F', day-of-week-in-month, "3" - - 'a', am-pm-of-day, "PM" - - 'h', clock-hour-of-am-pm (1-12), "12" - - 'K', hour-of-am-pm (0-11), "0" - - 'k', clock-hour-of-day (1-24), "24" - - 'H', hour-of-day (0-23), "0" - - 'm', minute-of-hour, "30" - - 's', second-of-minute, "55" - - 'S', fraction-of-second, "978" - - 'A', milli-of-day, "1234" - - 'n', nano-of-second, "987654321" - - 'N', nano-of-day, "1234000000" - - 'V', time-zone ID, "America/Los_Angeles; Z; -08:30" - - 'v', generic time-zone name, "Pacific Time; PT" - - 'z', time-zone name, "Pacific Standard Time; PST" - - 'O', localized zone-offset, "GMT+8; GMT+08:00; UTC-08:00" - - 'X', zone-offset 'Z' for zero, "Z; -08; -0830; -08:30; -083015; -08:30:15" - - 'x', zone-offset, "+0000; -08; -0830; -08:30; -083015; -08:30:15" - - 'Z', zone-offset, "+0000; -0800; -08:00" - - 'p', pad next, "1" - - ''', (single quote) escape for text, "'Text'" - - '''', (double quote) single quote, "'" - - '[', optional section start - - ']', optional section end - - The count of pattern letters determines the format. - - > Example - Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as - "2020-10-08T16:41:13+03:00[Europe/Moscow]". - - import Standard.Base.Data.Time - - example_format = - Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "yyyy-MM-dd'T'HH:mm:ssZZZZ'['VV']'" - - > Example - Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as - "Thursday October 8 4:41 PM". - import Standard.Base.Data.Time - - example_format = - Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "EEEE MMMM d h:mm a" - - > Example - Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as - "Thu Oct 8 (16:41)". - - import Standard.Base.Data.Time - - example_format = - Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "EEE MMM d (HH:mm)" - format : Text -> Text - format self pattern = - DateTimeFormatter.ofPattern pattern . format self.internal_zoned_date_time - - ## Compares `self` to `that` to produce an ordering. - - Arguments: - - that: The other `Time` to compare against. - - > Example - Compare two times for their ordering. - - (Time.new 2000).compare_to (Time.new 2001) - compare_to : Time -> Ordering - compare_to self that = - sign = self.internal_zoned_date_time.compareTo that.internal_zoned_date_time - Ordering.from_sign sign - - ## Compares two Time for equality. - == : Time -> Boolean - == self that = case that of - Time _ -> self.internal_zoned_date_time.equals that.internal_zoned_date_time - _ -> False - type Time_Error ## UNSTABLE - An error produced while working with time. + An error produced while working with time- and date-related methods. Arguments: - error_message: The message for the error. type Time_Error error_message - diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso index 3cc3dc1a53..af68afcd41 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date.enso @@ -1,6 +1,7 @@ from Standard.Base import all import Standard.Base.Data.Time +import Standard.Base.Data.Time.Date_Time import Standard.Base.Data.Time.Duration import Standard.Base.Data.Time.Time_Of_Day import Standard.Base.Data.Time.Zone @@ -193,7 +194,7 @@ type Date containing the first Thursday of the year. Therefore it is important to properly specify the `locale` argument. week_of_year : Locale.Locale -> Integer - week_of_year self locale=Locale.default = Time_Utils.week_of_year self locale.java_locale + week_of_year self locale=Locale.default = Time_Utils.week_of_year_localdate self locale.java_locale ## ALIAS Date to Time @@ -210,8 +211,8 @@ type Date import Standard.Base.Data.Time.Zone example_to_time = Date.new 2020 2 3 . to_time Time_Of_Day.new Zone.utc - to_time : Time_Of_Day -> Zone -> Time - to_time self time_of_day (zone = Zone.system) = Time.Time (Time_Utils.date_with_time self time_of_day.internal_local_time zone.internal_zone_id) + to_time : Time_Of_Day -> Zone -> Date_Time + to_time self time_of_day (zone=Zone.system) = self.to_time_builtin time_of_day zone ## Add the specified amount of time to this instant to get another date. @@ -226,7 +227,7 @@ type Date example_add = Date.new 2020 + 6.months + : Duration -> Date + self amount = if amount.is_time then Error.throw (Time.Time_Error "Date does not support time intervals") else - (Time_Utils.date_adjust self 1 amount.internal_period) . internal_local_date + (Time_Utils.date_adjust self Time_Utils.AdjustOp.PLUS amount.internal_period) . internal_local_date ## Subtract the specified amount of time from this instant to get another date. @@ -243,7 +244,7 @@ type Date example_subtract = Date.new 2020 - 7.days - : Duration -> Date - self amount = if amount.is_time then Error.throw (Time.Time_Error "Date does not support time intervals") else - (Time_Utils.date_adjust self -1 amount.internal_period) . internal_local_date + (Time_Utils.date_adjust self Time_Utils.AdjustOp.MINUS amount.internal_period) . internal_local_date ## A Date to Json conversion. @@ -304,11 +305,11 @@ type Date (Date.new 2000).compare_to (Date.new 2001) compare_to : Date -> Ordering compare_to self that = - sign = Time_Utils.compare_to self that + sign = Time_Utils.compare_to_localdate self that Ordering.from_sign sign ## Compares two Dates for equality. == : Date -> Boolean == self that = - sign = Time_Utils.compare_to self that + sign = Time_Utils.compare_to_localdate self that 0 == sign diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso new file mode 100644 index 0000000000..472661f896 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time.enso @@ -0,0 +1,483 @@ +from Standard.Base import all + +from Standard.Base.Data.Time import Time_Error +import Standard.Base.Data.Time.Duration +import Standard.Base.Data.Time.Time_Of_Day +import Standard.Base.Data.Time.Zone + +polyglot java import java.time.format.DateTimeFormatter +polyglot java import java.time.ZonedDateTime +polyglot java import org.enso.base.Time_Utils + +## ALIAS Current Time + + Obtains the current date-time from the system clock in the system timezone. + + > Example + Get the current time + + import Standard.Base.Data.Time + + example_now = Date_Time.now +now : Date_Time +now = @Builtin_Method "Date_Time.now" + +## Obtains an instance of `Date_Time` from a year, month, day, hour, minute, + second, nanosecond and timezone. + + Arguments: + - month: the month-of-year to represent, from 1 (January) to 12 (December) + - day: the day-of-month to represent, from 1 to 31 and must be valid for the + year and month + - hour: the hour-of-day to represent, from 0 to 23 + - minute: the minute-of-hour to represent, from 0 to 59 + - second: the second-of-minute to represent, from 0 to 59 + - nanosecond: the nano-of-second to represent, from 0 to 999,999,999 + - zone: the timezone + + Returns a `Time_Error` if the provided time cannot be represented. + + > Example + Create a new zoned date time at Unix epoch. + + import Standard.Base.Data.Time.Date_Time + import Standard.Base.Data.Time.Zone + + example_new = Date_Time.new 1970 (zone = Zone.utc) + + > Example + Get the 5 August 1986 at midnight. + + import Standard.Base.Data.Time.Date_Time + import Standard.Base.Data.Time.Zone + + example_new = Date_Time.new 1986 8 5 +new : Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Integer -> Zone -> Date_Time ! Time_Error +new year (month = 1) (day = 1) (hour = 0) (minute = 0) (second = 0) (nanosecond = 0) (zone = Zone.system) = + Panic.catch_java Any (Date_Time.new_builtin year month day hour minute second nanosecond zone) java_exception-> + Error.throw (Time_Error java_exception.getMessage) + +## ALIAS Time from Text + + Obtains an instance of `Time` from a text such as + "2007-12-03T10:15:30+01:00 Europe/Paris". + + Arguments: + - text: The text representing the time to be parsed. + - pattern: The pattern to use for parsing the input text. + - locale: The locale in which the pattern should be interpreted. + + ? Pattern Syntax + For the list of accepted symbols in pattern refer to `Time.format` doc. + + ? Default Date_Time Format + The text must represent a valid date-time and is parsed using the ISO-8601 + extended offset date-time format to add the timezone. The section in square + brackets is not part of the ISO-8601 standard. The format consists of: + + - The ISO offset date time. + - If the zone ID is not available or is a zone offset then the format is + complete. + - An open square bracket '['. + - The zone ID. This is not part of the ISO-8601 standard. Parsing is case + sensitive. + - A close square bracket ']'. + + This method will return a `Time_Error` if the provided time cannot be parsed + using the above format. + + > Example + Parse UTC time. + + import Standard.Base.Data.Time + + example_parse = Date_Time.parse "2020-10-01T04:11:12Z" + + > Example + Parse UTC-04:00 time. + + import Standard.Base.Data.Time + + example_parse = Date_Time.parse "2020-10-01T04:11:12-04:00" + + > Example + Parse UTC-04:00 time specifying New York timezone. + + import Standard.Base.Data.Time + + example_parse = Date_Time.parse "2020-10-01T04:11:12-04:00[America/New_York]" + + > Example + Parse UTC-04:00 time with nanoseconds. + + import Standard.Base.Data.Time + + example_parse = Date_Time.parse "2020-10-01T04:11:12.177528-04:00" + + > Example + Recover from the parse error. + + import Standard.Base.Data.Time + + example_parse = Date_Time.parse "2020-10-01" . catch Time_Error (_->Date_Time.now) + + > Example + Parse "2020-05-06 04:30:20" as Date_Time + + import Standard.Base.Data.Time + + example_parse = Date.parse "2020-05-06 04:30:20" "yyyy-MM-dd HH:mm:ss" + + > Example + Parse "06 of May 2020 at 04:30AM" as Date_Tme + + import Standard.Base.Data.Time + + example_parse = + Date_Time.parse "06 of May 2020 at 04:30AM" "dd 'of' MMMM yyyy 'at' hh:mma" +parse : Text -> Text | Nothing -> Locale -> Date_Time ! Time_Error +parse text pattern=Nothing locale=Locale.default = + Panic.catch_java Any handler=(java_exception -> Error.throw (Time_Error java_exception.getMessage)) <| + case pattern of + Nothing -> Date_Time.parse_builtin text + Text -> Time_Utils.parse_datetime_format text pattern locale.java_locale + +type Date_Time + + ## PRIVATE + + A date-time with a timezone in the ISO-8601 calendar system, such as + "2007-12-03T10:15:30+01:00 Europe/Paris". + + Time is a representation of a date-time with a timezone. This class + stores all date and time fields, to a precision of nanoseconds, and a + timezone, with a zone offset used to handle ambiguous local + date-times. + + For example, the value "2nd October 2007 at 13:45.30.123456789 +02:00 in + the Europe/Paris timezone" can be stored as `Time`. + @Builtin_Type + type Date_Time + + ## Get the year portion of the time. + + > Example + Get the current year. + + import Standard.Base.Data.Time + + example_year = Date_Time.now.year + year : Integer + year self = @Builtin_Method "Date_Time.year" + + ## Get the month portion of the time as a number from 1 to 12. + + > Example + Get the current month. + + import Standard.Base.Data.Time + + example_month = Date_Time.now.month + month : Integer + month self = @Builtin_Method "Date_Time.month" + + ## Get the day portion of the time. + + > Example + Get the current day. + + import Standard.Base.Data.Time + + example_day = Date_Time.now.day + day : Integer + day self = @Builtin_Method "Date_Time.day" + + ## Get the hour portion of the time. + + > Example + Get the current hour. + + import Standard.Base.Data.Time + + example_hour = Date_Time.now.hour + hour : Integer + hour self = @Builtin_Method "Date_Time.hour" + + ## Get the minute portion of the time. + + > Example + Get the current minute. + + import Standard.Base.Data.Time + + example_minute = Date_Time.now.minute + minute : Integer + minute self = @Builtin_Method "Date_Time.minute" + + ## Get the second portion of the time. + + > Example + Get the current second. + + import Standard.Base.Data.Time + + example_second = Date_Time.now.second + second : Integer + second self = @Builtin_Method "Date_Time.second" + + ## Get the nanosecond portion of the time. + + > Example + Get the current nanosecond. + + import Standard.Base.Data.Time + + example_nanosecond = Date_Time.now.nanosecond + nanosecond : Integer + nanosecond self = @Builtin_Method "Date_Time.nanosecond" + + ## Get the timezone for the time. + + > Example + Get the current timezone. + + import Standard.Base.Data.Time + + example_zone = Date_Time.now.zone + zone : Zone + zone self = @Builtin_Method "Date_Time.zone" + + ## Return the number of seconds from the Unix epoch. + + > Example + Get the current number of seconds from the Unix epoch. + + import Standard.Base.Data.Time + + example_epoch = Date_Time.now.to_epoch_seconds + to_epoch_seconds : Integer + to_epoch_seconds self = @Builtin_Method "Date_Time.to_epoch_seconds" + + ## Return the number of milliseconds from the Unix epoch. + + > Example + Get the current number of milliseconds from the unix epoch. + + import Standard.Base.Data.Time + + example_epoch = Date_Time.now.to_epoch_milliseconds + to_epoch_milliseconds : Integer + to_epoch_milliseconds self = @Builtin_Method "Date_Time.to_epoch_milliseconds" + + ## Convert this point in time to time of day, discarding the time zone + information. + + > Example + Convert the current time to a time of day. + + import Standard.Base.Data.Time + + example_time_of_day = Date_Time.now.time_of_day + time_of_day : Time_Of_Day + time_of_day self = self.to_localtime_builtin + + ## Returns the number of week of year this date falls into. + + Arguments: + - locale: the locale used to define the notion of weeks of year. + + ! Locale Dependency + Note that this operation is locale-specific. It varies both by the + local definition of the first day of week and the definition of the + first week of year. For example, in the US, the first day of the week + is Sunday and week 1 is the week containing January 1. In the UK on the + other hand, the first day of the week is Monday, and week 1 is the week + containing the first Thursday of the year. Therefore it is important to + properly specify the `locale` argument. + week_of_year : Locale.Locale -> Integer + week_of_year self locale=Locale.default = Time_Utils.week_of_year_zoneddatetime self locale.java_locale + + ## ALIAS Time to Date + + Convert this point in time to date, discarding the time of day + information. + + > Example + Convert the current time to a date. + + import Standard.Base.Data.Time + + example_date = Date_Time.now.date + date : Date + date self = self.to_localdate_builtin + + ## ALIAS Change Time Zone + + Convert the time instant to the same instant in the provided time zone. + + Arguments: + - zone: The timezone to convert the time instant into. + + > Example + Convert time instance to -04:00 timezone. + + import Standard.Base.Data.Time.Date_Time + import Standard.Base.Data.Time.Zone + + example_at_zone = Date_Time.new 2020 . at_zone (Zone.new -4) + at_zone : Zone -> Date_Time + at_zone self zone = @Builtin_Method "Date_Time.at_zone" + + ## Add the specified amount of time to this instant to produce a new instant. + + Arguments: + - amount: The amount of time to add to this instant. + + > Example + Add 15 years and 3 hours to a zoned date time. + + import Standard.Base.Data.Time.Date_Time + import Standard.Base.Data.Time.Duration + + example_plus = Date_Time.new 2020 + 15.years + 3.hours + + : Duration -> Date_Time + + self amount = + Time_Utils.datetime_adjust self Time_Utils.AdjustOp.PLUS amount.internal_period amount.internal_duration + + ## Subtract the specified amount of time from this instant to get a new + instant. + + Arguments: + - amount: The amount of time to subtract from this instant. + + > Example + Subtract 1 year and 9 months from a zoned date time. + + import Standard.Base.Data.Time.Date_Time + import Standard.Base.Data.Time.Duration + + example_minus = Date_Time.new 2020 - 1.year - 9.months + - : Duration -> Date_Time + - self amount = + Time_Utils.datetime_adjust self Time_Utils.AdjustOp.MINUS amount.internal_period amount.internal_duration + + ## Convert this time to text using the default formatter. + + > Example + Convert the current time to text. + + import Standard.Base.Data.Time + + example_to_text = Date_Time.now.to_text + to_text : Text + to_text self = @Builtin_Method "Date_Time.to_text" + + ## Convert the time to JSON. + + > Example + Convert the current time to JSON. + + import Standard.Base.Data.Time + + example_to_json = Date_Time.now.to_json + to_json : Json.Object + to_json self = Json.from_pairs [["type", "Time"], ["year", self.year], ["month", self.month], ["day", self.day], ["hour", self.hour], ["minute", self.minute], ["second", self.second], ["nanosecond", self.nanosecond], ["zone", self.zone]] + + ## Format this time as text using the specified format specifier. + + Arguments: + - pattern: The pattern that specifies how to format the time. + + ? Pattern Syntax + Patterns are based on a simple sequence of letters and symbols. For + example, "d MMM uuuu" will format "2011-12-03" as "3 Dec 2011". + + The list of accepted symbols with examples: + + - 'G', era, "AD; Anno Domini" + - 'u', year, "2004; 04" + - 'y', year-of-era, "2004; 04" + - 'D', day-of-year, "189" + - 'M/L', month-of-year, "7; 07; Jul; July; J" + - 'd', day-of-month, "10" + - 'g', modified-julian-day, "2451334" + - 'Q/q', quarter-of-year, "3; 03; Q3; 3rd quarter" + - 'Y', week-based-year, "1996; 96" + - 'w', week-of-week-based-year, "27" + - 'W', week-of-month, "4" + - 'E', day-of-week, "Tue; Tuesday; T" + - 'e/c', localized day-of-week, "2; 02; Tue; Tuesday; T" + - 'F', day-of-week-in-month, "3" + - 'a', am-pm-of-day, "PM" + - 'h', clock-hour-of-am-pm (1-12), "12" + - 'K', hour-of-am-pm (0-11), "0" + - 'k', clock-hour-of-day (1-24), "24" + - 'H', hour-of-day (0-23), "0" + - 'm', minute-of-hour, "30" + - 's', second-of-minute, "55" + - 'S', fraction-of-second, "978" + - 'A', milli-of-day, "1234" + - 'n', nano-of-second, "987654321" + - 'N', nano-of-day, "1234000000" + - 'V', timezone ID, "America/Los_Angeles; Z; -08:30" + - 'v', generic timezone name, "Pacific Time; PT" + - 'z', timezone name, "Pacific Standard Time; PST" + - 'O', localized zone-offset, "GMT+8; GMT+08:00; UTC-08:00" + - 'X', zone-offset 'Z' for zero, "Z; -08; -0830; -08:30; -083015; -08:30:15" + - 'x', zone-offset, "+0000; -08; -0830; -08:30; -083015; -08:30:15" + - 'Z', zone-offset, "+0000; -0800; -08:00" + - 'p', pad next, "1" + - ''', (single quote) escape for text, "'Text'" + - '''', (double quote) single quote, "'" + - '[', optional section start + - ']', optional section end + + The count of pattern letters determines the format. + + > Example + Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as + "2020-10-08T16:41:13+03:00[Europe/Moscow]". + + import Standard.Base.Data.Time + + example_format = + Date_Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "yyyy-MM-dd'T'HH:mm:ssZZZZ'['VV']'" + + > Example + Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as + "Thursday October 8 4:41 PM". + import Standard.Base.Data.Time + + example_format = + Date_Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "EEEE MMMM d h:mm a" + + > Example + Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as + "Thu Oct 8 (16:41)". + + import Standard.Base.Data.Time + + example_format = + Date_Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "EEE MMM d (HH:mm)" + format : Text -> Text + format self pattern = @Builtin_Method "Date_Time.format" + + ## Compares `self` to `that` to produce an ordering. + + Arguments: + - that: The other `Date_Time` to compare against. + + > Example + Compare two times for their ordering. + + (Date_Time.new 2000).compare_to (Date_Time.new 2001) + compare_to : Time -> Ordering + compare_to self that = + sign = Time_Utils.compare_to_zoneddatetime self that + Ordering.from_sign sign + + ## Compares two Date_Time for equality. + == : Date_Time -> Boolean + == self that = + sign = Time_Utils.compare_to_zoneddatetime self that + 0 == sign diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Duration.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Duration.enso index afce7a3b40..d6066d02f6 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Duration.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Duration.enso @@ -5,26 +5,26 @@ import Standard.Base.System polyglot java import java.time.Duration as Java_Duration polyglot java import java.time.Period as Java_Period +polyglot java import org.enso.base.Time_Utils ## Create an interval representing the duration between two points in time. Arguments: - start_inclusive: The start time of the duration. - end_inclusive: The end time of the duration. + - timezone_aware: Should the creation of the interval be timezone-aware. > Example An hour interval between two points in time. import Standard.Base.Data.Time.Duration - import Standard.Base.Data.Time + import Standard.Base.Data.Time.Date_Time - example_between = Duration.between Time.now (Time.new 2010 10 20) -between : Time -> Time -> Duration -between start_inclusive end_exclusive = + example_between = Duration.between Date_Time.now (Date_Time.new 2010 10 20) +between : Date_Time -> Date_Time -> Duration +between start_inclusive end_exclusive timezone_aware=True = period = Java_Period.ofDays 0 . normalized - start = start_inclusive.internal_zoned_date_time - end = end_exclusive.internal_zoned_date_time - duration = Java_Duration.between start end + duration = Time_Utils.duration_between start_inclusive end_exclusive timezone_aware Duration period duration diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso index 5899e75ed8..a96e6a8cb8 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Time_Of_Day.enso @@ -18,7 +18,7 @@ polyglot java import org.enso.base.Time_Utils example_now = Time_Of_Day.now now : Time_Of_Day -now = Time_Of_Day LocalTime.now +now = @Builtin_Method "Time_Of_Day.now" ## Obtains an instance of `Time_Of_Day` from an hour, minute, second and nanosecond. @@ -46,7 +46,7 @@ now = Time_Of_Day LocalTime.now example_epoch = Time_Of_Day.new hour=9 minute=30 new : Integer -> Integer -> Integer -> Integer -> Time_Of_Day ! Time.Time_Error new (hour = 0) (minute = 0) (second = 0) (nanosecond = 0) = - Panic.catch_java Any (Time_Of_Day (LocalTime.of hour minute second nanosecond)) java_exception-> + Panic.catch_java Any (Time_Of_Day.new_builtin hour minute second nanosecond) java_exception-> Error.throw (Time.Time_Error java_exception.getMessage) ## Obtains an instance of `Time_Of_Day` from a text such as "10:15". @@ -112,15 +112,10 @@ new (hour = 0) (minute = 0) (second = 0) (nanosecond = 0) = example_parse = Time_Of_Day.parse "4:30AM" "h:mma" parse : Text -> Text | Nothing -> Locale -> Time_Of_Day ! Time.Time_Error parse text pattern=Nothing locale=Locale.default = - result = Panic.recover Any <| case pattern of - Nothing -> LocalTime.parse text - Text -> - formatter = DateTimeFormatter.ofPattern pattern - LocalTime.parse text (formatter.withLocale locale.java_locale) - _ -> Panic.throw (Time.Time_Error "An invalid pattern was provided.") - Time_Of_Day result . map_error <| case _ of - Polyglot_Error err -> Time.Time_Error err.getMessage - x -> x + Panic.catch_java Any handler=(java_exception -> Error.throw (Time.Time_Error java_exception.getMessage)) <| + case pattern of + Nothing -> Time_Of_Day.parse_builtin text + Text -> Time_Utils.parse_time text pattern locale.java_locale type Time_Of_Day @@ -129,12 +124,10 @@ type Time_Of_Day This type is a date-time object that represents a time, often viewed as hour-minute-second. - Arguments: - - internal_local_time: The internal representation of the time of day. - Time is represented to nanosecond precision. For example, the value "13:45.30.123456789" can be stored in a `Time_Of_Day`. - type Time_Of_Day internal_local_time + @Builtin_Type + type Time_Of_Day ## Get the hour portion of the time of day. @@ -145,7 +138,7 @@ type Time_Of_Day example_hour = Time_Of_Day.now.hour hour : Integer - hour self = self . internal_local_time . getHour + hour self = @Builtin_Method "Time_Of_Day.hour" ## Get the minute portion of the time of day. @@ -156,7 +149,7 @@ type Time_Of_Day example_minute = Time_Of_Day.now.minute minute : Integer - minute self = self . internal_local_time . getMinute + minute self = @Builtin_Method "Time_Of_Day.minute" ## Get the second portion of the time of day. @@ -167,7 +160,7 @@ type Time_Of_Day example_second = Time_Of_Day.now.second second : Integer - second self = self . internal_local_time . getSecond + second self = @Builtin_Method "Time_Of_Day.second" ## Get the nanosecond portion of the time of day. @@ -178,7 +171,7 @@ type Time_Of_Day example_nanosecond = Time_Of_Day.now.nanosecond nanosecond : Integer - nanosecond self = self . internal_local_time . getNano + nanosecond self = @Builtin_Method "Time_Of_Day.nanosecond" ## Extracts the time as the number of seconds, from 0 to 24 * 60 * 60 - 1. @@ -189,7 +182,7 @@ type Time_Of_Day example_to_seconds = Time_Of_Day.now.to_seconds to_seconds : Integer - to_seconds self = self . internal_local_time . toSecondOfDay + to_seconds self = @Builtin_Method "Time_Of_Day.to_seconds" ## Combine this time of day with a date to create a point in time. @@ -204,8 +197,7 @@ type Time_Of_Day example_to_time = Time_Of_Day.new 12 30 . to_time (Date.new 2020) to_time : Date -> Zone -> Time - to_time self date (zone = Zone.system) = - Time.Time (self . internal_local_time . atDate date . atZone zone.internal_zone_id) + to_time self date (zone=Zone.system) = self.to_time_builtin date zone ## Add the specified amount of time to this instant to get a new instant. @@ -220,7 +212,7 @@ type Time_Of_Day example_plus = Time_Of_Day.new + 3.seconds + : Duration -> Time_Of_Day + self amount = if amount.is_date then Error.throw (Time.Time_Error "Time_Of_Day does not support date intervals") else - Time_Of_Day (self . internal_local_time . plus amount.internal_duration) + Time_Utils.time_adjust self Time_Utils.AdjustOp.PLUS amount.internal_duration ## Subtract the specified amount of time from this instant to get a new instant. @@ -237,7 +229,7 @@ type Time_Of_Day example_minus = Time_Of_Day.now - 12.hours - : Duration -> Time_Of_Day - self amount = if amount.is_date then Error.throw (Time.Time_Error "Time_Of_Day does not support date intervals") else - Time_Of_Day (self . internal_local_time . minus amount.internal_duration) + Time_Utils.time_adjust self Time_Utils.AdjustOp.MINUS amount.internal_duration ## Format this time of day as text using the default formatter. @@ -248,7 +240,7 @@ type Time_Of_Day example_to_text = Time_Of_Day.now.to_text to_text : Text - to_text self = Time_Utils.default_time_of_day_formatter . format self.internal_local_time + to_text self = @Builtin_Method "Time_Of_Day.to_text" ## A Time_Of_Day to Json conversion. @@ -308,8 +300,7 @@ type Time_Of_Day example_format = Time_Of_Day.new 16 21 10 . format "'hour:'h" format : Text -> Text - format self pattern = - DateTimeFormatter.ofPattern pattern . format self.internal_local_time + format self pattern = @Builtin_Method "Time_Of_Day.format" ## Compares `self` to `that` to produce an ordering. @@ -327,11 +318,11 @@ type Time_Of_Day time_1.compare_to time_2 compare_to : Time_Of_Day -> Ordering compare_to self that = - sign = self.internal_local_time.compareTo that.internal_local_time + sign = Time_Utils.compare_to_localtime self that Ordering.from_sign sign ## Compares two Time_Of_Day for equality. == : Date -> Boolean - == self that = case that of - Time_Of_Day _ -> self.internal_local_time.equals that.internal_local_time - _ -> False + == self that = + sign = Time_Utils.compare_to_localtime self that + 0 == sign diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Zone.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Zone.enso index 6d8104c5ac..44d070819b 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Zone.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Zone.enso @@ -1,7 +1,10 @@ from Standard.Base import all +import Standard.Base.Data.Time + polyglot java import java.time.ZoneId polyglot java import java.time.ZoneOffset +polyglot java import org.enso.base.Time_Utils ## The system default timezone. @@ -12,7 +15,7 @@ polyglot java import java.time.ZoneOffset example_system = Zone.system system : Zone -system = Zone ZoneId.systemDefault +system = @Builtin_Method "Zone.system" ## ALIAS Current Time Zone @@ -58,7 +61,7 @@ utc = parse "UTC" example_new = Zone.new 1 1 50 new : Integer -> Integer -> Integer -> Zone new (hours = 0) (minutes = 0) (seconds = 0) = - Zone (ZoneOffset.ofHoursMinutesSeconds hours minutes seconds . normalized) + Zone.new_builtin hours minutes seconds ## ALIAS Time Zone from Text @@ -95,7 +98,9 @@ new (hours = 0) (minutes = 0) (seconds = 0) = example_parse = Zone.parse "+03:02:01" parse : Text -> Zone -parse text = Zone (ZoneId.of text) +parse text = + Panic.catch_java Any handler=(java_exception -> Error.throw (Time.Time_Error java_exception.getMessage)) <| + Zone.parse_builtin text type Zone @@ -109,7 +114,8 @@ type Zone A time zone can be eiter offset-based like "-06:00" or id-based like "Europe/Paris". - type Zone internal_zone_id + @Builtin_Type + type Zone ## Get the unique timezone ID. @@ -120,7 +126,7 @@ type Zone example_zone_id = Zone.system.zone_id zone_id : Text - zone_id self = self.internal_zone_id . getId + zone_id self = @Builtin_Method "Zone.zone_id" ## Convert the time zone to JSON. @@ -132,3 +138,7 @@ type Zone example_to_json = Zone.system.to_json to_json : Json.Object to_json self = Json.from_pairs [["type", "Zone"], ["id", self.zone_id]] + + ## Compares two Zones for equality. + == : Zone -> Boolean + == self that = Time_Utils.equals_zone self that diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso index c1895a95e5..eafcf0d0f0 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File.enso @@ -335,8 +335,7 @@ type File example_exists = Examples.csv.creation_time creation_time : Time ! File_Error creation_time self = - handle_java_exceptions self <| - Time (self.creation_time_builtin) + handle_java_exceptions self <| self.creation_time_builtin ## PRIVATE @@ -356,8 +355,7 @@ type File example_exists = Examples.csv.last_modified_time last_modified_time : Time ! File_Error last_modified_time self = - handle_java_exceptions self <| - Time (self.last_modified_time_builtin) + handle_java_exceptions self <| self.last_modified_time_builtin ## PRIVATE diff --git a/distribution/lib/Standard/Examples/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Examples/0.0.0-dev/src/Main.enso index a98d66b8fd..aacedf3250 100644 --- a/distribution/lib/Standard/Examples/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Examples/0.0.0-dev/src/Main.enso @@ -1,6 +1,6 @@ from Standard.Base import all -import Standard.Base.Data.Time +import Standard.Base.Data.Time.Date_Time import Standard.Base.Network.Http import Standard.Base.System.Platform import Standard.Base.Data.Time.Duration @@ -70,7 +70,7 @@ scratch_file = ## An example duration for experimenting with duration APIs. duration : Duration -duration = Duration.between (Time.new 2020 10 20) Time.now +duration = Duration.between (Date_Time.new 2020 10 20) Date_Time.now ## An example amount of JSON as text. json_text : Text diff --git a/distribution/lib/Standard/Searcher/0.0.0-dev/src/Data_Science/Date_And_Time.enso b/distribution/lib/Standard/Searcher/0.0.0-dev/src/Data_Science/Date_And_Time.enso index c16e9a46a2..a18543ff55 100644 --- a/distribution/lib/Standard/Searcher/0.0.0-dev/src/Data_Science/Date_And_Time.enso +++ b/distribution/lib/Standard/Searcher/0.0.0-dev/src/Data_Science/Date_And_Time.enso @@ -9,7 +9,7 @@ import Standard.Base.Data.Time - example_now = Time.now + example_now = Date_Time.now > Example Parse UTC time. @@ -21,14 +21,14 @@ > Example Convert time instance to -04:00 timezone. - import Standard.Base.Data.Time + import Standard.Base.Data.Time.Date_Time import Standard.Base.Data.Time.Zone - exaomple_at_zone = Time.new 2020 . at_zone (Zone.new -4) + exaomple_at_zone = Date_Time.new 2020 . at_zone (Zone.new -4) > Example Convert the current time to a date. import Standard.Base.Data.Time - example_date = Time.now.date + example_date = Date_Time.now.date diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso index 124bd953ee..d0366edabf 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso @@ -1,6 +1,6 @@ from Standard.Base import all -from Standard.Base.Data.Time import Time +from Standard.Base.Data.Time import Date_Time from Standard.Base.Data.Time.Time_Of_Day import Time_Of_Day import Standard.Base.Error.Common as Errors @@ -67,7 +67,7 @@ type Data_Formatter By default, a warning is issued, but the operation proceeds. If set to `Report_Error`, the operation fails with a dataflow error. If set to `Ignore`, the operation proceeds without errors or warnings. - parse : Text -> (Auto|Integer|Number|Date|Time|DateTime|Boolean) -> Problem_Behavior -> Any + parse : Text -> (Auto|Integer|Number|Date|Date_Time|Time_Of_Day|Boolean) -> Problem_Behavior -> Any parse self text datatype=Auto on_problems=Problem_Behavior.Report_Warning = parser = case datatype of Auto -> self.make_auto_parser @@ -171,14 +171,13 @@ type Data_Formatter ## PRIVATE make_datatype_parser self datatype = case datatype of - Integer -> self.make_integer_parser - Decimal -> self.make_decimal_parser - Boolean -> self.make_boolean_parser - _ -> - if datatype == Date then self.make_date_parser else - if datatype == Time then self.make_datetime_parser else - if datatype == Time_Of_Day then self.make_time_parser else - Error.throw (Illegal_Argument_Error "Unsupported datatype: "+datatype.to_text) + Integer -> self.make_integer_parser + Decimal -> self.make_decimal_parser + Boolean -> self.make_boolean_parser + Date -> self.make_date_parser + Date_Time -> self.make_datetime_parser + Time_Of_Day -> self.make_time_parser + _ -> Error.throw (Illegal_Argument_Error "Unsupported datatype: "+datatype.to_text) ## PRIVATE get_specific_type_parsers self = diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Test/0.0.0-dev/src/Main.enso index 3357f8d017..39b76ca0af 100644 --- a/distribution/lib/Standard/Test/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Test/0.0.0-dev/src/Main.enso @@ -1,5 +1,5 @@ from Standard.Base import all -import Standard.Base.Data.Time +import Standard.Base.Data.Time.Date_Time import Standard.Base.Runtime.State import Standard.Base.System @@ -769,7 +769,7 @@ wrap_junit_testsuites config builder ~action = report_pending_group : Text -> Text -> Suite_Config -> (StringBuilder|Nothing) -> Nothing report_pending_group name reason config builder = if config.should_output_junit then - builder.append (' \n') + builder.append (' \n') builder.append (' \n') builder.append ' \n' @@ -782,7 +782,7 @@ report_pending_group name reason config builder = Spec.print_report : Suite_Config -> (StringBuilder|Nothing) -> Nothing Spec.print_report self config builder = if config.should_output_junit then - builder.append (' Accepts: + * + *
    + *
  • Local date time, such as '2011-12-03T10:15:30' adding system dafault timezone. + *
  • Offset date time, such as '2011-12-03T10:15:30+01:00' parsing offset as a timezone. + *
  • Zoned date time, such as '2011-12-03T10:15:30+01:00[Europe/Paris]' with optional region + * id in square brackets. + *
+ * + * @param text the string to parse. + * @return parsed ZonedDateTime instance wrapped in EnsoDateTime. + */ + @Builtin.Method( + name = "parse_builtin", + description = "Constructs a new DateTime from text with optional pattern") + @Builtin.Specialize + @Builtin.WrapException( + from = DateTimeParseException.class, + to = PolyglotError.class, + propagate = true) + public static EnsoDateTime parse(String text) { + TemporalAccessor time = TIME_FORMAT.parseBest(text, ZonedDateTime::from, LocalDateTime::from); + if (time instanceof ZonedDateTime) { + return new EnsoDateTime((ZonedDateTime) time); + } else if (time instanceof LocalDateTime) { + return new EnsoDateTime(((LocalDateTime) time).atZone(ZoneId.systemDefault())); + } + throw new DateTimeException("Text '" + text + "' could not be parsed as Time."); + } + + @Builtin.Method( + name = "new_builtin", + description = "Constructs a new Date from a year, month, and day") + @Builtin.WrapException(from = DateTimeException.class, to = PolyglotError.class, propagate = true) + public static EnsoDateTime create( + long year, + long month, + long day, + long hour, + long minute, + long second, + long nanosecond, + EnsoZone zone) { + return new EnsoDateTime( + ZonedDateTime.of( + Math.toIntExact(year), + Math.toIntExact(month), + Math.toIntExact(day), + Math.toIntExact(hour), + Math.toIntExact(minute), + Math.toIntExact(second), + Math.toIntExact(nanosecond), + zone.asTimeZone())); + } + + @Builtin.Method(description = "Gets the year") + @CompilerDirectives.TruffleBoundary + public long year() { + return dateTime.getYear(); + } + + @Builtin.Method(description = "Gets the month") + @CompilerDirectives.TruffleBoundary + public long month() { + return dateTime.getMonthValue(); + } + + @Builtin.Method(description = "Gets the day") + @CompilerDirectives.TruffleBoundary + public long day() { + return dateTime.getDayOfMonth(); + } + + @Builtin.Method(description = "Gets the hour") + @CompilerDirectives.TruffleBoundary + public long hour() { + return dateTime.getHour(); + } + + @Builtin.Method(description = "Gets the minute") + @CompilerDirectives.TruffleBoundary + public long minute() { + return dateTime.getMinute(); + } + + @Builtin.Method(description = "Gets the second") + @CompilerDirectives.TruffleBoundary + public long second() { + return dateTime.getSecond(); + } + + @Builtin.Method(description = "Gets the nanosecond") + @CompilerDirectives.TruffleBoundary + public long nanosecond() { + return dateTime.getNano(); + } + + @Builtin.Method(name = "zone", description = "Gets the zone") + public EnsoZone zone() { + return new EnsoZone(dateTime.getZone()); + } + + @Builtin.Method(description = "Return the number of seconds from the Unix epoch.") + @CompilerDirectives.TruffleBoundary + public long toEpochSeconds() { + return dateTime.toEpochSecond(); + } + + @Builtin.Method(description = "Return the number of milliseconds from the Unix epoch.") + @CompilerDirectives.TruffleBoundary + public long toEpochMilliseconds() { + return dateTime.toInstant().toEpochMilli(); + } + + @Builtin.Method( + name = "to_localtime_builtin", + description = "Return the localtime of this date time value.") + public EnsoTimeOfDay toLocalTime() { + return new EnsoTimeOfDay(dateTime.toLocalTime()); + } + + @Builtin.Method( + name = "to_localdate_builtin", + description = "Return the localdate of this date time value.") + public EnsoDate toLocalDate() { + return new EnsoDate(dateTime.toLocalDate()); + } + + @Builtin.Method(description = "Return this datetime in the provided time zone.") + public EnsoDateTime atZone(EnsoZone zone) { + return new EnsoDateTime(dateTime.withZoneSameInstant(zone.asTimeZone())); + } + + @Builtin.Method( + name = "to_time_builtin", + description = "Combine this day with time to create a point in time.") + public EnsoDateTime toTime(EnsoTimeOfDay timeOfDay, EnsoZone zone) { + return new EnsoDateTime( + dateTime.toLocalDate().atTime(timeOfDay.asTime()).atZone(zone.asTimeZone())); + } + + @Builtin.Method(description = "Return this datetime to the datetime in the provided time zone.") + public Text toText() { + return Text.create(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(dateTime)); + } + + @Builtin.Method(description = "Return this datetime to the datetime in the provided time zone.") + @Builtin.Specialize + public Text format(String pattern) { + return Text.create(DateTimeFormatter.ofPattern(pattern).format(dateTime)); + } + + @ExportMessage + boolean isDate() { + return true; + } + + @ExportMessage + LocalDate asDate() { + return dateTime.toLocalDate(); + } + + @ExportMessage + boolean isTime() { + return true; + } + + @ExportMessage + LocalTime asTime() { + return dateTime.toLocalTime(); + } + + @ExportMessage + boolean isTimeZone() { + return true; + } + + @ExportMessage + ZoneId asTimeZone() { + return dateTime.getZone(); + } + + @ExportMessage + boolean hasFunctionalDispatch() { + return true; + } + + @ExportMessage + static class GetFunctionalDispatch { + @CompilerDirectives.TruffleBoundary + static Function doResolve(InteropLibrary my, UnresolvedSymbol symbol) { + Context context = Context.get(my); + return symbol.resolveFor(context.getBuiltins().dateTime(), context.getBuiltins().any()); + } + + @Specialization( + guards = {"cachedSymbol == symbol", "function != null"}, + limit = "3") + static Function resolveCached( + EnsoDateTime self, + UnresolvedSymbol symbol, + @Cached("symbol") UnresolvedSymbol cachedSymbol, + @CachedLibrary("self") InteropLibrary mySelf, + @Cached("doResolve(mySelf, cachedSymbol)") Function function) { + return function; + } + + @Specialization(replaces = "resolveCached") + static Function resolve( + EnsoDateTime self, UnresolvedSymbol symbol, @CachedLibrary("self") InteropLibrary mySelf) + throws MethodDispatchLibrary.NoSuchMethodException { + Function function = doResolve(mySelf, symbol); + if (function == null) { + throw new MethodDispatchLibrary.NoSuchMethodException(); + } + return function; + } + } + + @ExportMessage + @CompilerDirectives.TruffleBoundary + public final Object toDisplayString(boolean allowSideEffects) { + return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(dateTime); + } + + private static final DateTimeFormatter TIME_FORMAT = + new DateTimeFormatterBuilder() + .parseCaseInsensitive() + .append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + .parseLenient() + .optionalStart() + .appendZoneOrOffsetId() + .optionalEnd() + .parseStrict() + .optionalStart() + .appendLiteral('[') + .parseCaseSensitive() + .appendZoneRegionId() + .appendLiteral(']') + .optionalEnd() + .toFormatter(); +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java index 86f061e356..b452eedf18 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoFile.java @@ -91,15 +91,17 @@ public class EnsoFile implements TruffleObject { @Builtin.Method(name = "creation_time_builtin") @Builtin.WrapException(from = IOException.class, to = PolyglotError.class, propagate = true) @Builtin.ReturningGuestObject - public ZonedDateTime getCreationTime() throws IOException { - return ZonedDateTime.ofInstant(truffleFile.getCreationTime().toInstant(), ZoneOffset.UTC); + public EnsoDateTime getCreationTime() throws IOException { + return new EnsoDateTime( + ZonedDateTime.ofInstant(truffleFile.getCreationTime().toInstant(), ZoneOffset.UTC)); } @Builtin.Method(name = "last_modified_time_builtin") @Builtin.WrapException(from = IOException.class, to = PolyglotError.class, propagate = true) @Builtin.ReturningGuestObject - public ZonedDateTime getLastModifiedTime() throws IOException { - return ZonedDateTime.ofInstant(truffleFile.getLastModifiedTime().toInstant(), ZoneOffset.UTC); + public EnsoDateTime getLastModifiedTime() throws IOException { + return new EnsoDateTime( + ZonedDateTime.ofInstant(truffleFile.getLastModifiedTime().toInstant(), ZoneOffset.UTC)); } @Builtin.Method(name = "posix_permissions_builtin") diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeOfDay.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeOfDay.java new file mode 100644 index 0000000000..982a24dada --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoTimeOfDay.java @@ -0,0 +1,163 @@ +package org.enso.interpreter.runtime.data; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.interop.UnsupportedMessageException; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import org.enso.interpreter.dsl.Builtin; +import org.enso.interpreter.node.expression.builtin.error.PolyglotError; +import org.enso.interpreter.runtime.Context; +import org.enso.interpreter.runtime.callable.UnresolvedSymbol; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.data.text.Text; +import org.enso.interpreter.runtime.library.dispatch.MethodDispatchLibrary; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +@ExportLibrary(InteropLibrary.class) +@ExportLibrary(MethodDispatchLibrary.class) +@Builtin(pkg = "date", name = "TimeOfDay", stdlibName = "Standard.Base.Data.Time.Time_Of_Day") +public class EnsoTimeOfDay implements TruffleObject { + private LocalTime localTime; + + public EnsoTimeOfDay(LocalTime localTime) { + this.localTime = localTime; + } + + @Builtin.Method( + name = "parse_builtin", + description = "Constructs a new DateTime from text with optional pattern") + @Builtin.Specialize + @Builtin.WrapException( + from = DateTimeParseException.class, + to = PolyglotError.class, + propagate = true) + public static EnsoTimeOfDay parse(String text) { + return new EnsoTimeOfDay(LocalTime.parse(text)); + } + + @Builtin.Method(name = "new_builtin", description = "Constructs a new Time_OF_Day from an hour") + @Builtin.WrapException(from = DateTimeException.class, to = PolyglotError.class, propagate = true) + public static EnsoTimeOfDay create(long hour, long minute, long second, long nanosecond) { + return new EnsoTimeOfDay( + LocalTime.of( + Math.toIntExact(hour), + Math.toIntExact(minute), + Math.toIntExact(second), + Math.toIntExact(nanosecond))); + } + + @Builtin.Method(description = "Gets a value of hour") + public static EnsoTimeOfDay now() { + return new EnsoTimeOfDay(LocalTime.now()); + } + + @Builtin.Method(description = "Gets a value of hour") + public long hour() { + return localTime.getHour(); + } + + @Builtin.Method(description = "Gets a value minute") + public long minute() { + return localTime.getMinute(); + } + + @Builtin.Method(description = "Gets a value second") + public long second() { + return localTime.getSecond(); + } + + @Builtin.Method(description = "Gets a value nanosecond") + public long nanosecond() { + return localTime.getNano(); + } + + @Builtin.Method(description = "Gets a value second") + public long toSeconds() { + return localTime.toSecondOfDay(); + } + + @Builtin.Method( + name = "to_time_builtin", + description = "Combine this time of day with a date to create a point in time.") + public EnsoDateTime toTime(EnsoDate date, EnsoZone zone) { + return new EnsoDateTime(localTime.atDate(date.asDate()).atZone(zone.asTimeZone())); + } + + @Builtin.Method(description = "Return this datetime to the datetime in the provided time zone.") + public Text toText() { + return Text.create(DateTimeFormatter.ISO_LOCAL_TIME.format(localTime)); + } + + @Builtin.Method(description = "Return this datetime to the datetime in the provided time zone.") + @Builtin.Specialize + public Text format(String pattern) { + return Text.create(DateTimeFormatter.ofPattern(pattern).format(localTime)); + } + + @ExportMessage + boolean isTime() { + return true; + } + + @ExportMessage + LocalTime asTime() { + return localTime; + } + + @ExportMessage + final boolean isDate() { + return false; + } + + @ExportMessage + LocalDate asDate() throws UnsupportedMessageException { + throw UnsupportedMessageException.create(); + } + + @ExportMessage + boolean hasFunctionalDispatch() { + return true; + } + + @ExportMessage + static class GetFunctionalDispatch { + @CompilerDirectives.TruffleBoundary + static Function doResolve(InteropLibrary my, UnresolvedSymbol symbol) { + Context context = Context.get(my); + return symbol.resolveFor(context.getBuiltins().timeOfDay(), context.getBuiltins().any()); + } + + @Specialization( + guards = {"cachedSymbol == symbol", "function != null"}, + limit = "3") + static Function resolveCached( + EnsoTimeOfDay self, + UnresolvedSymbol symbol, + @Cached("symbol") UnresolvedSymbol cachedSymbol, + @CachedLibrary("self") InteropLibrary mySelf, + @Cached("doResolve(mySelf, cachedSymbol)") Function function) { + return function; + } + + @Specialization(replaces = "resolveCached") + static Function resolve( + EnsoTimeOfDay self, UnresolvedSymbol symbol, @CachedLibrary("self") InteropLibrary mySelf) + throws MethodDispatchLibrary.NoSuchMethodException { + Function function = doResolve(mySelf, symbol); + if (function == null) { + throw new MethodDispatchLibrary.NoSuchMethodException(); + } + return function; + } + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoZone.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoZone.java new file mode 100644 index 0000000000..53a471e2ef --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoZone.java @@ -0,0 +1,112 @@ +package org.enso.interpreter.runtime.data; + +import com.oracle.truffle.api.CompilerDirectives; +import com.oracle.truffle.api.dsl.Cached; +import com.oracle.truffle.api.dsl.Specialization; +import com.oracle.truffle.api.interop.InteropLibrary; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.library.CachedLibrary; +import com.oracle.truffle.api.library.ExportLibrary; +import com.oracle.truffle.api.library.ExportMessage; +import org.enso.interpreter.dsl.Builtin; +import org.enso.interpreter.node.expression.builtin.error.PolyglotError; +import org.enso.interpreter.runtime.Context; +import org.enso.interpreter.runtime.callable.UnresolvedSymbol; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.data.text.Text; +import org.enso.interpreter.runtime.library.dispatch.MethodDispatchLibrary; + +import java.time.DateTimeException; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; +import java.time.zone.ZoneRulesException; + +@ExportLibrary(InteropLibrary.class) +@ExportLibrary(MethodDispatchLibrary.class) +@Builtin(pkg = "date", name = "Zone", stdlibName = "Standard.Base.Data.Time.Zone") +public final class EnsoZone implements TruffleObject { + private final ZoneId zone; + + public EnsoZone(ZoneId zone) { + this.zone = zone; + } + + @Builtin.Method(description = "Get the unique identifier for your system's current timezone.") + public Text zoneId() { + return Text.create(this.zone.getId()); + } + + @Builtin.Method(name = "parse_builtin", description = "Parse the ID producing EnsoZone.") + @Builtin.Specialize + @Builtin.WrapException( + from = ZoneRulesException.class, + to = PolyglotError.class, + propagate = true) + public static EnsoZone parse(String text) { + return new EnsoZone(ZoneId.of(text)); + } + + @Builtin.Method( + name = "new_builtin", + description = + "Obtains an instance of `Zone` using an offset in hours, minutes and seconds from the UTC zone.") + @Builtin.WrapException(from = DateTimeException.class, to = PolyglotError.class, propagate = true) + public static EnsoZone create(long hours, long minutes, long seconds) { + return new EnsoZone( + ZoneOffset.ofHoursMinutesSeconds( + Math.toIntExact(hours), Math.toIntExact(minutes), Math.toIntExact(seconds))); + } + + @Builtin.Method(name = "system", description = "The system default timezone.") + public static EnsoZone system() { + return new EnsoZone(ZoneId.systemDefault()); + } + + @ExportMessage + boolean isTimeZone() { + return true; + } + + @ExportMessage + ZoneId asTimeZone() { + return zone; + } + + @ExportMessage + boolean hasFunctionalDispatch() { + return true; + } + + @ExportMessage + static class GetFunctionalDispatch { + @CompilerDirectives.TruffleBoundary + static Function doResolve(InteropLibrary my, UnresolvedSymbol symbol) { + Context context = Context.get(my); + return symbol.resolveFor(context.getBuiltins().zone(), context.getBuiltins().any()); + } + + @Specialization( + guards = {"cachedSymbol == symbol", "function != null"}, + limit = "3") + static Function resolveCached( + EnsoZone self, + UnresolvedSymbol symbol, + @Cached("symbol") UnresolvedSymbol cachedSymbol, + @CachedLibrary("self") InteropLibrary mySelf, + @Cached("doResolve(mySelf, cachedSymbol)") Function function) { + return function; + } + + @Specialization(replaces = "resolveCached") + static Function resolve( + EnsoZone self, UnresolvedSymbol symbol, @CachedLibrary("self") InteropLibrary mySelf) + throws MethodDispatchLibrary.NoSuchMethodException { + Function function = doResolve(mySelf, symbol); + if (function == null) { + throw new MethodDispatchLibrary.NoSuchMethodException(); + } + return function; + } + } +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java index 693adebd9b..4a8f2122c2 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/type/Types.java @@ -50,7 +50,10 @@ import org.enso.polyglot.data.TypeGraph; PanicSentinel.class, Warning.class, EnsoFile.class, - EnsoDate.class + EnsoDate.class, + EnsoDateTime.class, + EnsoTimeOfDay.class, + EnsoZone.class, }) public class Types { @@ -221,6 +224,10 @@ public class Types { graph.insert(ConstantsGen.PANIC, ConstantsGen.ANY); graph.insert(ConstantsGen.REF, ConstantsGen.ANY); graph.insert(ConstantsGen.TEXT, ConstantsGen.ANY); + graph.insert(ConstantsGen.DATE, ConstantsGen.ANY); + graph.insert(ConstantsGen.DATE_TIME, ConstantsGen.ANY); + graph.insert(ConstantsGen.TIME_OF_DAY, ConstantsGen.ANY); + graph.insert(ConstantsGen.ZONE, ConstantsGen.ANY); graph.insertWithoutParent(ConstantsGen.PANIC); graph.insertWithoutParent(Constants.THUNK); graph.insertWithoutParent(Constants.UNRESOLVED_SYMBOL); diff --git a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/DateTest.scala b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/DateTest.scala index b52d1e1205..be401d1e91 100644 --- a/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/DateTest.scala +++ b/engine/runtime/src/test/scala/org/enso/interpreter/test/semantic/DateTest.scala @@ -33,23 +33,6 @@ class DateTest extends InterpreterTest { consumeOut shouldEqual List("2022-04-01") } - "send enso date into java" in { - val code = - s"""import Standard.Base.IO - |polyglot java import java.time.LocalTime - |import Standard.Base.Data.Time.Date - | - |main = - | ensodate = Date.new 2022 04 01 - | javatime = LocalTime.of 10 26 - | javatimedate = javatime.atDate ensodate - | javadate = javatimedate . toLocalDate - | IO.println javadate - |""".stripMargin - eval(code) - consumeOut shouldEqual List("2022-04-01") - } - "check java date has enso methods" in { val code = s"""import Standard.Base.IO diff --git a/lib/scala/interpreter-dsl/src/main/java/org/enso/interpreter/dsl/builtins/TypeWithKind.java b/lib/scala/interpreter-dsl/src/main/java/org/enso/interpreter/dsl/builtins/TypeWithKind.java index eea2ad5bf7..a47c9637b8 100644 --- a/lib/scala/interpreter-dsl/src/main/java/org/enso/interpreter/dsl/builtins/TypeWithKind.java +++ b/lib/scala/interpreter-dsl/src/main/java/org/enso/interpreter/dsl/builtins/TypeWithKind.java @@ -43,6 +43,9 @@ public record TypeWithKind(String baseType, TypeKind kind) { "org.enso.interpreter.runtime.data.ArrayOverBuffer", "org.enso.interpreter.runtime.data.EnsoFile", "org.enso.interpreter.runtime.data.EnsoDate", + "org.enso.interpreter.runtime.data.EnsoDateTime", + "org.enso.interpreter.runtime.data.EnsoTimeOfDay", + "org.enso.interpreter.runtime.data.EnsoZone", "org.enso.interpreter.runtime.data.ManagedResource", "org.enso.interpreter.runtime.data.Ref", "org.enso.interpreter.runtime.data.text.Text", diff --git a/std-bits/base/src/main/java/org/enso/base/Time_Utils.java b/std-bits/base/src/main/java/org/enso/base/Time_Utils.java index 11bc6f3814..8fef4239d6 100644 --- a/std-bits/base/src/main/java/org/enso/base/Time_Utils.java +++ b/std-bits/base/src/main/java/org/enso/base/Time_Utils.java @@ -1,12 +1,6 @@ package org.enso.base; -import java.time.DateTimeException; -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.LocalTime; -import java.time.Period; -import java.time.ZonedDateTime; -import java.time.ZoneId; +import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.TemporalAccessor; @@ -41,6 +35,11 @@ public class Time_Utils { .toFormatter(); } + public enum AdjustOp { + PLUS, + MINUS + } + /** @return default Time formatter. */ public static DateTimeFormatter default_time_formatter() { return DateTimeFormatter.ISO_ZONED_DATE_TIME; @@ -64,18 +63,71 @@ public class Time_Utils { return date.atTime(time).atZone(zone); } - public static LocalDate date_adjust(LocalDate date, long add, Period duration) { - return add == 1 ? date.plus(duration) : date.minus(duration); + public static LocalDate date_adjust(LocalDate date, AdjustOp op, Period duration) { + switch (op) { + case PLUS: + return date.plus(duration); + case MINUS: + return date.minus(duration); + default: + throw new DateTimeException("Unknown adjust operation"); + } } - public static long week_of_year(LocalDate date, Locale locale) { + public static ZonedDateTime datetime_adjust( + ZonedDateTime datetime, AdjustOp op, Period period, Duration duration) { + switch (op) { + case PLUS: + return datetime.plus(period).plus(duration); + case MINUS: + return datetime.minus(period).minus(duration); + default: + throw new DateTimeException("Unknown adjust operation"); + } + } + + public static LocalTime time_adjust(LocalTime time, AdjustOp op, Duration duration) { + switch (op) { + case PLUS: + return time.plus(duration); + case MINUS: + return time.minus(duration); + default: + throw new DateTimeException("Unknown adjust operation"); + } + } + + public static long week_of_year_localdate(LocalDate date, Locale locale) { return WeekFields.of(locale).weekOfYear().getFrom(date); } - public static int compare_to(LocalDate self, LocalDate that) { + public static long week_of_year_zoneddatetime(ZonedDateTime date, Locale locale) { + return WeekFields.of(locale).weekOfYear().getFrom(date); + } + + public static Duration duration_between( + ZonedDateTime start, ZonedDateTime end, boolean timezoneAware) { + return timezoneAware + ? Duration.between(start, end) + : Duration.between(start.toLocalDateTime(), end.toLocalDateTime()); + } + + public static int compare_to_localdate(LocalDate self, LocalDate that) { return self.compareTo(that); } + public static int compare_to_zoneddatetime(ZonedDateTime self, ZonedDateTime that) { + return self.compareTo(that); + } + + public static int compare_to_localtime(LocalTime self, LocalTime that) { + return self.compareTo(that); + } + + public static boolean equals_zone(ZoneId self, ZoneId that) { + return self.equals(that); + } + /** * Obtains an instance of ZonedDateTime from a text string. * @@ -91,7 +143,7 @@ public class Time_Utils { * @param text the string to parse. * @return parsed ZonedDateTime instance. */ - public static ZonedDateTime parse_time(String text) { + public static ZonedDateTime parse_datetime(String text) { TemporalAccessor time = TIME_FORMAT.parseBest(text, ZonedDateTime::from, LocalDateTime::from); if (time instanceof ZonedDateTime) { return (ZonedDateTime) time; @@ -109,9 +161,10 @@ public class Time_Utils { * * @param text the string to parse. * @param pattern the format string. + * @param locale localization config to be uses in the formatter. * @return parsed ZonedDateTime instance. */ - public static ZonedDateTime parse_time_format(String text, String pattern, Locale locale) { + public static ZonedDateTime parse_datetime_format(String text, String pattern, Locale locale) { TemporalAccessor time = DateTimeFormatter.ofPattern(pattern) .withLocale(locale) @@ -123,4 +176,17 @@ public class Time_Utils { } throw new DateTimeException("Text '" + text + "' could not be parsed as Time."); } + + /** + * Obtains an instance of LocalTime from a text string using a custom string. + * + * @param text the string to parse. + * @param pattern the format string. + * @param locale localization config to be uses in the formatter. + * @return parsed LocalTime instance. + */ + public static LocalTime parse_time(String text, String pattern, Locale locale) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); + return (LocalTime.parse(text, formatter.withLocale(locale))); + } } diff --git a/std-bits/table/src/main/java/org/enso/table/data/table/Column.java b/std-bits/table/src/main/java/org/enso/table/data/table/Column.java index 5b71a84286..53d3ad62ed 100644 --- a/std-bits/table/src/main/java/org/enso/table/data/table/Column.java +++ b/std-bits/table/src/main/java/org/enso/table/data/table/Column.java @@ -1,5 +1,7 @@ package org.enso.table.data.table; +import java.time.LocalDate; +import java.time.LocalDateTime; import java.util.BitSet; import java.util.List; import java.util.function.Function; @@ -120,11 +122,30 @@ public class Column { public static Column fromItems(String name, List items) { InferredBuilder builder = new InferredBuilder(items.size()); for (var item : items) { - builder.appendNoGrow(item.isDate() ? item.asDate() : item.as(Object.class)); + builder.appendNoGrow(convertDateOrTime(item)); } return new Column(name, new DefaultIndex(items.size()), builder.seal()); } + private static Object convertDateOrTime(Value item) { + if (item.isDate()) { + LocalDate d = item.asDate(); + if (item.isTime()) { + LocalDateTime dtime = d.atTime(item.asTime()); + if (item.isTimeZone()) { + return dtime.atZone(item.asTimeZone()); + } else { + return dtime; + } + } else { + return d; + } + } else if (item.isTime()) { + return item.asTime(); + } + return item.as(Object.class); + } + /** * Changes the index of this column. * diff --git a/std-bits/table/src/main/java/org/enso/table/formatting/DateFormatter.java b/std-bits/table/src/main/java/org/enso/table/formatting/DateFormatter.java index 78eea42170..496ac5c30c 100644 --- a/std-bits/table/src/main/java/org/enso/table/formatting/DateFormatter.java +++ b/std-bits/table/src/main/java/org/enso/table/formatting/DateFormatter.java @@ -31,6 +31,6 @@ public class DateFormatter implements DataFormatter { @Override public boolean canFormat(Object value) { - return value instanceof LocalDate || (value instanceof Value v && v.isDate()); + return value instanceof LocalDate || (value instanceof Value v && v.isDate() && !v.isTime()); } } diff --git a/std-bits/table/src/main/java/org/enso/table/formatting/DateTimeFormatter.java b/std-bits/table/src/main/java/org/enso/table/formatting/DateTimeFormatter.java index 4aab9a978a..3c4921bd0d 100644 --- a/std-bits/table/src/main/java/org/enso/table/formatting/DateTimeFormatter.java +++ b/std-bits/table/src/main/java/org/enso/table/formatting/DateTimeFormatter.java @@ -1,5 +1,7 @@ package org.enso.table.formatting; +import org.graalvm.polyglot.Value; + import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.util.Locale; @@ -17,13 +19,19 @@ public class DateTimeFormatter implements DataFormatter { return NULL_REPRESENTATION; } + if (value instanceof Value v && v.isDate() && v.isTime()) { + value = v.asDate().atTime(v.asTime()); + if (v.isTimeZone()) { + value = ((LocalDateTime) value).atZone(v.asTimeZone()); + } + } + if (value instanceof LocalDateTime date) { return date.format(formatter); } - // Currently Enso uses ZonedDateTime for the date-time type. This should be revisited along with the Datetime API. if (value instanceof ZonedDateTime date) { - return date.toLocalDateTime().format(formatter); + return date.format(formatter); } throw new IllegalArgumentException("Unsupported type for DateTimeFormatter."); @@ -31,6 +39,6 @@ public class DateTimeFormatter implements DataFormatter { @Override public boolean canFormat(Object value) { - return value instanceof LocalDateTime || value instanceof ZonedDateTime; + return value instanceof LocalDateTime || value instanceof ZonedDateTime || (value instanceof Value v && v.isDate() && v.isTime()); } } diff --git a/std-bits/table/src/main/java/org/enso/table/formatting/TimeFormatter.java b/std-bits/table/src/main/java/org/enso/table/formatting/TimeFormatter.java index 0985998246..00e6db50ef 100644 --- a/std-bits/table/src/main/java/org/enso/table/formatting/TimeFormatter.java +++ b/std-bits/table/src/main/java/org/enso/table/formatting/TimeFormatter.java @@ -1,5 +1,7 @@ package org.enso.table.formatting; +import org.graalvm.polyglot.Value; + import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.util.Locale; @@ -17,6 +19,10 @@ public class TimeFormatter implements DataFormatter { return NULL_REPRESENTATION; } + if (value instanceof Value v && v.isTime()) { + value = v.asTime(); + } + if (value instanceof LocalTime date) { return date.format(formatter); } @@ -26,6 +32,6 @@ public class TimeFormatter implements DataFormatter { @Override public boolean canFormat(Object value) { - return value instanceof LocalTime; + return value instanceof LocalTime || (value instanceof Value v && !v.isDate() && v.isTime()); } } diff --git a/test/Table_Tests/data/datetime_sample.csv b/test/Table_Tests/data/datetime_sample.csv new file mode 100644 index 0000000000..44c21c4f43 --- /dev/null +++ b/test/Table_Tests/data/datetime_sample.csv @@ -0,0 +1,7 @@ +Serial number,Movement type,Posting date +2LMXK1,101,2015-01-05 09:00:00 +2LMXK1,301,2015-01-05 14:00:00 +JEMLP3,101,2015-01-06 09:00:00 +JEMLP3,203,2015-01-07 17:30:00 +BR83GP,101,2011-01-05 09:00:00 +BR83GP,301,2011-01-09 15:30:00 diff --git a/test/Table_Tests/data/time_of_day_sample.csv b/test/Table_Tests/data/time_of_day_sample.csv new file mode 100644 index 0000000000..c31093b372 --- /dev/null +++ b/test/Table_Tests/data/time_of_day_sample.csv @@ -0,0 +1,7 @@ +Serial number,Movement type,Posting time +2LMXK1,101,09:00:00 +2LMXK1,301,14:00:12 +JEMLP3,101,09:00:00 +JEMLP3,203,17:30:00 +BR83GP,101,09:00:04 +BR83GP,301,15:30:00 diff --git a/test/Table_Tests/src/Data_Formatter_Spec.enso b/test/Table_Tests/src/Data_Formatter_Spec.enso index 83146ac5c6..04ee07bb5d 100644 --- a/test/Table_Tests/src/Data_Formatter_Spec.enso +++ b/test/Table_Tests/src/Data_Formatter_Spec.enso @@ -1,6 +1,8 @@ from Standard.Base import all import Standard.Base.Data.Time +import Standard.Base.Data.Time.Date_Time import Standard.Base.Data.Time.Time_Of_Day +import Standard.Base.Data.Time.Zone import Standard.Table from Standard.Table import Column, Data_Formatter, Quote_Style @@ -137,14 +139,17 @@ spec = Test.specify "should format dates" <| formatter = Data_Formatter formatter.format (Date.new 2022) . should_equal "2022-01-01" - formatter.format (Time.new 1999 . internal_zoned_date_time) . should_equal "1999-01-01 00:00:00" - formatter.format (Time_Of_Day.new . internal_local_time) . should_equal "00:00:00" + formatter.format (Date_Time.new 1999) . should_equal "1999-01-01 00:00:00" + formatter.format (Date_Time.new 1999 zone=Zone.utc) . should_equal "1999-01-01 00:00:00" + formatter.format (Date_Time.new 1999 zone=(Zone.parse "America/Los_Angeles")) . should_equal "1999-01-01 00:00:00" + formatter.format (Time_Of_Day.new) . should_equal "00:00:00" Test.specify "should allow custom date formats" <| - formatter = Data_Formatter date_formats=["E, d MMM y", "d MMM y[ G]"] datetime_formats=["dd/MM/yyyy HH:mm"] time_formats=["h:mma"] datetime_locale=Locale.uk + formatter = Data_Formatter date_formats=["E, d MMM y", "d MMM y[ G]"] datetime_formats=["dd/MM/yyyy HH:mm [z]"] time_formats=["h:mma"] datetime_locale=Locale.uk formatter.format (Date.new 2022 06 21) . should_equal "Tue, 21 Jun 2022" - formatter.format (Time.new 1999 02 03 04 56 11 . internal_zoned_date_time) . should_equal "03/02/1999 04:56" - formatter.format (Time_Of_Day.new 13 55 . internal_local_time) . should_equal "1:55pm" + formatter.format (Date_Time.new 1999 02 03 04 56 11 zone=Zone.utc) . should_equal "03/02/1999 04:56 UTC" + formatter.format (Date_Time.new 1999 02 03 04 56 11 zone=(Zone.parse "America/Los_Angeles")) . should_equal "03/02/1999 04:56 GMT-08:00" + formatter.format (Time_Of_Day.new 13 55) . should_equal "1:55pm" Test.specify "should act as identity on Text" <| formatter = Data_Formatter diff --git a/test/Table_Tests/src/Delimited_Write_Spec.enso b/test/Table_Tests/src/Delimited_Write_Spec.enso index 4bd1a2fe85..aeba6f0a3e 100644 --- a/test/Table_Tests/src/Delimited_Write_Spec.enso +++ b/test/Table_Tests/src/Delimited_Write_Spec.enso @@ -166,7 +166,7 @@ spec = Test.specify 'should allow to always quote text and custom values, but for non-text primitves only if absolutely necessary' <| format = Delimited "," value_formatter=(Data_Formatter thousand_separator='"' date_formats=["E, d MMM y"]) . with_quotes always_quote=True quote_escape='\\' - table = Table.new [['The Column "Name"', ["foo","'bar'",'"baz"', 'one, two, three']], ["B", [1.0, 1000000.5, 2.2, -1.5]], ["C", ["foo", My_Type 44, (Date.new 2022 06 21), 42]], ["D", [1,2,3,4000]], ["E", [Nothing, (Time_Of_Day.new 13 55 . internal_local_time), Nothing, Nothing]]] + table = Table.new [['The Column "Name"', ["foo","'bar'",'"baz"', 'one, two, three']], ["B", [1.0, 1000000.5, 2.2, -1.5]], ["C", ["foo", My_Type 44, (Date.new 2022 06 21), 42]], ["D", [1,2,3,4000]], ["E", [Nothing, (Time_Of_Day.new 13 55), Nothing, Nothing]]] file = (enso_project.data / "transient" / "quote_always.csv") file.delete_if_exists table.write file format on_problems=Report_Error . should_succeed diff --git a/test/Table_Tests/src/Table_Time_Of_Day_Spec.enso b/test/Table_Tests/src/Table_Time_Of_Day_Spec.enso new file mode 100644 index 0000000000..39662b2338 --- /dev/null +++ b/test/Table_Tests/src/Table_Time_Of_Day_Spec.enso @@ -0,0 +1,53 @@ +from Standard.Base import all +import Standard.Base.Data.Text.Line_Ending_Style +import Standard.Base.Data.Time.Time_Of_Day + +import Standard.Table +import Standard.Table.Data.Column +import Standard.Table.IO.File_Format +from Standard.Table.Data.Data_Formatter as Data_Formatter_Module import Data_Formatter + +import Standard.Test + +from project.Util import all + +spec = + c_number = ["Serial number", ["2LMXK1", "2LMXK1", "JEMLP3", "JEMLP3", "BR83GP", "BR83GP"]] + c_type = ["Movement type", [101, 301, 101, 203, 101, 301]] + c_time = ["Posting time", [Time_Of_Day.new 9 0, Time_Of_Day.new 14 0 12, Time_Of_Day.new 9 0, Time_Of_Day.new 17 30, Time_Of_Day.new 9 0 4, Time_Of_Day.new 15 30]] + expected = Table.new [c_number, c_type, c_time] + + Test.group "File.read (Delimited) should work with Time_Of_Days" <| + table = (enso_project.data / "time_of_day_sample.csv").read + Test.specify "should be able to read in a table with dates" <| + table.column_count.should_equal 3 + table.columns.map (.name) . should_equal ['Serial number','Movement type', 'Posting time'] + table.row_count.should_equal 6 + + Test.specify "should be able to treat a single value as a Time_Of_Days" <| + from_column = table.at 'Posting time' + from_column.at 5 . hour . should_equal 15 + from_column.at 5 . minute . should_equal 30 + from_column.at 5 . should_equal (Time_Of_Day.new 15 30) + + Test.specify "should be able to compare columns and table" <| + table.at 'Serial number' . should_equal (Column.from_vector c_number.first c_number.second) + table.at 'Movement type' . should_equal (Column.from_vector c_type.first c_type.second) + table.at 'Posting time' . should_equal (Column.from_vector c_time.first c_time.second) + table.should_equal expected + + Test.group "Should be able to serialise a table with Time_Of_Days to Text" <| + Test.specify "should serialise back to input" <| + expected_text = normalize_lines <| + (enso_project.data / "time_of_day_sample.csv").read_text + delimited = Text.from expected format=(File_Format.Delimited "," line_endings=Line_Ending_Style.Unix) + delimited.should_equal expected_text + + Test.specify "should serialise dates with format" <| + test_table = Table.new [c_time] + expected_text = 'Posting time\n09-00-00\n14-00-12\n09-00-00\n17-30-00\n09-00-04\n15-30-00\n' + data_formatter = Data_Formatter . with_datetime_formats time_formats=["HH-mm-ss"] + delimited = Text.from test_table format=(File_Format.Delimited "," value_formatter=data_formatter line_endings=Line_Ending_Style.Unix) + delimited.should_equal expected_text + +main = Test.Suite.run_main spec diff --git a/test/Table_Tests/src/Table_Time_Spec.enso b/test/Table_Tests/src/Table_Time_Spec.enso new file mode 100644 index 0000000000..33252be8e2 --- /dev/null +++ b/test/Table_Tests/src/Table_Time_Spec.enso @@ -0,0 +1,52 @@ +from Standard.Base import all +import Standard.Base.Data.Text.Line_Ending_Style +import Standard.Base.Data.Time + +import Standard.Table +import Standard.Table.Data.Column +import Standard.Table.IO.File_Format +from Standard.Table.Data.Data_Formatter as Data_Formatter_Module import Data_Formatter + +import Standard.Test + +from project.Util import all + +spec = + c_number = ["Serial number", ["2LMXK1", "2LMXK1", "JEMLP3", "JEMLP3", "BR83GP", "BR83GP"]] + c_type = ["Movement type", [101, 301, 101, 203, 101, 301]] + c_date = ["Posting date", [Date_Time.new 2015 1 5 9 0, Date_Time.new 2015 1 5 14 0, Date_Time.new 2015 1 6 9 0, Date_Time.new 2015 1 7 17 30, Date_Time.new 2011 1 5 9 0, Date_Time.new 2011 1 9 15 30]] + expected = Table.new [c_number, c_type, c_date] + + Test.group "File.read (Delimited) should work with Date_Time" <| + table = (enso_project.data / "datetime_sample.csv").read + Test.specify "should be able to read in a table with dates" <| + table.column_count.should_equal 3 + table.columns.map (.name) . should_equal ['Serial number','Movement type', 'Posting date'] + table.row_count.should_equal 6 + + Test.specify "should be able to treat a single value as a Date_Time" <| + from_column = table.at 'Posting date' + from_column.at 5 . year . should_equal 2011 + from_column.at 5 . should_equal (Date_Time.new 2011 1 9 15 30) + + Test.specify "should be able to compare columns and table" <| + table.at 'Serial number' . should_equal (Column.from_vector c_number.first c_number.second) + table.at 'Movement type' . should_equal (Column.from_vector c_type.first c_type.second) + table.at 'Posting date' . should_equal (Column.from_vector c_date.first c_date.second) + table.should_equal expected + + Test.group "Should be able to serialise a table with DateTimes to Text" <| + Test.specify "should serialise back to input" <| + expected_text = normalize_lines <| + (enso_project.data / "datetime_sample.csv").read_text + delimited = Text.from expected format=(File_Format.Delimited "," line_endings=Line_Ending_Style.Unix) + delimited.should_equal expected_text + + Test.specify "should serialise dates with format" <| + test_table = Table.new [c_date] + expected_text = 'Posting date\n05.01.2015 09-00\n05.01.2015 14-00\n06.01.2015 09-00\n07.01.2015 17-30\n05.01.2011 09-00\n09.01.2011 15-30\n' + data_formatter = Data_Formatter . with_datetime_formats datetime_formats=["dd.MM.yyyy HH-mm"] + delimited = Text.from test_table format=(File_Format.Delimited "," value_formatter=data_formatter line_endings=Line_Ending_Style.Unix) + delimited.should_equal expected_text + +main = Test.Suite.run_main spec diff --git a/test/Tests/src/Data/Time/Date_Spec.enso b/test/Tests/src/Data/Time/Date_Spec.enso index 99a28570f2..43c1440c9d 100644 --- a/test/Tests/src/Data/Time/Date_Spec.enso +++ b/test/Tests/src/Data/Time/Date_Spec.enso @@ -148,12 +148,20 @@ js_parse text format=Nothing = d = Date.parse text format js_date d.year d.month d.day +## JSDate is not only a date but also time and timezone. +## Here, we explicitly convert JS ZonedDateTime to LocalDate +js_set_zone local_datetime = + zone = Zone.utc + datetime_with_tz = local_datetime.at_zone(zone) + diff = Duration.between datetime_with_tz local_datetime (timezone_aware=False) + (datetime_with_tz + diff).to_localdate_builtin + js_date year month=1 day=1 = - Panic.catch Any (js_date_impl year month day) (err -> Error.throw (Time.Time_Error err.payload.cause)) + Panic.catch Any (js_set_zone (js_date_impl year month day)) (err -> Error.throw (Time.Time_Error err.payload.cause)) js_array_date year month=1 day=1 = arr = Panic.catch Any (js_array_dateCreate year month day) (err -> Error.throw (Time.Time_Error err.payload.cause)) - arr.at(0) + js_set_zone arr.at(0) java_parse date_text pattern=Nothing = if pattern == Nothing then Panic.catch Polyglot_Error (LocalDate.parse date_text) (err -> Error.throw (Time.Time_Error err.payload.cause.getMessage)) else diff --git a/test/Tests/src/Data/Time/Duration_Spec.enso b/test/Tests/src/Data/Time/Duration_Spec.enso index 34be2dc3a5..afba93e8e5 100644 --- a/test/Tests/src/Data/Time/Duration_Spec.enso +++ b/test/Tests/src/Data/Time/Duration_Spec.enso @@ -1,7 +1,7 @@ from Standard.Base import all import Standard.Base.Data.Time.Duration -import Standard.Base.Data.Time +import Standard.Base.Data.Time.Date_Time import Standard.Test @@ -37,8 +37,8 @@ spec = interval.to_vector . should_equal [-11, 0, 0, 2, 0, 12, 0] Test.specify "should create interval between two points in time" <| - time1 = Time.new 2001 1 2 - time2 = Time.new 2001 2 1 + time1 = Date_Time.new 2001 1 2 + time2 = Date_Time.new 2001 2 1 interval = Duration.between time1 time2 interval.to_vector . should_equal [0, 0, 0, 720, 0, 0, 0] @@ -99,3 +99,5 @@ spec = duration_1!=duration_2 . should_be_true duration_1>duration_2 . should_be_true duration_1 msg . should_equal "Invalid value for HourOfDay (valid values 0 - 23): 24" result -> Test.fail ("Unexpected result: " + result.to_text) Test.specify "should format local time using provided pattern" <| - text = Time_Of_Day.new 12 20 44 . format "HHmmss" + text = create_new_time 12 20 44 . format "HHmmss" text . should_equal "122044" Test.specify "should format local time using default pattern" <| - text = Time_Of_Day.new 12 20 44 . to_text + text = create_new_time 12 20 44 . to_text text . should_equal "12:20:44" Test.specify "should convert to Json" <| - time = Time_Of_Day.new 1 2 3 + time = create_new_time 1 2 3 time.to_json.should_equal <| time_pairs = [["hour", time.hour], ["minute", time.minute], ["second", time.second], ["nanosecond", time.nanosecond]] Json.from_pairs ([["type", "Time_Of_Day"]] + time_pairs) Test.specify "should parse default time format" <| - text = Time_Of_Day.new 12 20 44 . to_text + text = create_new_time 12 20 44 . to_text time = Time_Of_Day.parse text time.to_seconds . should_equal 44444 Test.specify "should parse local time" <| - time = Time_Of_Day.parse "10:00:00" + time = parse_time "10:00:00" time.to_seconds . should_equal 36000 Test.specify "should throw error when parsing invalid time" <| - case Time_Of_Day.parse "1200" . catch of + case parse_time "1200" . catch of Time.Time_Error msg -> msg . should_equal "Text '1200' could not be parsed at index 2" result -> Test.fail ("Unexpected result: " + result.to_text) Test.specify "should parse custom format" <| - time = Time_Of_Day.parse "12:30AM" "hh:mma" + time = parse_time "12:30AM" "hh:mma" time.to_seconds . should_equal 1800 Test.specify "should throw error when parsing custom format" <| - time = Time_Of_Day.parse "12:30" "HH:mm:ss" + time = parse_time "12:30" "HH:mm:ss" case time.catch of Time.Time_Error msg -> msg . should_equal "Text '12:30' could not be parsed at index 5" @@ -67,48 +74,60 @@ spec = Test.fail ("Unexpected result: " + result.to_text) Test.specify "should convert to time" <| - time = Time_Of_Day.new 1 0 0 . to_time (Date.new 2000 12 21) Zone.utc - time . year . should_equal 2000 - time . month . should_equal 12 - time . day . should_equal 21 - time . hour . should_equal 1 - time . minute . should_equal 0 - time . second . should_equal 0 - time . nanosecond . should_equal 0 - time . zone . zone_id . should_equal Zone.utc.zone_id + datetime = create_new_time 1 0 0 . to_time (Date.new 2000 12 21) Zone.utc + datetime . year . should_equal 2000 + datetime . month . should_equal 12 + datetime . day . should_equal 21 + datetime . hour . should_equal 1 + datetime . minute . should_equal 0 + datetime . second . should_equal 0 + datetime . nanosecond . should_equal 0 + datetime . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should add time-based interval" <| - time = Time_Of_Day.new + 1.minute + time = create_new_time 0 + 1.minute time . to_seconds . should_equal 60 Test.specify "should subtract time-based interval" <| - time = Time_Of_Day.new - 1.minute + time = create_new_time 0 - 1.minute time . to_seconds . should_equal 86340 Test.specify "should support mixed interval operators" <| - time = Time_Of_Day.new + 1.hour - 1.second + time = create_new_time 0 + 1.hour - 1.second time . to_seconds . should_equal 3599 Test.specify "should throw error when adding date-based interval" <| - case (Time_Of_Day.new + 1.day) . catch of + case (create_new_time 0 + 1.day) . catch of Time.Time_Error message -> message . should_equal "Time_Of_Day does not support date intervals" result -> Test.fail ("Unexpected result: " + result.to_text) Test.specify "should throw error when subtracting date-based interval" <| - case (Time_Of_Day.new - (1.day - 1.minute)) . catch of + case (create_new_time 0 - (1.day - 1.minute)) . catch of Time.Time_Error message -> message . should_equal "Time_Of_Day does not support date intervals" result -> Test.fail ("Unexpected result: " + result.to_text) Test.specify "should be comparable" <| - time_1 = Time_Of_Day.parse "12:30:12.7102" - time_2 = Time_Of_Day.parse "04:00:10.0" + time_1 = parse_time "12:30:12.7102" + time_2 = parse_time "04:00:10.0" (time_1 == time_2) . should_be_false time_1==time_1 . should_be_true time_1!=time_2 . should_be_true time_1>time_2 . should_be_true time_1 Error.throw (Time.Time_Error <| err.payload.to_display_text.drop (Text_Sub_Range.First 16))) + +java_parse time_text pattern=Nothing = + if pattern == Nothing then Panic.catch Polyglot_Error (LocalTime.parse time_text) (err -> Error.throw (Time.Time_Error err.payload.cause.getMessage)) else + formatter = DateTimeFormatter.ofPattern(pattern) + Panic.catch Polyglot_Error (LocalTime.parse time_text formatter) (err -> Error.throw (Time.Time_Error err.payload.cause.getMessage)) + +main = Test.Suite.run_main spec diff --git a/test/Tests/src/Data/Time/Time_Spec.enso b/test/Tests/src/Data/Time/Time_Spec.enso index daaa44ca18..4b82a4079e 100644 --- a/test/Tests/src/Data/Time/Time_Spec.enso +++ b/test/Tests/src/Data/Time/Time_Spec.enso @@ -1,16 +1,27 @@ from Standard.Base import all -import Standard.Base.Data.Time.Duration import Standard.Base.Data.Time +import Standard.Base.Data.Time.Date_Time +import Standard.Base.Data.Time.Duration import Standard.Base.Data.Time.Zone import Standard.Test +polyglot java import java.time.ZonedDateTime +polyglot java import java.time.LocalDateTime +polyglot java import java.time.format.DateTimeFormatter + spec = - Test.group "Time" <| + specWith "Date_Time" Date_Time.new Date_Time.parse + specWith "JavascriptDate" js_datetime js_parse nanoseconds_loss_in_precision=True + specWith "JavaZonedDateTime" java_datetime java_parse + specWith "JavascriptDataInArray" js_array_datetime js_parse nanoseconds_loss_in_precision=True + +specWith name create_new_datetime parse_datetime nanoseconds_loss_in_precision=False = + Test.group name <| Test.specify "should create time" <| - time = Time.new 1970 (zone = Zone.utc) + time = create_new_datetime 1970 (zone = Zone.utc) time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -21,30 +32,30 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should handle errors when creating time" <| - case Time.new 1970 0 0 . catch of + case create_new_datetime 1970 0 0 . catch of Time.Time_Error msg -> msg . should_equal "Invalid value for MonthOfYear (valid values 1 - 12): 0" result -> Test.fail ("Unexpected result: " + result.to_text) Test.specify "should format using provided pattern" <| - text = Time.new 1970 (zone = Zone.utc) . format "yyyy-MM-dd'T'HH:mm:ss" + text = create_new_datetime 1970 (zone = Zone.utc) . format "yyyy-MM-dd'T'HH:mm:ss" text . should_equal "1970-01-01T00:00:00" Test.specify "should format using default pattern" <| - text = Time.new 1970 (zone = Zone.utc) . to_text + text = create_new_datetime 1970 (zone = Zone.utc) . to_text text . should_equal "1970-01-01T00:00:00Z[UTC]" Test.specify "should convert to Json" <| - time = Time.new 1970 12 21 (zone = Zone.utc) + time = create_new_datetime 1970 12 21 (zone = Zone.utc) time.to_json.should_equal <| zone_pairs = [["zone", Zone.utc]] time_pairs = [["year", time.year], ["month", time.month], ["day", time.day], ["hour", time.hour], ["minute", time.minute], ["second", time.second], ["nanosecond", time.nanosecond]] Json.from_pairs ([["type", "Time"]] + time_pairs + zone_pairs) Test.specify "should parse default time format" <| - text = Time.new 1970 (zone = Zone.utc) . to_text - time = Time.parse text + text = create_new_datetime 1970 (zone = Zone.utc) . to_text + time = parse_datetime text time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -55,7 +66,7 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should parse local time adding system zone" <| - time = Time.parse "1970-01-01T00:00:01" + time = parse_datetime "1970-01-01T00:00:01" time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -63,31 +74,32 @@ spec = time . minute . should_equal 0 time . second . should_equal 1 time . nanosecond . should_equal 0 - time . zone . zone_id . should_equal Zone.system.zone_id + time . zone . should_equal Zone.system Test.specify "should parse time Z" <| - time = Time.parse "1970-01-01T00:00:01Z" + time = parse_datetime "1970-01-01T00:00:01Z" time . to_epoch_seconds . should_equal 1 time . zone . zone_id . should_equal "Z" Test.specify "should parse time UTC" <| - time = Time.parse "1970-01-01T00:00:01Z[UTC]" + time = parse_datetime "1970-01-01T00:00:01Z[UTC]" time . to_epoch_seconds . should_equal 1 time . zone . zone_id . should_equal "UTC" Test.specify "should parse time with nanoseconds" <| - time = Time.parse "1970-01-01T00:00:01.123456789Z" + time = parse_datetime "1970-01-01T00:00:01.123456789Z" time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 time . hour . should_equal 0 time . minute . should_equal 0 time . second . should_equal 1 - time . nanosecond . should_equal 123456789 + if nanoseconds_loss_in_precision then time . nanosecond . should_equal 123000000 else + time . nanosecond . should_equal 123456789 time . zone . zone_id . should_equal "Z" Test.specify "should parse time with offset-based zone" <| - time = Time.parse "1970-01-01T00:00:01+01:00" + time = parse_datetime "1970-01-01T00:00:01+01:00" time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -98,7 +110,7 @@ spec = time . zone . zone_id . should_equal (Zone.new 1 . zone_id) Test.specify "should parse time with id-based zone" <| - time = Time.parse "1970-01-01T00:00:01+01:00[Europe/Paris]" + time = parse_datetime "1970-01-01T00:00:01+01:00[Europe/Paris]" time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -109,14 +121,14 @@ spec = time . zone . zone_id . should_equal "Europe/Paris" Test.specify "should throw error when parsing invalid time" <| - case Time.parse "2008-1-1" . catch of + case parse_datetime "2008-1-1" . catch of Time.Time_Error msg -> msg . should_equal "Text '2008-1-1' could not be parsed at index 5" result -> Test.fail ("Unexpected result: " + result.to_text) Test.specify "should parse custom format of zoned time" <| - time = Time.parse "2020-05-06 04:30:20 UTC" "yyyy-MM-dd HH:mm:ss z" + time = parse_datetime "2020-05-06 04:30:20 UTC" "yyyy-MM-dd HH:mm:ss z" time . year . should_equal 2020 time . month . should_equal 5 time . day . should_equal 6 @@ -127,7 +139,7 @@ spec = time . zone . zone_id . should_equal "Etc/UTC" Test.specify "should parse custom format of local time" <| - time = Time.parse "06 of May 2020 at 04:30AM" "dd 'of' MMMM yyyy 'at' hh:mma" + time = parse_datetime "06 of May 2020 at 04:30AM" "dd 'of' MMMM yyyy 'at' hh:mma" time . year . should_equal 2020 time . month . should_equal 5 time . day . should_equal 6 @@ -137,7 +149,7 @@ spec = time . nanosecond . should_equal 0 Test.specify "should throw error when parsing custom format" <| - time = Time.parse "2008-01-01" "yyyy-MM-dd'T'HH:mm:ss'['z']'" + time = parse_datetime "2008-01-01" "yyyy-MM-dd'T'HH:mm:ss'['z']'" case time.catch of Time.Time_Error msg -> msg . should_equal "Text '2008-01-01' could not be parsed at index 10" @@ -145,16 +157,16 @@ spec = Test.fail ("Unexpected result: " + result.to_text) Test.specify "should get epoch seconds" <| - time = Time.new 1970 1 1 0 0 8 (zone = Zone.utc) + time = create_new_datetime 1970 1 1 0 0 8 (zone = Zone.utc) time . to_epoch_seconds . should_equal 8 Test.specify "should get epoch millis" <| - time = Time.new 1970 1 1 0 0 8 (zone = Zone.utc) + time = create_new_datetime 1970 1 1 0 0 8 (zone = Zone.utc) time . to_epoch_milliseconds . should_equal 8000 Test.specify "should set offset-based timezone" <| tz = Zone.new 1 1 1 - time = Time.new 1970 (zone = Zone.utc) . at_zone tz + time = create_new_datetime 1970 (zone = Zone.utc) . at_zone tz time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -166,7 +178,7 @@ spec = Test.specify "should set id-based timezone" <| tz = Zone.parse "Europe/Moscow" - time = Time.new 1970 (zone = Zone.utc) . at_zone tz + time = create_new_datetime 1970 (zone = Zone.utc) . at_zone tz time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -177,33 +189,33 @@ spec = time . zone . zone_id . should_equal tz.zone_id Test.specify "should get time of day from offsed-based time" <| - time = Time.parse "1970-01-01T00:00:01+01:00" . time_of_day + time = parse_datetime "1970-01-01T00:00:01+01:00" . time_of_day time . hour . should_equal 0 time . minute . should_equal 0 time . second . should_equal 1 time . nanosecond . should_equal 0 Test.specify "should get time of day from id-based time" <| - time = Time.parse "1970-01-01T00:00:01+01:00[Europe/Paris]" . time_of_day + time = parse_datetime "1970-01-01T00:00:01+01:00[Europe/Paris]" . time_of_day time . hour . should_equal 0 time . minute . should_equal 0 time . second . should_equal 1 time . nanosecond . should_equal 0 Test.specify "should get date from offsed-based time" <| - time = Time.parse "1970-01-01T00:00:01+01:00" . date + time = parse_datetime "1970-01-01T00:00:01+01:00" . date time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 Test.specify "should get date from id-based time" <| - time = Time.parse "1970-01-01T00:00:01+01:00[Europe/Paris]" . date + time = parse_datetime "1970-01-01T00:00:01+01:00[Europe/Paris]" . date time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 Test.specify "should add time interval" <| - time = Time.new 1970 (zone = Zone.utc) + 1.nanosecond + time = create_new_datetime 1970 (zone = Zone.utc) + 1.nanosecond time . year . should_equal 1970 time . month . should_equal 1 time . day . should_equal 1 @@ -211,10 +223,10 @@ spec = time . minute . should_equal 0 time . second . should_equal 0 time . nanosecond . should_equal 1 - time . zone . zone_id . should_equal Zone.utc.zone_id + time . zone . should_equal Zone.utc Test.specify "should add date interval" <| - time = Time.new 1970 (zone = Zone.utc) + 1.month + time = create_new_datetime 1970 (zone = Zone.utc) + 1.month time . year . should_equal 1970 time . month . should_equal 2 time . day . should_equal 1 @@ -225,7 +237,7 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should add mixed date time interval" <| - time = Time.new 1970 (zone = Zone.utc) + (1.month + 3.hours) + time = create_new_datetime 1970 (zone = Zone.utc) + (1.month + 3.hours) time . year . should_equal 1970 time . month . should_equal 2 time . day . should_equal 1 @@ -236,7 +248,7 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should subtract time interval" <| - time = Time.new 1970 (zone = Zone.utc) - 1.hour + time = create_new_datetime 1970 (zone = Zone.utc) - 1.hour time . year . should_equal 1969 time . month . should_equal 12 time . day . should_equal 31 @@ -247,7 +259,7 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should subtract date interval" <| - time = Time.new 1970 (zone = Zone.utc) - 1.month + time = create_new_datetime 1970 (zone = Zone.utc) - 1.month time . year . should_equal 1969 time . month . should_equal 12 time . day . should_equal 1 @@ -258,7 +270,7 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should subtract mixed date time interval" <| - time = Time.new 1970 (zone = Zone.utc) - (1.month - 3.hours) + time = create_new_datetime 1970 (zone = Zone.utc) - (1.month - 3.hours) time . year . should_equal 1969 time . month . should_equal 12 time . day . should_equal 1 @@ -269,7 +281,7 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should support mixed interval operators" <| - time = Time.new 1970 (zone = Zone.utc) - 1.month + 12.hours + time = create_new_datetime 1970 (zone = Zone.utc) - 1.month + 12.hours time . year . should_equal 1969 time . month . should_equal 12 time . day . should_equal 1 @@ -280,11 +292,64 @@ spec = time . zone . zone_id . should_equal Zone.utc.zone_id Test.specify "should be comparable" <| - time_1 = Time.parse "2021-01-01T00:30:12.7102[UTC]" - time_2 = Time.parse "2021-01-01T04:00:10.0+04:00" + time_1 = parse_datetime "2021-01-01T00:30:12.7102[UTC]" + time_2 = parse_datetime "2021-01-01T04:00:10.0+04:00" (time_1 == time_2) . should_be_false time_1==time_1 . should_be_true time_1!=time_2 . should_be_true time_1>time_2 . should_be_true time_1 Error.throw (Time.Time_Error err.payload.cause)) + +# This ensures that date returned by javascript has the right timezone specified by the zone parameter. +# Javascript's toLocaleString will accept the timezone but it will just adapt the datetime while keeping the local timezone. +js_datetime_with_zone year month day hour minute second nanosecond zone = + js_set_zone (js_local_datetime_impl year month day hour minute second nanosecond) zone + +js_set_zone local_datetime zone = + datetime_with_tz = local_datetime.at_zone(zone) + diff = Duration.between datetime_with_tz local_datetime (timezone_aware=False) + datetime_with_tz + diff + +foreign js js_local_datetime_impl year month day hour minute second nanosecond = """ + if (month > 12 || month < 1) { + throw `Invalid value for MonthOfYear (valid values 1 - 12): ${month}`; + } + return new Date(year, month - 1, day, hour, minute, second, nanosecond / 1000000); + +js_parse text format=Nothing = + d = Date_Time.parse text format + js_datetime d.year d.month d.day d.hour d.minute d.second d.nanosecond d.zone + +js_array_datetime year month=1 day=1 hour=0 minute=0 second=0 nanosecond=0 zone=Zone.system = + arr = Panic.catch Any (js_array_datetimeCreate year month day hour minute second nanosecond) (err -> Error.throw (Time.Time_Error err.payload.cause)) + js_set_zone arr.at(0) zone + +foreign js js_array_datetimeCreate year month day hour minute second nanosecond = """ + if (month > 12 || month < 1) { + throw `Invalid value for MonthOfYear (valid values 1 - 12): ${month}`; + } + return [ new Date(year, month - 1, day, hour, minute, second, nanosecond / 1000000) ]; + +java_datetime year month=1 day=1 hour=0 minute=0 second=0 nanosecond=0 zone=Zone.system = + Panic.catch Any (ZonedDateTime.of year month day hour minute second nanosecond zone) (err -> Error.throw (Time.Time_Error <| err.payload.to_display_text.drop (Text_Sub_Range.First 16))) + +maybe_parse_java_zoned text pattern=Nothing = + if pattern == Nothing then ZonedDateTime.parse text else + ZonedDateTime.parse text pattern + +parse_java_local original_error text pattern=Nothing = + if pattern == Nothing then Panic.catch Polyglot_Error (LocalDateTime.parse text) (_ -> Error.throw (Time.Time_Error original_error.payload.cause.getMessage)) else + formatter = DateTimeFormatter.ofPattern(pattern) + Panic.catch Polyglot_Error (LocalDateTime.parse text formatter) (_ -> Error.throw (Time.Time_Error original_error.payload.cause.getMessage)) + +java_parse date_text_raw pattern=Nothing = + utc_replaced = date_text_raw.replace "[UTC]" "Z" + date_text = if utc_replaced.ends_with "ZZ" then date_text_raw else utc_replaced + if pattern == Nothing then Panic.catch Polyglot_Error (maybe_parse_java_zoned date_text) (err -> parse_java_local err date_text pattern) else + formatter = DateTimeFormatter.ofPattern(pattern) + Panic.catch Polyglot_Error (maybe_parse_java_zoned date_text formatter) (err -> parse_java_local err date_text pattern) + +main = Test.Suite.run_main spec diff --git a/test/Tests/src/Data/Time/Zone_Spec.enso b/test/Tests/src/Data/Time/Zone_Spec.enso index 9e5f91f593..cb9763a86c 100644 --- a/test/Tests/src/Data/Time/Zone_Spec.enso +++ b/test/Tests/src/Data/Time/Zone_Spec.enso @@ -1,9 +1,13 @@ from Standard.Base import all +import Standard.Base.Data.Time import Standard.Base.Data.Time.Zone import Standard.Test +polyglot java import java.time.ZoneId +polyglot java import java.time.ZoneOffset + spec = Test.group "Zone" <| Test.specify "should get system zone id" <| @@ -29,3 +33,36 @@ spec = Json.from_pairs [["type", "Zone"], ["id", "+01:02:03"]] Zone.utc.to_json.should_equal <| Json.from_pairs [["type", "Zone"], ["id", "UTC"]] + Test.specify "should throw error when parsing invalid zone id" <| + case Zone.parse "foo" . catch of + Time.Time_Error msg -> + msg . should_equal "Unknown time-zone ID: foo" + result -> + Test.fail ("Unexpected result: " + result.to_text) + Test.group "JavaZoneId" <| + Test.specify "should get system zone id" <| + defaultZone = ZoneId.systemDefault + Zone.system . should_equal defaultZone + Test.specify "should parse UTC zone" <| + zone = "UTC" + id = ZoneId.of zone + id . should_equal Zone.utc + Test.specify "should parse id-based zone" <| + zone = "Europe/Warsaw" + id = ZoneId.of zone + id . zone_id . should_equal zone + Test.specify "should parse offset-based zone" <| + zone = "+01:02:03" + id = ZoneId.of zone + id . zone_id . should_equal zone + Test.specify "should get utc zone id" <| + zone = ZoneId.of "UTC" + zone . should_equal Zone.utc + Test.specify "should convert to Json" <| + zone = ZoneOffset.ofHoursMinutesSeconds 1 2 3 + zone.to_json.should_equal <| + Json.from_pairs [["type", "Zone"], ["id", "+01:02:03"]] + (ZoneId.of "UTC").to_json.should_equal <| + Json.from_pairs [["type", "Zone"], ["id", "UTC"]] + +main = Test.Suite.run_main spec diff --git a/test/Tests/src/Semantic/Java_Interop_Spec.enso b/test/Tests/src/Semantic/Java_Interop_Spec.enso index f68b892905..08bab77f34 100644 --- a/test/Tests/src/Semantic/Java_Interop_Spec.enso +++ b/test/Tests/src/Semantic/Java_Interop_Spec.enso @@ -47,8 +47,8 @@ spec = Test.specify "send Enso date into Java" <| ensodate = Date.new 2022 04 01 javatime = LocalTime.of 10 26 - javatimedate = javatime.atDate ensodate - april1st = javatimedate . toLocalDate + javatimedate = javatime . to_time ensodate + april1st = javatimedate . date april1st.year.should_equal 2022 april1st.month.should_equal 4 april1st.day.should_equal 1