Follow up improvements to Date_Time_Formatter (#7875)

- Closes #7872
- Also closes #7866
This commit is contained in:
Radosław Waśko 2023-09-28 11:38:00 +02:00 committed by GitHub
parent c690559ec4
commit 8d926166ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 632 additions and 219 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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`.)")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 =

View File

@ -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 _ ->

View File

@ -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->

View File

@ -57,15 +57,15 @@ print_report spec config builder =
Test_Result.Failure msg details ->
escaped_message = escape_xml msg . replace '\n' '&#10;'
builder.append ('\n <failure message="' + escaped_message + '">\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 '</failure>\n'
builder.append '\n </failure>\n'
Test_Result.Pending msg -> builder.append ('\n <skipped message="' + (escape_xml msg) + '"/>\n ')
builder.append '</testcase>\n'
builder.append ' </testcase>\n'
builder.append ' </testsuite>\n'
should_print_behavior = config.print_only_failures.not || spec.behaviors.any (b -> b.result.is_fail)

View File

@ -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<Key, Value> {
protected static final int DEFAULT_LRU_SIZE = 5;
protected final int lruSize;
// Circular buffer containing the most recent cache keys.
private static final List<Pair<String, Value>> lru = new ArrayList<>(lruSize);
private final List<Pair<Key, Value>> 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<Void, Value> value_producer) {
public Value get_or_set(Key key, Function<Void, Value> 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<String, Value> pair = lru.get(i);
Pair<Key, Value> 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;
}
}

View File

@ -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<String, Value> {
public static Replacer_Cache INSTANCE = new Replacer_Cache(DEFAULT_LRU_SIZE);
protected Replacer_Cache(int lruSize) {
super(lruSize);
}
}

View File

@ -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<TemporalField, Long> 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) {

View File

@ -0,0 +1,13 @@
package org.enso.base.time;
import org.enso.base.Cache;
import org.graalvm.polyglot.Value;
public class FormatterCache extends Cache<FormatterCacheKey, Value> {
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);
}

View File

@ -0,0 +1,5 @@
package org.enso.base.time;
import java.util.Locale;
public record FormatterCacheKey(String pattern, Locale locale) {}

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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" <|

View File

@ -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

View File

@ -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

View File

@ -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" <|

View File

@ -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{<no offset>}" . 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

View File

@ -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)

View File

@ -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

View File

@ -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