From 8d926166ea3b6d8bb3709277682639ba29b109e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rados=C5=82aw=20Wa=C5=9Bko?= Date: Thu, 28 Sep 2023 11:38:00 +0200 Subject: [PATCH] Follow up improvements to `Date_Time_Formatter` (#7875) - Closes #7872 - Also closes #7866 --- .../Base/0.0.0-dev/src/Data/Text/Regex.enso | 1 - .../Data/Text/Regex/Internal/Replacer.enso | 8 +- .../src/Data/Time/Date_Time_Formatter.enso | 97 ++++++++--- .../Base/0.0.0-dev/src/Data/Time/Errors.enso | 22 +++ .../src/Internal/Time/Format/Analyzer.enso | 137 +++++++++++++++ .../Format/As_Java_Formatter_Interpreter.enso | 33 +++- .../src/Internal/Time/Format/Parser.enso | 41 ++--- .../src/Internal/Time/Format/Tokenizer.enso | 40 +++-- .../Standard/Base/0.0.0-dev/src/Warning.enso | 4 +- .../0.0.0-dev/src/Data/Data_Formatter.enso | 34 ++-- .../Standard/Test/0.0.0-dev/src/Bench.enso | 22 ++- .../Test/0.0.0-dev/src/Test_Reporter.enso | 12 +- .../base/{Replacer_Cache.java => Cache.java} | 22 +-- .../org/enso/base/text/Replacer_Cache.java | 12 ++ .../enso/base/time/EnsoDateTimeFormatter.java | 54 +----- .../org/enso/base/time/FormatterCache.java | 13 ++ .../org/enso/base/time/FormatterCacheKey.java | 5 + .../enso/table/parsing/BaseTimeParser.java | 1 + test/Benchmarks/src/Main.enso | 11 +- test/Benchmarks/src/Time/Format.enso | 49 ++++++ .../src/Formatting/Data_Formatter_Spec.enso | 48 +++--- .../src/Formatting/Parse_Values_Spec.enso | 2 +- .../src/In_Memory/Column_Format_Spec.enso | 2 +- test/Tests/src/Data/Text/Regex_Spec.enso | 2 - .../Data/Time/Date_Time_Formatter_Spec.enso | 161 +++++++++++++++++- .../Tests/src/Data/Time/Time_Of_Day_Spec.enso | 7 +- test/Tests/src/Network/Http_Spec.enso | 9 - test/Tests/src/Semantic/Conversion_Spec.enso | 2 - 28 files changed, 632 insertions(+), 219 deletions(-) create mode 100644 distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Errors.enso create mode 100644 distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Analyzer.enso rename std-bits/base/src/main/java/org/enso/base/{Replacer_Cache.java => Cache.java} (61%) create mode 100644 std-bits/base/src/main/java/org/enso/base/text/Replacer_Cache.java create mode 100644 std-bits/base/src/main/java/org/enso/base/time/FormatterCache.java create mode 100644 std-bits/base/src/main/java/org/enso/base/time/FormatterCacheKey.java create mode 100644 test/Benchmarks/src/Time/Format.enso diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex.enso index dda2bed0fa..9e10138003 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex.enso @@ -31,7 +31,6 @@ from project.Data.Range.Extensions import all from project.Data.Text.Extensions import all polyglot java import org.enso.base.Regex_Utils -polyglot java import org.enso.base.Replacer_Cache polyglot java import org.enso.base.Text_Utils type Regex diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex/Internal/Replacer.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex/Internal/Replacer.enso index 67cb3dfeeb..ce2c5d867d 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex/Internal/Replacer.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Regex/Internal/Replacer.enso @@ -14,7 +14,7 @@ from project.Data.Boolean import Boolean, False, True from project.Data.Text.Extensions import all polyglot java import java.lang.StringBuilder -polyglot java import org.enso.base.Replacer_Cache +polyglot java import org.enso.base.text.Replacer_Cache type Replacer ## PRIVATE @@ -62,13 +62,13 @@ type Replacer Get the size of the Replacer LRU cache. For testing. get_lru_size : Integer -get_lru_size = Replacer_Cache.getLruSize +get_lru_size = Replacer_Cache.INSTANCE.getLruSize ## PRIVATE Look up a replacement string in the Replacer LRU cache. For testing. replacer_cache_lookup : Text -> Replacer | Nothing -replacer_cache_lookup replacement_string = Replacer_Cache.get replacement_string +replacer_cache_lookup replacement_string = Replacer_Cache.INSTANCE.get replacement_string ## PRIVATE group_reference_regex = "\$(([0-9]+)|(\$)|(&)|(<([^>]+)>))" @@ -84,7 +84,7 @@ group_reference_regex = "\$(([0-9]+)|(\$)|(&)|(<([^>]+)>))" replacement strings. build_replacement_vector_cached : Text -> Regex -> Vector Replacement ! No_Such_Group build_replacement_vector_cached replacement_string pattern = - Replacer_Cache.get_or_set replacement_string _-> + Replacer_Cache.INSTANCE.get_or_set replacement_string _-> build_replacement_vector replacement_string pattern ## PRIVATE 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 index da83facdda..a450bc86f3 100644 --- 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 @@ -2,27 +2,31 @@ 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.Errors.Date_Time_Format_Parse_Error import project.Data.Time.Time_Of_Day.Time_Of_Day import project.Error.Error import project.Errors.Illegal_Argument.Illegal_Argument +import project.Errors.Problem_Behavior.Problem_Behavior 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.Analyzer.Analyzer import project.Internal.Time.Format.As_Java_Formatter_Interpreter +import project.Internal.Time.Format.Parser +import project.Internal.Time.Format.Tokenizer.Tokenizer 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.FormatterCacheKey +polyglot java import org.enso.base.time.FormatterCache 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) + Value (underlying : EnsoDateTimeFormatter) (~deferred_parsing_warnings = []) ## Creates a formatter from a simple date-time format pattern. @@ -120,10 +124,14 @@ type Date_Time_Formatter Date.parse "1 Nov '95" "d MMM ''yy{2099}" == (Date.new 2095 11 01) @locale Locale.default_widget + from_simple_pattern : Text -> Locale -> Date_Time_Formatter ! Date_Time_Format_Parse_Error 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) + FormatterCache.SIMPLE_FORMAT.get_or_set (FormatterCacheKey.new pattern locale.java_locale) _-> + parsed = Tokenizer.tokenize pattern |> Parser.parse_simple_date_pattern + analyzer = Analyzer.new parsed + java_formatter = analyzer.validate_after_parsing <| + As_Java_Formatter_Interpreter.interpret locale parsed + Date_Time_Formatter.Value (EnsoDateTimeFormatter.new java_formatter pattern FormatterKind.SIMPLE) deferred_parsing_warnings=analyzer.get_parsing_only_warnings ## Creates a formatter from a pattern for the ISO 8601 leap week calendar. @@ -167,10 +175,14 @@ type Date_Time_Formatter 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 : Text -> Locale -> Date_Time_Formatter ! Date_Time_Format_Parse_Error 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) + FormatterCache.ISO_WEEK_DATE_FORMAT.get_or_set (FormatterCacheKey.new pattern locale.java_locale) _-> + parsed = Tokenizer.tokenize pattern |> Parser.parse_iso_week_year_pattern + analyzer = Analyzer.new parsed + java_formatter = analyzer.validate_after_parsing <| + As_Java_Formatter_Interpreter.interpret locale parsed + Date_Time_Formatter.Value (EnsoDateTimeFormatter.new java_formatter pattern FormatterKind.ISO_WEEK_DATE) deferred_parsing_warnings=analyzer.get_parsing_only_warnings ## ADVANCED Creates a formatter from a Java `DateTimeFormatter` instance or a text @@ -186,13 +198,14 @@ type Date_Time_Formatter 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 : Text|DateTimeFormatter -> Locale -> Date_Time_Formatter ! Illegal_Argument 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 -> + _ : Text -> Illegal_Argument.handle_java_exception <| 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) @@ -203,6 +216,7 @@ type Date_Time_Formatter 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 default_enso_zoned_date_time = Date_Time_Formatter.Value EnsoDateTimeFormatter.default_enso_zoned_date_time_formatter @@ -210,6 +224,7 @@ type Date_Time_Formatter 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 iso_zoned_date_time = Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_ZONED_DATE_TIME "iso_zoned_date_time") @@ -217,6 +232,7 @@ type Date_Time_Formatter 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 iso_offset_date_time = Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_OFFSET_DATE_TIME "iso_offset_date_time") @@ -225,18 +241,21 @@ type Date_Time_Formatter 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 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 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 iso_time = Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_TIME "iso_time") @@ -255,6 +274,7 @@ type Date_Time_Formatter Nothing -> "Date_Time_Formatter.from_java " + self.underlying.getFormatter.to_text ## Parses a human-readable representation of this formatter. + to_display_text : Text to_display_text self = self.to_text ## Returns a copy of this formatter with a changed locale. @@ -265,19 +285,23 @@ type Date_Time_Formatter ## PRIVATE handle_java_errors self ~action = Panic.catch JException action caught_panic-> - Error.throw (Time_Error.Error caught_panic.payload.getMessage caught_panic.payload) + message = caught_panic.payload.getMessage + " (Expected date/time format: " + self.pattern_approximation_as_text + ")" + Error.throw (Time_Error.Error message caught_panic.payload) ## PRIVATE parse_date self (text:Text) = self.handle_java_errors <| - self.underlying.parseLocalDate text + self.with_parsing_warnings <| + self.underlying.parseLocalDate text ## PRIVATE parse_date_time self (text:Text) = self.handle_java_errors <| - self.underlying.parseZonedDateTime text + self.with_parsing_warnings <| + self.underlying.parseZonedDateTime text ## PRIVATE parse_time self (text:Text) = self.handle_java_errors <| - self.underlying.parseLocalTime text + self.with_parsing_warnings <| + self.underlying.parseLocalTime text ## PRIVATE format_date self (date:Date) = self.handle_java_errors <| @@ -291,17 +315,38 @@ type Date_Time_Formatter format_time self (time:Time_Of_Day) = self.handle_java_errors <| self.underlying.formatLocalTime time + ## PRIVATE + Adds parsing warnings, if any, to the result of `continuation`. + with_parsing_warnings self ~continuation = + Problem_Behavior.Report_Warning.attach_problems_after continuation self.deferred_parsing_warnings + + ## PRIVATE + Returns the `underlying` formatter, also ensuring that parse-only + warnings are attached to it, to be propagated. + get_java_formatter_for_parsing : EnsoDateTimeFormatter + get_java_formatter_for_parsing self = + self.with_parsing_warnings self.underlying + + ## PRIVATE + Returns a pattern that is associated with this formatter. + + For formatters created using `from_simple_pattern` and + `from_iso_week_date_pattern` or `from_java` with a Text pattern this will + just be the pattern. For constants, it will be a pattern that best + resembles that constant. For formatters created from a Java formatter + instance, this will be the text representation of that formatter. + pattern_approximation_as_text : Text + pattern_approximation_as_text self = + case self.underlying.toString of + "default_enso_zoned_date_time" -> "(default) yyyy-MM-dd HH:mm[:ss[.f]][ZZZZZ]['['VV']']" + "iso_zoned_date_time" -> "(ISO zoned date time) yyyy-MM-dd'T'HH:mm:ss[.f]ZZZZZ'['VV']'" + "iso_offset_date_time" -> "(ISO offset date time) yyyy-MM-dd'T'HH:mm:ss[.f]ZZZZZ" + "iso_local_date_time" -> "(ISO local date time) yyyy-MM-dd'T'HH:mm:ss[.f]" + "iso_date" -> "(ISO date) yyyy-MM-dd" + "iso_time" -> "(ISO time) HH:mm[:ss[.f]]" + other -> other + + ## 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/Errors.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Errors.enso new file mode 100644 index 0000000000..2d38d789fe --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Errors.enso @@ -0,0 +1,22 @@ +import project.Data.Text.Text + +## PRIVATE +type Date_Time_Format_Parse_Error + ## PRIVATE + Indicates an error during parsing of a date time format pattern. + Error message:Text + + ## PRIVATE + to_display_text : Text + to_display_text self = + "Error parsing date/time format pattern: " + self.message + +## A warning indicating an unexpected date time format pattern. +type Suspicious_Date_Time_Format + ## PRIVATE + Indicates a warning when parsing a date time format. + Warning message:Text + + ## PRIVATE + to_display_text : Text + to_display_text self = self.message diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Analyzer.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Analyzer.enso new file mode 100644 index 0000000000..c593b36483 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/Analyzer.enso @@ -0,0 +1,137 @@ +import project.Any.Any +import project.Data.Text.Text +import project.Data.Time.Date.Date +import project.Data.Time.Errors.Suspicious_Date_Time_Format +import project.Data.Vector.Vector +import project.Errors.Illegal_Argument.Illegal_Argument +import project.Errors.Problem_Behavior.Problem_Behavior +import project.Meta +import project.Nothing.Nothing +import project.Panic.Panic +from project.Data.Boolean import Boolean, False, True + +from project.Internal.Time.Format.Parser import all + +## PRIVATE +type Analyzer + ## PRIVATE + + Fields: + - nodes: The raw list of nodes as returned from the parser. + - flattened: The list of nodes after flattening the optional sections and + removing literals - so it just contains raw patterns. + Value (nodes : Vector (Common_Nodes | Standard_Date_Patterns | ISO_Week_Year_Patterns | Time_Patterns | Time_Zone_Patterns)) ~flattened + + ## PRIVATE + new nodes = + get_pattern_nodes node = case node of + Common_Nodes.Optional_Section inner -> inner.flat_map get_pattern_nodes + Common_Nodes.Literal _ -> [] + _ -> [node] + + Analyzer.Value nodes (nodes.flat_map get_pattern_nodes) + + ## PRIVATE + Checks if the given node is contained as one of the nodes, ignoring the optional ones. + has_required : Any -> Boolean + has_required self constructor = + meta_ctor = Meta.meta constructor + if meta_ctor.is_a Meta.Constructor . not then + Panic.throw (Illegal_Argument.Error "Expected a constructor, but got: "+meta_ctor.to_text) + self.nodes.any node-> + case Meta.meta node of + atom : Meta.Atom -> + atom.constructor == meta_ctor + _ -> False + + ## PRIVATE + Runs basic validations that can happen on construction of the formatter, regardless of the context. + validate_after_parsing self ~continuation = + problem_builder = Vector.new_builder + self.check_possible_m_mismatches problem_builder + self.check_possible_seconds_aliasing problem_builder + self.check_24h_and_am_pm_collision problem_builder + Problem_Behavior.Report_Warning.attach_problems_after continuation problem_builder.to_vector + + ## PRIVATE + Prepares a list of warnings that are only reported when parsing using the + formatter. + get_parsing_only_warnings : Vector + get_parsing_only_warnings self = + problem_builder = Vector.new_builder + self.check_missing_am_pm_in_hour_parse problem_builder + self.check_missing_year_in_date_parse problem_builder + problem_builder.to_vector + + ## PRIVATE + check_possible_m_mismatches self problem_builder = + pattern_nodes = self.flattened + pattern_nodes.each_with_index ix-> value-> case value of + Standard_Date_Patterns.Month _ -> + # Warns only if surrounded from both sides or if it has a time node on the left and is the last node. + has_time_on_left = pattern_nodes.get ix-1 . is_a Time_Patterns + has_time_on_right_or_is_last = + next = pattern_nodes.get ix+1 + next.is_nothing || next.is_a Time_Patterns + if has_time_on_left && has_time_on_right_or_is_last then + problem_builder.append (Suspicious_Date_Time_Format.Warning "A Month pattern 'M' is used next to time patterns. Did you mean 'm' for minutes? (You can remove this warning using `remove_warnings Suspicious_Date_Time_Format`.)") + + Time_Patterns.Minute _ -> + has_date_on_both_sides = (pattern_nodes.get ix-1 . is_a Standard_Date_Patterns) && (pattern_nodes.get ix+1 . is_a Standard_Date_Patterns) + if has_date_on_both_sides then + problem_builder.append (Suspicious_Date_Time_Format.Warning "A Minute pattern 'm' is used between date patterns. Did you mean 'M' for months? (You can remove this warning using `remove_warnings Suspicious_Date_Time_Format`.)") + + _ -> Nothing + + ## PRIVATE + check_possible_seconds_aliasing self problem_builder = + pattern_nodes = self.flattened + seconds = pattern_nodes.filter node-> case node of + Time_Patterns.Second _ -> True + _ -> False + if seconds.length == 2 then + problem_builder.append (Suspicious_Date_Time_Format.Warning "Two Second patterns have been detected ('s'/'S'). Our simple format treats seconds in a case-insensitive way. If you want to indicate a fraction of a second, use 'f' instead. (You can remove this warning using `remove_warnings Suspicious_Date_Time_Format`.)") + + ## PRIVATE + has_24h : Boolean + has_24h self = + self.flattened.any node-> case node of + Time_Patterns.Hour _ is24h -> is24h + _ -> False + + ## PRIVATE + has_12h : Boolean + has_12h self = + self.flattened.any node-> case node of + Time_Patterns.Hour _ is24h -> is24h.not + _ -> False + + ## PRIVATE + has_am_pm : Boolean + has_am_pm self = + self.flattened.any node-> case node of + Time_Patterns.AM_PM -> True + _ -> False + + ## PRIVATE + check_24h_and_am_pm_collision self problem_builder = + if self.has_24h && self.has_am_pm && self.has_12h.not then + problem_builder.append (Suspicious_Date_Time_Format.Warning "A 24-hour pattern 'H' is used with an AM/PM pattern. Did you mean 'h' for 12-hour format? (You can remove this warning using `remove_warnings Suspicious_Date_Time_Format`.)") + + ## PRIVATE + check_missing_am_pm_in_hour_parse self problem_builder = + if self.has_12h && self.has_am_pm.not then + problem_builder.append (Suspicious_Date_Time_Format.Warning "A 12-hour pattern 'h' is used without an AM/PM pattern. Without it, the 12-hour pattern is ambiguous - the hours will default to AM. Did you mean 'H' for 24-hour format? (You can remove this warning using `remove_warnings Suspicious_Date_Time_Format`.)") + + ## PRIVATE + has_day_and_month_but_not_year : Boolean + has_day_and_month_but_not_year self = + has_month = self.has_required Standard_Date_Patterns.Month + has_day = self.has_required Standard_Date_Patterns.Day_Of_Month + has_year = self.has_required Standard_Date_Patterns.Year + has_month && has_day && has_year.not + + ## PRIVATE + check_missing_year_in_date_parse self problem_builder = + if self.has_day_and_month_but_not_year then + problem_builder.append (Suspicious_Date_Time_Format.Warning "A date pattern with a day and month but without a year has been detected. The year will default to the current year - note that the results may change over time. (You can remove this warning using `remove_warnings Suspicious_Date_Time_Format`.)") 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 index 07a65f5e48..41d1a13cb7 100644 --- 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 @@ -6,6 +6,7 @@ import project.Errors.Illegal_Argument.Illegal_Argument import project.Panic.Panic from project.Data.Boolean import Boolean, False, True +import project.Internal.Time.Format.Analyzer.Analyzer 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 @@ -18,8 +19,8 @@ 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 = +interpret : Locale -> Vector (Common_Nodes | Standard_Date_Patterns | ISO_Week_Year_Patterns | Time_Patterns | Time_Zone_Patterns) -> Boolean -> DateTimeFormatter +interpret locale nodes prepare_defaults=True = builder = DateTimeFormatterBuilder.new interpret_node node = case node of Common_Nodes.Literal text -> @@ -47,17 +48,33 @@ interpret locale nodes = 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 + + if prepare_defaults then + analyzer = Analyzer.new nodes + + if analyzer.has_required Standard_Date_Patterns.Year && analyzer.has_required Standard_Date_Patterns.Month then + if analyzer.has_required Standard_Date_Patterns.Day_Of_Month . not then + builder.parseDefaulting ChronoField.DAY_OF_MONTH 1 + + if analyzer.has_day_and_month_but_not_year then + current_year = Date.today.year + builder.parseDefaulting ChronoField.YEAR current_year + + if analyzer.has_required ISO_Week_Year_Patterns.Week_Based_Year && analyzer.has_required ISO_Week_Year_Patterns.Week_Of_Year then + if analyzer.has_required ISO_Week_Year_Patterns.Day_Of_Week . not then + builder.parseDefaulting ChronoField.DAY_OF_WEEK 1 + + if analyzer.has_required Standard_Date_Patterns.Year && analyzer.has_required Standard_Date_Patterns.Quarter then + builder.parseDefaulting IsoFields.DAY_OF_QUARTER 1 + + if analyzer.has_12h && analyzer.has_24h.not && analyzer.has_am_pm.not then + builder.parseDefaulting ChronoField.AMPM_OF_DAY 0 + builder.toFormatter locale.java_locale ## PRIVATE 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 index 0254d672cc..664eee996d 100644 --- 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 @@ -2,7 +2,7 @@ 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.Time.Errors.Date_Time_Format_Parse_Error import project.Data.Vector.Builder as Vector_Builder import project.Data.Vector.Vector import project.Error.Error @@ -124,32 +124,25 @@ type Parser ## PRIVATE run self = Panic.recover Date_Time_Format_Parse_Error <| - result_builder = Vector.new_builder - go _ = case self.consume_token of - Nothing -> Nothing + go current_builder optional_nesting_level = case self.consume_token of + Nothing -> + if optional_nesting_level > 0 then + Panic.throw (Illegal_State.Error "Unterminated optional section. This should have been caught by the tokenizer.") Format_Token.Optional_Section_Start -> - inner_nodes = self.run_optional - result_builder.append (Common_Nodes.Optional_Section inner_nodes) - @Tail_Call go Nothing + inner_builder = Vector.new_builder + go inner_builder (optional_nesting_level+1) + current_builder.append (Common_Nodes.Optional_Section inner_builder.to_vector) + @Tail_Call go current_builder optional_nesting_level + Format_Token.Optional_Section_End -> + if optional_nesting_level <= 0 then + Panic.throw (Illegal_State.Error "Unexpected end of optional section. This should have been caught by the tokenizer.") 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 + current_builder.append parsed_node + @Tail_Call go current_builder optional_nesting_level + root_builder = Vector.new_builder + go root_builder 0 + root_builder.to_vector ## PRIVATE parse_common_token self token = case token of 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 index e7b7b5be39..1b0c30b7ff 100644 --- 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 @@ -1,9 +1,10 @@ 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.Time.Errors.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 @@ -45,23 +46,44 @@ type Tokenizer 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) + Instance (original_text : Text) (chars : Vector Text) (tokens_builder : Vector_Builder Format_Token) (optional_nesting : Ref Integer) ## 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) + Tokenizer.Instance text text.characters+[Nothing] Vector.new_builder (Ref.new 0) ## PRIVATE finalize_token self current_token = case current_token of Nothing -> Nothing _ -> self.tokens_builder.append current_token + ## PRIVATE + Checks if we are inside of an optional section. + is_in_optional : Boolean + is_in_optional self = self.optional_nesting.get > 0 + + ## PRIVATE + enter_optional_section : Nothing + enter_optional_section self = + i = self.optional_nesting.get + self.optional_nesting.put i+1 + self.tokens_builder.append Format_Token.Optional_Section_Start + + ## PRIVATE + exit_optional_section : Nothing + exit_optional_section self = + i = self.optional_nesting.get + if i <= 0 then + Panic.throw (Illegal_State.Error "Invariant violation: leaving optional section while not in one. This is a bug in the Tokenizer.") + self.optional_nesting.put i-1 + self.tokens_builder.append Format_Token.Optional_Section_End + ## PRIVATE parse_normal self position current_token = case self.chars.at position of Nothing -> - if self.is_in_optional.get then + if self.is_in_optional 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 @@ -69,18 +91,14 @@ type Tokenizer 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 + self.enter_optional_section @Tail_Call self.parse_normal position+1 Nothing "]" -> - if self.is_in_optional.get.not then + if self.is_in_optional.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 + self.exit_optional_section @Tail_Call self.parse_normal position+1 Nothing "{" -> self.finalize_token current_token diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Warning.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Warning.enso index 3370c6035b..f93fcc46f9 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Warning.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Warning.enso @@ -186,8 +186,8 @@ type Warning > Example Detach warnings of a specific type. - result = Warning.detach_selected_warnings value (_.is_a Illegal_State.Error) - result.first # `value` with the matched warnings removed + result = Warning.detach_selected_warnings value (_.is_a Illegal_State) + result.first # `value` with the matched warnings removed result.second # the list of matched warnings detach_selected_warnings : Any -> (Any -> Boolean) -> Pair Any Vector detach_selected_warnings value predicate = 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 feddce91db..59efdff9f0 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 @@ -8,6 +8,7 @@ from Standard.Base.Widget_Helpers import make_date_format_selector, make_time_fo import project.Data.Type.Storage import project.Internal.Java_Problems import project.Internal.Parse_Values_Helper +import project.Internal.Widget_Helpers from project.Data.Type.Value_Type import Auto, Bits, Value_Type polyglot java import java.lang.Exception as Java_Exception @@ -66,17 +67,17 @@ type Data_Formatter Arguments: - text: Text value to parse. - - datatype: The expected Enso type to parse the value into. If set to + - type: The expected Enso type to parse the value into. If set to `Auto`, the type will be inferred automatically. - on_problems: Specifies the behavior when a problem occurs. By default, a warning is issued, but the operation proceeds. If set to `Report_Error`, the operation fails with a dataflow error. If set to `Ignore`, the operation proceeds without errors or warnings. - parse : Text -> (Auto|Integer|Number|Date|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) <| + @type Widget_Helpers.parse_type_selector + parse : Text -> (Value_Type | Auto) -> Problem_Behavior -> Any + parse self text type=Auto on_problems=Problem_Behavior.Report_Warning = + parser = self.make_value_type_parser type + Java_Problems.unpack_value_with_aggregated_problems on_problems problem_mapping=(Parse_Values_Helper.translate_parsing_problem type) <| parser.parseIndependentValue text ## PRIVATE @@ -203,36 +204,21 @@ type Data_Formatter ## PRIVATE make_date_parser self = self.wrap_base_parser <| Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - DateParser.new (self.date_formats.map .underlying) + DateParser.new (self.date_formats.map .get_java_formatter_for_parsing) ## PRIVATE make_date_time_parser self = self.wrap_base_parser <| Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - DateTimeParser.new (self.datetime_formats.map .underlying) + DateTimeParser.new (self.datetime_formats.map .get_java_formatter_for_parsing) ## PRIVATE make_time_of_day_parser self = self.wrap_base_parser <| Panic.catch Java_Exception handler=(caught_panic-> Error.throw (Illegal_Argument.Error caught_panic.payload.getMessage)) <| - TimeOfDayParser.new (self.time_formats.map .underlying) + TimeOfDayParser.new (self.time_formats.map .get_java_formatter_for_parsing) ## PRIVATE make_identity_parser self = self.wrap_base_parser IdentityParser.new - ## PRIVATE - make_datatype_parser self datatype = case datatype of - Integer -> self.make_integer_parser - Float -> self.make_decimal_parser - Boolean -> self.make_boolean_parser - Date -> self.make_date_parser - Date_Time -> self.make_date_time_parser - Time_Of_Day -> self.make_time_of_day_parser - Auto -> self.make_auto_parser - _ -> - type_name = case datatype.to_text of - text : Text -> text - _ -> Meta.meta datatype . to_text - Error.throw (Illegal_Argument.Error "Unsupported datatype: "+type_name) - ## PRIVATE make_value_type_parser self value_type = case value_type of Value_Type.Integer _ -> diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Bench.enso b/distribution/lib/Standard/Test/0.0.0-dev/src/Bench.enso index 5fa72fa06d..1064ddf306 100644 --- a/distribution/lib/Standard/Test/0.0.0-dev/src/Bench.enso +++ b/distribution/lib/Standard/Test/0.0.0-dev/src/Bench.enso @@ -1,5 +1,6 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.Errors.Illegal_State.Illegal_State ## Configuration for a measurement phase or a warmup phase of a benchmark. type Phase_Conf @@ -134,9 +135,28 @@ type Bench Bench.Group _ _ specs -> specs.fold value (v-> s-> fn v self s) Bench.Spec _ _ -> fn value self self + ## Counts all the specs in the benchmark. + total_specs : Integer + total_specs self = self.fold 0 v-> _-> _-> v+1 + + ## Estimates the runtime based on configurations. + estimated_runtime : Duration + estimated_runtime self = + total_seconds = self.fold 0 acc-> group-> spec-> + single_call_runtime = case group of + Bench.Group _ conf _ -> + warmup = conf.warmup.seconds * conf.warmup.iterations + measure = conf.measure.seconds * conf.measure.iterations + warmup + measure + _ -> + Panic.throw (Illegal_State.Error "Encountered a specification "+spec.to_text+" outside of a group - cannot estimate runtime without knowing the configuration.") + acc + single_call_runtime + + Duration.new seconds=total_seconds + ## Run the specified set of benchmarks. run_main self = - count = self.fold 0 v-> _-> _-> v+1 + count = self.total_specs IO.println <| "Found " + count.to_text + " cases to execute" self.fold Nothing _-> g-> s-> diff --git a/distribution/lib/Standard/Test/0.0.0-dev/src/Test_Reporter.enso b/distribution/lib/Standard/Test/0.0.0-dev/src/Test_Reporter.enso index 352096dfdd..d530a44542 100644 --- a/distribution/lib/Standard/Test/0.0.0-dev/src/Test_Reporter.enso +++ b/distribution/lib/Standard/Test/0.0.0-dev/src/Test_Reporter.enso @@ -57,15 +57,15 @@ print_report spec config builder = Test_Result.Failure msg details -> escaped_message = escape_xml msg . replace '\n' ' ' builder.append ('\n \n') + # We always print the message again as content - otherwise the GitHub action may fail to parse it. + builder.append (escape_xml msg) if details.is_nothing.not then - ## We duplicate the message, because sometimes the - attribute is skipped if the node has any content. - builder.append (escape_xml msg) - builder.append '\n' + ## If there are additional details, we print them as well. + builder.append '\n\n' builder.append (escape_xml details) - builder.append '\n' + builder.append '\n \n' Test_Result.Pending msg -> builder.append ('\n \n ') - builder.append '\n' + builder.append ' \n' builder.append ' \n' should_print_behavior = config.print_only_failures.not || spec.behaviors.any (b -> b.result.is_fail) diff --git a/std-bits/base/src/main/java/org/enso/base/Replacer_Cache.java b/std-bits/base/src/main/java/org/enso/base/Cache.java similarity index 61% rename from std-bits/base/src/main/java/org/enso/base/Replacer_Cache.java rename to std-bits/base/src/main/java/org/enso/base/Cache.java index 1f085e1613..a1be89e35e 100644 --- a/std-bits/base/src/main/java/org/enso/base/Replacer_Cache.java +++ b/std-bits/base/src/main/java/org/enso/base/Cache.java @@ -4,24 +4,26 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Function; import org.graalvm.collections.Pair; -import org.graalvm.polyglot.Value; -public class Replacer_Cache { - private static final int lruSize = 5; +public abstract class Cache { + protected static final int DEFAULT_LRU_SIZE = 5; + protected final int lruSize; // Circular buffer containing the most recent cache keys. - private static final List> lru = new ArrayList<>(lruSize); + private final List> lru; - static { + protected Cache(int lruSize) { + this.lruSize = lruSize; + lru = new ArrayList<>(lruSize); for (int i = 0; i < lruSize; ++i) { lru.add(null); } } // Index into the circular buffer. - private static int nextSlot = 0; + private int nextSlot = 0; - public static Value get_or_set(String key, Function value_producer) { + public Value get_or_set(Key key, Function value_producer) { Value value = get(key); if (value == null) { value = value_producer.apply(null); @@ -32,9 +34,9 @@ public class Replacer_Cache { } // Visible for testing. - public static Value get(String key) { + public Value get(Key key) { for (int i = 0; i < lruSize; ++i) { - Pair pair = lru.get(i); + Pair pair = lru.get(i); if (pair != null && pair.getLeft().equals(key)) { return lru.get(i).getRight(); } @@ -42,7 +44,7 @@ public class Replacer_Cache { return null; } - public static int getLruSize() { + public int getLruSize() { return lruSize; } } diff --git a/std-bits/base/src/main/java/org/enso/base/text/Replacer_Cache.java b/std-bits/base/src/main/java/org/enso/base/text/Replacer_Cache.java new file mode 100644 index 0000000000..9d783cf161 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/text/Replacer_Cache.java @@ -0,0 +1,12 @@ +package org.enso.base.text; + +import org.enso.base.Cache; +import org.graalvm.polyglot.Value; + +public class Replacer_Cache extends Cache { + public static Replacer_Cache INSTANCE = new Replacer_Cache(DEFAULT_LRU_SIZE); + + protected Replacer_Cache(int lruSize) { + super(lruSize); + } +} 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 index e0fafa5c05..676ef0dde1 100644 --- 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 @@ -100,64 +100,14 @@ public class EnsoDateTimeFormatter { return switch (formatterKind) { case SIMPLE -> originalPattern; case ISO_WEEK_DATE -> "(ISO Week Date Format) " + originalPattern; - case RAW_JAVA -> "(Java Format Pattern) " + originalPattern; + case RAW_JAVA -> "(Java DateTimeFormatter) " + (originalPattern != null ? originalPattern : formatter.toString()); case CONSTANT -> originalPattern; }; } public LocalDate parseLocalDate(String dateString) { dateString = normaliseInput(dateString); - - TemporalAccessor parsed = formatter.parse(dateString); - - if (parsed.isSupported(ChronoField.EPOCH_DAY)) { - return LocalDate.ofEpochDay(parsed.getLong(ChronoField.EPOCH_DAY)); - } - - // Allow Year and Month to be parsed without a day (use first day of month). - if (parsed.isSupported(ChronoField.YEAR) && parsed.isSupported(ChronoField.MONTH_OF_YEAR)) { - int dayOfMonth = - parsed.isSupported(ChronoField.DAY_OF_MONTH) ? parsed.get(ChronoField.DAY_OF_MONTH) : 1; - return LocalDate.of( - parsed.get(ChronoField.YEAR), parsed.get(ChronoField.MONTH_OF_YEAR), dayOfMonth); - } - - // Allow Year and Quarter to be parsed without a day (use first day of the quarter). - if (parsed.isSupported(ChronoField.YEAR) && parsed.isSupported(IsoFields.QUARTER_OF_YEAR)) { - int dayOfQuarter = - parsed.isSupported(IsoFields.DAY_OF_QUARTER) ? parsed.get(IsoFields.DAY_OF_QUARTER) : 1; - int year = parsed.get(ChronoField.YEAR); - int quarter = parsed.get(IsoFields.QUARTER_OF_YEAR); - int monthsToShift = 3 * (quarter - 1); - LocalDate firstDay = LocalDate.of(year, 1, 1); - return firstDay.plusMonths(monthsToShift).plusDays(dayOfQuarter - 1); - } - - // Allow Month and Day to be parsed without a year (use current year). - if (parsed.isSupported(ChronoField.DAY_OF_MONTH) - && parsed.isSupported(ChronoField.MONTH_OF_YEAR)) { - return LocalDate.of( - LocalDate.now().getYear(), - parsed.get(ChronoField.MONTH_OF_YEAR), - parsed.get(ChronoField.DAY_OF_MONTH)); - } - - if (parsed.isSupported(IsoFields.WEEK_BASED_YEAR) && parsed.isSupported(IsoFields.WEEK_OF_WEEK_BASED_YEAR)) { - // Get the day of week or default to first day if not present. - long dayOfWeek = parsed.isSupported(ChronoField.DAY_OF_WEEK) ? parsed.get(ChronoField.DAY_OF_WEEK) : 1; - HashMap fields = new HashMap<>(); - fields.put(IsoFields.WEEK_BASED_YEAR, parsed.getLong(IsoFields.WEEK_BASED_YEAR)); - fields.put(IsoFields.WEEK_OF_WEEK_BASED_YEAR, parsed.getLong(IsoFields.WEEK_OF_WEEK_BASED_YEAR)); - fields.put(ChronoField.DAY_OF_WEEK, dayOfWeek); - - TemporalAccessor resolved = IsoFields.WEEK_OF_WEEK_BASED_YEAR.resolve(fields, parsed, ResolverStyle.SMART); - if (resolved.isSupported(ChronoField.EPOCH_DAY)) { - return LocalDate.ofEpochDay(resolved.getLong(ChronoField.EPOCH_DAY)); - } - } - - // This will usually throw at this point, but it will construct a more informative exception than we could. - return LocalDate.from(parsed); + return LocalDate.parse(dateString, formatter); } public ZonedDateTime parseZonedDateTime(String dateString) { diff --git a/std-bits/base/src/main/java/org/enso/base/time/FormatterCache.java b/std-bits/base/src/main/java/org/enso/base/time/FormatterCache.java new file mode 100644 index 0000000000..0492543f73 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/time/FormatterCache.java @@ -0,0 +1,13 @@ +package org.enso.base.time; + +import org.enso.base.Cache; +import org.graalvm.polyglot.Value; + +public class FormatterCache extends Cache { + protected FormatterCache(int lruSize) { + super(lruSize); + } + + public static FormatterCache SIMPLE_FORMAT = new FormatterCache(DEFAULT_LRU_SIZE); + public static FormatterCache ISO_WEEK_DATE_FORMAT = new FormatterCache(DEFAULT_LRU_SIZE); +} diff --git a/std-bits/base/src/main/java/org/enso/base/time/FormatterCacheKey.java b/std-bits/base/src/main/java/org/enso/base/time/FormatterCacheKey.java new file mode 100644 index 0000000000..c0e1018bc0 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/time/FormatterCacheKey.java @@ -0,0 +1,5 @@ +package org.enso.base.time; + +import java.util.Locale; + +public record FormatterCacheKey(String pattern, Locale locale) {} diff --git a/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java b/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java index 3a217bd178..dc3928f4b0 100644 --- a/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java +++ b/std-bits/table/src/main/java/org/enso/table/parsing/BaseTimeParser.java @@ -25,6 +25,7 @@ public abstract class BaseTimeParser extends IncrementalDatatypeParser { } catch (DateTimeParseException ignored) { // TODO I think ideally we should try to return Option instead of throwing, as throwing is // inefficient + // See: https://github.com/enso-org/enso/issues/7878 } } diff --git a/test/Benchmarks/src/Main.enso b/test/Benchmarks/src/Main.enso index adedcaadc3..44860995ff 100644 --- a/test/Benchmarks/src/Main.enso +++ b/test/Benchmarks/src/Main.enso @@ -16,6 +16,7 @@ import project.Text.Contains import project.Text.Pretty import project.Text.Reverse import project.Time.Work_Days +import project.Time.Format import project.Collections import project.Column_Numeric import project.Equality @@ -55,8 +56,10 @@ all_benchmarks = builder.append Reverse.collect_benches # Time + builder.append Format.collect_benches builder.append Work_Days.collect_benches + # Vector builder.append Collections.collect_benches builder.append Column_Numeric.collect_benches builder.append Equality.collect_benches @@ -70,10 +73,14 @@ all_benchmarks = builder.to_vector main = - all_benchmarks.each suite-> + benchmarks = all_benchmarks + total_specs = benchmarks.map .total_specs . fold 0 (+) + IO.println "Found "+benchmarks.length.to_text+" benchmark suites, containing "+total_specs.to_text+" specs in total." + estimated_duration = benchmarks.map .estimated_runtime . fold Duration.zero (+) + IO.println "The minimal estimated run time based on configurations is "+estimated_duration.to_display_text+"." + benchmarks.each suite-> suite.run_main - ## Prints all benchmarks along with their configuration list_names = builder = Vector.new_builder diff --git a/test/Benchmarks/src/Time/Format.enso b/test/Benchmarks/src/Time/Format.enso new file mode 100644 index 0000000000..0511ea845f --- /dev/null +++ b/test/Benchmarks/src/Time/Format.enso @@ -0,0 +1,49 @@ +from Standard.Base import all +from Standard.Table import all + +from Standard.Test import Bench + +type Data + Value ~vec ~column + + create = + Data.Value create_date_vector create_date_column + +create_date_vector = + base = Date.new 1999 1 1 + n = 10000 + Vector.new n i-> + base.date_add i Date_Period.Day + +create_date_column = + Column.from_vector "Dates" create_date_vector + +## A flag that can be changed to run additional benchmarks. + By default, only the benchmarks that we care about are run on CI. + However, during development it may be useful to also compare a few more + approaches as a baseline check - so they can be enabled manually by changing + this flag. +run_optional_benchmarks = False + +options = Bench.options . set_warmup (Bench.phase_conf 1 4) . set_measure (Bench.phase_conf 2 4) + +collect_benches = Bench.build builder-> + data = Data.create + + builder.group "Format_Vector_Of_Dates" options group_builder-> + group_builder.specify "Naive" <| + data.vec.map d-> d.format "dd.MM.yyyy" + + if run_optional_benchmarks then group_builder.specify "Prepared_Formatter" <| + formatter = Date_Time_Formatter.from "dd.MM.yyyy" + data.vec.map d-> d.format formatter + + if run_optional_benchmarks then group_builder.specify "Java_Formatter_recreated_on_each" <| + data.vec.map d-> + java_formatter = Date_Time_Formatter.from_java "dd.MM.yyyy" + d.format java_formatter + + group_builder.specify "Column_Format" <| + data.column.format "dd.MM.yyyy" + +main = collect_benches . run_main diff --git a/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso b/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso index 405e7523a8..8f45c6cf50 100644 --- a/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso +++ b/test/Table_Tests/src/Formatting/Data_Formatter_Spec.enso @@ -1,9 +1,9 @@ from Standard.Base import all -import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error +import Standard.Base.Data.Time.Errors.Date_Time_Format_Parse_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Errors.Illegal_State.Illegal_State -from Standard.Table import Table, Column, Data_Formatter, Quote_Style +from Standard.Table import Table, Column, Data_Formatter, Quote_Style, Value_Type from Standard.Table.Errors import all from Standard.Test import Test, Test_Suite, Problems @@ -64,18 +64,18 @@ spec = exponential_formatter = Data_Formatter.Value allow_exponential_notation=True plain_formatter.parse "1E3" . should_equal "1E3" - r1 = plain_formatter.parse "1E3" Float + r1 = plain_formatter.parse "1E3" Value_Type.Float r1.should_equal Nothing - Problems.get_attached_warnings r1 . should_equal [(Invalid_Format.Error Nothing Float ["1E3"])] + Problems.get_attached_warnings r1 . should_equal [(Invalid_Format.Error Nothing Value_Type.Float ["1E3"])] exponential_formatter.parse "1E3" . should_equal 1000.0 - exponential_formatter.parse "1E3" Float . should_equal 1000.0 - exponential_formatter.parse "1E3" Integer . should_equal Nothing + exponential_formatter.parse "1E3" Value_Type.Float . should_equal 1000.0 + exponential_formatter.parse "1E3" Value_Type.Integer . should_equal Nothing plain_formatter.parse "1.2E-3" . should_equal "1.2E-3" - plain_formatter.parse "1.2E-3" Float . should_equal Nothing + plain_formatter.parse "1.2E-3" Value_Type.Float . should_equal Nothing exponential_formatter.parse "1.2E-3" . should_equal 0.0012 - exponential_formatter.parse "1.2E-3" Float . should_equal 0.0012 + exponential_formatter.parse "1.2E-3" Value_Type.Float . should_equal 0.0012 Test.specify "handle leading zeros, only if enabled" <| Data_Formatter.Value.parse "0100" . should_equal "0100" @@ -95,12 +95,12 @@ spec = formatter = Data_Formatter.Value true_values=["YES", "1", "true"] false_values=["NO", "0", "false"] formatter.parse "YES" . should_equal True formatter.parse "NO" . should_equal False - (Data_Formatter.Value true_values=[] false_values=[]).parse "True" datatype=Boolean . should_equal Nothing + (Data_Formatter.Value true_values=[] false_values=[]).parse "True" type=Value_Type.Boolean . should_equal Nothing Test.specify "should parse dates" <| formatter = Data_Formatter.Value formatter.parse "2022-01-01" . should_equal (Date.new 2022) - formatter.parse "2020-05-07" datatype=Date . should_equal (Date.new 2020 5 7) + formatter.parse "2020-05-07" type=Value_Type.Date . should_equal (Date.new 2020 5 7) formatter.parse "1999-01-01 00:00:00" . should_equal (Date_Time.new 1999) formatter.parse "1999-02-03 04:05:06" . should_equal (Date_Time.new 1999 2 3 4 5 6) formatter.parse "1999-02-03T04:05:06" . should_equal (Date_Time.new 1999 2 3 4 5 6) @@ -108,7 +108,7 @@ spec = formatter.parse "1999-02-03 04:05:06.000456" . should_equal (Date_Time.new 1999 2 3 4 5 6 0 456) formatter.parse "1999-02-03 04:05:06.000000789" . should_equal (Date_Time.new 1999 2 3 4 5 6 0 0 789) formatter.parse "1999-02-03 04:05:06.000000789[Europe/Madrid]" . should_equal (Date_Time.new 1999 2 3 4 5 6 0 0 789 zone=(Time_Zone.parse "Europe/Madrid")) - formatter.parse "1999-01-01 00:00" datatype=Date_Time . should_equal (Date_Time.new 1999) + formatter.parse "1999-01-01 00:00" type=Value_Type.Date_Time . should_equal (Date_Time.new 1999) formatter.parse "1999-02-03 04:05" . should_equal (Date_Time.new 1999 2 3 4 5 0) formatter.parse "00:00:00" . should_equal (Time_Of_Day.new) formatter.parse "17:34:59" . should_equal (Time_Of_Day.new 17 34 59) @@ -116,12 +116,12 @@ spec = formatter.parse "17:34:59.000456" . should_equal (Time_Of_Day.new 17 34 59 0 456) formatter.parse "17:34:59.000000789" . should_equal (Time_Of_Day.new 17 34 59 0 0 789) formatter.parse "00:00" . should_equal (Time_Of_Day.new) - formatter.parse "17:34" datatype=Time_Of_Day . should_equal (Time_Of_Day.new 17 34) + formatter.parse "17:34" type=Value_Type.Time . should_equal (Time_Of_Day.new 17 34) - formatter.parse "00:00:65" datatype=Time_Of_Day . should_equal Nothing - formatter.parse "30:00:65" datatype=Time_Of_Day . should_equal Nothing - formatter.parse "1999-01-01 00:00" datatype=Time_Of_Day . should_equal Nothing - formatter.parse "1999-01-01 00:00" datatype=Date . should_equal Nothing + formatter.parse "00:00:65" type=Value_Type.Time . should_equal Nothing + formatter.parse "30:00:65" type=Value_Type.Time . should_equal Nothing + formatter.parse "1999-01-01 00:00" type=Value_Type.Time . should_equal Nothing + formatter.parse "1999-01-01 00:00" type=Value_Type.Date . should_equal Nothing formatter.parse "30:00:65" . should_equal "30:00:65" Test.specify "should fallback to Text" <| @@ -138,20 +138,20 @@ spec = r.should_equal Nothing Problems.expect_only_warning Invalid_Format r - r1 = formatter.parse "Text" datatype=Float + r1 = formatter.parse "Text" type=Value_Type.Float w1 = expect_warning r1 - w1.value_type . should_equal Float + w1.value_type . should_equal Value_Type.Float w1.column . should_equal Nothing - expect_warning <| formatter.parse "Text" datatype=Integer - expect_warning <| formatter.parse "Text" datatype=Boolean - expect_warning <| formatter.parse "Text" datatype=Date - expect_warning <| formatter.parse "Text" datatype=Date_Time - expect_warning <| formatter.parse "Text" datatype=Time_Of_Day + expect_warning <| formatter.parse "Text" type=Value_Type.Integer + expect_warning <| formatter.parse "Text" type=Value_Type.Boolean + expect_warning <| formatter.parse "Text" type=Value_Type.Date + expect_warning <| formatter.parse "Text" type=Value_Type.Date_Time + expect_warning <| formatter.parse "Text" type=Value_Type.Time Test.specify "should not allow unexpected types" <| formatter = Data_Formatter.Value - formatter.parse "Text" datatype=List . should_fail_with Illegal_Argument + formatter.parse "Text" type=List . should_fail_with Illegal_Argument Test.group "DataFormatter.format" <| Test.specify "should handle Nothing" <| diff --git a/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso b/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso index 78970f5c3a..ff79a728ca 100644 --- a/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso +++ b/test/Table_Tests/src/Formatting/Parse_Values_Spec.enso @@ -1,6 +1,6 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument -import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error +import Standard.Base.Data.Time.Errors.Date_Time_Format_Parse_Error from Standard.Table import Table, Data_Formatter, Column from Standard.Table.Data.Type.Value_Type import Value_Type, Auto diff --git a/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso b/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso index 08f051dc86..3a043d66e5 100644 --- a/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso +++ b/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso @@ -2,7 +2,7 @@ from Standard.Base import all import Standard.Base.Errors.Common.Type_Error import Standard.Base.Errors.Time_Error.Time_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument -import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error +import Standard.Base.Data.Time.Errors.Date_Time_Format_Parse_Error import Standard.Table.Data.Type.Value_Type.Bits diff --git a/test/Tests/src/Data/Text/Regex_Spec.enso b/test/Tests/src/Data/Text/Regex_Spec.enso index 5e1c68aa02..40a159546e 100644 --- a/test/Tests/src/Data/Text/Regex_Spec.enso +++ b/test/Tests/src/Data/Text/Regex_Spec.enso @@ -13,8 +13,6 @@ from Standard.Base.Data.Text.Regex.Internal.Replacer import get_lru_size, replac from Standard.Test import Test, Test_Suite import Standard.Test.Extensions -polyglot java import org.enso.base.Replacer_Cache - spec = Test.group "Compile" <| Test.specify "should be able to be compiled" <| diff --git a/test/Tests/src/Data/Time/Date_Time_Formatter_Spec.enso b/test/Tests/src/Data/Time/Date_Time_Formatter_Spec.enso index e9fba55799..3985db3a66 100644 --- a/test/Tests/src/Data/Time/Date_Time_Formatter_Spec.enso +++ b/test/Tests/src/Data/Time/Date_Time_Formatter_Spec.enso @@ -1,8 +1,11 @@ from Standard.Base import all -import Standard.Base.Data.Time.Date_Time_Formatter.Date_Time_Format_Parse_Error +import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Errors.Time_Error.Time_Error +from Standard.Base.Data.Time.Errors import Date_Time_Format_Parse_Error, Suspicious_Date_Time_Format -from Standard.Test import Test, Test_Suite +from Standard.Table import Column, Value_Type + +from Standard.Test import Test, Test_Suite, Problems import Standard.Test.Extensions polyglot java import java.time.format.DateTimeFormatter @@ -33,8 +36,41 @@ spec = Date_Time_Formatter.from "MM{baz}" . should_fail_with Date_Time_Format_Parse_Error Date_Time_Formatter.from "{baz}" . should_fail_with Date_Time_Format_Parse_Error Date_Time_Formatter.from "]" . should_fail_with Date_Time_Format_Parse_Error + Date_Time_Formatter.from "[]]" . should_fail_with Date_Time_Format_Parse_Error Date_Time_Formatter.from "'" . should_fail_with Date_Time_Format_Parse_Error + Test.specify "should gracefully handle Java pattern errors" <| + Date_Time_Formatter.from_java "}}{{,.,..} INVALID PATTERN FORMAT" . should_fail_with Illegal_Argument + + Test.specify "should warn about likely M/m mistakes" <| + f1 = Date_Time_Formatter.from "yyyy-mm-dd" + w1 = Problems.expect_only_warning Suspicious_Date_Time_Format f1 + w1.to_display_text . should_contain "Did you mean 'M'" + Date.parse "2020-01-02" f1 . should_fail_with Time_Error + + Problems.expect_only_warning Suspicious_Date_Time_Format (Date_Time_Formatter.from "yyyymmdd") + Problems.expect_only_warning Suspicious_Date_Time_Format (Date_Time_Formatter.from "yyyy-mm-dd hh:mm:ss") + + f2 = Date_Time_Formatter.from "HH:MM[:SS]" + w2 = Problems.expect_only_warning Suspicious_Date_Time_Format f2 + w2.to_display_text . should_contain "Did you mean 'm'" + + Problems.expect_only_warning Suspicious_Date_Time_Format (Date_Time_Formatter.from "HH:MM") + Problems.expect_only_warning Suspicious_Date_Time_Format (Date_Time_Formatter.from "hh:MM:ss") + Problems.expect_only_warning Suspicious_Date_Time_Format (Date_Time_Formatter.from "hhMMss") + + f3 = Date_Time_Formatter.from "HH:mm:ss[.S]" + w3 = Problems.expect_only_warning Suspicious_Date_Time_Format f3 + w3.to_display_text . should_contain "'f' instead" + + Problems.assume_no_problems (Date_Time_Formatter.from "mm") + Problems.assume_no_problems (Date_Time_Formatter.from "MM") + Problems.assume_no_problems (Date_Time_Formatter.from "YYYY-MM-DD") + + f4 = Date_Time_Formatter.from "HH:mma" + w4 = Problems.expect_only_warning Suspicious_Date_Time_Format f4 + w4.to_display_text . should_contain "Did you mean 'h'" + Test.group "Formatting date/time values" <| Test.specify "should allow printing month names" <| d = Date.new 2020 6 30 @@ -70,6 +106,12 @@ spec = dt2.format "yyyy/MM/dd HH:mm:ss ZZ{Z}" . should_equal "2020/01/02 12:00:00 -1000" dt2.format "yyyy/MM/dd HH:mm:ss ZZZZZ{}" . should_equal "2020/01/02 12:00:00 -10:00" + Test.specify "should work with optional parts" <| + f = Date_Time_Formatter.from "[('Date:' yyyy-MM-dd)][('Time:' HH:mm)]" + Date_Time.new 2020 01 02 12 30 . format f . should_equal "(Date: 2020-01-02)(Time: 12:30)" + Date.new 2020 01 02 . format f . should_equal "(Date: 2020-01-02)" + Time_Of_Day.new 12 30 . format f . should_equal "(Time: 12:30)" + Test.group "Parsing date/time values" <| Test.specify "should allow short month names" <| Date.parse "30. Jun 2020" "d. MMM yyyy" . should_equal (Date.new 2020 6 30) @@ -203,11 +245,6 @@ spec = Test.specify "should be able to parse a quarter without day" <| Date.parse "Q2 of 2022" "'Q'Q 'of' yyyy" . should_equal (Date.new 2022 4 1) - Test.specify "should be able to parse a day and month without year - defaulting to current year" <| - current_year = Date.today.year - Date.parse "07/23" "MM/dd" . should_equal (Date.new current_year 7 23) - Date.parse "14. of May" "d. 'of' MMMM" . should_equal (Date.new current_year 5 14) - Test.specify "should be able to parse 2-digit year" <| Date.parse "22-05-06" "yy-MM-dd" . should_equal (Date.new 2022 5 6) Date.parse "99-01-02" "yy-MM-dd" . should_equal (Date.new 1999 1 2) @@ -237,4 +274,114 @@ spec = # Just week will parse to first day of the week: Date.parse "1978-W01" (Date_Time_Formatter.from_iso_week_date_pattern "YYYY-'W'WW") . should_equal (Date.new 1978 01 02) + Test.specify "should include the pattern in the parse failure message" <| + r1 = "1999.01.02".parse_date + r1.should_fail_with Time_Error + r1.to_display_text . should_contain "Expected date/time format: (ISO date) yyyy-MM-dd" + + r2 = "FOOBAR".parse_date "yyyy.MM.dd" + r2.should_fail_with Time_Error + r2.to_display_text . should_contain "Expected date/time format: yyyy.MM.dd" + + r3 = "FOOBAR".parse_date_time + r3.should_fail_with Time_Error + r3.to_display_text . should_contain "Expected date/time format: (default) yyyy-MM-dd HH:mm[:ss[.f]][ZZZZZ]['['VV']']" + + r4 = "FOOBAR".parse_time_of_day + r4.should_fail_with Time_Error + r4.to_display_text . should_contain "Expected date/time format: (ISO time) HH:mm[:ss[.f]]" + + r5 = Date.parse "FOOBAR" (Date_Time_Formatter.from_java DateTimeFormatter.ISO_ORDINAL_DATE) + r5.should_fail_with Time_Error + r5.to_display_text . should_contain "Expected date/time format: (Java DateTimeFormatter) ParseCaseSensitive(false)Value(Year,4,10,EXCEEDS_PAD)'-'Value(DayOfYear,3)[Offset(+HH:MM:ss,'Z')]" + + Test.specify "should allow to use 12h hours without am/pm and default to am, but issue a warning (only in parsing)" <| + f1 = Date_Time_Formatter.from "hh:mm" + # No warning yet. + Problems.assume_no_problems f1 + + s1 = Time_Of_Day.new 16 24 . format f1 + s1.should_equal "04:24" + # No warnings on formatting. + Problems.assume_no_problems s1 + + # But warn when parsing: + r1 = Time_Of_Day.parse "04:24" f1 + r1.should_equal (Time_Of_Day.new 4 24) + w1 = Problems.expect_only_warning Suspicious_Date_Time_Format r1 + w1.to_display_text . should_contain "ambiguous" + w1.to_display_text . should_contain "default to AM" + w1.to_display_text . should_contain "Did you mean 'H'" + + Test.specify "the warning should be removable as indicated in the message" pending="TODO: bug https://github.com/enso-org/enso/issues/7892" <| + f1 = Date_Time_Formatter.from "hh:mm" + r1 = Time_Of_Day.parse "04:24" f1 + w1 = Problems.expect_only_warning Suspicious_Date_Time_Format r1 + w1.to_display_text . should_contain "You can remove this warning using `remove_warnings Suspicious_Date_Time_Format`" + + r2 = r1.remove_warnings Suspicious_Date_Time_Format + r2.should_equal (Time_Of_Day.new 4 24) + Problems.assume_no_problems r2 + + Test.specify "should allow to parse MM-dd without a year, defaulting to current year but adding a warning (only in parsing)" <| + f1 = Date_Time_Formatter.from "dd.MM" + + s1 = Date.new 2020 12 31 . format f1 + s1.should_equal "31.12" + # No warnings on formatting. + Problems.assume_no_problems s1 + + # But warn when parsing: + r1 = Date.parse "31.12" f1 + current_year = Date.today.year + r1.should_equal (Date.new current_year 12 31) + w1 = Problems.expect_only_warning Suspicious_Date_Time_Format r1 + w1.to_display_text . should_contain "current year" + + Date.parse "07/23" "MM/dd" . should_equal (Date.new current_year 7 23) + Date.parse "14. of May" "d. 'of' MMMM" . should_equal (Date.new current_year 5 14) + + Test.specify "should report the warnings when parsing a column as well" <| + c1 = Column.from_vector "strs" ["31.12", "01.01"] + c2 = c1.parse Value_Type.Date "dd.MM" + current_year = Date.today.year + c2.to_vector . should_equal [Date.new current_year 12 31, Date.new current_year 01 01] + Problems.expect_only_warning Suspicious_Date_Time_Format c2 + + c3 = Column.from_vector "strs" ["04:24", "16:25"] + t3 = c3.to_table + t4 = t3.parse type=Value_Type.Time format="hh:mm" + # The entry `16:25` does not fit the 12h format, so it is not parsed. + t4.at "strs" . to_vector . should_equal [Time_Of_Day.new 4 24, Nothing] + Problems.expect_warning Suspicious_Date_Time_Format t4 + + # But no warnings on format + c5 = Column.from_vector "Y" [Date.new 2023 12 25, Date.new 2011 07 31] + c6 = c5.format "dd.MM" + c6.to_vector . should_equal ["25.12", "31.07"] + Problems.assume_no_problems c6 + + Test.specify "should allow nested patterns" <| + # Difference between a nested pattern and two optional patterns next to each other. + Date.parse "2023-01-02 XY" "yyyy-MM-dd ['X']['Y']" . should_equal (Date.new 2023 1 2) + Date.parse "2023-01-02 X" "yyyy-MM-dd ['X']['Y']" . should_equal (Date.new 2023 1 2) + Date.parse "2023-01-02 Y" "yyyy-MM-dd ['X']['Y']" . should_equal (Date.new 2023 1 2) + Date.parse "2023-01-02 XY" "yyyy-MM-dd ['X'['Y']]" . should_equal (Date.new 2023 1 2) + Date.parse "2023-01-02 X" "yyyy-MM-dd ['X'['Y']]" . should_equal (Date.new 2023 1 2) + Date.parse "2023-01-02 Y" "yyyy-MM-dd ['X'['Y']]" . should_fail_with Time_Error + + Time_Of_Day.parse "12:00:22.33" "HH:mm[:ss[.f]]" . should_equal (Time_Of_Day.new 12 0 22 330) + + very_nested = "yyyy-MM-dd ['X'['Y'[['W']'Z']][HH:mm[:ss]]]" + Date.parse "2023-01-02 X" very_nested . should_equal (Date.new 2023 1 2) + Date.parse "2023-01-03 XYWZ" very_nested . should_equal (Date.new 2023 1 3) + Date.parse "2023-01-04 X23:24" very_nested . should_equal (Date.new 2023 1 4) + Date.parse "2023-01-05 X23:24:25" very_nested . should_equal (Date.new 2023 1 5) + Date.parse "2023-01-06 XYZ23:24:25" very_nested . should_equal (Date.new 2023 1 6) + Date.parse "2023-01-07 XY" very_nested . should_equal (Date.new 2023 1 7) + Date.parse "2023-01-08 XZ" very_nested . should_fail_with Time_Error + + Test.specify "should allow to parse even when some patterns are unused" <| + "2020-01-02 14:55".parse_date "yyyy-MM-dd HH:mm" . should_equal (Date.new 2020 1 2) + main = Test_Suite.run_main spec diff --git a/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso b/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso index 96a309ea11..737905ed83 100644 --- a/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso +++ b/test/Tests/src/Data/Time/Time_Of_Day_Spec.enso @@ -66,7 +66,10 @@ specWith name create_new_time parse_time nanoseconds_loss_in_precision=False = Test.specify "should throw error when parsing invalid time" <| case parse_time "1200" . catch of Time_Error.Error msg _ -> - msg . should_equal "Text '1200' could not be parsed at index 2" + ## This error message may or may not contain the suffix: + > (Expected date/time format: (ISO time) HH:mm[:ss[.f]]) + That depends if the Enso or Java parse is used. + msg . should_contain "Text '1200' could not be parsed at index 2" result -> Test.fail ("Unexpected result: " + result.to_text) @@ -78,7 +81,7 @@ specWith name create_new_time parse_time nanoseconds_loss_in_precision=False = time = parse_time "12:30" "HH:mm:ss" case time.catch of Time_Error.Error msg _ -> - msg . should_equal "Text '12:30' could not be parsed at index 5" + msg . should_contain "Text '12:30' could not be parsed at index 5" result -> Test.fail ("Unexpected result: " + result.to_text) diff --git a/test/Tests/src/Network/Http_Spec.enso b/test/Tests/src/Network/Http_Spec.enso index 9090b7a038..f2196316a5 100644 --- a/test/Tests/src/Network/Http_Spec.enso +++ b/test/Tests/src/Network/Http_Spec.enso @@ -72,15 +72,6 @@ spec = } response . should_equal expected_response - Test.specify "Can perform a GET with a file response" <| - f = enso_project.data / "spreadsheet.xls" - f.delete_if_exists - url = "https://enso-data-samples.s3.us-west-1.amazonaws.com/spreadsheet.xls" - Data.fetch url . body . to_file f - f.size . should_equal 7168 - f.delete - f.exists.should_be_false - Test.specify "Can perform a HEAD" <| response = Data.fetch url_head method=HTTP_Method.Head response.code.code . should_equal 200 diff --git a/test/Tests/src/Semantic/Conversion_Spec.enso b/test/Tests/src/Semantic/Conversion_Spec.enso index 4c61c10cc7..12fe3cec30 100644 --- a/test/Tests/src/Semantic/Conversion_Spec.enso +++ b/test/Tests/src/Semantic/Conversion_Spec.enso @@ -55,8 +55,6 @@ foreign js call_function fn arg_1 = """ Number.foo self = "foo called" -from Standard.Base import all - type Fool Value fool