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