diff --git a/CHANGELOG.md b/CHANGELOG.md index 2887e652eb00..b38c05cee03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -576,6 +576,8 @@ - [Added `Data.post` method to write to HTTP endpoints.][7700] - [Added support for S3. Using `Input_Stream` more for reading.][7776] - [Renamed `Decimal` to `Float`.][7807] +- [Implemented `Date_Time_Formatter` for more user-friendly date/time format + parsing.][7826] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -817,6 +819,7 @@ [7709]: https://github.com/enso-org/enso/pull/7709 [7776]: https://github.com/enso-org/enso/pull/7776 [7807]: https://github.com/enso-org/enso/pull/7807 +[7826]: https://github.com/enso-org/enso/pull/7826 #### Enso Compiler diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso index eaa9ff077383..bf07a4e15c76 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso @@ -18,6 +18,7 @@ import project.Data.Text.Text_Sub_Range.Codepoint_Ranges import project.Data.Text.Text_Sub_Range.Text_Sub_Range import project.Data.Time.Date.Date import project.Data.Time.Date_Time.Date_Time +import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter import project.Data.Time.Time_Of_Day.Time_Of_Day import project.Data.Time.Time_Zone.Time_Zone import project.Data.Vector.Vector @@ -1473,17 +1474,10 @@ Text.parse_json self = Json.parse self Converts text containing a date into a Date object. - Arguments: - - format: An optional format describing how to parse the text. - - Returns a `Time_Error` if `self`` cannot be parsed using the provided - `format`. + This method will return a `Time_Error` if the provided time cannot be parsed. - ? Format Syntax - A custom format string consists of one or more custom date and time format - specifiers. For example, "d MMM yyyy" will format "2011-12-03" as - "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. + Arguments: + - format: The format to use for parsing the input text. ? Default Date Formatting Unless you provide a custom format, the text must represent a valid date @@ -1500,6 +1494,34 @@ Text.parse_json self = Json.parse self - Two digits for the day-of-month. This is pre-padded by zero to ensure two digits. + ? Pattern Syntax + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - y: Year. The number of pattern letters determines the minimum number of + digits. + - y: The year using any number of digits. + - yy: The year, using at most two digits. The default range is + 1950-2049, but this can be changed by including the end year in + braces e.g. `yy{2099}`. + - yyyy: The year, using exactly four digits. + - M: Month of year. The number of pattern letters determines the format: + - M: Any number (1-12). + - MM: Month number with zero padding required (01-12). + - MMM: Short name of the month (Jan-Dec). + - MMMM: Full name of the month (January-December). + The month names depend on the selected locale. + - d: Day. The number of pattern letters determines the format: + - d: Any number (1-31). + - dd: Day number with zero padding required (01-31). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + Both day of week and day of month may be included in a single pattern - + in such case the day of week is used as a sanity check. + - Q: Quarter of year. + If only year and quarter are provided in the pattern, when parsing a + date, the result will be the first day of that quarter. + > Example Parse the date of 23rd December 2020. @@ -1533,9 +1555,9 @@ Text.parse_json self = Json.parse self date = "1999-1-1".parse_date "yyyy-MM-dd" date.catch Time_Error (_->Date.new 2000 1 1) @format make_date_format_selector -@locale Locale.default_widget -Text.parse_date : Text -> Locale -> Date ! Time_Error -Text.parse_date self format:Text="" locale:Locale=Locale.default = Date.parse self format locale +Text.parse_date : Date_Time_Formatter -> Date ! Time_Error +Text.parse_date self format:Date_Time_Formatter=Date_Time_Formatter.iso_date = + Date.parse self format ## ALIAS date_time from text GROUP Conversions @@ -1543,22 +1565,17 @@ Text.parse_date self format:Text="" locale:Locale=Locale.default = Date.parse se Obtains an instance of `Date_Time` from a text such as "2007-12-03T10:15:30+01:00 Europe/Paris". + This method will return a `Time_Error` if the provided time cannot be parsed. + Arguments: - format: The format to use for parsing the input text. - - locale: The locale in which the format should be interpreted. - - ? Format Syntax - A custom format string consists of one or more custom date and time format - specifiers. For example, "d MMM yyyy" will format "2011-12-03" as - "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. ? Default Date_Time Format - The text must represent a valid date-time as defined by the ISO-8601 - format. (See https://en.wikipedia.org/wiki/ISO_8601.) If a time zone is - present, it must be in the ISO-8601 Extended Date/Time Format (EDTF). - (See https://en.wikipedia.org/wiki/ISO_8601#EDTF.) The time zone format - consists of: + Unless you provide a custom format, the text must represent a valid + date-time as defined by the ISO-8601 format (see https://en.wikipedia.org/wiki/ISO_8601). + If a time zone is present, it must be in the ISO-8601 Extended Date/Time + Format (EDTF) (see https://en.wikipedia.org/wiki/ISO_8601#EDTF). The time + zone format consists of: - The ISO offset date time. - If the zone ID is not available or is a zone offset then the format is @@ -1568,8 +1585,45 @@ Text.parse_date self format:Text="" locale:Locale=Locale.default = Date.parse se sensitive. - A close square bracket ']'. - This method will return a `Time_Error` if the provided time cannot be parsed - using the above format. + ? Pattern Syntax + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - y: Year. The number of pattern letters determines the minimum number of + digits. + - y: The year using any number of digits. + - yy: The year, using at most two digits. The default range is + 1950-2049, but this can be changed by including the end year in + braces e.g. `yy{2099}`. + - yyyy: The year, using exactly four digits. + - M: Month of year. The number of pattern letters determines the format: + - M: Any number (1-12). + - MM: Month number with zero padding required (01-12). + - MMM: Short name of the month (Jan-Dec). + - MMMM: Full name of the month (January-December). + The month names depend on the selected locale. + - d: Day. The number of pattern letters determines the format: + - d: Any number (1-31). + - dd: Day number with zero padding required (01-31). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + Both day of week and day of month may be included in a single pattern - + in such case the day of week is used as a sanity check. + - Q: Quarter of year. + If only year and quarter are provided in the pattern, when parsing a + date, the result will be the first day of that quarter. + - H: 24h hour of day (0-23). + - h: 12h hour of day (0-12). The `a` pattern is needed to disambiguate + between AM and PM. + - m: Minute of hour. + - s: Second of minute. + - f: Fractional part of the second. The number of pattern letters + determines the number of digits. If one letter is used, any number of + digits will be accepted. + - a: AM/PM marker. + - T: If repeated 3 or less times - Time zone ID (e.g. Europe/Warsaw, Z, + -08:30), otherwise - Time zone name (e.g. Central European Time, CET). + - Z: Zone offset (e.g. +0000, -0830, +08:30:15). > Example Parse UTC time. @@ -1621,31 +1675,24 @@ Text.parse_date self format:Text="" locale:Locale=Locale.default = Date.parse se example_parse = "06 of May 2020 at 04:30AM".parse_date_time "dd 'of' MMMM yyyy 'at' hh:mma" @format make_date_time_format_selector -@locale Locale.default_widget -Text.parse_date_time : Text -> Locale -> Date_Time ! Time_Error -Text.parse_date_time self format:Text="" locale:Locale=Locale.default = Date_Time.parse self format locale +Text.parse_date_time : Date_Time_Formatter -> Date_Time ! Time_Error +Text.parse_date_time self format:Date_Time_Formatter=Date_Time_Formatter.default_enso_zoned_date_time = + Date_Time.parse self format ## ALIAS time_of_day from text, to_time_of_day GROUP Conversions Obtains an instance of `Time_Of_Day` from a text such as "10:15". + This method will return a `Time_Error` if the provided time cannot be parsed. + Arguments: - format: The format to use for parsing the input text. - - locale: The locale in which the format should be interpreted. - - Returns a `Time_Error` if the provided text cannot be parsed using the - default format. - - ? Format Syntax - A custom format string consists of one or more custom date and time format - specifiers. For example, "d MMM yyyy" will format "2011-12-03" as - "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. ? Default Time Format - The text must represent a valid time and is parsed using the ISO-8601 - extended local time format. The format consists of: + Unless you provide a custom format, the text must represent a valid time + and is parsed using the ISO-8601 extended local time format. + The format consists of: - Two digits for the hour-of-day. This is pre-padded by zero to ensure two digits. @@ -1662,6 +1709,19 @@ Text.parse_date_time self format:Text="" locale:Locale=Locale.default = Date_Tim - One to nine digits for the nano-of-second. As many digits will be output as required. + ? Pattern Syntax + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - H: 24h hour of day (0-23). + - h: 12h hour of day (0-12). The `a` pattern is needed to disambiguate + between AM and PM. + - m: Minute of hour. + - s: Second of minute. + - f: Fractional part of the second. The number of pattern letters + determines the number of digits. If one letter is used, any number of + digits will be accepted. + - a: AM/PM marker. + > Example Get the time 15:05:30. @@ -1692,9 +1752,9 @@ Text.parse_date_time self format:Text="" locale:Locale=Locale.default = Date_Tim example_parse = "4:30AM".parse_time_of_day "h:mma" @format make_time_format_selector -@locale Locale.default_widget -Text.parse_time_of_day : Text -> Locale -> Time_Of_Day ! Time_Error -Text.parse_time_of_day self format:Text="" locale:Locale=Locale.default = Time_Of_Day.parse self format locale +Text.parse_time_of_day : Date_Time_Formatter -> Time_Of_Day ! Time_Error +Text.parse_time_of_day self format:Date_Time_Formatter=Date_Time_Formatter.iso_time = + Time_Of_Day.parse self format ## ALIAS time_zone from text, to_time_zone GROUP Conversions 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 61c6bcaeec34..b0e29ea286d2 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 @@ -8,6 +8,7 @@ import project.Data.Text.Text import project.Data.Time.Date_Period.Date_Period import project.Data.Time.Date_Range.Date_Range import project.Data.Time.Date_Time.Date_Time +import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter import project.Data.Time.Day_Of_Week.Day_Of_Week import project.Data.Time.Day_Of_Week_From import project.Data.Time.Duration.Duration @@ -105,17 +106,11 @@ type Date Arguments: - text: The text to try and parse as a date. - - pattern: An optional pattern describing how to parse the text. - - locale: The locale in which the pattern should be interpreted. + - format: A pattern describing how to parse the text, + or a `Date_Time_Formatter`. Returns a `Time_Error` if the provided `text` cannot be parsed using the - provided `pattern`. - - ? Pattern Syntax - A custom pattern string consists of one or more custom date and time - format specifiers. For example, "d MMM yyyy" will format "2011-12-03" - as "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. + provided `format`. ? Default Date Formatting Unless you provide a custom format, the text must represent a valid date @@ -132,6 +127,34 @@ type Date - Two digits for the day-of-month. This is pre-padded by zero to ensure two digits. + ? Pattern Syntax + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - y: Year. The number of pattern letters determines the minimum number of + digits. + - y: The year using any number of digits. + - yy: The year, using at most two digits. The default range is + 1950-2049, but this can be changed by including the end year in + braces e.g. `yy{2099}`. + - yyyy: The year, using exactly four digits. + - M: Month of year. The number of pattern letters determines the format: + - M: Any number (1-12). + - MM: Month number with zero padding required (01-12). + - MMM: Short name of the month (Jan-Dec). + - MMMM: Full name of the month (January-December). + The month names depend on the selected locale. + - d: Day. The number of pattern letters determines the format: + - d: Any number (1-31). + - dd: Day number with zero padding required (01-31). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + Both day of week and day of month may be included in a single pattern - + in such case the day of week is used as a sanity check. + - Q: Quarter of year. + If only year and quarter are provided in the pattern, when parsing a + date, the result will be the first day of that quarter. + > Example Parse the date of 23rd December 2020. @@ -164,17 +187,10 @@ type Date example_parse_err = date = Date.parse "1999-1-1" "yyyy-MM-dd" date.catch Time_Error (_->Date.new 2000 1 1) - @pattern make_date_format_selector - @locale Locale.default_widget - parse : Text -> Text -> Locale -> Date ! Time_Error - parse text:Text pattern:Text="" locale:Locale=Locale.default = - result = Panic.recover Any <| - formatter = if pattern.is_empty then Time_Utils.default_date_formatter else - Time_Utils.make_formatter pattern locale.java_locale - Time_Utils.parse_date text.trim formatter - result . map_error <| case _ of - err : JException -> Time_Error.Error err.getMessage - ex -> ex + @format make_date_format_selector + parse : Text -> Date_Time_Formatter -> Date ! Time_Error + parse text:Text format:Date_Time_Formatter=Date_Time_Formatter.iso_date = + format.parse_date text ## GROUP Metadata Get the year field. @@ -709,15 +725,36 @@ type Date Format this date using the provided format specifier. Arguments: - - pattern: The text specifying the format for formatting the date. - - locale: The locale in which the format should be interpreted. - (Defaults to Locale.default.) + - format: A pattern describing how to format the text, + or a `Date_Time_Formatter`. ? Pattern Syntax - A custom pattern string consists of one or more custom date and time - format specifiers. For example, "d MMM yyyy" will format "2011-12-03" - as "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - y: Year. The number of pattern letters determines the minimum number of + digits. + - y: The year using any number of digits. + - yy: The year, using at most two digits. The default range is + 1950-2049, but this can be changed by including the end year in + braces e.g. `yy{2099}`. + - yyyy: The year, using exactly four digits. + - M: Month of year. The number of pattern letters determines the format: + - M: Any number (1-12). + - MM: Month number with zero padding required (01-12). + - MMM: Short name of the month (Jan-Dec). + - MMMM: Full name of the month (January-December). + The month names depend on the selected locale. + - d: Day. The number of pattern letters determines the format: + - d: Any number (1-31). + - dd: Day number with zero padding required (01-31). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + Both day of week and day of month may be included in a single pattern - + in such case the day of week is used as a sanity check. + - Q: Quarter of year. + If only year and quarter are provided in the pattern, when parsing a + date, the result will be the first day of that quarter. > Example Format "2020-06-02" as "2 Jun 2020" @@ -749,14 +786,11 @@ type Date > Example Format "2020-06-21" with French locale as "21. juin 2020" - example_format = Date.new 2020 6 21 . format "d. MMMM yyyy" (Locale.new "fr") - @pattern (value-> make_date_format_selector value) - @locale Locale.default_widget - format : Text -> Locale -> Text - format self pattern:Text locale=Locale.default = - formatter = if pattern.is_empty then Time_Utils.default_date_formatter else - Time_Utils.make_formatter pattern locale.java_locale - Time_Utils.date_format self formatter + example_format = Date.new 2020 6 21 . format (Date_Time_Formatter.from "d. MMMM yyyy" (Locale.new "fr")) + @format (value-> make_date_format_selector value) + format : Date_Time_Formatter -> Text + format self format:Date_Time_Formatter=Date_Time_Formatter.iso_date = + format.format_date self ## PRIVATE week_days_between start end = 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 index 57a0cc7a1abc..9e2181617230 100644 --- 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 @@ -7,6 +7,7 @@ import project.Data.Ordering.Ordering import project.Data.Text.Text import project.Data.Time.Date.Date import project.Data.Time.Date_Period.Date_Period +import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter import project.Data.Time.Day_Of_Week.Day_Of_Week import project.Data.Time.Day_Of_Week_From import project.Data.Time.Duration.Duration @@ -156,23 +157,67 @@ type Date_Time Obtains an instance of `Time` from a text such as "2007-12-03T10:15:30+01:00 Europe/Paris". + This method will return a `Time_Error` if the provided time cannot be + parsed. + 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. + - format: A pattern describing how to parse the text, + or a `Date_Time_Formatter`. ? Pattern Syntax - A custom pattern string consists of one or more custom date and time - format specifiers. For example, "d MMM yyyy" will format "2011-12-03" - as "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - y: Year. The number of pattern letters determines the minimum number of + digits. + - y: The year using any number of digits. + - yy: The year, using at most two digits. The default range is + 1950-2049, but this can be changed by including the end year in + braces e.g. `yy{2099}`. + - yyyy: The year, using exactly four digits. + - M: Month of year. The number of pattern letters determines the format: + - M: Any number (1-12). + - MM: Month number with zero padding required (01-12). + - MMM: Short name of the month (Jan-Dec). + - MMMM: Full name of the month (January-December). + The month names depend on the selected locale. + - d: Day. The number of pattern letters determines the format: + - d: Any number (1-31). + - dd: Day number with zero padding required (01-31). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + Both day of week and day of month may be included in a single pattern - + in such case the day of week is used as a sanity check. + - Q: Quarter of year. + If only year and quarter are provided in the pattern, when parsing a + date, the result will be the first day of that quarter. + - H: 24h hour of day (0-23). + - h: 12h hour of day (0-12). The `a` pattern is needed to disambiguate + between AM and PM. + - m: Minute of hour. + - s: Second of minute. + - f: Fractional part of the second. The number of pattern letters + determines the number of digits. If one letter is used, any number of + digits will be accepted. + - a: AM/PM marker. + - T: If repeated 3 or less times - Time zone ID (e.g. Europe/Warsaw, Z, + -08:30), otherwise - Time zone name (e.g. Central European Time, CET). + - Z: Zone offset. + - Z, ZZ, ZZZ: A short offset form (+HHmm). + No offset is indicated by "+0000". This can be customized by setting + an alternative no offset string in curly braces, e.g. `zz{Z}`. + - ZZZZ: Localized offset (e.g. GMT-08:00). + - ZZZZZ: A full offset form (+HH:mm:ss). + No offset is indicated by "Z". This can be customized as above, e.g. + `ZZZZZ{0}`. ? Default Date_Time Format - The text must represent a valid date-time as defined by the ISO-8601 - format. (See https://en.wikipedia.org/wiki/ISO_8601.) If a time zone is - present, it must be in the ISO-8601 Extended Date/Time Format (EDTF). - (See https://en.wikipedia.org/wiki/ISO_8601#EDTF.) The time zone format - consists of: + Unless you provide a custom format, the text must represent a valid + date-time as defined by the ISO-8601 format (see https://en.wikipedia.org/wiki/ISO_8601). + If a time zone is present, it must be in the ISO-8601 Extended + Date/Time Format (EDTF) (see https://en.wikipedia.org/wiki/ISO_8601#EDTF). + The time zone format consists of: - The ISO offset date time. - If the zone ID is not available or is a zone offset then the format is @@ -182,9 +227,6 @@ type Date_Time 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. @@ -234,20 +276,10 @@ type Date_Time example_parse = Date_Time.parse "06 of May 2020 at 04:30AM" "dd 'of' MMMM yyyy 'at' hh:mma" - @pattern make_date_time_format_selector - @locale Locale.default_widget - parse : Text -> Text -> Locale -> Date_Time ! Time_Error - parse text:Text pattern:Text="" locale:Locale=Locale.default = - result = Panic.recover Any <| - formatter = if pattern.is_empty then Time_Utils.default_date_time_formatter else - Time_Utils.make_formatter pattern locale.java_locale - - needs_normalised = if pattern.is_empty then True else Time_Utils.is_iso_datetime_based pattern - normalised = if needs_normalised then Time_Utils.normalise_iso_datetime text.trim else text.trim - Time_Utils.parse_date_time normalised formatter - result . map_error <| case _ of - err : JException -> Time_Error.Error err.getMessage - ex -> ex + @format make_date_time_format_selector + parse : Text -> Date_Time_Formatter -> Date_Time ! Time_Error + parse text:Text format:Date_Time_Formatter=Date_Time_Formatter.default_enso_zoned_date_time = + format.parse_date_time text ## GROUP Metadata Get the year portion of the time. @@ -717,8 +749,9 @@ type Date_Time Convert to a display representation of this Date_Time. to_display_text : Text to_display_text self = + # TODO note that we are using a format that will not be parsed by our default formatters and needs custom formatter to be parsed. Is that OK? time_format = if self.nanosecond include_milliseconds=True == 0 then "HH:mm:ss" else "HH:mm:ss.n" - self.format "yyyy-MM-dd "+time_format+" VV" + self.format "yyyy-MM-dd "+time_format+" TT" ## PRIVATE Convert to a JavaScript Object representing a Date_Time. @@ -737,15 +770,55 @@ type Date_Time Format this time as text using the specified format specifier. Arguments: - - pattern: The pattern that specifies how to format the time. - - locale: The locale in which the format should be interpreted. - (Defaults to Locale.default.) + - format: A pattern describing how to format the text, + or a `Date_Time_Formatter`. ? Pattern Syntax - A custom pattern string consists of one or more custom date and time - format specifiers. For example, "d MMM yyyy" will format "2011-12-03" - as "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - y: Year. The number of pattern letters determines the minimum number of + digits. + - y: The year using any number of digits. + - yy: The year, using at most two digits. The default range is + 1950-2049, but this can be changed by including the end year in + braces e.g. `yy{2099}`. + - yyyy: The year, using exactly four digits. + - M: Month of year. The number of pattern letters determines the format: + - M: Any number (1-12). + - MM: Month number with zero padding required (01-12). + - MMM: Short name of the month (Jan-Dec). + - MMMM: Full name of the month (January-December). + The month names depend on the selected locale. + - d: Day. The number of pattern letters determines the format: + - d: Any number (1-31). + - dd: Day number with zero padding required (01-31). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + Both day of week and day of month may be included in a single pattern - + in such case the day of week is used as a sanity check. + - Q: Quarter of year. + If only year and quarter are provided in the pattern, when parsing a + date, the result will be the first day of that quarter. + - H: 24h hour of day (0-23). + - h: 12h hour of day (0-12). The `a` pattern is needed to disambiguate + between AM and PM. + - m: Minute of hour. + - s: Second of minute. + - f: Fractional part of the second. The number of pattern letters + determines the number of digits. If one letter is used, any number of + digits will be accepted. + - a: AM/PM marker. + - T: If repeated 3 or less times - Time zone ID (e.g. Europe/Warsaw, Z, + -08:30), otherwise - Time zone name (e.g. Central European Time, CET). + - Z: Zone offset. + - Z, ZZ, ZZZ: A short offset form (+HHmm). + No offset is indicated by "+0000". This can be customized by setting + an alternative no offset string in curly braces, e.g. `zz{Z}`. + - ZZZZ: Localized offset (e.g. GMT-08:00). + - ZZZZZ: A full offset form (+HH:mm:ss). + No offset is indicated by "Z". This can be customized as above, e.g. + `ZZZZZ{0}`. > Example Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as @@ -754,7 +827,7 @@ type Date_Time from Standard.Base import Date_Time example_format = - Date_Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "yyyy-MM-dd'T'HH:mm:ssZZZZ'['VV']'" + Date_Time.parse "2020-10-08T16:41:13+03:00[Europe/Moscow]" . format "yyyy-MM-dd'T'HH:mm:ssZZZZ'['tt']'" > Example Format "2020-10-08T16:41:13+03:00[Europe/Moscow]" as @@ -781,10 +854,7 @@ type Date_Time example_format = Date_Time.parse "2020-06-21T16:41:13+03:00" . format "d. MMMM yyyy" (Locale.new "fr") - @pattern (value-> make_date_time_format_selector value) - @locale Locale.default_widget - format : Text -> Locale -> Text - format self pattern:Text locale:Locale=Locale.default = - formatter = if pattern.is_empty then Time_Utils.default_output_date_time_formatter else - Time_Utils.make_formatter pattern locale.java_locale - Time_Utils.date_time_format self formatter + @format (value-> make_date_time_format_selector value) + format : Date_Time_Formatter -> Text + format self format:Date_Time_Formatter = + format.format_date_time self diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso new file mode 100644 index 000000000000..da83facdda11 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso @@ -0,0 +1,307 @@ +import project.Data.Locale.Locale +import project.Data.Text.Text +import project.Data.Time.Date.Date +import project.Data.Time.Date_Time.Date_Time +import project.Data.Time.Time_Of_Day.Time_Of_Day +import project.Error.Error +import project.Errors.Illegal_Argument.Illegal_Argument +import project.Errors.Time_Error.Time_Error +import project.Nothing.Nothing +import project.Panic.Panic +from project.Data.Boolean import Boolean, False, True + +import project.Internal.Time.Format.Tokenizer.Tokenizer +import project.Internal.Time.Format.Parser +import project.Internal.Time.Format.As_Java_Formatter_Interpreter + +polyglot java import java.lang.Exception as JException +polyglot java import java.time.format.DateTimeFormatter +polyglot java import org.enso.base.time.EnsoDateTimeFormatter +polyglot java import org.enso.base.time.FormatterKind + +## TODO compatibility check? can do Date can do Time? +type Date_Time_Formatter + ## PRIVATE + Value (underlying : EnsoDateTimeFormatter) + + ## Creates a formatter from a simple date-time format pattern. + + Every letter in the pattern is interpreted as a pattern character as + described in the table below. Any character that is not a letter in the + pattern is treated as a literal character. If a sequence of letters needs + to be put in as a literal, it can be escaped using single quotes. Use two + single quotes in a row to represent a single quote in the result. As + explained below, curly braces can have special meaning (see 'yy'); to + enter a literal curly brace, put it inside a quoted literal. + + Pattern characters are interpreted case insensitively, with the exception + of `M/m' and 'H/h'. + + Date pattern characters: + - y: Year. The number of pattern letters determines the minimum number of + digits. + - y: The year using any number of digits. + - yy: The year, using at most two digits. The default range is + 1950-2049, but this can be changed by including the end year in + braces e.g. `yy{2099}`. + - yyyy: The year, using exactly four digits. + - M: Month of year. The number of pattern letters determines the format: + - M: Any number (1-12). + - MM: Month number with zero padding required (01-12). + - MMM: Short name of the month (Jan-Dec). + - MMMM: Full name of the month (January-December). + The month names depend on the selected locale. + - d: Day. The number of pattern letters determines the format: + - d: Any number (1-31). + - dd: Day number with zero padding required (01-31). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + Both day of week and day of month may be included in a single pattern - + in such case the day of week is used as a sanity check. + - e: An alternative notation: single `e` maps to `ddd` and `ee` or more + map to `dddd` meaning name of day of week. + - Q: Quarter of year. + If only year and quarter are provided in the pattern, when parsing a + date, the result will be the first day of that quarter. + + Time pattern characters: + - H: 24h hour of day (0-23). + - h: 12h hour of day (0-12). The `a` pattern is needed to disambiguate + between AM and PM. + - m: Minute of hour. + - s: Second of minute. + - f: Fractional part of the second. The number of pattern letters + determines the number of digits. If one letter is used, any number of + digits will be accepted. + - a: AM/PM marker. + + Time zone pattern characters: + - T: If repeated 3 or less times - Time zone ID (e.g. Europe/Warsaw, Z, + -08:30), otherwise - Time zone name (e.g. Central European Time, CET). + - Z: Zone offset. + - Z, ZZ, ZZZ: A short offset form (+HHmm). + No offset is indicated by "+0000". This can be customized by setting + an alternative no offset string in curly braces, e.g. `zz{Z}`. + - ZZZZ: Localized offset (e.g. GMT-08:00). + - ZZZZZ: A full offset form (+HH:mm:ss). + No offset is indicated by "Z". This can be customized as above, e.g. + `ZZZZZ{0}`. + - v: Time zone name (same as TTTT). + - V: Time zone ID (same as T). + + Some parts, like fractions of a second may not be required. The square + brackets `[]` can be used to surround such optional sections. + + > Example + Parsing date/time values + + Date_Time.parse "2021-10-12T12:34:56.789+0200" "yyyy-MM-dd'T'HH:mm:ss.fZ" == (Date_Time.new 2021 10 12 12 34 56 millisecond=789 zone=(Time_Zone.new hours=2)) + Date.parse "Tue, 12 Oct 2021" "ddd, d MMM yyyy" == (Date.new 2021 10 12) + Date_Time.parse "12/10/2021 5:34 PM" "d/M/Y h:mm a" == (Date_Time.new 2021 10 12 17 34 00) + + > Example + Note that the default locale may not support full-length day/month names, so you may need to set a specific locale for this to work. + + Date.parse "Thursday, 1 October '98" (Date_Time_Formatter.from "dddd, d MMMM ''yy" Locale.uk) == (Date.new 1998 10 01) + + > Example + Omitting the day will yield the first day of the month. + + Date.parse "2021-10" "yyyy-MM" == (Date.new 2021 10 01) + + > Example + Omitting the year will yield the current year. + + Date.parse "10-12" "MM-dd" == (Date.new (Date.today.year) 10 12) + + > Example + Parsing a two-digit year with a custom base year. + + Date.parse "1 Nov '95" "d MMM ''yy{2099}" == (Date.new 2095 11 01) + @locale Locale.default_widget + from_simple_pattern pattern:Text locale:Locale=Locale.default = + java_formatter = Tokenizer.tokenize pattern |> Parser.parse_simple_date_pattern |> + As_Java_Formatter_Interpreter.interpret locale + Date_Time_Formatter.Value (EnsoDateTimeFormatter.new java_formatter pattern FormatterKind.SIMPLE) + + ## Creates a formatter from a pattern for the ISO 8601 leap week calendar. + + The ISO 8601 leap week calendar is a variation of the ISO 8601 calendar + that defines a leap week as the week that contains the 29th of February. + This calendar is used by some European and Middle Eastern countries. + + The pattern is a sequence of letters and symbols that are interpreted as + follows: + - Y: The week based year. + - In case the year is parsed in two digit mode (`YY`), the default + range is 1950-2049, but this can be changed by including the end year + in braces e.g. `YY{2099}` + - w: Week of year. + - d: Day of week. + - d: Numeric day of week (1-7). 1 is Monday. + - dd: Numeric day of week with zero padding (01-07). + - ddd: Short name of the day of week (Mon-Sun). + - dddd: Full name of the day of week (Monday-Sunday). + The weekday names depend on the selected locale. + - e: An alternative notation: single `e` maps to `ddd` and `ee` or more + map to `dddd` meaning name of day of week. + + Moreover, all time and timezone pattern characters like in `Simple` case + are supported too - in case you need to parse a date time value with the + date part in ISO week date format. + + The same as in the `Simple` pattern, the single quotes can be used to + escape letter literals and square brackets can be used to indicate + optional sections. + + > Example + Parsing a date in the ISO week date format + + Date.parse "1976-W53-6" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW-d") == (Date.new 1977 01 01) + Date.parse "1978-W01, Mon" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW, eee") == (Date.new 1978 01 02) + Date_Time.parse "1978-W01-4 12:34:56" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW-d HH:mm:ss") == (Date_Time.new 1978 01 05 12 34 56) + + > Example + Omitting the day of the week will result in the first day of that week. + + Date.parse (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW") "1978-W01" == (Date.new 1978 01 02) + @locale Locale.default_widget + from_iso_week_date_pattern pattern:Text locale:Locale=Locale.default = + java_formatter = Tokenizer.tokenize pattern |> Parser.parse_iso_week_year_pattern |> + As_Java_Formatter_Interpreter.interpret locale + Date_Time_Formatter.Value (EnsoDateTimeFormatter.new java_formatter pattern FormatterKind.ISO_WEEK_DATE) + + ## ADVANCED + Creates a formatter from a Java `DateTimeFormatter` instance or a text + pattern parsed using the Java parser: `DateTimeFormatter.ofPattern`. + + See the Java documentation for explanation of the pattern format: + https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/time/format/DateTimeFormatter.html#patterns + + Arguments: + - pattern: The pattern string to parse using the Java pattern rules, or + an existing `DateTimeFormatter` instance. + - locale: A locale to use when constructing the formatter from a text + pattern. If not specified, defaults to `Locale.default`. If passing a + `DateTimeFormatter` instance and this argument is set, it will + overwrite the original locale of that formatter. + from_java pattern (locale:Locale|Nothing=Nothing) = case pattern of + java_formatter : DateTimeFormatter -> + amended_formatter = case locale of + Nothing -> java_formatter + _ : Locale -> java_formatter.withLocale locale.java_locale + Date_Time_Formatter.Value (EnsoDateTimeFormatter.new amended_formatter Nothing FormatterKind.RAW_JAVA) + _ : Text -> + java_locale = (locale.if_nothing Locale.default).java_locale + java_formatter = DateTimeFormatter.ofPattern pattern java_locale + Date_Time_Formatter.Value (EnsoDateTimeFormatter.new java_formatter pattern FormatterKind.RAW_JAVA) + _ -> Error.throw (Illegal_Argument.Error "The pattern must either be a string or a Java DateTimeFormatter instance.") + + ## The default format for date-time used in Enso. + It acts as `ISO_Zoned_Date_Time` but both offset and timezone are optional. + + For example, it may parse date of the form `2011-12-03 10:15:30+01:00[Europe/Paris]`, + as well as `2011-12-03T10:15:30` assuming the default timezone. + default_enso_zoned_date_time = + Date_Time_Formatter.Value EnsoDateTimeFormatter.default_enso_zoned_date_time_formatter + + ## The ISO 8601 format for date-time with offset and timezone. + The date and time parts may be separated by a single space or a `T`. + + For example, it may parse date of the form `2011-12-03 10:15:30+01:00[Europe/Paris]`. + iso_zoned_date_time = + Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_ZONED_DATE_TIME "iso_zoned_date_time") + + ## The ISO 8601 format for date-time with offset. + The date and time parts may be separated by a single space or a `T`. + + For example, it may parse date of the form `2011-12-03 10:15:30+01:00`. + iso_offset_date_time = + Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_OFFSET_DATE_TIME "iso_offset_date_time") + + ## The ISO 8601 format for date-time without a timezone. + The date and time parts may be separated by a single space or a `T`. + + For example, it may parse date of the form `2011-12-03 10:15:30`. The + timezone will be set to `Time_Zone.system`. + iso_local_date_time = + Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_LOCAL_DATE_TIME "iso_local_date_time") + + ## The ISO 8601 format for date. + + For example, it may parse date of the form `2011-12-03`. + iso_date = + Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_DATE "iso_date") + + ## The ISO 8601 format for time. + + For example, it may parse time of the form `10:15:30`. + iso_time = + Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_TIME "iso_time") + + ## Returns a text representation of this formatter. + to_text : Text + to_text self = case self.underlying.getFormatterKind of + FormatterKind.CONSTANT -> + "Date_Time_Formatter." + self.underlying.getOriginalPattern + FormatterKind.SIMPLE -> + # TODO locale too? + "Date_Time_Formatter.from_simple_pattern " + self.underlying.getOriginalPattern.pretty + FormatterKind.ISO_WEEK_DATE -> + "Date_Time_Formatter.from_iso_week_date_pattern " + self.underlying.getOriginalPattern.pretty + FormatterKind.RAW_JAVA -> case self.underlying.getOriginalPattern of + original_pattern : Text -> "Date_Time_Formatter.from_java " + original_pattern.pretty + Nothing -> "Date_Time_Formatter.from_java " + self.underlying.getFormatter.to_text + + ## Parses a human-readable representation of this formatter. + to_display_text self = self.to_text + + ## Returns a copy of this formatter with a changed locale. + with_locale : Locale -> Date_Time_Formatter + with_locale self (locale : Locale) = + Date_Time_Formatter.Value (self.underlying.withLocale locale.java_locale) + + ## PRIVATE + handle_java_errors self ~action = + Panic.catch JException action caught_panic-> + Error.throw (Time_Error.Error caught_panic.payload.getMessage caught_panic.payload) + + ## PRIVATE + parse_date self (text:Text) = self.handle_java_errors <| + self.underlying.parseLocalDate text + + ## PRIVATE + parse_date_time self (text:Text) = self.handle_java_errors <| + self.underlying.parseZonedDateTime text + + ## PRIVATE + parse_time self (text:Text) = self.handle_java_errors <| + self.underlying.parseLocalTime text + + ## PRIVATE + format_date self (date:Date) = self.handle_java_errors <| + self.underlying.formatLocalDate date + + ## PRIVATE + format_date_time self (date_time:Date_Time) = self.handle_java_errors <| + self.underlying.formatZonedDateTime date_time + + ## PRIVATE + format_time self (time:Time_Of_Day) = self.handle_java_errors <| + self.underlying.formatLocalTime time + +## PRIVATE +Date_Time_Formatter.from (that:Text) (locale:Locale = Locale.default) = + Date_Time_Formatter.from_simple_pattern that locale + +## PRIVATE +type Date_Time_Format_Parse_Error + ## PRIVATE + Indicates an error during parsing of a date time format pattern. + Error message + + ## PRIVATE + to_display_text : Text + to_display_text self = + "Error parsing date/time format pattern: " + self.message 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 e92fe9875a65..e5068a23a3eb 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 @@ -6,6 +6,7 @@ import project.Data.Ordering.Comparable import project.Data.Text.Text import project.Data.Time.Date.Date import project.Data.Time.Date_Time.Date_Time +import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter import project.Data.Time.Duration.Duration import project.Data.Time.Period.Period import project.Data.Time.Time_Period.Time_Period @@ -100,21 +101,16 @@ type Time_Of_Day Arguments: - text: The text to parse as a time of day. - - pattern: The pattern to use for parsing the input text. - - locale: The locale in which the pattern should be interpreted. + - format: A pattern describing how to parse the text, + or a `Date_Time_Formatter`. Returns a `Time_Error` if the provided text cannot be parsed using the default format. - ? Pattern Syntax - A custom pattern string consists of one or more custom date and time - format specifiers. For example, "d MMM yyyy" will format "2011-12-03" - as "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. - ? Default Time Format - The text must represent a valid time and is parsed using the ISO-8601 - extended local time format. The format consists of: + Unless you provide a custom format, the text must represent a valid + time and is parsed using the ISO-8601 extended local time format. + The format consists of: - Two digits for the hour-of-day. This is pre-padded by zero to ensure two digits. @@ -131,6 +127,19 @@ type Time_Of_Day - One to nine digits for the nano-of-second. As many digits will be output as required. + ? Pattern Syntax + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - H: 24h hour of day (0-23). + - h: 12h hour of day (0-12). The `a` pattern is needed to disambiguate + between AM and PM. + - m: Minute of hour. + - s: Second of minute. + - f: Fractional part of the second. The number of pattern letters + determines the number of digits. If one letter is used, any number of + digits will be accepted. + - a: AM/PM marker. + > Example Get the time 15:05:30. @@ -160,17 +169,10 @@ type Time_Of_Day from Standard.Base import Time_Of_Day example_parse = Time_Of_Day.parse "4:30AM" "h:mma" - @pattern make_time_format_selector - @locale Locale.default_widget - parse : Text -> Text -> Locale -> Time_Of_Day ! Time_Error - parse text:Text pattern:Text="" locale:Locale=Locale.default = - result = Panic.recover Any <| - formatter = if pattern.is_empty then Time_Utils.default_time_of_day_formatter else - Time_Utils.make_formatter pattern locale.java_locale - Time_Utils.parse_time_of_day text.trim formatter - result . map_error <| case _ of - err : JException -> Time_Error.Error err.getMessage - ex -> ex + @format make_time_format_selector + parse : Text -> Date_Time_Formatter -> Time_Of_Day ! Time_Error + parse text:Text format:Date_Time_Formatter=Date_Time_Formatter.iso_time = + format.parse_time text ## GROUP Metadata Get the hour portion of the time of day. @@ -415,15 +417,21 @@ type Time_Of_Day Format this time of day using the provided formatter pattern. Arguments: - - pattern: The pattern specifying how to format the time of day. - - locale: The locale in which the format should be interpreted. - (Defaults to Locale.default.) + - format: A pattern describing how to format the text, + or a `Date_Time_Formatter`. ? Pattern Syntax - A custom pattern string consists of one or more custom date and time - format specifiers. For example, "d MMM yyyy" will format "2011-12-03" - as "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. + If the pattern is provided as `Text`, it is parsed using the format + described below. See `Date_Time_Formatter` for more options. + - H: 24h hour of day (0-23). + - h: 12h hour of day (0-12). The `a` pattern is needed to disambiguate + between AM and PM. + - m: Minute of hour. + - s: Second of minute. + - f: Fractional part of the second. The number of pattern letters + determines the number of digits. If one letter is used, any number of + digits will be accepted. + - a: AM/PM marker. > Example Format "16:21:10" as "16:21:00.1234" @@ -459,10 +467,7 @@ type Time_Of_Day from Standard.Base import Time_Of_Day example_format = Time_Of_Day.new 16 21 10 . format "'hour:'h" - @pattern (value-> make_time_format_selector value) - @locale Locale.default_widget - format : Text -> Locale -> Text - format self pattern:Text locale:Locale=Locale.default = - formatter = if pattern.is_empty then Time_Utils.default_time_of_day_formatter else - Time_Utils.make_formatter pattern locale.java_locale - Time_Utils.time_of_day_format self formatter + @format (value-> make_time_format_selector value) + format : Date_Time_Formatter -> Text + format self format:Date_Time_Formatter = + format.format_time self diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/Time_Error.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/Time_Error.enso index 587761dbf38f..c37620bd2876 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/Time_Error.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/Time_Error.enso @@ -1,4 +1,5 @@ import project.Data.Text.Text +import project.Nothing.Nothing type Time_Error ## PRIVATE @@ -8,7 +9,8 @@ type Time_Error Arguments: - error_message: The message for the error. - Error error_message + - cause: An optional exception that caused this error (usually a Java Exception). + Error error_message cause=Nothing ## PRIVATE epoch_start : Time_Error diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/As_Java_Formatter_Interpreter.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/As_Java_Formatter_Interpreter.enso new file mode 100644 index 000000000000..07a65f5e483a --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/As_Java_Formatter_Interpreter.enso @@ -0,0 +1,96 @@ +import project.Data.Locale.Locale +import project.Data.Text.Text +import project.Data.Time.Date.Date +import project.Data.Vector.Vector +import project.Errors.Illegal_Argument.Illegal_Argument +import project.Panic.Panic +from project.Data.Boolean import Boolean, False, True + +from project.Internal.Time.Format.Parser import Common_Nodes, Standard_Date_Patterns, ISO_Week_Year_Patterns, Time_Patterns, Time_Zone_Patterns +from project.Internal.Time.Format.Parser import Text_Representation, Numeric_Representation, Two_Digit_Year_Representation + +polyglot java import java.time.format.DateTimeFormatter +polyglot java import java.time.format.DateTimeFormatterBuilder +polyglot java import java.time.format.SignStyle +polyglot java import java.time.format.TextStyle +polyglot java import java.time.temporal.ChronoField +polyglot java import java.time.temporal.IsoFields +polyglot java import org.enso.base.Time_Utils + +## PRIVATE +interpret : Locale -> Vector (Common_Nodes | Standard_Date_Patterns | ISO_Week_Year_Patterns | Time_Patterns | Time_Zone_Patterns) -> DateTimeFormatter +interpret locale nodes = + builder = DateTimeFormatterBuilder.new + interpret_node node = case node of + Common_Nodes.Literal text -> + builder.appendLiteral text + Common_Nodes.Optional_Section nested_nodes -> + builder.optionalStart + nested_nodes.each interpret_node + builder.optionalEnd + + Time_Zone_Patterns.Time_Zone_ID -> + builder.appendZoneId + Time_Zone_Patterns.Time_Zone_Name representation -> + builder.appendZoneText (text_representation_to_java_style representation) + Time_Zone_Patterns.Time_Zone_Offset pattern zero -> + builder.appendOffset pattern zero + Time_Zone_Patterns.Time_Zone_Localized_Offset representation -> + builder.appendLocalizedOffset (text_representation_to_java_style representation) + Time_Patterns.AM_PM -> + builder.appendText ChronoField.AMPM_OF_DAY + Time_Patterns.Fraction_Of_Second representation -> + min_digits = 1 + max_digits = case representation.digits of + 1 -> 9 + digits -> digits + includes_decimal_point = False + builder.appendFraction ChronoField.NANO_OF_SECOND min_digits max_digits includes_decimal_point + + Standard_Date_Patterns.Quarter _ -> + field = get_field_for node + append_field builder field node.representation + # We currently don't even have a way to specify day of quarter, we expect just (year, quarter) pairs - so to make them parseable, we default to first day of the quarter. + builder.parseDefaulting IsoFields.DAY_OF_QUARTER 1 + + _ -> + field = get_field_for node + append_field builder field node.representation + + nodes.each interpret_node + builder.toFormatter locale.java_locale + +## PRIVATE +get_field_for node = case node of + Standard_Date_Patterns.Year _ -> ChronoField.YEAR + Standard_Date_Patterns.Quarter _ -> IsoFields.QUARTER_OF_YEAR + Standard_Date_Patterns.Month _ -> ChronoField.MONTH_OF_YEAR + Standard_Date_Patterns.Day_Of_Month _ -> ChronoField.DAY_OF_MONTH + Standard_Date_Patterns.Day_Of_Week _ -> ChronoField.DAY_OF_WEEK + + ISO_Week_Year_Patterns.Week_Based_Year _ -> IsoFields.WEEK_BASED_YEAR + ISO_Week_Year_Patterns.Week_Of_Year _ -> IsoFields.WEEK_OF_WEEK_BASED_YEAR + ISO_Week_Year_Patterns.Day_Of_Week _ -> ChronoField.DAY_OF_WEEK + + Time_Patterns.Hour _ is24h -> case is24h of + True -> ChronoField.HOUR_OF_DAY + False -> ChronoField.CLOCK_HOUR_OF_AMPM + Time_Patterns.Minute _ -> ChronoField.MINUTE_OF_HOUR + Time_Patterns.Second _ -> ChronoField.SECOND_OF_MINUTE + + _ -> Panic.throw (Illegal_Argument.Error "Cannot extract a TemporalField from "+node.to_text) + +## PRIVATE +append_field builder field representation = case representation of + Numeric_Representation.Value digits -> case digits of + 2 -> builder.appendValue field 2 + _ -> builder.appendValue field digits 19 SignStyle.NORMAL + text_representation : Text_Representation -> + builder.appendText field (text_representation_to_java_style text_representation) + Two_Digit_Year_Representation.Value max_year -> + Time_Utils.appendTwoDigitYear builder field max_year + +## PRIVATE +text_representation_to_java_style representation = case representation of + Text_Representation.Short_Form -> TextStyle.SHORT + Text_Representation.Long_Form -> TextStyle.FULL diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Parser.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Parser.enso new file mode 100644 index 000000000000..0254d672ccde --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Parser.enso @@ -0,0 +1,296 @@ +import project.Data.Numbers.Integer +import project.Data.Numbers.Number_Parse_Error +import project.Data.Text.Case.Case +import project.Data.Text.Text +import project.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error +import project.Data.Vector.Builder as Vector_Builder +import project.Data.Vector.Vector +import project.Error.Error +import project.Errors.Illegal_State.Illegal_State +import project.Nothing.Nothing +import project.Panic.Panic +import project.Runtime.Ref.Ref +from project.Data.Boolean import Boolean, False, True +from project.Data.Text.Extensions import all + +import project.Internal.Time.Format.Tokenizer.Format_Token + +## PRIVATE +type Text_Representation + ## PRIVATE + Short_Form + + ## PRIVATE + Long_Form + +## PRIVATE +type Numeric_Representation + ## PRIVATE + Value digits:Integer + +## PRIVATE +type Two_Digit_Year_Representation + ## PRIVATE + Value max_year:Integer + +## PRIVATE +type Common_Nodes + ## PRIVATE + Literal text:Text + + ## PRIVATE + Optional_Section inner_nodes:Vector + +## PRIVATE +type Standard_Date_Patterns + ## PRIVATE + Year (representation : Numeric_Representation | Two_Digit_Year_Representation) + + ## PRIVATE + Quarter (representation : Numeric_Representation) + + ## PRIVATE + Month (representation : Numeric_Representation | Text_Representation) + + ## PRIVATE + Day_Of_Month (representation : Numeric_Representation) + + ## PRIVATE + Day_Of_Week (representation : Text_Representation) + +## PRIVATE +type ISO_Week_Year_Patterns + ## PRIVATE + Week_Based_Year (representation : Numeric_Representation | Two_Digit_Year_Representation) + + ## PRIVATE + Week_Of_Year (representation : Numeric_Representation) + + ## PRIVATE + Day_Of_Week (representation : Numeric_Representation | Text_Representation) + +## PRIVATE +type Time_Patterns + ## PRIVATE + Hour (representation : Numeric_Representation) is_24h:Boolean + + ## PRIVATE + AM_PM + + ## PRIVATE + Minute (representation : Numeric_Representation) + + ## PRIVATE + Second (representation : Numeric_Representation) + + ## PRIVATE + Fraction_Of_Second (representation : Numeric_Representation) + +## PRIVATE +type Time_Zone_Patterns + ## PRIVATE + Time_Zone_Name representation:Text_Representation + + ## PRIVATE + Time_Zone_ID + + ## PRIVATE + Time_Zone_Offset pattern:Text zero:Text + + ## PRIVATE + Time_Zone_Localized_Offset representation:Text_Representation + +## PRIVATE +type Parser_Mode + ## PRIVATE + Simple + + ## PRIVATE + ISO_Week_Year + + ## PRIVATE + pattern_format_name self = case self of + Parser_Mode.Simple -> "Simple" + Parser_Mode.ISO_Week_Year -> "ISO Week-Date" + +## PRIVATE +type Parser + ## PRIVATE + Instance (tokens : Vector (Format_Token | Nothing)) (position : Ref Integer) (mode : Parser_Mode) + + ## PRIVATE + new tokens mode = + Parser.Instance tokens=tokens+[Nothing] position=(Ref.new 0) mode=mode + + ## PRIVATE + run self = Panic.recover Date_Time_Format_Parse_Error <| + result_builder = Vector.new_builder + go _ = case self.consume_token of + Nothing -> Nothing + Format_Token.Optional_Section_Start -> + inner_nodes = self.run_optional + result_builder.append (Common_Nodes.Optional_Section inner_nodes) + @Tail_Call go Nothing + other_token -> + parsed_node = self.parse_common_token other_token + result_builder.append parsed_node + @Tail_Call go Nothing + go Nothing + result_builder.to_vector + + ## PRIVATE + run_optional self = + result_builder = Vector.new_builder + go _ = case self.consume_token of + Nothing -> Panic.throw (Illegal_State.Error "Unterminated optional section. This should have been caught by the tokenizer.") + Format_Token.Optional_Section_End -> Nothing + other_token -> + parsed_node = self.parse_common_token other_token + result_builder.append parsed_node + @Tail_Call go Nothing + go Nothing + result_builder.to_vector + + ## PRIVATE + parse_common_token self token = case token of + Format_Token.Literal text -> + Common_Nodes.Literal text + Format_Token.Curly_Section inner_text -> + Panic.throw (Date_Time_Format_Parse_Error.Error "Unexpected section in curly braces: {"+inner_text+"}. If you want to include a curly brace as literal, escape it with single quotes like '{"+inner_text+"}'") + Format_Token.Pattern character count -> + date_pattern = case self.mode of + Parser_Mode.Simple -> self.parse_simple_date_pattern character count + Parser_Mode.ISO_Week_Year -> self.parse_iso_week_year_pattern character count + any_pattern = date_pattern.if_nothing <| + self.parse_time_or_timezone_pattern character count + any_pattern.if_nothing <| + self.fail_invalid_pattern character count + any_pattern + _ -> Panic.throw (Illegal_State.Error "Unexpected (here) token type: "+token.to_text) + + fail_invalid_pattern self character count extra_message="" = + Panic.throw (Date_Time_Format_Parse_Error.Error "The pattern "+(character*count)+" is not a valid pattern for the "+self.mode.pattern_format_name+" format."+extra_message) + + ## PRIVATE + consume_token self = + current_position = self.position.get + self.position.put current_position+1 + self.tokens.at current_position + + ## PRIVATE + Checks if the next token is a curly brace parameter. + If it is, it is consumed and its value (as Text) is returned. + Otherwise, returns Nothing and does not move the cursor. + consume_curly_parameter_if_exists : Text | Nothing + consume_curly_parameter_if_exists self = + current_position = self.position.get + case self.tokens.at current_position of + Format_Token.Curly_Section inner_text -> + self.position.put current_position+1 + inner_text + _ -> + # If no Curly_Section is set, do not advance the pointer and return Nothing. + Nothing + + ## PRIVATE + resolve_year_representation self count = case count of + 2 -> + max_year = case self.consume_curly_parameter_if_exists of + Nothing -> default_max_year + parameter_text -> + Integer.parse parameter_text . catch Number_Parse_Error _-> + Panic.throw (Date_Time_Format_Parse_Error.Error "The curly braces setting the maximum year for `yy` must be an integer, but got: {"+parameter_text+"}.") + Two_Digit_Year_Representation.Value max_year=max_year + _ -> + Numeric_Representation.Value count + + ## PRIVATE + parse_simple_date_pattern self character count = + lowercase = character.to_case Case.Lower + case lowercase of + "y" -> Standard_Date_Patterns.Year (self.resolve_year_representation count) + "q" -> Standard_Date_Patterns.Quarter (Numeric_Representation.Value count) + "m" -> case character of + "M" -> + representation = case count of + 1 -> Numeric_Representation.Value 1 + 2 -> Numeric_Representation.Value 2 + 3 -> Text_Representation.Short_Form + 4 -> Text_Representation.Long_Form + _ -> self.fail_invalid_pattern character count " The month pattern takes at most 4 letters." + Standard_Date_Patterns.Month representation + # Lowercase form is reserved for minutes - handled elsewhere. + "m" -> Nothing + "d" -> case count of + 1 -> Standard_Date_Patterns.Day_Of_Month (Numeric_Representation.Value 1) + 2 -> Standard_Date_Patterns.Day_Of_Month (Numeric_Representation.Value 2) + 3 -> Standard_Date_Patterns.Day_Of_Week (Text_Representation.Short_Form) + 4 -> Standard_Date_Patterns.Day_Of_Week (Text_Representation.Long_Form) + _ -> self.fail_invalid_pattern character count " The day pattern takes at most 4 letters." + "e" -> case count of + 1 -> Standard_Date_Patterns.Day_Of_Week (Text_Representation.Short_Form) + _ -> Standard_Date_Patterns.Day_Of_Week (Text_Representation.Long_Form) + "l" -> self.fail_invalid_pattern character count " If you want to represent the month as text use `MMM` for the short form and `MMMM` for the long form, or use `from_java` for the Java syntax." + "w" -> self.fail_invalid_pattern character count " If you want to use the week of year, consider using `from_iso_week_date_pattern` that handles the ISO 8601 leap week calendar." + _ -> Nothing + + ## PRIVATE + parse_iso_week_year_pattern self character count = + lowercase = character.to_case Case.Lower + case lowercase of + "y" -> ISO_Week_Year_Patterns.Week_Based_Year (self.resolve_year_representation count) + "w" -> ISO_Week_Year_Patterns.Week_Of_Year (Numeric_Representation.Value count) + "d" -> case count of + 1 -> ISO_Week_Year_Patterns.Day_Of_Week (Numeric_Representation.Value 1) + 2 -> ISO_Week_Year_Patterns.Day_Of_Week (Numeric_Representation.Value 2) + 3 -> ISO_Week_Year_Patterns.Day_Of_Week (Text_Representation.Short_Form) + 4 -> ISO_Week_Year_Patterns.Day_Of_Week (Text_Representation.Long_Form) + _ -> self.fail_invalid_pattern character count " The day pattern takes at most 4 letters." + "e" -> case count of + 1 -> ISO_Week_Year_Patterns.Day_Of_Week (Text_Representation.Short_Form) + _ -> ISO_Week_Year_Patterns.Day_Of_Week (Text_Representation.Long_Form) + _ -> Nothing + + ## PRIVATE + parse_time_or_timezone_pattern self character count = + lowercase = character.to_case Case.Lower + text_representation = if count <= 3 then Text_Representation.Short_Form else Text_Representation.Long_Form + case lowercase of + "h" -> case character of + "H" -> Time_Patterns.Hour (Numeric_Representation.Value count) is_24h=True + "h" -> Time_Patterns.Hour (Numeric_Representation.Value count) is_24h=False + "m" -> case character of + "m" -> Time_Patterns.Minute (Numeric_Representation.Value count) + # Lowercase form is reserved for months - handled elsewhere. + "M" -> Nothing + "s" -> Time_Patterns.Second (Numeric_Representation.Value count) + "f" -> + if count > 9 then + Panic.throw (Date_Time_Format_Parse_Error.Error "It is meaningless to have more than 9 digits in the fractional-of-second pattern, because at most nanosecond precision of seconds is currently supported.") + Time_Patterns.Fraction_Of_Second (Numeric_Representation.Value count) + "a" -> Time_Patterns.AM_PM + "t" -> if count <= 3 then Time_Zone_Patterns.Time_Zone_ID else Time_Zone_Patterns.Time_Zone_Name text_representation + "v" -> case character of + "V" -> Time_Zone_Patterns.Time_Zone_ID + "v" -> Time_Zone_Patterns.Time_Zone_Name text_representation + "z" -> case count of + 4 -> Time_Zone_Patterns.Time_Zone_Localized_Offset Text_Representation.Long_Form + 5 -> + no_offset_string = self.consume_curly_parameter_if_exists.if_nothing "Z" + Time_Zone_Patterns.Time_Zone_Offset "+HH:MM:ss" no_offset_string + _ -> if count > 5 then self.fail_invalid_pattern character count " Too many characters for timezone format, 5 is a maximum." else + no_offset_string = self.consume_curly_parameter_if_exists.if_nothing "+0000" + Time_Zone_Patterns.Time_Zone_Offset "+HHMM" no_offset_string + "x" -> self.fail_invalid_pattern character count " If you want to represent the time zone offset use `zz`, or use `from_java` for the Java syntax." + _ -> Nothing + +## PRIVATE +default_max_year = 2049 + +## PRIVATE +parse_simple_date_pattern tokens = + Parser.new tokens mode=Parser_Mode.Simple . run + +## PRIVATE +parse_iso_week_year_pattern tokens = + Parser.new tokens mode=Parser_Mode.ISO_Week_Year . run diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Tokenizer.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Tokenizer.enso new file mode 100644 index 000000000000..e7b7b5be3945 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Tokenizer.enso @@ -0,0 +1,144 @@ +import project.Data.Numbers.Integer +import project.Data.Text.Text +import project.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error +import project.Data.Vector.Builder as Vector_Builder +import project.Data.Vector.Vector +import project.Error.Error +import project.Nothing.Nothing +import project.Panic.Panic +import project.Runtime.Ref.Ref +from project.Data.Boolean import Boolean, False, True +from project.Data.Text.Extensions import all + +polyglot java import org.enso.base.Text_Utils + +## PRIVATE +type Format_Token + ## PRIVATE + A format pattern described by a single character and count. + Pattern character:Text count:Integer + + ## PRIVATE + A literal text string. + Literal text:Text + + ## PRIVATE + Indicates beginning of an optional section. + Optional_Section_Start + + ## PRIVATE + Indicates end of an optional section. + Optional_Section_End + + ## PRIVATE + A special parameter in curly braces. + + Currently only used to customize base year for `yy`, i.e. `yy{2099}`. + Curly_Section (inner_text : Text) + +## PRIVATE +type Tokenizer + ## PRIVATE + A helper type to hold the state of the tokenizer. + Normally, we could keep these in the closure, inside of a method. + But our 3 parse methods need to be able to call each other, and mutual + recursion of variables defined inside of a method is not supported in + Enso. So to achieve the mutual recursion, we instead define these as + member methods. + Instance (original_text : Text) (chars : Vector Text) (tokens_builder : Vector_Builder Format_Token) (is_in_optional : Ref Boolean) + + ## PRIVATE + new : Text -> Tokenizer + new text = + # Nothing is appended at the and as a guard to avoid checking for length. + Tokenizer.Instance text text.characters+[Nothing] Vector.new_builder (Ref.new False) + + ## PRIVATE + finalize_token self current_token = case current_token of + Nothing -> Nothing + _ -> self.tokens_builder.append current_token + + ## PRIVATE + parse_normal self position current_token = case self.chars.at position of + Nothing -> + if self.is_in_optional.get then + Panic.throw (Date_Time_Format_Parse_Error.Error "Unterminated optional section within the pattern "+self.original_text.to_display_text) + self.finalize_token current_token + Nothing + "'" -> + self.finalize_token current_token + @Tail_Call self.parse_quoted position+1 "" + "[" -> + if self.is_in_optional.get then + Panic.throw (Date_Time_Format_Parse_Error.Error "Nested optional sections are not allowed (at position "+position.to_text+" in pattern "+self.original_text.to_display_text+").") + self.finalize_token current_token + self.tokens_builder.append Format_Token.Optional_Section_Start + self.is_in_optional.put True + @Tail_Call self.parse_normal position+1 Nothing + "]" -> + if self.is_in_optional.get.not then + Panic.throw (Date_Time_Format_Parse_Error.Error "Unmatched closing bracket ] (at position "+position.to_text+" in pattern "+self.original_text.to_display_text+").") + self.finalize_token current_token + self.tokens_builder.append Format_Token.Optional_Section_End + self.is_in_optional.put False + @Tail_Call self.parse_normal position+1 Nothing + "{" -> + self.finalize_token current_token + @Tail_Call self.parse_curly position+1 "" + new_character -> + case Text_Utils.is_all_letters new_character of + True -> + is_matching_current_token = case current_token of + Format_Token.Pattern current_pattern_character _ -> + current_pattern_character == new_character + _ -> False + case is_matching_current_token of + True -> + @Tail_Call self.parse_normal position+1 (Format_Token.Pattern current_token.character current_token.count+1) + False -> + self.finalize_token current_token + @Tail_Call self.parse_normal position+1 (Format_Token.Pattern new_character 1) + False -> + self.finalize_token current_token + self.tokens_builder.append (Format_Token.Literal new_character) + @Tail_Call self.parse_normal position+1 Nothing + + ## PRIVATE + parse_quoted self position text_accumulator = case self.chars.at position of + Nothing -> + Panic.throw (Date_Time_Format_Parse_Error.Error "Unterminated quoted sequence within the pattern "+self.original_text.to_display_text) + "'" -> + # Next letter is always accessible, but it may be Nothing. + next_letter = self.chars.at position+1 + case next_letter of + # If the next letter is a quote, that means an escaped single quote within a quoted section. + "'" -> + @Tail_Call self.parse_quoted position+2 text_accumulator+"'" + + # If the next letter is not a quote, that means the end of the quoted sequence. + _ -> + case text_accumulator.is_empty of + # If there is no text between the quotes, that means this whole quoted sequence was just an escaped single quote OUTSIDE a quoted section. + True -> + self.tokens_builder.append (Format_Token.Literal "'") + False -> + self.tokens_builder.append (Format_Token.Literal text_accumulator) + @Tail_Call self.parse_normal position+1 Nothing + other_character -> @Tail_Call self.parse_quoted position+1 text_accumulator+other_character + + ## PRIVATE + parse_curly self position text_accumulator = case self.chars.at position of + Nothing -> + Panic.throw (Date_Time_Format_Parse_Error.Error "Unterminated curly sequence within the pattern "+self.original_text.to_display_text) + "}" -> + self.tokens_builder.append (Format_Token.Curly_Section text_accumulator) + @Tail_Call self.parse_normal position+1 Nothing + other_character -> + @Tail_Call self.parse_curly position+1 text_accumulator+other_character + + ## PRIVATE + tokenize : Text -> Vector Format_Token + tokenize text = Panic.recover Date_Time_Format_Parse_Error <| + tokenizer = Tokenizer.new text + tokenizer.parse_normal 0 Nothing + tokenizer.tokens_builder.to_vector diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso index 627f627c2ce1..60f5a8a8ef57 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso @@ -40,6 +40,7 @@ import project.Data.Time.Date.Date import project.Data.Time.Date_Period.Date_Period import project.Data.Time.Date_Range.Date_Range import project.Data.Time.Date_Time.Date_Time +import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter import project.Data.Time.Day_Of_Week.Day_Of_Week import project.Data.Time.Day_Of_Week_From import project.Data.Time.Duration.Duration @@ -128,6 +129,7 @@ export project.Data.Time.Date.Date export project.Data.Time.Date_Period.Date_Period export project.Data.Time.Date_Range.Date_Range export project.Data.Time.Date_Time.Date_Time +export project.Data.Time.Date_Time_Formatter.Date_Time_Formatter export project.Data.Time.Day_Of_Week.Day_Of_Week export project.Data.Time.Day_Of_Week_From export project.Data.Time.Duration.Duration @@ -175,6 +177,7 @@ from project.Data.Numbers export Float, Integer, Number from project.Data.Range.Extensions export all from project.Data.Statistics export all hiding to_moment_statistic, wrap_java_call, calculate_correlation_statistics, calculate_spearman_rank, calculate_correlation_statistics_matrix, compute_fold, empty_value, is_valid from project.Data.Text.Extensions export all +from project.Data.Time.Conversions export all from project.Errors.Problem_Behavior.Problem_Behavior export all from project.Function export all from project.Meta.Enso_Project export enso_project diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso index 8c4cfb284871..8101fa15ecc9 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso @@ -1,6 +1,9 @@ +import project.Data.Locale.Locale import project.Data.Time.Date.Date import project.Data.Time.Date_Time.Date_Time import project.Data.Time.Time_Of_Day.Time_Of_Day +import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter +import project.Meta import project.Metadata.Widget from project.Metadata import make_single_choice @@ -21,28 +24,51 @@ make_delimiter_selector = Creates a Single_Choice Widget for parsing dates. make_date_format_selector : Date -> Widget make_date_format_selector (date:Date=(Date.new 2012 3 14)) = - iso_format = ['ISO-Format (e.g. ' + date.to_text + ')', '""'] - formats = ['d/M/yyyy', 'dd/MM/yyyy', 'd-MMM-yy', 'd MMMM yyyy', 'M/d/yyyy', 'MM/dd/yyyy', 'MMMM d, yyyy'].map f-> [f + " (e.g. " + date.format f + ")", f.pretty] + fqn = Meta.get_qualified_type_name Date_Time_Formatter + iso_format = ['ISO-Format (e.g. ' + date.format Date_Time_Formatter.iso_date + ')', fqn+".iso_date"] + formats = ['d/M/yyyy', 'dd/MM/yyyy', 'd-MMM-yy', 'd MMMM yyyy', 'M/d/yyyy', 'MM/dd/yyyy', 'MMMM d, yyyy', 'yyyy-MM'].map f-> + [f + " (e.g. " + date.format f + ")", f.pretty] + custom_locale_format = + format = Date_Time_Formatter.from 'd MMMM yyyy' locale=Locale.france + ['d MMMM yyyy - with custom Locale (e.g. ' + date.format format + ')', "(Date_Time_Formatter.from 'd MMMM yyyy' locale=Locale.france)"] + week_date_formats = ['YYYY-ww-d', 'YYYY-ww', 'ddd, YYYY-ww'].map f-> + format = Date_Time_Formatter.from_iso_week_date_pattern f + ["ISO Week-based Date: " + f + " (e.g. " + date.format format + ")", "("+fqn+".from_iso_week_date_pattern " + f.pretty + ")"] - make_single_choice ([iso_format] + formats) + make_single_choice ([iso_format] + formats + [custom_locale_format] + week_date_formats) ## PRIVATE Creates a Single_Choice Widget for parsing date times. make_date_time_format_selector : Date_Time -> Widget make_date_time_format_selector (date_time:Date_Time=(Date_Time.new 2012 3 14 15 9 26 123)) = - enso_format = ['Default (e.g. ' + date_time.to_text + ')', '""'] - iso_format = ['ISO-Format (e.g. ' + (date_time.format "ISO_ZONED_DATE_TIME") + ')', '"ISO_ZONED_DATE_TIME"'] - iso_local = ['ISO-Local (e.g. ' + (date_time.format "ISO_LOCAL_DATE_TIME") + ')', '"ISO_LOCAL_DATE_TIME"'] - formats = ['yyyy-MM-dd HH:mm:ss.S', 'yyyy-MM-dd HH:mm:ss.S VV', 'd/M/yyyy h:mm a', 'dd/MM/yyyy HH:mm:ss', 'd-MMM-yy HH:mm:ss', 'd-MMM-yy h:mm:ss a', 'd MMMM yyyy h:mm a', 'M/d/yyyy h:mm:ss a', 'MM/dd/yyyy HH:mm:ss'] + fqn = Meta.get_qualified_type_name Date_Time_Formatter + enso_format = + format = Date_Time_Formatter.default_enso_zoned_date_time + ['Default (e.g. ' + date_time.format format + ')', fqn+".default_enso_zoned_date_time"] + iso_format = + format = Date_Time_Formatter.iso_zoned_date_time + ['ISO-Format (e.g. ' + (date_time.format format) + ')', fqn+".iso_zoned_date_time"] + iso_local = + format = Date_Time_Formatter.iso_local_date_time + ['ISO-Local (e.g. ' + (date_time.format format) + ')', fqn+".iso_local_date_time"] + formats = ['yyyy-MM-dd HH:mm:ss.f', 'yyyy-MM-dd HH:mm:ss.f TT', 'd/M/yyyy h:mm a', 'dd/MM/yyyy HH:mm:ss', 'd-MMM-yy HH:mm:ss', 'd-MMM-yy h:mm:ss a', 'd MMMM yyyy h:mm a', 'M/d/yyyy h:mm:ss a', 'MM/dd/yyyy HH:mm:ss'] mapped_formats = formats.map f-> [f + " (e.g. " + date_time.format f + ")", f.pretty] + custom_locale_format = + format = Date_Time_Formatter.from 'd MMMM yyyy HH:mm' locale=Locale.france + ['d MMMM yyyy HH:mm - with custom Locale (e.g. ' + date_time.format format + ')', "(Date_Time_Formatter.from 'd MMMM yyyy HH:mm' locale=Locale.france)"] + week_date_formats = ['YYYY-ww-d HH:mm:ss.f'].map f-> + format = Date_Time_Formatter.from_iso_week_date_pattern f + ["ISO Week-based Date-Time: " + f + " (e.g. " + date_time.format format + ")", "(Date_Time_Formatter.from_iso_week_date_pattern " + f.pretty + ")"] - make_single_choice ([enso_format, iso_format, iso_local] + mapped_formats) + make_single_choice ([enso_format, iso_format, iso_local] + mapped_formats + [custom_locale_format] + week_date_formats) ## PRIVATE Creates a Single_Choice Widget for parsing times. make_time_format_selector : Time_Of_Day -> Widget make_time_format_selector (time:Time_Of_Day=(Time_Of_Day.new 13 30 55 123)) = - iso_format = ['ISO-Format (e.g. ' + time.to_text + ')', '""'] - formats = ['HH:mm[:ss]', 'HH:mm:ss', 'h:mm[:ss] a', 'hh:mm:ss a', 'HH:mm:ss.S'].map f-> [f + " (e.g. " + time.format f + ")", f.pretty] + fqn = Meta.get_qualified_type_name Date_Time_Formatter + iso_format = ['ISO-Format (e.g. ' + time.format Date_Time_Formatter.iso_time + ')', fqn+".iso_time"] + formats = ['HH:mm[:ss]', 'HH:mm:ss', 'h:mm[:ss] a', 'hh:mm:ss a', 'HH:mm:ss.S'].map f-> + [f + " (e.g. " + time.format f + ")", f.pretty] make_single_choice ([iso_format] + formats) diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso index 66feea10b6a9..bd4b51cc806a 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso @@ -1522,8 +1522,8 @@ type Column ## GROUP Standard.Base.Conversions Formatting values is not supported in database columns. @locale Locale.default_widget - format : Text | Column -> Locale -> Column ! Illegal_Argument - format self format=Nothing locale=Locale.default = + format : Text | Date_Time_Formatter | Column -> Locale -> Column ! Illegal_Argument + format self (format : Text | Date_Time_Formatter | Column | Nothing)=Nothing locale=Locale.default = _ = [format, locale] Error.throw <| Unsupported_Database_Operation.Error "`Column.format` is not implemented yet for the Database backends." diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso index bb7bf7bc3f25..d9c38acdf63d 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso @@ -1619,7 +1619,12 @@ type Column Arguments: - format: The type-dependent format string to use to format the values. If `format` is `""` or `Nothing`, .to_text is used to format the value. + In case of date/time columns, the format can also be a + `Date_Time_Formatter`. - locale: The locale in which the format should be interpreted. + If a `Date_Time_Formatter` is provided for `format` and the `locale` is + set to anything else than `Locale.default`, then that locale will + override the formatters locale. ! Error Conditions @@ -1637,15 +1642,7 @@ type Column ? `Value_Type.Date`, `Value_Type.Date_Time`, `Value_Type.Time` format strings - A custom pattern string consists of one or more custom date and time - format specifiers. For example, "d MMM yyyy" will format "2011-12-03" - as "3 Dec 2011". See https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/time/format/DateTimeFormatter.html - for a complete format specification. - - Note that the format string can specify decimal point and digit - separators, but these characters are interpreted in the context of the - Locale used. The format string specifies their location, but the Locale - has the final decision about which characters are used. + See `Date_Time_Formatter` for more details. ? `Value_Type.Integer`, `Value_Type.Float` format strings @@ -1695,26 +1692,17 @@ type Column input.format "#,##0.00" locale=(Locale.new "fr") # ==> ["100 000 000,00", "2 222,00", "3,00"] @locale Locale.default_widget - format : Text | Column -> Locale -> Column ! Illegal_Argument - format self format=Nothing locale=Locale.default = - create_formatter = make_value_formatter_for_value_type self.value_type locale - + format : Text | Date_Time_Formatter | Column -> Locale -> Column ! Illegal_Argument + format self (format : Text | Date_Time_Formatter | Column | Nothing)=Nothing locale=Locale.default = new_column = case format of - "" -> - formatter = .to_text - Column_Ops.map_over_storage self formatter make_string_builder on_problems=Problem_Behavior.Report_Error - Nothing -> - formatter = .to_text - Column_Ops.map_over_storage self formatter make_string_builder on_problems=Problem_Behavior.Report_Error - _ : Text -> - formatter = create_formatter - formatter.if_not_error <| - Column_Ops.map_over_storage self (formatter format=format) make_string_builder on_problems=Problem_Behavior.Report_Error format_column : Column -> Value_Type.expect_text format_column <| - formatter = create_formatter + formatter = make_value_formatter_for_value_type self.value_type locale formatter.if_not_error <| - Column_Ops.map_2_over_storage self format_column formatter make_string_builder - _ -> Error.throw <| Illegal_Argument.Error <| "Unsupported format type: " + format.to_text + formatter_flipped value format = formatter format value + Column_Ops.map_2_over_storage self format_column formatter_flipped make_string_builder + _ -> + formatter = make_value_formatter_for_value_type self.value_type locale format + Column_Ops.map_over_storage self formatter make_string_builder on_problems=Problem_Behavior.Report_Error new_column ## GROUP Standard.Base.Conversions 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 b0997fd5adf9..feddce91dbb9 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,11 +1,16 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.Metadata.Display +from Standard.Base.Metadata.Widget import Vector_Editor +from Standard.Base.Widget_Helpers import make_date_format_selector, make_time_format_selector, make_date_time_format_selector + import project.Data.Type.Storage import project.Internal.Java_Problems import project.Internal.Parse_Values_Helper from project.Data.Type.Value_Type import Auto, Bits, Value_Type +polyglot java import java.lang.Exception as Java_Exception polyglot java import java.lang.IllegalArgumentException polyglot java import org.enso.table.formatting.AnyObjectFormatter polyglot java import org.enso.table.formatting.BooleanFormatter @@ -28,10 +33,7 @@ type Data_Formatter ## Specifies options for reading text data in a table to more specific types and serializing them back. - For date and time formats, a Java format string can be used or one of - `ENSO_ZONED_DATE_TIME`, `ISO_ZONED_DATE_TIME`, `ISO_LOCAL_DATE_TIME`, - `ISO_OFFSET_DATE_TIME`, `ISO_LOCAL_DATE`, `ISO_LOCAL_TIME` to use a - predefined format. + For date and time formats, see `Date_Time_Formatter`. Arguments: - trim_values: Trim whitespace before parsing. @@ -51,10 +53,12 @@ type Data_Formatter - datetime_formats: Expected datetime formats. - date_formats: Expected date formats. - time_formats: Expected time formats. - - datetime_locale: The locale to use when parsing dates and times. - true_values: Values representing True. - false_values: Values representing False. - Value trim_values:Boolean=True allow_leading_zeros:Boolean=False decimal_point:Text|Auto=Auto thousand_separator:Text='' allow_exponential_notation:Boolean=False datetime_formats:(Vector Text)=["ENSO_ZONED_DATE_TIME"] date_formats:(Vector Text)=["ISO_LOCAL_DATE"] time_formats:(Vector Text)=["ISO_LOCAL_TIME"] datetime_locale:Locale=Locale.default true_values:(Vector Text)=["True","true","TRUE"] false_values:(Vector Text)=["False","false","FALSE"] + @datetime_formats (make_vector_widget make_date_time_format_selector) + @date_formats (make_vector_widget make_date_format_selector) + @time_formats (make_vector_widget make_time_format_selector) + Value trim_values:Boolean=True allow_leading_zeros:Boolean=False decimal_point:Text|Auto=Auto thousand_separator:Text='' allow_exponential_notation:Boolean=False datetime_formats:(Vector Date_Time_Formatter)=[Date_Time_Formatter.default_enso_zoned_date_time] date_formats:(Vector Date_Time_Formatter)=[Date_Time_Formatter.iso_date] time_formats:(Vector Text)=[Date_Time_Formatter.iso_time] true_values:(Vector Text)=["True","true","TRUE"] false_values:(Vector Text)=["False","false","FALSE"] ## PRIVATE ADVANCED @@ -70,6 +74,7 @@ type Data_Formatter If set to `Ignore`, the operation proceeds without errors or warnings. 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 = + # TODO [RW] move to value_type: https://github.com/enso-org/enso/issues/7866 parser = self.make_datatype_parser datatype Java_Problems.unpack_value_with_aggregated_problems on_problems problem_mapping=(Parse_Values_Helper.translate_parsing_problem datatype) <| parser.parseIndependentValue text @@ -98,19 +103,34 @@ type Data_Formatter ## Specify values for Date/Time parsing. - A Java format string can be used or one of `ENSO_ZONED_DATE_TIME`, - `ISO_ZONED_DATE_TIME`, `ISO_LOCAL_DATE_TIME`, `ISO_OFFSET_DATE_TIME`, - `ISO_LOCAL_DATE`, `ISO_LOCAL_TIME` to use a predefined format. + A plain text pattern can be provided and it will be automatically + converted into a `Date_Time_Formatter` using simple pattern parsing + rules. See `Date_Time_Formatter` for available options. Arguments: - datetime_formats: Expected datetime formats. - date_formats: Expected date formats. - time_formats: Expected time formats. - with_datetime_formats : Text|(Vector Text) -> Text|(Vector Text) -> Text|(Vector Text) -> Data_Formatter - with_datetime_formats self datetime_formats=self.datetime_formats date_formats=self.date_formats time_formats=self.time_formats = - datetime_vector = wrap_text_in_vector datetime_formats - date_vector = wrap_text_in_vector date_formats - time_vector = wrap_text_in_vector time_formats + @datetime_formats (make_vector_widget make_date_time_format_selector) + @date_formats (make_vector_widget make_date_format_selector) + @time_formats (make_vector_widget make_time_format_selector) + with_datetime_formats : ((Vector Date_Time_Formatter) | Date_Time_Formatter) -> ((Vector Date_Time_Formatter) | Date_Time_Formatter) -> ((Vector Date_Time_Formatter) | Date_Time_Formatter) -> Data_Formatter + with_datetime_formats self (datetime_formats:Vector|Date_Time_Formatter = self.datetime_formats) (date_formats:Vector|Date_Time_Formatter = self.date_formats) (time_formats:Vector|Date_Time_Formatter = self.time_formats) = + convert_formats formats = + vector = case formats of + v : Vector -> v + singleton -> [singleton] + converted = vector.map elem-> + ## Ensure the element is a `Date_Time_Formatter` or is converted to it. + We need to convert _each_ element - we cannot perform a 'bulk' conversion like `vector : Vector Date_Time_Formatter` because of erasure. + checked = elem : Date_Time_Formatter + # Temporary variable is a workaround for https://github.com/enso-org/enso/issues/7841 + checked + converted + + datetime_vector = convert_formats datetime_formats + date_vector = convert_formats date_formats + time_vector = convert_formats time_formats self.clone datetime_formats=datetime_vector date_formats=date_vector time_formats=time_vector ## Specify values for Boolean parsing. @@ -124,14 +144,6 @@ type Data_Formatter false_vector = wrap_text_in_vector false_values self.clone true_values=true_vector false_values=false_vector - ## Create a clone of self with a specified Locale. - - Arguments: - - locale: The locale to use when parsing dates and times. - @datetime_locale Locale.default_widget - with_locale : Locale -> Data_Formatter - with_locale self datetime_locale = self.clone datetime_locale=datetime_locale - ## Create a clone of self with a changed format string for a particular datatype. @@ -143,14 +155,14 @@ type Data_Formatter - format: The new format string to set. For dates, it is the usual date format notation, and for booleans it should be two values that represent true and false, separated by a `|`. - with_format : Value_Type | Auto -> Text -> Data_Formatter + with_format : Value_Type | Auto -> (Text | Date_Time_Formatter) -> Data_Formatter with_format self type format = case type of Value_Type.Date -> self.with_datetime_formats date_formats=[format] Value_Type.Time -> self.with_datetime_formats time_formats=[format] Value_Type.Date_Time _ -> self.with_datetime_formats datetime_formats=[format] Value_Type.Boolean -> - formats = format.split "|" + formats = (format : Text).split "|" if formats.length != 2 then Error.throw (Illegal_Argument.Error "The `format` for Booleans must be a string with two values separated by `|`, for example: 'Yes|No'.") else self.with_boolean_values true_values=[formats.at 0] false_values=[formats.at 1] Auto -> @@ -161,8 +173,8 @@ type Data_Formatter ## PRIVATE Clone the instance with some properties overridden. clone : Boolean -> Boolean -> Text -> Text -> Boolean -> Vector Text -> Vector Text -> Vector Text -> Locale -> Vector Text -> Vector Text -> Data_Formatter - clone self (trim_values=self.trim_values) (allow_leading_zeros=self.allow_leading_zeros) (decimal_point=self.decimal_point) (thousand_separator=self.thousand_separator) (allow_exponential_notation=self.allow_exponential_notation) (datetime_formats=self.datetime_formats) (date_formats=self.date_formats) (time_formats=self.time_formats) (datetime_locale=self.datetime_locale) (true_values=self.true_values) (false_values=self.false_values) = - Data_Formatter.Value trim_values=trim_values allow_leading_zeros=allow_leading_zeros decimal_point=decimal_point thousand_separator=thousand_separator allow_exponential_notation=allow_exponential_notation datetime_formats=datetime_formats date_formats=date_formats time_formats=time_formats datetime_locale=datetime_locale true_values=true_values false_values=false_values + clone self (trim_values=self.trim_values) (allow_leading_zeros=self.allow_leading_zeros) (decimal_point=self.decimal_point) (thousand_separator=self.thousand_separator) (allow_exponential_notation=self.allow_exponential_notation) (datetime_formats=self.datetime_formats) (date_formats=self.date_formats) (time_formats=self.time_formats) (true_values=self.true_values) (false_values=self.false_values) = + Data_Formatter.Value trim_values=trim_values allow_leading_zeros=allow_leading_zeros decimal_point=decimal_point thousand_separator=thousand_separator allow_exponential_notation=allow_exponential_notation datetime_formats=datetime_formats date_formats=date_formats time_formats=time_formats true_values=true_values false_values=false_values ## PRIVATE get_thousand_separator self = @@ -190,18 +202,18 @@ type Data_Formatter ## PRIVATE make_date_parser self = self.wrap_base_parser <| - Panic.catch Any handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - DateParser.new self.date_formats self.datetime_locale.java_locale + Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| + DateParser.new (self.date_formats.map .underlying) ## PRIVATE make_date_time_parser self = self.wrap_base_parser <| - Panic.catch Any handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - DateTimeParser.new self.datetime_formats self.datetime_locale.java_locale + Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| + DateTimeParser.new (self.datetime_formats.map .underlying) ## PRIVATE make_time_of_day_parser self = self.wrap_base_parser <| - Panic.catch Any handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - TimeOfDayParser.new self.time_formats self.datetime_locale.java_locale + Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| + TimeOfDayParser.new (self.time_formats.map .underlying) ## PRIVATE make_identity_parser self = self.wrap_base_parser IdentityParser.new @@ -209,7 +221,7 @@ type Data_Formatter ## PRIVATE make_datatype_parser self datatype = case datatype of Integer -> self.make_integer_parser - Float -> self.make_decimal_parser + Float -> self.make_decimal_parser Boolean -> self.make_boolean_parser Date -> self.make_date_parser Date_Time -> self.make_date_time_parser @@ -256,20 +268,20 @@ type Data_Formatter ## PRIVATE make_date_formatter self = if self.date_formats.is_empty then Error.throw (Illegal_Argument.Error "Formatting dates requires at least one entry in the `date_formats` parameter") else - Panic.catch Any handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - DateFormatter.new self.date_formats.first self.datetime_locale.java_locale + Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| + DateFormatter.new self.date_formats.first.underlying ## PRIVATE make_time_of_day_formatter self = if self.time_formats.is_empty then Error.throw (Illegal_Argument.Error "Formatting times requires at least one entry in the `time_formats` parameter") else - Panic.catch Any handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - TimeFormatter.new self.time_formats.first self.datetime_locale.java_locale + Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| + TimeFormatter.new self.time_formats.first.underlying ## PRIVATE make_date_time_formatter self = if self.datetime_formats.is_empty then Error.throw (Illegal_Argument.Error "Formatting date-times requires at least one entry in the `datetime_formats` parameter") else - Panic.catch Any handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - DateTimeFormatter.new self.datetime_formats.first self.datetime_locale.java_locale + Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| + DateTimeFormatter.new self.datetime_formats.first.underlying ## PRIVATE make_boolean_formatter self = @@ -307,3 +319,7 @@ type Data_Formatter wrap_text_in_vector v = case v of _ : Text -> [v] _ -> v + +## PRIVATE +make_vector_widget single_choice_widget display=Display.Always = + Vector_Editor item_editor=single_choice_widget item_default=single_choice_widget.values.first.value display=display diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Format.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Format.enso index 08a5be12231f..4e5fb86aff68 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Format.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Format.enso @@ -17,11 +17,11 @@ polyglot java import org.enso.table.operations.OrderBuilder ## PRIVATE Create a formatter for the specified `Value_Type`. -make_value_formatter_for_value_type : Value_Type -> Locale -> (Any -> Text) +make_value_formatter_for_value_type : Value_Type -> Locale -> (Text | Date_Time_Formatter | Nothing -> Any -> Text) make_value_formatter_for_value_type value_type locale = case value_type of - Value_Type.Date -> make_value_formatter locale - Value_Type.Date_Time _ -> make_value_formatter locale - Value_Type.Time -> make_value_formatter locale + Value_Type.Date -> make_datetime_formatter locale + Value_Type.Date_Time _ -> make_datetime_formatter locale + Value_Type.Time -> make_datetime_formatter locale Value_Type.Boolean -> make_boolean_formatter Value_Type.Integer _ -> make_value_formatter locale Value_Type.Float _ -> make_value_formatter locale @@ -33,19 +33,31 @@ make_value_formatter_for_value_type value_type locale = case value_type of Create a formatter for the given format string. The `value` parameter has to have a `format` method that takes a format and locale. -make_value_formatter : Locale -> (Any -> Text) -make_value_formatter locale = value-> format-> - handle_illegal_argument_exception format <| - if format.is_nothing || format.is_empty then value.to_text else - value.format format locale +make_value_formatter : Locale -> (Text -> Any -> Text) +make_value_formatter locale (format : Text | Nothing) = + if format.is_nothing || format.is_empty then .to_text else + value-> + handle_illegal_argument_exception format <| + value.format format locale ## PRIVATE Create a `Boolean` formatter that takes the format string as the second parameter. -make_boolean_formatter : (Boolean -> Text -> Text) -make_boolean_formatter = bool-> format-> - if format.is_nothing || format.is_empty then bool.to_text else +make_boolean_formatter : (Text -> Boolean -> Text) +make_boolean_formatter (format : Text | Nothing) = + if format.is_nothing || format.is_empty then .to_text else data_formatter = Data_Formatter.Value.with_format Value_Type.Boolean format - data_formatter.format bool + bool -> data_formatter.format bool + +## PRIVATE +make_datetime_formatter : Locale -> Date_Time_Formatter | Text | Nothing -> (Any -> Text) +make_datetime_formatter (locale_override : Locale) (format : Text | Date_Time_Formatter | Nothing) = + use_default = format.is_nothing || (format == "") + if use_default then .to_text else + # If the format was Text, we now ensure it gets converted. + effective_formatter = format : Date_Time_Formatter + # If locale is set to default, keep the locale of the formatter, otherwise override it. + formatter_with_updated_locale = if locale_override == Locale.default then effective_formatter else effective_formatter.with_locale locale_override + date_time_value -> date_time_value.format formatter_with_updated_locale ## PRIVATE Rethrow a Java IllegalArgumentException as an Illegal_Argument. diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDate.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDate.java index 8bc82110a6ab..6c4679d96744 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDate.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDate.java @@ -10,7 +10,6 @@ import java.time.DateTimeException; import java.time.LocalDate; import java.time.LocalTime; -import java.time.format.DateTimeFormatter; import org.enso.interpreter.dsl.Builtin; import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.library.dispatch.TypesLibrary; @@ -101,9 +100,6 @@ Type getType(@CachedLibrary("this") TypesLibrary thisLib, @Cached("1") int ignor @CompilerDirectives.TruffleBoundary @ExportMessage public Object toDisplayString(boolean allowSideEffects) { - return DATE_FORMATTER.format(date); + return Core_Date_Utils.defaultLocalDateFormatter.format(date); } - - private static final DateTimeFormatter DATE_FORMATTER = - Core_Date_Utils.defaultLocalDateFormatter(); } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDateTime.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDateTime.java index b5644bda019f..0cae721d84f0 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDateTime.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/data/EnsoDateTime.java @@ -12,11 +12,11 @@ import java.time.LocalTime; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; import org.enso.interpreter.dsl.Builtin; import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.data.text.Text; import org.enso.interpreter.runtime.library.dispatch.TypesLibrary; +import org.enso.polyglot.common_utils.Core_Date_Utils; @ExportLibrary(InteropLibrary.class) @ExportLibrary(TypesLibrary.class) @@ -168,10 +168,10 @@ public EnsoDate toLocalDate() { return new EnsoDate(dateTime.toLocalDate()); } - @Builtin.Method(description = "Return this datetime to the datetime in the provided time zone.") + @Builtin.Method(description = "Return a text representation of this date-time.") @CompilerDirectives.TruffleBoundary public Text toText() { - return Text.create(DateTimeFormatter.ISO_ZONED_DATE_TIME.format(dateTime)); + return Text.create(Core_Date_Utils.defaultZonedDateTimeFormatter.format(dateTime)); } @ExportMessage @@ -227,7 +227,7 @@ Type getType(@CachedLibrary("this") TypesLibrary thisLib, @Cached("1") int ignor @ExportMessage @CompilerDirectives.TruffleBoundary public Object toDisplayString(boolean allowSideEffects) { - return DateTimeFormatter.ISO_ZONED_DATE_TIME.format(dateTime); + return Core_Date_Utils.defaultZonedDateTimeFormatter.format(dateTime); } // 15. October 1582 diff --git a/lib/scala/common-polyglot-core-utils/src/main/java/org/enso/polyglot/common_utils/Core_Date_Utils.java b/lib/scala/common-polyglot-core-utils/src/main/java/org/enso/polyglot/common_utils/Core_Date_Utils.java index 95cb3813a6ee..02d8105f6ba9 100644 --- a/lib/scala/common-polyglot-core-utils/src/main/java/org/enso/polyglot/common_utils/Core_Date_Utils.java +++ b/lib/scala/common-polyglot-core-utils/src/main/java/org/enso/polyglot/common_utils/Core_Date_Utils.java @@ -1,53 +1,50 @@ package org.enso.polyglot.common_utils; -import java.time.*; +import static java.time.temporal.ChronoField.INSTANT_SECONDS; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoField; import java.time.temporal.TemporalQueries; -import java.util.Locale; - -import static java.time.temporal.ChronoField.INSTANT_SECONDS; -import static java.time.temporal.ChronoField.NANO_OF_SECOND; public class Core_Date_Utils { - /** - * Replace space with T in ISO date time string to make it compatible with ISO format. - * @param dateString Raw date time string with either space or T as separator - * @return ISO format date time string - */ - public static String normaliseISODateTime(String dateString) { - if (dateString != null && dateString.length() > 10 && dateString.charAt(10) == ' ') { - var builder = new StringBuilder(dateString); - builder.replace(10, 11, "T"); - return builder.toString(); - } - - return dateString; - } - - /** @return default Date Time formatter for parsing a Date_Time. */ - public static DateTimeFormatter defaultZonedDateTimeFormatter() { - return new DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE_TIME) - .optionalStart().parseLenient().appendOffsetId().optionalEnd() - .optionalStart().appendLiteral('[').parseCaseSensitive().appendZoneRegionId().appendLiteral(']') - .toFormatter(); - } - - /** @return default Date formatter for parsing a Date. */ - public static DateTimeFormatter defaultLocalDateFormatter() { - return DateTimeFormatter.ISO_LOCAL_DATE; - } - - /** @return default Time formatter for parsing a Time_Of_Day. */ - public static DateTimeFormatter defaultLocalTimeFormatter() { - return DateTimeFormatter.ISO_LOCAL_TIME; - } + /** default Date Time formatter for parsing a Date_Time. */ + public static final DateTimeFormatter defaultZonedDateTimeFormatter = + new DateTimeFormatterBuilder() + .parseLenient() + .append(DateTimeFormatter.ISO_LOCAL_DATE) + .appendLiteral(' ') + .append(DateTimeFormatter.ISO_LOCAL_TIME) + .optionalStart() + .parseLenient() + .appendOffsetId() + .optionalEnd() + .optionalStart() + .appendLiteral('[') + .parseCaseSensitive() + .appendZoneRegionId() + .appendLiteral(']') + .toFormatter(); + + /** default Date formatter for parsing a Date. */ + public static final DateTimeFormatter defaultLocalDateFormatter = + DateTimeFormatter.ISO_LOCAL_DATE; + + /** default Time formatter for parsing a Time_Of_Day. */ + public static final DateTimeFormatter defaultLocalTimeFormatter = + DateTimeFormatter.ISO_LOCAL_TIME; /** - * Parse a date string into a LocalDate. - * Allows missing day (assumes first day of month) or missing year (assumes current year). + * Parse a date string into a LocalDate. Allows missing day (assumes first day of month) or + * missing year (assumes current year). * * @param dateString the date time string * @param formatter the formatter to use @@ -62,19 +59,22 @@ public static LocalDate parseLocalDate(String dateString, DateTimeFormatter form // Allow Year and Month to be parsed without a day (use first day of month). if (parsed.isSupported(ChronoField.YEAR) && parsed.isSupported(ChronoField.MONTH_OF_YEAR)) { - var dayOfMonth = parsed.isSupported(ChronoField.DAY_OF_MONTH) - ? parsed.get(ChronoField.DAY_OF_MONTH) - : 1; - return LocalDate.of(parsed.get(ChronoField.YEAR), parsed.get(ChronoField.MONTH_OF_YEAR), dayOfMonth); + var dayOfMonth = + parsed.isSupported(ChronoField.DAY_OF_MONTH) ? parsed.get(ChronoField.DAY_OF_MONTH) : 1; + return LocalDate.of( + parsed.get(ChronoField.YEAR), parsed.get(ChronoField.MONTH_OF_YEAR), dayOfMonth); } // Allow Month and Day to be parsed without a year (use current year). - if (parsed.isSupported(ChronoField.DAY_OF_MONTH) && parsed.isSupported(ChronoField.MONTH_OF_YEAR)) { - return LocalDate.of(LocalDate.now().getYear(), parsed.get(ChronoField.MONTH_OF_YEAR), parsed.get(ChronoField.DAY_OF_MONTH)); + if (parsed.isSupported(ChronoField.DAY_OF_MONTH) + && parsed.isSupported(ChronoField.MONTH_OF_YEAR)) { + return LocalDate.of( + LocalDate.now().getYear(), + parsed.get(ChronoField.MONTH_OF_YEAR), + parsed.get(ChronoField.DAY_OF_MONTH)); } throw new DateTimeParseException("Unable to parse date.", dateString, 0); - } /** @@ -90,16 +90,19 @@ public static ZonedDateTime parseZonedDateTime(String dateString, DateTimeFormat try { // Resolve Zone var zone = resolved.query(TemporalQueries.zoneId()); - zone = zone != null ? zone : - (resolved.isSupported(ChronoField.OFFSET_SECONDS) - ? ZoneOffset.ofTotalSeconds(resolved.get(ChronoField.OFFSET_SECONDS)) - : ZoneId.systemDefault()); + zone = + zone != null + ? zone + : (resolved.isSupported(ChronoField.OFFSET_SECONDS) + ? ZoneOffset.ofTotalSeconds(resolved.get(ChronoField.OFFSET_SECONDS)) + : ZoneId.systemDefault()); // Instant Based if (resolved.isSupported(INSTANT_SECONDS)) { long epochSecond = resolved.getLong(INSTANT_SECONDS); int nanoOfSecond = resolved.get(NANO_OF_SECOND); - return ZonedDateTime.ofInstant(java.time.Instant.ofEpochSecond(epochSecond, nanoOfSecond), zone); + return ZonedDateTime.ofInstant( + java.time.Instant.ofEpochSecond(epochSecond, nanoOfSecond), zone); } // Local Based @@ -109,27 +112,8 @@ public static ZonedDateTime parseZonedDateTime(String dateString, DateTimeFormat } catch (DateTimeException e) { throw new DateTimeException("Unable to parse Text '" + dateString + "' to Date_Time.", e); } catch (ArithmeticException e) { - throw new DateTimeException("Unable to parse Text '" + dateString + "' to Date_Time due to arithmetic error.", e); + throw new DateTimeException( + "Unable to parse Text '" + dateString + "' to Date_Time due to arithmetic error.", e); } } - - /** - * Creates a DateTimeFormatter from a format string, supporting building standard formats. - * - * @param format format string - * @param locale locale needed for custom formats - * @return DateTimeFormatter - */ - public static DateTimeFormatter make_formatter(String format, Locale locale) { - var usedLocale = locale == Locale.ROOT ? Locale.US : locale; - return switch (format) { - case "ENSO_ZONED_DATE_TIME" -> defaultZonedDateTimeFormatter(); - case "ISO_ZONED_DATE_TIME" -> DateTimeFormatter.ISO_ZONED_DATE_TIME; - case "ISO_OFFSET_DATE_TIME" -> DateTimeFormatter.ISO_OFFSET_DATE_TIME; - case "ISO_LOCAL_DATE_TIME" -> DateTimeFormatter.ISO_LOCAL_DATE_TIME; - case "ISO_LOCAL_DATE" -> DateTimeFormatter.ISO_LOCAL_DATE; - case "ISO_LOCAL_TIME" -> DateTimeFormatter.ISO_LOCAL_TIME; - default -> DateTimeFormatter.ofPattern(format, usedLocale); - }; - } } diff --git a/std-bits/base/src/main/java/org/enso/base/Text_Utils.java b/std-bits/base/src/main/java/org/enso/base/Text_Utils.java index 60aea905fc92..a236a05a505c 100644 --- a/std-bits/base/src/main/java/org/enso/base/Text_Utils.java +++ b/std-bits/base/src/main/java/org/enso/base/Text_Utils.java @@ -635,6 +635,13 @@ public static boolean is_all_whitespace(String text) { return text.codePoints().allMatch(UCharacter::isUWhiteSpace); } + /** + * Checks if the given string consists only of letters. + */ + public static boolean is_all_letters(String text) { + return text.codePoints().allMatch(UCharacter::isLetter); + } + /** * Replaces all provided spans within the text with {@code newSequence}. * 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 be2e7b9a066a..c381a24fa794 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 @@ -30,85 +30,6 @@ public enum AdjustOp { MINUS } - /** - * Creates a DateTimeFormatter from a format string, supporting building standard formats. - * - * @param format format string - * @param locale locale needed for custom formats - * @return DateTimeFormatter - */ - public static DateTimeFormatter make_formatter(String format, Locale locale) { - return Core_Date_Utils.make_formatter(format, locale); - } - - /** - * Creates a DateTimeFormatter from a format string, supporting building standard formats. - * For Enso format, return the default output formatter. - * - * @param format format string - * @param locale locale needed for custom formats - * @return DateTimeFormatter - */ - public static DateTimeFormatter make_output_formatter(String format, Locale locale) { - return format.equals("ENSO_ZONED_DATE_TIME") - ? Time_Utils.default_output_date_time_formatter() - : Core_Date_Utils.make_formatter(format, locale); - } - - /** - * Given a format string, returns true if it is a format that is based on ISO date time. - * - * @param format format string - * @return True if format is based on ISO date time - */ - public static boolean is_iso_datetime_based(String format) { - return switch (format) { - case "ENSO_ZONED_DATE_TIME", "ISO_ZONED_DATE_TIME", "ISO_OFFSET_DATE_TIME", "ISO_LOCAL_DATE_TIME" -> true; - default -> false; - }; - } - - /** - * @return default DateTimeFormatter for parsing a Date_Time. - */ - public static DateTimeFormatter default_date_time_formatter() { - return Core_Date_Utils.defaultZonedDateTimeFormatter(); - } - - /** - * @return default DateTimeFormatter for parsing a Date. - */ - public static DateTimeFormatter default_date_formatter() { - return Core_Date_Utils.defaultLocalDateFormatter(); - } - - /** - * @return default DateTimeFormatter for parsing a Time_Of_Day. - */ - public static DateTimeFormatter default_time_of_day_formatter() { - return Core_Date_Utils.defaultLocalTimeFormatter(); - } - - /** - * @return default Date Time formatter for writing a Date_Time. - */ - public static DateTimeFormatter default_output_date_time_formatter() { - return new DateTimeFormatterBuilder().append(DateTimeFormatter.ISO_LOCAL_DATE) - .appendLiteral(' ') - .append(DateTimeFormatter.ISO_LOCAL_TIME) - .toFormatter(); - } - - /** - * Replace space with T in ISO date time string to make it compatible with ISO format. - * - * @param dateString Raw date time string with either space or T as separator - * @return ISO format date time string - */ - public static String normalise_iso_datetime(String dateString) { - return Core_Date_Utils.normaliseISODateTime(dateString); - } - /** * Format a LocalDate instance using a formatter. * @@ -142,10 +63,6 @@ public static String time_of_day_format(LocalTime localTime, DateTimeFormatter f return formatter.format(localTime); } - public static String time_of_day_format_with_locale(LocalTime localTime, Object format, Locale locale) { - return make_output_formatter(format.toString(), locale).format(localTime); - } - public static LocalDate date_adjust(LocalDate date, AdjustOp op, Period period) { return switch (op) { case PLUS -> date.plus(period); @@ -216,13 +133,13 @@ public static LocalDate parse_date(String text, DateTimeFormatter formatter) { * @param formatter the formatter to use. * @return parsed LocalTime instance. */ - public static LocalTime parse_time_of_day(String text,DateTimeFormatter formatter) { + public static LocalTime parse_time_of_day(String text, DateTimeFormatter formatter) { return LocalTime.parse(text, formatter); } /** - * Normally this method could be done in Enso by pattern matching, but currently matching on Time - * types is not supported, so this is a workaround. + * Normally this method could be done in Enso by pattern matching, but currently matching on Time types is not + * supported, so this is a workaround. * *

