mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 22:10:15 +03:00
Follow up improvements to Date_Time_Formatter
(#7875)
- Closes #7872 - Also closes #7866
This commit is contained in:
parent
c690559ec4
commit
8d926166ea
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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`.)")
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 =
|
||||
|
@ -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 _ ->
|
||||
|
@ -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->
|
||||
|
@ -57,15 +57,15 @@ print_report spec config builder =
|
||||
Test_Result.Failure msg details ->
|
||||
escaped_message = escape_xml msg . replace '\n' ' '
|
||||
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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package org.enso.base.time;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public record FormatterCacheKey(String pattern, Locale locale) {}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
49
test/Benchmarks/src/Time/Format.enso
Normal file
49
test/Benchmarks/src/Time/Format.enso
Normal 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
|
@ -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" <|
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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" <|
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user