TODO once the related issue is fixed, this workaround may be replaced with pattern matching * in Enso; Pivotal issue. @@ -315,4 +232,14 @@ public static LocalTime unit_time_add(TemporalUnit unit, LocalTime time, long am public static ZonedDateTime unit_datetime_add(TemporalUnit unit, ZonedDateTime datetime, long amount) { return datetime.plus(amount, unit); } + + /** + * This helper method is needed, because calling `appendValueReduced` directly from Enso fails to convert an EnsoDate + * to a LocalDate due to polyglot unable to handle the polymorphism of the method. + */ + public static void appendTwoDigitYear(DateTimeFormatterBuilder builder, TemporalField yearField, int maxYear) { + int minYear = maxYear - 99; + LocalDate baseDate = LocalDate.of(minYear, 1, 1); + builder.appendValueReduced(yearField, 2, 2, baseDate); + } } diff --git a/std-bits/base/src/main/java/org/enso/base/time/EnsoDateTimeFormatter.java b/std-bits/base/src/main/java/org/enso/base/time/EnsoDateTimeFormatter.java new file mode 100644 index 000000000000..e0fafa5c05e3 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/time/EnsoDateTimeFormatter.java @@ -0,0 +1,234 @@ +package org.enso.base.time; + +import org.enso.polyglot.common_utils.Core_Date_Utils; +import org.graalvm.collections.Pair; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; +import java.time.temporal.ChronoField; +import java.time.temporal.IsoFields; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalField; +import java.time.temporal.TemporalQueries; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import static java.time.temporal.ChronoField.INSTANT_SECONDS; +import static java.time.temporal.ChronoField.NANO_OF_SECOND; + +/** + * An Enso representation of the DateTimeFormatter. + *

+ * It adds some additional functionality to the Java formatter - including a workaround for making the `T` in ISO dates + * optional and tracking how it was constructed. + */ +public class EnsoDateTimeFormatter { + private final DateTimeFormatter formatter; + private final Pair isoReplacementPair; + private final String originalPattern; + private final FormatterKind formatterKind; + + private EnsoDateTimeFormatter(DateTimeFormatter formatter, Pair isoReplacementPair, + String originalPattern, FormatterKind formatterKind) { + this.formatter = formatter; + this.isoReplacementPair = isoReplacementPair; + this.originalPattern = originalPattern; + this.formatterKind = formatterKind; + } + + public EnsoDateTimeFormatter(DateTimeFormatter formatter, String originalPattern, FormatterKind formatterKind) { + this(formatter, null, originalPattern, formatterKind); + } + + public static EnsoDateTimeFormatter makeISOConstant(DateTimeFormatter formatter, String name) { + return new EnsoDateTimeFormatter(formatter, Pair.create(' ', "T"), name, FormatterKind.CONSTANT); + } + + public static EnsoDateTimeFormatter default_enso_zoned_date_time_formatter() { + return new EnsoDateTimeFormatter( + Core_Date_Utils.defaultZonedDateTimeFormatter, + Pair.create('T', " "), + "default_enso_zoned_date_time", + FormatterKind.CONSTANT + ); + } + + public EnsoDateTimeFormatter withLocale(Locale locale) { + return new EnsoDateTimeFormatter(formatter.withLocale(locale), isoReplacementPair, originalPattern, formatterKind); + } + + public DateTimeFormatter getRawJavaFormatter() { + return formatter; + } + + public String getOriginalPattern() { + return originalPattern; + } + + public FormatterKind getFormatterKind() { + return formatterKind; + } + + private String normaliseInput(String dateString) { + if (isoReplacementPair == null) { + // Nothing to do + return dateString; + } + + char from = isoReplacementPair.getLeft(); + String to = isoReplacementPair.getRight(); + + if (dateString != null && dateString.length() > 10 && dateString.charAt(10) == from) { + var builder = new StringBuilder(dateString); + builder.replace(10, 11, to); + return builder.toString(); + } + + return dateString; + } + + @Override + public String toString() { + return switch (formatterKind) { + case SIMPLE -> originalPattern; + case ISO_WEEK_DATE -> "(ISO Week Date Format) " + originalPattern; + case RAW_JAVA -> "(Java Format Pattern) " + originalPattern; + case CONSTANT -> originalPattern; + }; + } + + public LocalDate parseLocalDate(String dateString) { + dateString = normaliseInput(dateString); + + TemporalAccessor parsed = formatter.parse(dateString); + + if (parsed.isSupported(ChronoField.EPOCH_DAY)) { + return LocalDate.ofEpochDay(parsed.getLong(ChronoField.EPOCH_DAY)); + } + + // Allow Year and Month to be parsed without a day (use first day of month). + if (parsed.isSupported(ChronoField.YEAR) && parsed.isSupported(ChronoField.MONTH_OF_YEAR)) { + int dayOfMonth = + parsed.isSupported(ChronoField.DAY_OF_MONTH) ? parsed.get(ChronoField.DAY_OF_MONTH) : 1; + return LocalDate.of( + parsed.get(ChronoField.YEAR), parsed.get(ChronoField.MONTH_OF_YEAR), dayOfMonth); + } + + // Allow Year and Quarter to be parsed without a day (use first day of the quarter). + if (parsed.isSupported(ChronoField.YEAR) && parsed.isSupported(IsoFields.QUARTER_OF_YEAR)) { + int dayOfQuarter = + parsed.isSupported(IsoFields.DAY_OF_QUARTER) ? parsed.get(IsoFields.DAY_OF_QUARTER) : 1; + int year = parsed.get(ChronoField.YEAR); + int quarter = parsed.get(IsoFields.QUARTER_OF_YEAR); + int monthsToShift = 3 * (quarter - 1); + LocalDate firstDay = LocalDate.of(year, 1, 1); + return firstDay.plusMonths(monthsToShift).plusDays(dayOfQuarter - 1); + } + + // Allow Month and Day to be parsed without a year (use current year). + if (parsed.isSupported(ChronoField.DAY_OF_MONTH) + && parsed.isSupported(ChronoField.MONTH_OF_YEAR)) { + return LocalDate.of( + LocalDate.now().getYear(), + parsed.get(ChronoField.MONTH_OF_YEAR), + parsed.get(ChronoField.DAY_OF_MONTH)); + } + + if (parsed.isSupported(IsoFields.WEEK_BASED_YEAR) && parsed.isSupported(IsoFields.WEEK_OF_WEEK_BASED_YEAR)) { + // Get the day of week or default to first day if not present. + long dayOfWeek = parsed.isSupported(ChronoField.DAY_OF_WEEK) ? parsed.get(ChronoField.DAY_OF_WEEK) : 1; + HashMap fields = new HashMap<>(); + fields.put(IsoFields.WEEK_BASED_YEAR, parsed.getLong(IsoFields.WEEK_BASED_YEAR)); + fields.put(IsoFields.WEEK_OF_WEEK_BASED_YEAR, parsed.getLong(IsoFields.WEEK_OF_WEEK_BASED_YEAR)); + fields.put(ChronoField.DAY_OF_WEEK, dayOfWeek); + + TemporalAccessor resolved = IsoFields.WEEK_OF_WEEK_BASED_YEAR.resolve(fields, parsed, ResolverStyle.SMART); + if (resolved.isSupported(ChronoField.EPOCH_DAY)) { + return LocalDate.ofEpochDay(resolved.getLong(ChronoField.EPOCH_DAY)); + } + } + + // This will usually throw at this point, but it will construct a more informative exception than we could. + return LocalDate.from(parsed); + } + + public ZonedDateTime parseZonedDateTime(String dateString) { + dateString = normaliseInput(dateString); + + var resolved = formatter.parse(dateString); + + try { + // Resolve Zone + var zone = resolved.query(TemporalQueries.zoneId()); + zone = + zone != null + ? zone + : (resolved.isSupported(ChronoField.OFFSET_SECONDS) + ? ZoneOffset.ofTotalSeconds(resolved.get(ChronoField.OFFSET_SECONDS)) + : ZoneId.systemDefault()); + + // Instant Based + if (resolved.isSupported(INSTANT_SECONDS)) { + long epochSecond = resolved.getLong(INSTANT_SECONDS); + int nanoOfSecond = resolved.get(NANO_OF_SECOND); + return ZonedDateTime.ofInstant( + java.time.Instant.ofEpochSecond(epochSecond, nanoOfSecond), zone); + } + + // Local Based + var localDate = LocalDate.from(resolved); + var localTime = LocalTime.from(resolved); + return ZonedDateTime.of(localDate, localTime, zone); + } catch (DateTimeException e) { + throw new DateTimeException("Unable to parse Text '" + dateString + "' to Date_Time: " + e.getMessage(), e); + } catch (ArithmeticException e) { + throw new DateTimeException( + "Unable to parse Text '" + dateString + "' to Date_Time due to arithmetic error.", e); + } + } + + public LocalTime parseLocalTime(String text) { + text = normaliseInput(text); + return LocalTime.parse(text, formatter); + } + + public String formatLocalDate(LocalDate date) { + return formatter.format(date); + } + + public String formatZonedDateTime(ZonedDateTime dateTime) { + return formatter.format(dateTime); + } + + public String formatLocalTime(LocalTime time) { + return formatter.format(time); + } + + @Override + public int hashCode() { + // We ignore formatter here because it has identity semantics. + return Arrays.hashCode(new Object[]{isoReplacementPair, originalPattern, formatterKind}); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof EnsoDateTimeFormatter other) { + // The DateTimeFormatter has identity semantics, so instead we try to check the pattern instead, if available. + if (originalPattern != null) { + return formatterKind == other.formatterKind && originalPattern.equals(other.originalPattern) && isoReplacementPair.equals(other.isoReplacementPair) && formatter.getLocale().equals(other.formatter.getLocale()); + } else { + return formatterKind == other.formatterKind && formatter.equals(other.formatter); + } + } else { + return false; + } + } +} diff --git a/std-bits/base/src/main/java/org/enso/base/time/FormatterKind.java b/std-bits/base/src/main/java/org/enso/base/time/FormatterKind.java new file mode 100644 index 000000000000..8560b3d3be0c --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/time/FormatterKind.java @@ -0,0 +1,16 @@ +package org.enso.base.time; + +/** Specifies how the formatter was constrcuted. */ +public enum FormatterKind { + /** Formatters constructed using `from_simple_pattern`. */ + SIMPLE, + + /** Formatters constructed using `from_iso_week_date_pattern`. */ + ISO_WEEK_DATE, + + /** Formatters constructed from a raw Java formatter or using DateTimeFormatter.ofPattern. */ + RAW_JAVA, + + /** Formatters based on a constant, like ISO_ZONED_DATE_TIME, or the default Enso formatter. */ + CONSTANT +} diff --git a/std-bits/table/src/main/java/org/enso/table/data/column/operation/cast/ToTextStorageConverter.java b/std-bits/table/src/main/java/org/enso/table/data/column/operation/cast/ToTextStorageConverter.java index 7323932bfd85..b76839d32eb4 100644 --- a/std-bits/table/src/main/java/org/enso/table/data/column/operation/cast/ToTextStorageConverter.java +++ b/std-bits/table/src/main/java/org/enso/table/data/column/operation/cast/ToTextStorageConverter.java @@ -75,9 +75,9 @@ public Storage castFromMixed(Storage mixedStorage, CastProblemBuilder return builder.seal(); } - private final DateTimeFormatter dateFormatter = Core_Date_Utils.defaultLocalDateFormatter(); - private final DateTimeFormatter timeFormatter = Core_Date_Utils.defaultLocalTimeFormatter(); - private final DateTimeFormatter dateTimeFormatter = Core_Date_Utils.defaultZonedDateTimeFormatter(); + private final DateTimeFormatter dateFormatter = Core_Date_Utils.defaultLocalDateFormatter; + private final DateTimeFormatter timeFormatter = Core_Date_Utils.defaultLocalTimeFormatter; + private final DateTimeFormatter dateTimeFormatter = Core_Date_Utils.defaultZonedDateTimeFormatter; private String convertDate(LocalDate date) { diff --git a/std-bits/table/src/main/java/org/enso/table/expressions/ExpressionVisitorImpl.java b/std-bits/table/src/main/java/org/enso/table/expressions/ExpressionVisitorImpl.java index ba4478d22eb7..559d6c2b7b08 100644 --- a/std-bits/table/src/main/java/org/enso/table/expressions/ExpressionVisitorImpl.java +++ b/std-bits/table/src/main/java/org/enso/table/expressions/ExpressionVisitorImpl.java @@ -7,6 +7,7 @@ import org.antlr.v4.runtime.Recognizer; import org.enso.base.Time_Utils; +import org.enso.base.time.EnsoDateTimeFormatter; import org.enso.polyglot.common_utils.Core_Date_Utils; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.PolyglotException; @@ -315,12 +316,14 @@ public Value visitTime(ExpressionParser.TimeContext ctx) { } } + private static final EnsoDateTimeFormatter dateTimeFormatter = EnsoDateTimeFormatter.default_enso_zoned_date_time_formatter(); + @Override public Value visitDatetime(ExpressionParser.DatetimeContext ctx) { - var text = Time_Utils.normalise_iso_datetime(ctx.text.getText()); + var text = ctx.text.getText(); try { - var dateTime = Core_Date_Utils.parseZonedDateTime(text, Time_Utils.default_date_time_formatter()); + var dateTime = dateTimeFormatter.parseZonedDateTime(text); return Value.asValue(dateTime); } catch (DateTimeParseException ignored) { } 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 91df54669b75..671f1f61a004 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 @@ -1,17 +1,16 @@ package org.enso.table.formatting; +import org.enso.base.time.EnsoDateTimeFormatter; +import org.graalvm.polyglot.Value; + import java.time.LocalDate; import java.time.format.DateTimeFormatter; -import java.util.Locale; - -import org.enso.polyglot.common_utils.Core_Date_Utils; -import org.graalvm.polyglot.Value; public class DateFormatter implements DataFormatter { private final DateTimeFormatter formatter; - public DateFormatter(String formatString, Locale locale) { - formatter = Core_Date_Utils.make_formatter(formatString, locale); + public DateFormatter(EnsoDateTimeFormatter ensoFormatter) { + formatter = ensoFormatter.getRawJavaFormatter(); } @Override @@ -21,7 +20,7 @@ public String format(Object value) { } if (value instanceof Value v && v.isDate()) { - value = v.asDate(); + value = v.asDate(); } if (value instanceof LocalDate date) { 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 7e47908df895..0e91bba2aef9 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,17 +1,16 @@ package org.enso.table.formatting; -import org.enso.base.Time_Utils; +import org.enso.base.time.EnsoDateTimeFormatter; import org.graalvm.polyglot.Value; import java.time.LocalDateTime; import java.time.ZonedDateTime; -import java.util.Locale; public class DateTimeFormatter implements DataFormatter { private final java.time.format.DateTimeFormatter formatter; - public DateTimeFormatter(String formatString, Locale locale) { - formatter = Time_Utils.make_output_formatter(formatString, locale); + public DateTimeFormatter(EnsoDateTimeFormatter ensoFormatter) { + formatter = ensoFormatter.getRawJavaFormatter(); } @Override 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 985f615b931e..bd289b5e2107 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,17 +1,16 @@ package org.enso.table.formatting; -import org.enso.polyglot.common_utils.Core_Date_Utils; +import org.enso.base.time.EnsoDateTimeFormatter; import org.graalvm.polyglot.Value; import java.time.LocalTime; import java.time.format.DateTimeFormatter; -import java.util.Locale; public class TimeFormatter implements DataFormatter { private final DateTimeFormatter formatter; - public TimeFormatter(String formatString, Locale locale) { - formatter = Core_Date_Utils.make_formatter(formatString, locale); + public TimeFormatter(EnsoDateTimeFormatter ensoFormatter) { + formatter = ensoFormatter.getRawJavaFormatter(); } @Override diff --git a/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java b/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java index 2150dcbdf93d..3a217bd178d3 100644 --- a/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java +++ b/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java @@ -1,39 +1,30 @@ package org.enso.table.parsing; -import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.Locale; -import org.enso.base.Time_Utils; -import org.enso.polyglot.common_utils.Core_Date_Utils; +import org.enso.base.time.EnsoDateTimeFormatter; import org.enso.table.parsing.problems.ProblemAggregator; public abstract class BaseTimeParser extends IncrementalDatatypeParser { protected interface ParseStrategy { - Object parse(String text, DateTimeFormatter formatter) throws DateTimeParseException; + Object parse(String text, EnsoDateTimeFormatter formatter) throws DateTimeParseException; } - protected final DateTimeFormatter[] formatters; - protected final boolean[] replaceSpaces; + protected final EnsoDateTimeFormatter[] formatters; protected final ParseStrategy parseStrategy; - protected BaseTimeParser(String[] formats, Locale locale, ParseStrategy parseStrategy) { + protected BaseTimeParser(EnsoDateTimeFormatter[] formatters, ParseStrategy parseStrategy) { this.parseStrategy = parseStrategy; - - formatters = new DateTimeFormatter[formats.length]; - replaceSpaces = new boolean[formats.length]; - for (int i = 0; i < formats.length; i++) { - formatters[i] = Core_Date_Utils.make_formatter(formats[i], locale); - replaceSpaces[i] = Time_Utils.is_iso_datetime_based(formats[i]); - } + this.formatters = formatters; } @Override protected Object parseSingleValue(String text, ProblemAggregator problemAggregator) { - for (int i = 0; i < formatters.length; i++) { + for (EnsoDateTimeFormatter formatter : formatters) { try { - var replaced = replaceSpaces[i] ? Time_Utils.normalise_iso_datetime(text) : text; - return parseStrategy.parse(replaced, formatters[i]); + return parseStrategy.parse(text, formatter); } catch (DateTimeParseException ignored) { + // TODO I think ideally we should try to return Option instead of throwing, as throwing is + // inefficient } } diff --git a/std-bits/table/src/main/java/org/enso/table/parsing/DateParser.java b/std-bits/table/src/main/java/org/enso/table/parsing/DateParser.java index 029ed256da7b..bdcf9165320b 100644 --- a/std-bits/table/src/main/java/org/enso/table/parsing/DateParser.java +++ b/std-bits/table/src/main/java/org/enso/table/parsing/DateParser.java @@ -1,13 +1,14 @@ package org.enso.table.parsing; -import java.util.Locale; -import org.enso.polyglot.common_utils.Core_Date_Utils; +import org.enso.base.time.EnsoDateTimeFormatter; import org.enso.table.data.column.builder.Builder; import org.enso.table.data.column.builder.DateBuilder; public class DateParser extends BaseTimeParser { - public DateParser(String[] formats, Locale locale) { - super(formats, locale, Core_Date_Utils::parseLocalDate); + public DateParser(EnsoDateTimeFormatter[] formatters) { + super( + formatters, + (String text, EnsoDateTimeFormatter formatter) -> formatter.parseLocalDate(text)); } @Override diff --git a/std-bits/table/src/main/java/org/enso/table/parsing/DateTimeParser.java b/std-bits/table/src/main/java/org/enso/table/parsing/DateTimeParser.java index 24f4c95868d1..1151f4588eb3 100644 --- a/std-bits/table/src/main/java/org/enso/table/parsing/DateTimeParser.java +++ b/std-bits/table/src/main/java/org/enso/table/parsing/DateTimeParser.java @@ -1,13 +1,14 @@ package org.enso.table.parsing; -import java.util.Locale; -import org.enso.polyglot.common_utils.Core_Date_Utils; +import org.enso.base.time.EnsoDateTimeFormatter; import org.enso.table.data.column.builder.Builder; import org.enso.table.data.column.builder.DateTimeBuilder; public class DateTimeParser extends BaseTimeParser { - public DateTimeParser(String[] formats, Locale locale) { - super(formats, locale, Core_Date_Utils::parseZonedDateTime); + public DateTimeParser(EnsoDateTimeFormatter[] formatters) { + super( + formatters, + (String text, EnsoDateTimeFormatter formatter) -> formatter.parseZonedDateTime(text)); } @Override diff --git a/std-bits/table/src/main/java/org/enso/table/parsing/TimeOfDayParser.java b/std-bits/table/src/main/java/org/enso/table/parsing/TimeOfDayParser.java index 659e1e74ec3a..116c4bf0cdc1 100644 --- a/std-bits/table/src/main/java/org/enso/table/parsing/TimeOfDayParser.java +++ b/std-bits/table/src/main/java/org/enso/table/parsing/TimeOfDayParser.java @@ -1,13 +1,14 @@ package org.enso.table.parsing; -import java.time.LocalTime; -import java.util.Locale; +import org.enso.base.time.EnsoDateTimeFormatter; import org.enso.table.data.column.builder.Builder; import org.enso.table.data.column.builder.TimeOfDayBuilder; public class TimeOfDayParser extends BaseTimeParser { - public TimeOfDayParser(String[] formats, Locale locale) { - super(formats, locale, LocalTime::parse); + public TimeOfDayParser(EnsoDateTimeFormatter[] formatters) { + super( + formatters, + (String text, EnsoDateTimeFormatter formatter) -> formatter.parseLocalTime(text)); } @Override diff --git a/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso b/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso index d2e88706262c..405e7523a86c 100644 --- a/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso +++ b/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso @@ -1,4 +1,5 @@ from Standard.Base import all +import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Errors.Illegal_State.Illegal_State @@ -195,17 +196,17 @@ spec = Test.specify "should format dates" <| formatter = Data_Formatter.Value formatter.format (Date.new 2022) . should_equal "2022-01-01" - formatter.format (Date_Time.new 1999) . should_equal "1999-01-01 00:00:00" - formatter.format (Date_Time.new 1999 zone=Time_Zone.utc) . should_equal "1999-01-01 00:00:00" - formatter.format (Date_Time.new 1999 zone=(Time_Zone.parse "America/Los_Angeles")) . should_equal "1999-01-01 00:00:00" + formatter.format (Date_Time.new 1999) . should_contain "1999-01-01 00:00:00" + formatter.format (Date_Time.new 1999 zone=Time_Zone.utc) . should_equal '1999-01-01 00:00:00Z[UTC]' + formatter.format (Date_Time.new 1999 zone=(Time_Zone.parse "America/Los_Angeles")) . should_equal "1999-01-01 00:00:00-08:00[America/Los_Angeles]" formatter.format (Time_Of_Day.new) . should_equal "00:00:00" Test.specify "should allow custom date formats" <| - formatter = Data_Formatter.Value 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 = Data_Formatter.Value.with_datetime_formats date_formats=["ddd, d MMM y", Date_Time_Formatter.from_java "d MMM y[ G]"] datetime_formats=["dd/MM/yyyy HH:mm [ZZZZ]"] time_formats=["h:mma"] formatter.format (Date.new 2022 06 21) . should_equal "Tue, 21 Jun 2022" - formatter.format (Date_Time.new 1999 02 03 04 56 11 zone=Time_Zone.utc) . should_equal "03/02/1999 04:56 UTC" + formatter.format (Date_Time.new 1999 02 03 04 56 11 zone=Time_Zone.utc) . should_equal "03/02/1999 04:56 GMT" formatter.format (Date_Time.new 1999 02 03 04 56 11 zone=(Time_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" + formatter.format (Time_Of_Day.new 13 55) . should_equal "1:55PM" Test.specify "should act as identity on Text" <| formatter = Data_Formatter.Value @@ -237,7 +238,7 @@ spec = Test.group "DataFormatter builders" <| # We create a formatter with all non-default values to ensure that the builders keep the existing values of other properties instead of switching to the constructor's defaults. - formatter_1 = Data_Formatter.Value trim_values=False allow_leading_zeros=True decimal_point=',' thousand_separator='_' allow_exponential_notation=True datetime_formats=["yyyy/MM/dd HH:mm:ss"] date_formats=["dd/MM/yyyy"] time_formats=["HH/mm/ss"] datetime_locale=Locale.uk true_values=["YES"] false_values=["NO"] + formatter_1 = Data_Formatter.Value trim_values=False allow_leading_zeros=True decimal_point=',' thousand_separator='_' allow_exponential_notation=True datetime_formats=[Date_Time_Formatter.from "yyyy/MM/dd HH:mm:ss"] date_formats=[Date_Time_Formatter.from "dd/MM/yyyy"] time_formats=[Date_Time_Formatter.from "HH/mm/ss"] true_values=["YES"] false_values=["NO"] Test.specify "should allow changing number formatting settings" <| formatter_2 = formatter_1.with_number_formatting decimal_point="*" formatter_2.decimal_point . should_equal "*" @@ -249,7 +250,6 @@ spec = formatter_2.date_formats . should_equal formatter_1.date_formats formatter_2.datetime_formats . should_equal formatter_1.datetime_formats formatter_2.time_formats . should_equal formatter_1.time_formats - formatter_2.datetime_locale . should_equal formatter_1.datetime_locale formatter_2.trim_values . should_equal formatter_1.trim_values formatter_3 = formatter_1.with_number_formatting thousand_separator="" allow_exponential_notation=False allow_leading_zeros=False @@ -262,7 +262,7 @@ spec = formatter_1.with_datetime_formats . should_equal formatter_1 formatter_2 = formatter_1.with_datetime_formats date_formats="dd.MM.yyyy" - formatter_2.date_formats . should_equal ["dd.MM.yyyy"] + formatter_2.date_formats.to_text . should_equal [Date_Time_Formatter.from "dd.MM.yyyy"].to_text formatter_2.datetime_formats . should_equal formatter_1.datetime_formats formatter_2.time_formats . should_equal formatter_1.time_formats formatter_2.decimal_point . should_equal formatter_1.decimal_point @@ -271,35 +271,10 @@ spec = formatter_2.allow_exponential_notation . should_equal formatter_1.allow_exponential_notation formatter_2.true_values . should_equal formatter_1.true_values formatter_2.false_values . should_equal formatter_1.false_values - formatter_2.datetime_locale . should_equal formatter_1.datetime_locale formatter_2.trim_values . should_equal formatter_1.trim_values formatter_3 = formatter_1.with_datetime_formats date_formats=[] datetime_formats=["foobar"] time_formats="baz" - formatter_3.date_formats . should_equal [] - formatter_3.datetime_formats . should_equal ["foobar"] - formatter_3.time_formats . should_equal ["baz"] - formatter_3.decimal_point . should_equal formatter_1.decimal_point - formatter_3.thousand_separator . should_equal formatter_1.thousand_separator - formatter_3.allow_leading_zeros . should_equal formatter_1.allow_leading_zeros - formatter_3.allow_exponential_notation . should_equal formatter_1.allow_exponential_notation - formatter_3.true_values . should_equal formatter_1.true_values - formatter_3.false_values . should_equal formatter_1.false_values - formatter_3.datetime_locale . should_equal formatter_1.datetime_locale - formatter_3.trim_values . should_equal formatter_1.trim_values - - Test.specify "should allow changing the datetime locale" <| - formatter_2 = formatter_1.with_locale Locale.france - formatter_2.datetime_locale . should_equal Locale.france - formatter_2.date_formats . should_equal formatter_1.date_formats - formatter_2.datetime_formats . should_equal formatter_1.datetime_formats - formatter_2.time_formats . should_equal formatter_1.time_formats - formatter_2.decimal_point . should_equal formatter_1.decimal_point - formatter_2.thousand_separator . should_equal formatter_1.thousand_separator - formatter_2.allow_leading_zeros . should_equal formatter_1.allow_leading_zeros - formatter_2.allow_exponential_notation . should_equal formatter_1.allow_exponential_notation - formatter_2.true_values . should_equal formatter_1.true_values - formatter_2.false_values . should_equal formatter_1.false_values - formatter_2.trim_values . should_equal formatter_1.trim_values + formatter_3.should_fail_with Date_Time_Format_Parse_Error Test.specify "should allow changing booleans' representations" <| formatter_2 = formatter_1.with_boolean_values "1" "0" @@ -312,7 +287,6 @@ spec = formatter_2.allow_exponential_notation . should_equal formatter_1.allow_exponential_notation formatter_2.true_values . should_equal ["1"] formatter_2.false_values . should_equal ["0"] - formatter_2.datetime_locale . should_equal formatter_1.datetime_locale formatter_2.trim_values . should_equal formatter_1.trim_values formatter_3 = formatter_1.with_boolean_values false_values=[] true_values=[] diff --git a/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso b/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso index a3c7922902ae..78970f5c3a95 100644 --- a/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso +++ b/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso @@ -1,5 +1,6 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error from Standard.Table import Table, Data_Formatter, Column from Standard.Table.Data.Type.Value_Type import Value_Type, Auto @@ -91,13 +92,13 @@ spec = t8.at "dates" . to_vector . should_equal [Date.new 2022 5 7, Date.new 2001 1 1, Date.new 2010 12 31] Test.specify "should parse date and time in various formats" <| - opts = Data_Formatter.Value date_formats=["d.M.y", "d MMM y[ G]", "E, d MMM y"] datetime_formats=["yyyy-MM-dd'T'HH:mm:ss", "dd/MM/yyyy HH:mm"] time_formats=["H:mm:ss.n", "h:mma"] + opts = Data_Formatter.Value.with_datetime_formats date_formats=["d.M.y", (Date_Time_Formatter.from_java "d MMM y[ G]"), "ddd, d MMM y"] datetime_formats=["yyyy-MM-dd HH:mm:ss", "dd/MM/yyyy HH:mm"] time_formats=["H:mm:ss.f", "h:mma"] t1 = Table.new [["dates", ["1.2.476", "10 Jan 1900 AD", "Tue, 3 Jun 2008"]]] t2 = t1.parse format=opts type=Value_Type.Date t2.at "dates" . to_vector . should_equal [Date.new 476 2 1, Date.new 1900 1 10, Date.new 2008 6 3] - t3 = Table.new [["datetimes", ["2011-12-03T10:15:30", "31/12/2012 22:33"]]] + t3 = Table.new [["datetimes", ["2011-12-03 10:15:30", "31/12/2012 22:33"]]] t4 = t3.parse format=opts type=Value_Type.Date_Time t4.at "datetimes" . to_vector . should_equal [Date_Time.new 2011 12 3 10 15 30, Date_Time.new 2012 12 31 22 33] @@ -566,9 +567,9 @@ spec = Test.specify "should handle invalid format strings gracefully" <| c1 = Column.from_vector "date" ["5/7/2022", "1/1/2000", "12/31/2010"] - c1.parse type=Value_Type.Date "M/d/fqsrf" . should_fail_with Illegal_Argument - c1.parse type=Value_Type.Time "HH:mm:ss.fff" . should_fail_with Illegal_Argument - c1.parse type=Value_Type.Date_Time "M/d/fqsrf HH:mm:ss.fff" . should_fail_with Illegal_Argument + c1.parse type=Value_Type.Date "M/d/fqsrf" . should_fail_with Date_Time_Format_Parse_Error + c1.parse type=Value_Type.Time "ęęę" . should_fail_with Date_Time_Format_Parse_Error + c1.parse type=Value_Type.Date_Time "M/d/fqsrf HH:mm:ss.fff" . should_fail_with Date_Time_Format_Parse_Error Test.specify "should correctly work in Auto mode" <| c1 = Column.from_vector "A" ["1", "2", "3"] diff --git a/test/Table_Tests/src/IO/Delimited_Write_Spec.enso b/test/Table_Tests/src/IO/Delimited_Write_Spec.enso index a732b73da59e..12b50c884aa6 100644 --- a/test/Table_Tests/src/IO/Delimited_Write_Spec.enso +++ b/test/Table_Tests/src/IO/Delimited_Write_Spec.enso @@ -169,7 +169,7 @@ spec = file.delete Test.specify 'should allow to always quote text and custom values, but for non-text primitives only if absolutely necessary' <| - format = Delimited "," value_formatter=(Data_Formatter.Value thousand_separator='"' date_formats=["E, d MMM y"]) . with_quotes always_quote=True quote_escape='\\' + format = Delimited "," value_formatter=(Data_Formatter.Value thousand_separator='"' . with_datetime_formats date_formats=["dddd, 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.Value 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 diff --git a/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso b/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso index 30abd594bb05..08f051dc8602 100644 --- a/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso +++ b/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso @@ -1,5 +1,8 @@ from Standard.Base import all +import Standard.Base.Errors.Common.Type_Error +import Standard.Base.Errors.Time_Error.Time_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error import Standard.Table.Data.Type.Value_Type.Bits @@ -22,13 +25,13 @@ spec = Test.specify "Date with locale" <| input = Column.from_vector "values" [Date.new 2020 6 21, Date.new 2023 4 25] - expected_default = Column.from_vector "values" ["21. June 2020", "25. April 2023"] - expected_gb = Column.from_vector "values" ["21. Jun 2020", "25. Apr 2023"] + expected_default = Column.from_vector "values" ["21. Jun 2020", "25. Apr 2023"] + expected_gb = Column.from_vector "values" ["21. June 2020", "25. April 2023"] expected_fr = Column.from_vector "values" ["21. juin 2020", "25. avril 2023"] input.format "d. MMMM yyyy" . should_equal expected_default - input.format "d. MMMM yyyy" (Locale.default) . should_equal expected_default - input.format "d. MMMM yyyy" (Locale.new "gb") . should_equal expected_gb - input.format "d. MMMM yyyy" (Locale.new "fr") . should_equal expected_fr + input.format (Date_Time_Formatter.from "d. MMMM yyyy" Locale.default) . should_equal expected_default + input.format (Date_Time_Formatter.from "d. MMMM yyyy" Locale.uk) . should_equal expected_gb + input.format (Date_Time_Formatter.from "d. MMMM yyyy" Locale.france) . should_equal expected_fr Test.specify "Empty/Nothing format" <| input = Column.from_vector "values" [Date.new 2020 12 21, Date.new 2023 4 25] @@ -39,7 +42,7 @@ spec = Test.specify "Bad format" <| input = Column.from_vector "values" [Date.new 2020 6 21, Date.new 2023 4 25] - input.format "DDDDD" . should_fail_with Illegal_Argument + input.format "jjjjjj" . should_fail_with Date_Time_Format_Parse_Error Test.group "Date Column.format, with format Column" <| Test.specify "Date column" <| @@ -64,8 +67,8 @@ spec = Test.specify "Bad format" <| input = Column.from_vector "values" [Date.new 2020 6 21, Date.new 2023 4 25, Date.new 2023 4 26] - formats = Column.from_vector "formats" ["yyyyMMdd", "DDDDD", "FFF"] - input.format formats . should_fail_with Illegal_Argument + formats = Column.from_vector "formats" ["yyyyMMdd", "jjjjj", "FFF"] + input.format formats . should_fail_with Date_Time_Format_Parse_Error Test.specify "Bad format column type" <| input = Column.from_vector "values" [Date.new 2020 6 21, Date.new 2023 4 25, Date.new 2023 4 26] @@ -86,25 +89,35 @@ spec = Test.specify "Date_Time with locale" <| input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20, Date_Time.new 2023 4 25 14 25 2] - expected_default = Column.from_vector "values" ["21. June 2020 08.10.20", "25. April 2023 14.25.02"] - expected_gb = Column.from_vector "values" ["21. Jun 2020 08.10.20", "25. Apr 2023 14.25.02"] + expected_default = Column.from_vector "values" ["21. Jun 2020 08.10.20", "25. Apr 2023 14.25.02"] + expected_gb = Column.from_vector "values" ["21. June 2020 08.10.20", "25. April 2023 14.25.02"] expected_fr = Column.from_vector "values" ["21. juin 2020 08.10.20", "25. avril 2023 14.25.02"] input.format "d. MMMM yyyy HH.mm.ss" . should_equal expected_default - input.format "d. MMMM yyyy HH.mm.ss" (Locale.default) . should_equal expected_default - input.format "d. MMMM yyyy HH.mm.ss" (Locale.new "gb") . should_equal expected_gb - input.format "d. MMMM yyyy HH.mm.ss" (Locale.new "fr") . should_equal expected_fr + input.format (Date_Time_Formatter.from "d. MMMM yyyy HH.mm.ss" Locale.default) . should_equal expected_default + input.format (Date_Time_Formatter.from "d. MMMM yyyy HH.mm.ss" Locale.uk) . should_equal expected_gb + input.format (Date_Time_Formatter.from "d. MMMM yyyy HH.mm.ss" Locale.france) . should_equal expected_fr + + Test.specify "overriding the Locale with `format` argument" <| + formatter = Date_Time_Formatter.from "d. MMMM yyyy HH.mm.ss" Locale.france + input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20, Date_Time.new 2023 4 25 14 25 2] + expected_fr = Column.from_vector "values" ["21. juin 2020 08.10.20", "25. avril 2023 14.25.02"] + expected_pl = Column.from_vector "values" ["21. czerwca 2020 08.10.20", "25. kwietnia 2023 14.25.02"] + + input.format formatter . should_equal expected_fr + # If I provide a locale argument, it overrides what is already in the formatter: + input.format formatter Locale.poland . should_equal expected_pl Test.specify "Empty/Nothing format" <| zone = Time_Zone.parse "US/Hawaii" input = Column.from_vector "values" [Date_Time.new 2020 12 21 8 10 20 zone=zone, Date_Time.new 2023 4 25 14 25 2 zone=zone] - expected = Column.from_vector "values" ['2020-12-21T08:10:20-10:00[US/Hawaii]', '2023-04-25T14:25:02-10:00[US/Hawaii]'] + expected = Column.from_vector "values" ['2020-12-21 08:10:20-10:00[US/Hawaii]', '2023-04-25 14:25:02-10:00[US/Hawaii]'] input.format . should_equal expected input.format "" . should_equal expected input.format Nothing . should_equal expected Test.specify "Bad format" <| input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20, Date_Time.new 2023 4 25 14 25 2] - input.format "DDDDD" . should_fail_with Illegal_Argument + input.format "jjjjjjjj" . should_fail_with Date_Time_Format_Parse_Error Test.group "Date_Time Column.format, with format Column" <| Test.specify "Date_Time column" <| @@ -124,14 +137,14 @@ spec = zone = Time_Zone.parse "US/Hawaii" input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20 zone=zone, Date_Time.new 2023 4 25 14 25 2 zone=zone] formats = Column.from_vector "formats" ["", Nothing] - expected = Column.from_vector "values" ['2020-06-21T08:10:20-10:00[US/Hawaii]', '2023-04-25T14:25:02-10:00[US/Hawaii]'] + expected = Column.from_vector "values" ['2020-06-21 08:10:20-10:00[US/Hawaii]', '2023-04-25 14:25:02-10:00[US/Hawaii]'] actual = input.format formats actual . should_equal expected Test.specify "Bad format" <| input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20, Date_Time.new 2023 4 25 14 25 2, Date_Time.new 2023 4 26 3 4 5] - formats = Column.from_vector "formats" ["yyyyMMdd HH.mm.ss", "DDDDD", "FFF"] - input.format formats . should_fail_with Illegal_Argument + formats = Column.from_vector "formats" ["yyyyMMdd HH.mm.ss", "jjjjj", "FFF"] + input.format formats . should_fail_with Date_Time_Format_Parse_Error Test.specify "Bad format column type" <| input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20, Date_Time.new 2023 4 25 14 25 2] @@ -140,7 +153,7 @@ spec = Test.specify "column length mismatch" <| input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20, Date_Time.new 2023 4 25 14 25 2] - formats = Column.from_vector "formats" ["yyyyMMdd", "DDDDD", "w"] + formats = Column.from_vector "formats" ["yyyyMMdd", "jjjj", "w"] input.format formats . should_fail_with Illegal_Argument Test.group "Time_Of_Day Column.format, with format string" <| @@ -167,11 +180,11 @@ spec = Test.specify "Bad format" <| input = Column.from_vector "values" [Time_Of_Day.new 8 10 20, Time_Of_Day.new 14 25 2] - input.format "DDDDD" . should_fail_with Illegal_Argument + input.format "jjjj" . should_fail_with Date_Time_Format_Parse_Error Test.specify "Format for wrong date type" <| input = Column.from_vector "values" [Time_Of_Day.new 8 10 20, Time_Of_Day.new 14 25 2] - input.format "yyyyMMdd HH.mm.ss" . should_fail_with Illegal_Argument + input.format "yyyyMMdd HH.mm.ss" . should_fail_with Time_Error Test.group "Time_Of_Day Column.format, with format Column" <| Test.specify "Time_Of_Day column" <| @@ -196,8 +209,8 @@ spec = Test.specify "Bad format" <| input = Column.from_vector "values" [Time_Of_Day.new 8 10 20, Time_Of_Day.new 14 25 2, Time_Of_Day.new 3 4 5] - formats = Column.from_vector "formats" ["HH.mm.ss", "DDDDD", "FFF"] - input.format formats . should_fail_with Illegal_Argument + formats = Column.from_vector "formats" ["HH.mm.ss", "jjjjj", "FFF"] + input.format formats . should_fail_with Date_Time_Format_Parse_Error Test.specify "Bad format column type" <| input = Column.from_vector "values" [Time_Of_Day.new 8 10 20, Time_Of_Day.new 14 25 2] @@ -206,7 +219,7 @@ spec = Test.specify "column length mismatch" <| input = Column.from_vector "values" [Time_Of_Day.new 8 10 20, Time_Of_Day.new 14 25 2] - formats = Column.from_vector "formats" ["yyyyMMdd", "DDDDD", "w"] + formats = Column.from_vector "formats" ["yyyyMMdd", "jjjjj", "w"] input.format formats . should_fail_with Illegal_Argument Test.group "Boolean Column.format, with format string" <| @@ -289,7 +302,7 @@ spec = Test.specify "Format is not text" <| input = Column.from_vector "values" [Date.new 2020 12 21, Date.new 2023 4 25] - input.format 73 . should_fail_with Illegal_Argument + Test.expect_panic_with (input.format 73) Type_Error Test.group "Edge cases" <| Test.specify "empty table is ok" <| diff --git a/test/Table_Tests/src/In_Memory/Table_Date_Time_Spec.enso b/test/Table_Tests/src/In_Memory/Table_Date_Time_Spec.enso index a723f22c01b9..2f69ef48b991 100644 --- a/test/Table_Tests/src/In_Memory/Table_Date_Time_Spec.enso +++ b/test/Table_Tests/src/In_Memory/Table_Date_Time_Spec.enso @@ -38,7 +38,8 @@ spec = Test.specify "should serialise back to input" <| expected_text = normalize_lines <| (enso_project.data / "datetime_sample_normalized_hours.csv").read_text - delimited = Text.from expected format=(Delimited "," line_endings=Line_Ending_Style.Unix) + data_formatter = Data_Formatter.Value . with_datetime_formats datetime_formats=["yyyy-MM-dd HH:mm:ss"] + delimited = Text.from expected format=(Delimited "," line_endings=Line_Ending_Style.Unix value_formatter=data_formatter) delimited.should_equal expected_text Test.specify "should serialise dates with format" <| diff --git a/test/Table_Tests/src/Util.enso b/test/Table_Tests/src/Util.enso index acc35795a8ec..a8e1195067f8 100644 --- a/test/Table_Tests/src/Util.enso +++ b/test/Table_Tests/src/Util.enso @@ -35,7 +35,7 @@ In_Memory_Column.should_equal self expected frames_to_skip=0 = Test.fail "Expected column name "+expected.name+", but got "+self.name+" (at "+loc+")." if self.length != expected.length then Test.fail "Expected column length "+expected.length.to_text+", but got "+self.length.to_text+" (at "+loc+")." - self.to_vector.should_equal expected.to_vector + self.to_vector.should_equal expected.to_vector 2+frames_to_skip _ -> Test.fail "Got a Column, but expected a "+expected.to_display_text+' (at '+loc+').' Database_Table.should_equal : Database_Table -> Integer -> Test_Result diff --git a/test/Tests/src/Data/Time/Date_Spec.enso b/test/Tests/src/Data/Time/Date_Spec.enso index 46d58cafe856..e31409feb868 100644 --- a/test/Tests/src/Data/Time/Date_Spec.enso +++ b/test/Tests/src/Data/Time/Date_Spec.enso @@ -30,7 +30,7 @@ spec_with name create_new_date parse_date = Test.specify "should handle errors when creating local date" <| case create_new_date 2020 30 30 . catch of - Time_Error.Error msg -> + Time_Error.Error msg _ -> msg . should_equal "Invalid value for MonthOfYear (valid values 1 - 12): 30" result -> Test.fail ("Unexpected result: " + result.to_text) @@ -39,15 +39,11 @@ spec_with name create_new_date parse_date = text = create_new_date 2020 12 21 . format "yyyyMMdd" text . should_equal "20201221" - Test.specify "should format local date using provided pattern and US locale" <| - d = create_new_date 2020 6 21 - d.format "d. MMM yyyy" . should_equal "21. Jun 2020" - d.format "d. MMMM yyyy" . should_equal "21. June 2020" - Test.specify "should format local date using provided pattern and locale" <| d = create_new_date 2020 6 21 - d.format "d. MMMM yyyy" (Locale.new "gb") . should_equal "21. Jun 2020" - d.format "d. MMMM yyyy" (Locale.new "fr") . should_equal "21. juin 2020" + d.format "d. MMMM yyyy" . should_equal "21. Jun 2020" + d.format (Date_Time_Formatter.from "d. MMMM yyyy" locale=(Locale.uk)) . should_equal "21. June 2020" + d.format (Date_Time_Formatter.from "d. MMMM yyyy" locale=(Locale.france)) . should_equal "21. juin 2020" Test.specify "should format local date using default pattern" <| text = create_new_date 2020 12 21 . to_text @@ -62,8 +58,8 @@ spec_with name create_new_date parse_date = Test.specify "should throw error when parsing invalid date" <| case parse_date "birthday" . catch of - Time_Error.Error msg -> - msg . should_equal "Text 'birthday' could not be parsed at index 0" + Time_Error.Error msg _ -> + msg . should_contain "Text 'birthday' could not be parsed" result -> Test.fail ("Unexpected result: " + result.to_text) @@ -73,32 +69,6 @@ spec_with name create_new_date parse_date = date . month . should_equal 1 date . day . should_equal 1 - Test.specify "should parse custom format" <| - date = parse_date "1999 1 1" "yyyy M d" - date . year . should_equal 1999 - date . month . should_equal 1 - date . day . should_equal 1 - - Test.specify "should parse text month formats" <| - date = parse_date "1999 Jan 1" "yyyy MMM d" - date . year . should_equal 1999 - date . month . should_equal 1 - date . day . should_equal 1 - - Test.specify "should parse text long month formats" <| - date = parse_date "1999 January 1" "yyyy MMMM d" - date . year . should_equal 1999 - date . month . should_equal 1 - date . day . should_equal 1 - - Test.specify "should throw error when parsing custom format" <| - date = parse_date "1999-01-01" "yyyy M d" - case date.catch of - Time_Error.Error msg -> - msg . should_equal "Text '1999-01-01' could not be parsed at index 4" - result -> - Test.fail ("Unexpected result: " + result.to_text) - Test.specify "should convert to time" <| time = create_new_date 2000 12 21 . to_date_time (Time_Of_Day.new 12 30 45) Time_Zone.utc time . year . should_equal 2000 @@ -555,7 +525,7 @@ main = Test_Suite.run_main spec parseNormally x y = (Date.parse x y) . to_text -js_parse text format="" = +js_parse text format=Date_Time_Formatter.iso_date = d = Date.parse text format js_date d.year d.month d.day @@ -590,7 +560,7 @@ python_date year month=1 day=1 = "Invalid value for MonthOfYear (valid values 1 - 12): " + month.to_text Error.throw <| Time_Error.Error msg -python_parse text format="" = +python_parse text format=Date_Time_Formatter.iso_date = d = Date.parse text format python_date d.year d.month d.day diff --git a/test/Tests/src/Data/Time/Date_Time_Formatter_Spec.enso b/test/Tests/src/Data/Time/Date_Time_Formatter_Spec.enso new file mode 100644 index 000000000000..e9fba5579993 --- /dev/null +++ b/test/Tests/src/Data/Time/Date_Time_Formatter_Spec.enso @@ -0,0 +1,240 @@ +from Standard.Base import all +import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error +import Standard.Base.Errors.Time_Error.Time_Error + +from Standard.Test import Test, Test_Suite +import Standard.Test.Extensions + +polyglot java import java.time.format.DateTimeFormatter + +spec = + Test.group "Parsing formats" <| + Test.specify "should throw informative error for replacements of Java patterns in Simple format" <| + r1 = Date_Time_Formatter.from "d LLL yyyy" + r1.should_fail_with Date_Time_Format_Parse_Error + r1.catch.to_display_text . should_contain "use `MMM`" + + r2 = Date_Time_Formatter.from "dd-MM-yyyy HH:mm:ss '['XX']'" + r2.should_fail_with Date_Time_Format_Parse_Error + r2.catch.to_display_text . should_contain "use `zz`" + + r3 = Date_Time_Formatter.from "yyyy-ww-dd" + r3.should_fail_with Date_Time_Format_Parse_Error + r3.catch.to_display_text . should_contain "consider using `from_iso_week_date_pattern`" + + r4 = Date_Time_Formatter.from "yyyy-MMMMMMM-dd" + r4.should_fail_with Date_Time_Format_Parse_Error + r4.catch.to_display_text . should_contain "at most 4" + + Test.specify "should report format parse failures" <| + Date_Time_Formatter.from "yyyy[" . should_fail_with Date_Time_Format_Parse_Error + Date_Time_Formatter.from "yyyy{12}" . should_fail_with Date_Time_Format_Parse_Error + Date_Time_Formatter.from "yy{baz}" . should_fail_with Date_Time_Format_Parse_Error + Date_Time_Formatter.from "MM{baz}" . should_fail_with Date_Time_Format_Parse_Error + Date_Time_Formatter.from "{baz}" . should_fail_with Date_Time_Format_Parse_Error + Date_Time_Formatter.from "]" . should_fail_with Date_Time_Format_Parse_Error + Date_Time_Formatter.from "'" . should_fail_with Date_Time_Format_Parse_Error + + Test.group "Formatting date/time values" <| + Test.specify "should allow printing month names" <| + d = Date.new 2020 6 30 + d.format "d. MMM yyyy" . should_equal "30. Jun 2020" + d.format (Date_Time_Formatter.from "d. MMMM yyyy" Locale.us) . should_equal "30. June 2020" + # Note that the default (ROOT) locale returns a short name even for MMMM (full name) month format. + d.format (Date_Time_Formatter.from "d. MMMM yyyy" Locale.default) . should_equal "30. Jun 2020" + + Test.specify "should allow using a Java formatter" <| + jformatter = Date_Time_Formatter.from_java DateTimeFormatter.ISO_ORDINAL_DATE + Date.new 2020 2 1 . format jformatter . should_equal "2020-032" + + Test.specify "should allow parsing Java patterns" <| + Date.new 2020 2 1 . format (Date_Time_Formatter.from_java "E, d LLL yyyy") . should_equal "Sat, 1 Feb 2020" + + Test.specify "should handle various formats" <| + Date.new 2023 09 21 . format "E, dd.MM.yy" . should_equal "Thu, 21.09.23" + Date.new 2023 09 21 . format (Date_Time_Formatter.from "DDDD" Locale.poland) . should_equal "czwartek" + Date.new 2023 09 21 . format (Date_Time_Formatter.from_iso_week_date_pattern "eee, 'W'WW ''yy" Locale.uk) . should_equal "Thursday, W38 '23" + Date.new 2023 09 21 . format "'Q'Q ''yy{1999}" . should_equal "Q3 '23" + + tz = Time_Zone.parse "US/Hawaii" + Date_Time.new 2023 09 21 12 zone=tz . format "yyyy/MM/dd HH:mm:ss VV" . should_equal "2023/09/21 12:00:00 US/Hawaii" + + Test.specify "should allow to customize the 'zero' of a zone offset" <| + dt = Date_Time.new 2020 01 02 12 zone=(Time_Zone.utc) + dt.format "yyyy/MM/dd HH:mm:ss ZZ" . should_equal "2020/01/02 12:00:00 +0000" + dt.format "yyyy/MM/dd HH:mm:ss ZZ{Z}" . should_equal "2020/01/02 12:00:00 Z" + dt.format "yyyy/MM/dd HH:mm:ss ZZ{}" . should_equal "2020/01/02 12:00:00 " + + dt2 = Date_Time.new 2020 01 02 12 zone=(Time_Zone.parse "US/Hawaii") + dt2.format "yyyy/MM/dd HH:mm:ss ZZ" . should_equal "2020/01/02 12:00:00 -1000" + dt2.format "yyyy/MM/dd HH:mm:ss ZZ{Z}" . should_equal "2020/01/02 12:00:00 -1000" + dt2.format "yyyy/MM/dd HH:mm:ss ZZZZZ{}" . should_equal "2020/01/02 12:00:00 -10:00" + + Test.group "Parsing date/time values" <| + Test.specify "should allow short month names" <| + Date.parse "30. Jun 2020" "d. MMM yyyy" . should_equal (Date.new 2020 6 30) + + Test.specify "should allow long month names" <| + Date.parse "30. June 2020" (Date_Time_Formatter.from "d. MMMM yyyy" Locale.uk) . should_equal (Date.new 2020 6 30) + + Test.specify "should parse default time format" <| + text = Date_Time.new 1970 (zone = Time_Zone.utc) . to_text + time = Date_Time.parse text + 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 0 + time . nanosecond . should_equal 0 + time . zone . zone_id . should_equal Time_Zone.utc.zone_id + + Test.specify "should parse local time adding system zone" <| + time = Date_Time.parse "1970-01-01T00:00:01" + 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 0 + (time.zone.offset time) . should_equal (Time_Zone.system.offset time) + + Test.specify "should parse time Z" <| + time = Date_Time.parse "1582-10-15T00:00:01Z" + time . to_enso_epoch_seconds . should_equal 1 + time.zone.zone_id . should_equal "Z" + + Test.specify "should parse time UTC" <| + time = Date_Time.parse "1582-10-15T00:00:01Z[UTC]" + time . to_enso_epoch_seconds . should_equal 1 + time . zone . zone_id . should_equal "UTC" + + Test.specify "should parse time with nanoseconds" <| + time = Date_Time.parse "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 include_milliseconds=True . should_equal 123456789 + time . millisecond . should_equal 123 + time . microsecond . should_equal 456 + time . nanosecond . should_equal 789 + time.zone.zone_id . should_equal "Z" + + Test.specify "should parse time with offset-based zone" <| + time = Date_Time.parse "1970-01-01T00:00:01+01:00" + 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 . millisecond . should_equal 0 + time . microsecond . should_equal 0 + time . nanosecond . should_equal 0 + time.zone.zone_id . take (Last 6) . should_equal "+01:00" + + Test.specify "should parse time with id-based zone" <| + time = Date_Time.parse "1970-01-01T00:00:01+01:00[Europe/Paris]" + 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 . millisecond . should_equal 0 + time . microsecond . should_equal 0 + time . nanosecond . should_equal 0 + zone = time.zone + zone.offset time . should_equal 3600 + zone.zone_id . should_equal "Europe/Paris" + time.to_display_text . should_equal "1970-01-01 00:00:01 Europe/Paris" + + Test.specify "should throw error when parsing invalid time" <| + case Date_Time.parse "2008-1-1" . catch of + Time_Error.Error msg _ -> + msg . should_contain "Text '2008-1-1' could not be parsed" + result -> + Test.fail ("Unexpected result: " + result.to_text) + + Test.specify "should parse custom format of zoned time" <| + time = Date_Time.parse "2020-05-06 04:30:20 UTC" "yyyy-MM-dd HH:mm:ss VV" + time . year . should_equal 2020 + time . month . should_equal 5 + time . day . should_equal 6 + time . hour . should_equal 4 + time . minute . should_equal 30 + time . second . should_equal 20 + time . millisecond . should_equal 0 + time . microsecond . should_equal 0 + time . nanosecond . should_equal 0 + (time.zone.zone_id . take (Last 3) . to_case Case.Upper) . should_equal "UTC" + + Test.specify "should parse custom format of local time" <| + time = Date_Time.parse "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 + time . hour . should_equal 4 + time . minute . should_equal 30 + time . second . should_equal 0 + time . millisecond . should_equal 0 + time . microsecond . should_equal 0 + time . nanosecond . should_equal 0 + + Test.specify "should throw error when parsing custom format" <| + time = Date_Time.parse "2008-01-01" "yyyy-MM-dd'T'HH:mm:ss'['tt']'" + case time.catch of + Time_Error.Error msg _ -> + msg . should_contain "Text '2008-01-01' could not be parsed" + result -> + Test.fail ("Unexpected result: " + result.to_text) + + Test.specify "should be able to parse YYYY as well as yyyy" <| + Date.parse "2020-01-02" "YYYY-MM-dd" . should_equal (Date.new 2020 1 2) + Date.parse "2020-01-02" "yyyy-MM-dd" . should_equal (Date.new 2020 1 2) + + Test.specify "should be able to parse year-month without day" <| + Date.parse "2022-05" "yyyy-MM" . should_equal (Date.new 2022 5 1) + + Test.specify "should be able to parse a quarter without day" <| + Date.parse "Q2 of 2022" "'Q'Q 'of' yyyy" . should_equal (Date.new 2022 4 1) + + Test.specify "should be able to parse a day and month without year - defaulting to current year" <| + current_year = Date.today.year + Date.parse "07/23" "MM/dd" . should_equal (Date.new current_year 7 23) + Date.parse "14. of May" "d. 'of' MMMM" . should_equal (Date.new current_year 5 14) + + Test.specify "should be able to parse 2-digit year" <| + Date.parse "22-05-06" "yy-MM-dd" . should_equal (Date.new 2022 5 6) + Date.parse "99-01-02" "yy-MM-dd" . should_equal (Date.new 1999 1 2) + Date.parse "49-03-04" "yy-MM-dd" . should_equal (Date.new 2049 3 4) + Date.parse "50-03-04" "yy-MM-dd" . should_equal (Date.new 1950 3 4) + + Test.specify "should be able to parse 2-digit year with custom base-year" <| + Date.parse "22-05-06" "yy{1999}-MM-dd" . should_equal (Date.new 1922 5 6) + Date.parse "99-01-02" "yy{1949}-MM-dd" . should_equal (Date.new 1899 1 2) + Date.parse "49-03-04" "yy{3099}-MM-dd" . should_equal (Date.new 3049 3 4) + Date.parse "50-03-04" "yy{2099}-MM-dd" . should_equal (Date.new 2050 3 4) + + Test.specify "should work like in examples" <| + Date.parse "Tue, 12 Oct 2021" "ddd, d MMM yyyy" . should_equal (Date.new 2021 10 12) + Date.parse "Thursday, 1 October '98" (Date_Time_Formatter.from "dddd, d MMMM ''yy" Locale.uk) . should_equal (Date.new 1998 10 01) + Date_Time.parse "12/10/2021 5:34 PM" "d/M/Y h:mm a" . should_equal (Date_Time.new 2021 10 12 17 34 00) + Date.parse "2021-10" "yyyy-MM" . should_equal (Date.new 2021 10 01) + Date.parse "10-12" "MM-dd" . should_equal (Date.new (Date.today.year) 10 12) + Date.parse "1 Nov '95" "d MMM ''yy{2099}" . should_equal (Date.new 2095 11 01) + Date_Time.parse "2021-10-12T12:34:56.789+0200" "yyyy-MM-dd'T'HH:mm:ss.fZ" . should_equal (Date_Time.new 2021 10 12 12 34 56 millisecond=789 zone=(Time_Zone.new hours=2)) + + Test.specify "should be able to parse a week-based year format" <| + Date.parse "1976-W53-6" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW-d") . should_equal (Date.new 1977 01 01) + Date_Time.parse "1978-W01-4 12:34:56" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW-d HH:mm:ss") . should_equal (Date_Time.new 1978 01 05 12 34 56) + + Date.parse "1978-W01, Mon" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW, eee") . should_equal (Date.new 1978 01 02) + # Just week will parse to first day of the week: + Date.parse "1978-W01" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW") . should_equal (Date.new 1978 01 02) + +main = Test_Suite.run_main spec diff --git a/test/Tests/src/Data/Time/Date_Time_Spec.enso b/test/Tests/src/Data/Time/Date_Time_Spec.enso index b16292e6db02..613ff1b1d827 100644 --- a/test/Tests/src/Data/Time/Date_Time_Spec.enso +++ b/test/Tests/src/Data/Time/Date_Time_Spec.enso @@ -21,10 +21,7 @@ spec = spec_with "Date_Time" enso_datetime Date_Time.parse spec_with "JavascriptDate" js_datetime js_parse nanoseconds_loss_in_precision=True if Polyglot.is_language_installed "python" then - ignore_z_check a b = - if a == 'Z' || b == 'Z' then True else - a.should_equal frames_to_skip=2 b - spec_with "PythonDate" python_datetime python_parse nanoseconds_loss_in_precision=True loose_zone_equal=ignore_z_check + spec_with "PythonDate" python_datetime python_parse nanoseconds_loss_in_precision=True spec_with "JavaZonedDateTime" java_datetime java_parse spec_with "JavascriptDataInArray" js_array_datetime js_parse nanoseconds_loss_in_precision=True @@ -47,11 +44,7 @@ spec = (Date_Time.new 2022 12 12).should_equal (Date_Time.new 2022 12 12) (Date_Time.new 2022 12 12).should_not_equal (Date_Time.new 1996) -default_zone_equal z1 z2 = - z1 . should_equal frames_to_skip=1 z2 - False - -spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=False loose_zone_equal=default_zone_equal = +spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision=False = Test.group name <| Test.specify "should create time" <| @@ -67,7 +60,7 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision= Test.specify "should handle errors when creating time" <| case create_new_datetime 1970 0 0 . catch of - Time_Error.Error msg -> + Time_Error.Error msg _ -> msg.to_text . contains "0" . should_be_true msg.to_text . contains "1" . should_be_true msg.to_text . contains "12" . should_be_true @@ -80,12 +73,12 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision= Test.specify "should format using provided pattern and locale" <| d = create_new_datetime 2020 6 21 - d.format "d. MMMM yyyy" (Locale.new "gb") . should_equal "21. Jun 2020" - d.format "d. MMMM yyyy" (Locale.new "fr") . should_equal "21. juin 2020" + d.format (Date_Time_Formatter.from "d. MMMM yyyy" (Locale.new "gb")) . should_equal "21. Jun 2020" + d.format (Date_Time_Formatter.from "d. MMMM yyyy" (Locale.new "fr")) . should_equal "21. juin 2020" Test.specify "should format using default pattern" <| text = create_new_datetime 1970 (zone = Time_Zone.utc) . to_text - text . should_equal "1970-01-01T00:00:00Z[UTC]" + text . should_equal "1970-01-01 00:00:00Z[UTC]" Test.specify "should convert to Json" <| time = create_new_datetime 1970 12 21 (zone = Time_Zone.utc) @@ -94,130 +87,6 @@ spec_with name create_new_datetime parse_datetime nanoseconds_loss_in_precision= time_pairs = [["year", 1970], ["month", 12], ["day", 21], ["hour", 0], ["minute", 0], ["second", 0], ["nanosecond", 0]] JS_Object.from_pairs ([["type", "Date_Time"], ["constructor", "new"]] + time_pairs + zone_pairs) . to_text - Test.specify "should parse default time format" <| - text = create_new_datetime 1970 (zone = Time_Zone.utc) . to_text - time = parse_datetime text - 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 0 - time . nanosecond . should_equal 0 - time . zone . zone_id . should_equal Time_Zone.utc.zone_id - - Test.specify "should parse local time adding system zone" <| - time = parse_datetime "1970-01-01T00:00:01" - 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 0 - (time.zone.offset time) . should_equal (Time_Zone.system.offset time) - - Test.specify "should parse time Z" <| - time = parse_datetime "1582-10-15T00:00:01Z" - time . to_enso_epoch_seconds . should_equal 1 - loose_zone_equal time.zone.zone_id "Z" - - Test.specify "should parse time UTC" <| - time = parse_datetime "1582-10-15T00:00:01Z[UTC]" - time . to_enso_epoch_seconds . should_equal 1 - time . zone . zone_id . should_equal "UTC" - - Test.specify "should parse time with nanoseconds" <| - 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 - case nanoseconds_loss_in_precision of - True -> - time . nanosecond include_milliseconds=True . should_equal 123000000 - time . millisecond . should_equal 123 - time . microsecond . should_equal 0 - time . nanosecond . should_equal 0 - False -> - time . nanosecond include_milliseconds=True . should_equal 123456789 - time . millisecond . should_equal 123 - time . microsecond . should_equal 456 - time . nanosecond . should_equal 789 - loose_zone_equal time.zone.zone_id "Z" - - Test.specify "should parse time with offset-based zone" <| - 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 - time . hour . should_equal 0 - time . minute . should_equal 0 - time . second . should_equal 1 - time . millisecond . should_equal 0 - time . microsecond . should_equal 0 - time . nanosecond . should_equal 0 - time.zone.zone_id . take (Last 6) . should_equal "+01:00" - - Test.specify "should parse time with id-based zone" <| - 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 - time . hour . should_equal 0 - time . minute . should_equal 0 - time . second . should_equal 1 - time . millisecond . should_equal 0 - time . microsecond . should_equal 0 - time . nanosecond . should_equal 0 - zone = time.zone - zone.offset time . should_equal 3600 - if name.contains "Python" then time.zone.zone_id . should_equal "UTC+01:00" else - zone.zone_id . should_equal "Europe/Paris" - time.to_display_text . should_equal "1970-01-01 00:00:01 Europe/Paris" - - Test.specify "should throw error when parsing invalid time" <| - case parse_datetime "2008-1-1" . catch of - Time_Error.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 = 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 - time . hour . should_equal 4 - time . minute . should_equal 30 - time . second . should_equal 20 - time . millisecond . should_equal 0 - time . microsecond . should_equal 0 - time . nanosecond . should_equal 0 - (time.zone.zone_id . take (Last 3) . to_case Case.Upper) . should_equal "UTC" - - Test.specify "should parse custom format of local time" <| - 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 - time . hour . should_equal 4 - time . minute . should_equal 30 - time . second . should_equal 0 - time . millisecond . should_equal 0 - time . microsecond . should_equal 0 - time . nanosecond . should_equal 0 - - Test.specify "should throw error when parsing custom format" <| - time = parse_datetime "2008-01-01" "yyyy-MM-dd'T'HH:mm:ss'['z']'" - case time.catch of - Time_Error.Error msg -> - msg . should_equal "Text '2008-01-01' could not be parsed at index 10" - result -> - Test.fail ("Unexpected result: " + result.to_text) - Test.specify "should get Enso epoch seconds" <| (create_new_datetime 1582 10 15 0 0 8 (zone = Time_Zone.utc)).to_enso_epoch_seconds . should_equal 8 (Date_Time.enso_epoch_start + (Duration.new minutes=2)).to_enso_epoch_seconds . should_equal (2 * 60) @@ -877,7 +746,7 @@ python_datetime year month=1 day=1 hour=0 minute=0 second=0 nanosecond=0 zone=No Panic.catch Any (python_datetime_impl year month day hour minute second nanosecond z) err-> Error.throw (Time_Error.Error err.payload) -python_parse text format="" = +python_parse text format=Date_Time_Formatter.default_enso_zoned_date_time = d = Date_Time.parse text format python_datetime d.year d.month d.day d.hour d.minute d.second (d.nanosecond include_milliseconds=True) d.zone @@ -887,7 +756,7 @@ foreign js js_local_datetime_impl year month day hour minute second nanosecond = } return new Date(year, month - 1, day, hour, minute, second, nanosecond / 1000000); -js_parse text format="" = +js_parse text format=Date_Time_Formatter.default_enso_zoned_date_time = d = Date_Time.parse text format js_datetime d.year d.month d.day d.hour d.minute d.second (d.nanosecond include_milliseconds=True) d.zone diff --git a/test/Tests/src/Data/Time/Spec.enso b/test/Tests/src/Data/Time/Spec.enso index 1322d2dd627c..7cf4ce3d4c35 100644 --- a/test/Tests/src/Data/Time/Spec.enso +++ b/test/Tests/src/Data/Time/Spec.enso @@ -9,6 +9,7 @@ import project.Data.Time.Time_Of_Day_Spec import project.Data.Time.Date_Spec import project.Data.Time.Date_Range_Spec import project.Data.Time.Date_Time_Spec +import project.Data.Time.Date_Time_Formatter_Spec import project.Data.Time.Time_Zone_Spec import project.Data.Time.Day_Of_Week_Spec @@ -19,6 +20,7 @@ spec = Period_Spec.spec Time_Of_Day_Spec.spec Date_Time_Spec.spec + Date_Time_Formatter_Spec.spec Time_Zone_Spec.spec Day_Of_Week_Spec.spec diff --git a/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso b/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso index a26675b5d738..96a309ea1174 100644 --- a/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso +++ b/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso @@ -28,7 +28,7 @@ specWith name create_new_time parse_time nanoseconds_loss_in_precision=False = Test.specify "should handle errors when creating a time" <| case create_new_time 24 0 0 . catch of - Time_Error.Error msg -> + Time_Error.Error msg _ -> msg.to_text . contains "24" . should_not_equal -1 result -> Test.fail ("Unexpected result: " + result.to_text) @@ -41,8 +41,8 @@ specWith name create_new_time parse_time nanoseconds_loss_in_precision=False = d = create_new_time 12 20 44 # Note that the results are all the same. d.format "HH:mm" . should_equal "12:20" - d.format "HH:mm" (Locale.new "gb") . should_equal "12:20" - d.format "HH:mm" (Locale.new "fr") . should_equal "12:20" + d.format (Date_Time_Formatter.from "HH:mm" (Locale.new "gb")) . should_equal "12:20" + d.format (Date_Time_Formatter.from "HH:mm" (Locale.new "fr")) . should_equal "12:20" Test.specify "should format local time using default pattern" <| text = create_new_time 12 20 44 . to_text @@ -65,7 +65,7 @@ specWith name create_new_time parse_time nanoseconds_loss_in_precision=False = Test.specify "should throw error when parsing invalid time" <| case parse_time "1200" . catch of - Time_Error.Error msg -> + Time_Error.Error msg _ -> msg . should_equal "Text '1200' could not be parsed at index 2" result -> Test.fail ("Unexpected result: " + result.to_text) @@ -77,7 +77,7 @@ specWith name create_new_time parse_time nanoseconds_loss_in_precision=False = Test.specify "should throw error when parsing custom format" <| time = parse_time "12:30" "HH:mm:ss" case time.catch of - Time_Error.Error msg -> + Time_Error.Error msg _ -> msg . should_equal "Text '12:30' could not be parsed at index 5" result -> Test.fail ("Unexpected result: " + result.to_text) @@ -286,11 +286,8 @@ java_parse time_text pattern=Nothing = python_time hour minute=0 second=0 nanoOfSecond=0 = Panic.catch Any (python_time_impl hour minute second nanoOfSecond) (err -> Error.throw (Time_Error.Error <| err.payload)) -python_parse time_text pattern=Nothing = - t = Panic.catch Any handler=(err -> Error.throw (Time_Error.Error err.payload.getMessage)) <| - if pattern.is_nothing then LocalTime.parse time_text else - formatter = DateTimeFormatter.ofPattern pattern - LocalTime.parse time_text (formatter.withLocale Locale.default.java_locale) +python_parse time_text pattern=Date_Time_Formatter.iso_time = + t = Time_Of_Day.parse time_text pattern python_time t.hour t.minute t.second t.nanosecond main = Test_Suite.run_main spec diff --git a/test/Tests/src/Data/Time/Time_Zone_Spec.enso b/test/Tests/src/Data/Time/Time_Zone_Spec.enso index b05fa31c235c..46db0e86a179 100644 --- a/test/Tests/src/Data/Time/Time_Zone_Spec.enso +++ b/test/Tests/src/Data/Time/Time_Zone_Spec.enso @@ -37,7 +37,7 @@ spec = JS_Object.from_pairs [["type", "Time_Zone"], ["constructor", "new"], ["id", "UTC"]] . to_text Test.specify "should throw error when parsing invalid zone id" <| case Time_Zone.parse "foo" . catch of - Time_Error.Error msg -> + Time_Error.Error msg _ -> msg . should_equal "Unknown time-zone ID: foo" result -> Test.fail ("Unexpected result: " + result.to_text)