From 08584b0423c57d77d1e93f3babea6dbe7ae9df2a Mon Sep 17 00:00:00 2001 From: James Dunkerley Date: Wed, 14 Feb 2024 16:30:37 +0000 Subject: [PATCH] Improving experience with `format` and `parse`. (#9045) - Add format dropdown to `Number.format`. ![image](https://github.com/enso-org/enso/assets/4699705/8aa74910-c6ad-4480-a7f2-04dacd9686e8) - Support case insensitive month names and abbreviations in dates. https://github.com/enso-org/enso/assets/4699705/4dbd8755-e1c2-4207-a8a1-65b427ca4fab - Improve locale dropdown for `parse_date` and `parse_date_time`. ![image](https://github.com/enso-org/enso/assets/4699705/5d605a2d-1248-46be-bc74-34a4afecf609) - Added dropdown to `Table.parse` and amended so now doesn't accept `Nothing` (using empty string instead). ![image](https://github.com/enso-org/enso/assets/4699705/340dd093-77db-4685-a34b-45ce09e3c3b3) - Added dropdown to `Table.format` and amended so now doesn't accept `Nothing` (using empty string instead). - Altered `Column.parse` to not accept `Nothing` and added drop down for format. - Altered `Column.format` to not accept `Nothing` and added drop down for format conditional on type. - Improved the locale date/time format drop to have the suggested formats too. --- .../Base/0.0.0-dev/src/Data/Numbers.enso | 4 +- .../src/Data/Time/Date_Time_Formatter.enso | 3 +- .../Format/As_Java_Formatter_Interpreter.enso | 2 +- .../Standard/Base/0.0.0-dev/src/Metadata.enso | 1 + .../Base/0.0.0-dev/src/Widget_Helpers.enso | 45 ++++++++++++++++--- .../0.0.0-dev/src/Data/DB_Column.enso | 32 +++++++------ .../Database/0.0.0-dev/src/Data/Table.enso | 12 ++--- .../Table/0.0.0-dev/src/Data/Column.enso | 45 +++++++++---------- .../0.0.0-dev/src/Data/Data_Formatter.enso | 2 +- .../Table/0.0.0-dev/src/Data/Table.enso | 17 ++++--- .../0.0.0-dev/src/Data/Type/Value_Type.enso | 7 +++ .../src/Internal/Widget_Helpers.enso | 12 +++++ test/Base_Tests/src/Data/Text/Parse_Spec.enso | 1 + .../src/In_Memory/Column_Format_Spec.enso | 16 +++---- 14 files changed, 122 insertions(+), 77 deletions(-) diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso index 0f4a7654a840..c8756329d652 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso @@ -13,6 +13,7 @@ import project.Nothing.Nothing import project.Panic.Panic from project.Data.Boolean import Boolean, False, True from project.Internal.Number_Builtins import all +from project.Widget_Helpers import make_number_format_selector polyglot java import java.lang.Double polyglot java import java.lang.Long @@ -305,9 +306,10 @@ type Number Convert the value 5000 to a string. 5000.format "#,##0" + @format make_number_format_selector @locale Locale.default_widget format : Text -> Locale -> Text - format self format locale=Locale.default = + format self format:Text="" locale:Locale=Locale.default = symbols = DecimalFormatSymbols.new locale.java_locale formatter = DecimalFormat.new format symbols formatter.format self diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso index 14eba2c52218..0e6d818a384c 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso @@ -124,7 +124,7 @@ 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 = + from_simple_pattern pattern:Text='' locale:Locale=Locale.default = used_locale = if locale == Locale.default then Locale.us else locale FormatterCache.SIMPLE_FORMAT.get_or_set (FormatterCacheKey.new pattern used_locale.java_locale) _-> parsed = Tokenizer.tokenize pattern |> Parser.parse_simple_date_pattern @@ -350,7 +350,6 @@ type Date_Time_Formatter "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 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/As_Java_Formatter_Interpreter.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/As_Java_Formatter_Interpreter.enso index 85a7aaa4cee0..e0c39d3aba31 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/As_Java_Formatter_Interpreter.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Time/Format/As_Java_Formatter_Interpreter.enso @@ -19,7 +19,7 @@ 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) -> Boolean -> DateTimeFormatter interpret locale nodes prepare_defaults=True = - builder = DateTimeFormatterBuilder.new + builder = DateTimeFormatterBuilder.new.parseCaseInsensitive interpret_node node = case node of Common_Nodes.Literal text -> builder.appendLiteral text diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso index d06993024c71..dff3273f2256 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso @@ -84,4 +84,5 @@ make_single_choice values display=Display.Always = make_option value = case value of _ : Vector -> Choice.Option value.first value.second _ : Text -> Choice.Option value value.pretty + _ : Choice -> value Widget.Single_Choice (values.map make_option) Nothing display diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso index 6d3606c1e6ab..5e1eb713d003 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso @@ -1,11 +1,14 @@ import project.Data.Locale.Locale +import project.Data.Numbers.Number import project.Data.Time.Date.Date import project.Data.Time.Date_Time.Date_Time import project.Data.Time.Date_Time_Formatter.Date_Time_Formatter import project.Data.Time.Time_Of_Day.Time_Of_Day import project.Meta import project.Metadata.Widget +from project.Data.Boolean import Boolean, False, True from project.Metadata import make_single_choice +from project.Metadata.Choice import Option ## PRIVATE Creates a Regex / Text Widget for search and replace. @@ -23,7 +26,15 @@ make_delimiter_selector = Creates a Single_Choice Widget for file read delimiters. make_file_read_delimiter_selector : Widget make_file_read_delimiter_selector = - make_single_choice [',', ';', '|', ['{tab}', "'\t'"], ['{space}', "' '"], ['{none}', "''"], '_', ['Custom', "'?'"]] + make_single_choice [',', ';', '|', ['{tab}', "'\t'"], ['{space}', "' '"], ['{none}', "''"], '_', ['Custom', "'?'"]] + +## PRIVATE + Creates a Single_Choice Widget for formatting decimals. +make_number_format_selector : Widget +make_number_format_selector (value:Number=1234.564321) = + formats = ['0', '#,##0', '#,##0.00', '#,##0.00##', '#,##0.00;(#,##0.00)', '0.####', '0.######E0'] + mapped_formats = formats.map f-> [f + " (e.g. " + (value.format f) + ")", f.pretty] + make_single_choice mapped_formats ## PRIVATE Creates a Single_Choice Widget for parsing dates. @@ -34,8 +45,10 @@ make_date_format_selector (date:Date=(Date.new 2012 3 14)) = formats = ['d/M/yyyy', 'dd/MM/yyyy', 'd-MMM-yy', 'd MMMM yyyy', 'M/d/yyyy', 'MM/dd/yyyy', 'MMMM d, yyyy', 'yyyy-MM'].map f-> [f + " (e.g. " + date.format f + ")", f.pretty] custom_locale_format = - format = Date_Time_Formatter.from 'd MMMM yyyy' locale=Locale.france - ['d MMMM yyyy - with custom Locale (e.g. ' + date.format format + ')', "(Date_Time_Formatter.from 'd MMMM yyyy' locale=Locale.france)"] + format = Date_Time_Formatter.from_simple_pattern 'd MMMM yyyy' locale=Locale.france + label = 'd MMMM yyyy - with custom Locale (e.g. ' + date.format format + ')' + code = "(Date_Time_Formatter.from_simple_pattern 'd MMMM yyyy' locale=Locale.france)" + Option label code [["pattern", make_single_choice formats], ["locale", Locale.default_widget]] week_date_formats = ['YYYY-ww-d', 'YYYY-ww', 'ddd, YYYY-ww'].map f-> format = Date_Time_Formatter.from_iso_week_date_pattern f ["ISO Week-based Date: " + f + " (e.g. " + date.format format + ")", "("+fqn+".from_iso_week_date_pattern " + f.pretty + ")"] @@ -59,8 +72,10 @@ make_date_time_format_selector (date_time:Date_Time=(Date_Time.new 2012 3 14 15 formats = ['yyyy-MM-dd HH:mm:ss.f', 'yyyy-MM-dd HH:mm:ss.f TT', 'd/M/yyyy h:mm a', 'dd/MM/yyyy HH:mm:ss', 'd-MMM-yy HH:mm:ss', 'd-MMM-yy h:mm:ss a', 'd MMMM yyyy h:mm a', 'M/d/yyyy h:mm:ss a', 'MM/dd/yyyy HH:mm:ss'] mapped_formats = formats.map f-> [f + " (e.g. " + date_time.format f + ")", f.pretty] custom_locale_format = - format = Date_Time_Formatter.from 'd MMMM yyyy HH:mm' locale=Locale.france - ['d MMMM yyyy HH:mm - with custom Locale (e.g. ' + date_time.format format + ')', "(Date_Time_Formatter.from 'd MMMM yyyy HH:mm' locale=Locale.france)"] + format = Date_Time_Formatter.from_simple_pattern 'd MMMM yyyy HH:mm' locale=Locale.france + label = 'd MMMM yyyy HH:mm - with custom Locale (e.g. ' + date_time.format format + ')' + code = "(Date_Time_Formatter.from_simple_pattern 'd MMMM yyyy HH:mm' locale=Locale.france)" + Option label code [["pattern", make_single_choice mapped_formats], ["locale", Locale.default_widget]] week_date_formats = ['YYYY-ww-d HH:mm:ss.f'].map f-> format = Date_Time_Formatter.from_iso_week_date_pattern f ["ISO Week-based Date-Time: " + f + " (e.g. " + date_time.format format + ")", "(Date_Time_Formatter.from_iso_week_date_pattern " + f.pretty + ")"] @@ -77,3 +92,23 @@ make_time_format_selector (time:Time_Of_Day=(Time_Of_Day.new 13 30 55 123)) = [f + " (e.g. " + time.format f + ")", f.pretty] make_single_choice ([iso_format] + formats) + +## PRIVATE + Create a Single_Choice Widget for selecting a format value. +make_format_chooser : Boolean -> Boolean -> Boolean -> Boolean -> Boolean -> Widget +make_format_chooser include_number:Boolean=True include_date:Boolean=True include_date_time:Boolean=True include_time:Boolean=True include_boolean:Boolean=True = + numeric = if include_number.not then [] else + ['0', '#,##0', '#,##0.00', '#,##0.00;(#,##0.00)'].map f-> [f + " (e.g. " + (1234.5678.format f) + ")", f.pretty] + date = if include_date.not then [] else + ['d/M/yyyy', 'dd/MM/yyyy', 'M/d/yyyy', 'MM/dd/yyyy', 'd-MMM-yy', 'd MMMM yyyy'].map f-> [f + " (e.g. " + (Date.new 2012 3 14).format f + ")", f.pretty] + date_time = if include_date_time.not then [] else + ['yyyy-MM-dd HH:mm:ss.f', 'yyyy-MM-dd HH:mm:ss.f TT', 'd/M/yyyy h:mm a', 'dd/MM/yyyy HH:mm:ss', 'MM/dd/yyyy HH:mm:ss'].map f-> [f + " (e.g. " + (Date_Time.new 2012 3 14 15 9 26 123).format f + ")", f.pretty] + custom_locale_format = + format = Date_Time_Formatter.from_simple_pattern 'd MMMM yyyy HH:mm' locale=Locale.france + label = 'd MMMM yyyy HH:mm - with custom Locale (e.g. ' + Date_Time.now.format format + ')' + code = "(Date_Time_Formatter.from_simple_pattern 'd MMMM yyyy HH:mm' locale=Locale.france)" + [Option label code [["pattern", make_single_choice (date+date_time)], ["locale", Locale.default_widget]]] + time = if include_time.not then [] else + ['HH:mm[:ss]', 'h:mm[:ss] a'].map f-> [f + " (e.g. " + (Time_Of_Day.new 13 30 55 123).format f + ")", f.pretty] + boolean = if include_boolean.not then [] else ['Yes|No', '1|0'].map f-> [f + " (Boolean)", f.pretty] + make_single_choice (numeric + date + date_time + time + custom_locale_format + boolean) diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/DB_Column.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/DB_Column.enso index 6b28940ae361..641afc381d0e 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/DB_Column.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/DB_Column.enso @@ -3,7 +3,7 @@ import Standard.Base.Errors.Common.Index_Out_Of_Bounds import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Errors.Illegal_State.Illegal_State import Standard.Base.Internal.Rounding_Helpers -from Standard.Base.Widget_Helpers import make_regex_text_widget +from Standard.Base.Widget_Helpers import make_format_chooser, make_regex_text_widget import Standard.Table.Data.Column.Column as Materialized_Column import Standard.Table.Data.Constants.Previous_Value @@ -1648,8 +1648,8 @@ type DB_Column can be used. For `Boolean`, it should be two values that represent true and false, separated by a `|`. Alternatively, a `Data_Formatter` can be passed to provide complete customisation of the formatting. If - `Nothing` is provided, the default formatting settings of the backend - will be used. `Nothing` is currently the only setting accepted by the + `""` is provided, the default formatting settings of the backend + will be used. `""` is currently the only setting accepted by the Database backends. - on_problems: Specifies how to handle if a problem occurs, raising as a warning by default. @@ -1665,26 +1665,24 @@ type DB_Column does not support customization, an `Unsupported_Database_Operation` error is reported. @type (Widget_Helpers.parse_type_selector include_auto=False) - parse : Value_Type | Auto -> Text | Data_Formatter | Nothing -> Problem_Behavior -> DB_Column - parse self type format=Nothing on_problems=Report_Warning = - check_parameters = - if type == Auto then Error.throw (Unsupported_Database_Operation.Error "The `Auto` parse type is not supported by the Database backend. Either pick a specific type or materialize the table to memory using `.read`.") else - case format of - Nothing -> Nothing - _ -> Error.throw (Unsupported_Database_Operation.Error "Custom formatting is not supported by the Database backend. Please set the format to `Nothing` to indicate that the default Database settings can be used, or if custom formatting is needed, materialize the table to memory using `.read` first.") - check_parameters.if_not_error <| - Value_Type.expect_text self <| - ## In the future we may have some specific logic, for example - allowing to support formatting settings. For now, the - Database parse just boils down to a simple CAST. - self.internal_do_cast type on_problems + @format (make_format_chooser include_number=False) + parse : Value_Type | Auto -> Text | Data_Formatter -> Problem_Behavior -> DB_Column + parse self type format:(Text|Data_Formatter)="" on_problems=Report_Warning = + if type == Auto then Error.throw (Unsupported_Database_Operation.Error "The `Auto` parse type is not supported by the Database backend. Either pick a specific type or materialize the table to memory using `.read`.") else + if format != "" then Error.throw (Unsupported_Database_Operation.Error "Custom formatting is not supported by the Database backend. Please set the format to `''` to use the default settings, or if custom formatting is needed, materialize the table to memory using `.read` first.") else + Value_Type.expect_text self <| + ## In the future we may have some specific logic, for example + allowing to support formatting settings. For now, the + Database parse just boils down to a simple CAST. + self.internal_do_cast type on_problems ## GROUP Standard.Base.Conversions ICON convert Formatting values is not supported in database columns. @locale Locale.default_widget + @format (self-> Widget_Helpers.make_format_chooser_for_type self.value_type) format : Text | Date_Time_Formatter | DB_Column | Nothing -> Locale -> DB_Column ! Illegal_Argument - format self format:(Text | Date_Time_Formatter | DB_Column | Nothing)=Nothing locale=Locale.default = + format self format:(Text | Date_Time_Formatter | DB_Column)="" locale=Locale.default = _ = [format, locale] Error.throw <| Unsupported_Database_Operation.Error "`DB_Column.format` is not implemented yet for the Database backends." diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso index 2ef74dc54482..eb14736cc502 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso @@ -14,7 +14,7 @@ import Standard.Base.Errors.Unimplemented.Unimplemented import Standard.Base.System.File.Generic.Writable_File.Writable_File from Standard.Base.Metadata import make_single_choice from Standard.Base.Runtime import assert -from Standard.Base.Widget_Helpers import make_delimiter_selector +from Standard.Base.Widget_Helpers import make_delimiter_selector, make_format_chooser import Standard.Table.Data.Blank_Selector.Blank_Selector import Standard.Table.Data.Calculations.Column_Operation.Column_Operation @@ -2017,8 +2017,9 @@ type Table table.parse "birthday" Value_Type.Date @type (Widget_Helpers.parse_type_selector include_auto=False) @columns Widget_Helpers.make_column_name_vector_selector - parse : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Value_Type | Auto -> Text | Data_Formatter | Nothing -> Boolean -> Problem_Behavior -> Table - parse self columns=(self.columns . filter (c-> c.value_type.is_text) . map .name) type format=Nothing error_on_missing_columns=True on_problems=Report_Warning = + @format (make_format_chooser include_number=False) + parse : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Value_Type | Auto -> Text | Data_Formatter -> Boolean -> Problem_Behavior -> Table + parse self columns=(self.columns . filter (c-> c.value_type.is_text) . map .name) type format:(Text|Data_Formatter)='' error_on_missing_columns=True on_problems=Report_Warning = selected = self.columns_helper.select_columns columns Case_Sensitivity.Default reorder=False error_on_missing_columns=error_on_missing_columns on_problems=on_problems error_on_empty=False . map self.make_column selected.fold self table-> column_to_parse-> new_column = column_to_parse.parse type format on_problems @@ -2099,8 +2100,9 @@ type Table table.format @columns Widget_Helpers.make_column_name_vector_selector @locale Locale.default_widget - format : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Text | Date_Time_Formatter | DB_Column | Nothing -> Locale -> Boolean -> Problem_Behavior -> Table ! Date_Time_Format_Parse_Error | Illegal_Argument - format self columns format:(Text | Date_Time_Formatter | DB_Column | Nothing)=Nothing locale=Locale.default error_on_missing_columns=True on_problems=Report_Warning = + @format (make_format_chooser include_number=False) + format : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Text | Date_Time_Formatter | DB_Column -> Locale -> Boolean -> Problem_Behavior -> Table ! Date_Time_Format_Parse_Error | Illegal_Argument + format self columns format:(Text | Date_Time_Formatter | DB_Column)="" locale=Locale.default error_on_missing_columns=True on_problems=Report_Warning = _ = [columns, format, locale, error_on_missing_columns, on_problems] Error.throw (Unsupported_Database_Operation.Error "Table.format is not implemented yet for the Database backends.") diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso index 737aab4f0053..873cd1a61908 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso @@ -10,7 +10,7 @@ import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Errors.Illegal_State.Illegal_State import Standard.Base.Internal.Polyglot_Helpers import Standard.Base.Internal.Rounding_Helpers -from Standard.Base.Widget_Helpers import make_regex_text_widget +from Standard.Base.Widget_Helpers import make_format_chooser, make_regex_text_widget import project.Data.Constants.Previous_Value import project.Data.Data_Formatter.Data_Formatter @@ -1667,8 +1667,8 @@ type Column can be used. For `Boolean`, it should be two values that represent true and false, separated by a `|`. Alternatively, a `Data_Formatter` can be passed to provide complete customisation of the formatting. If - `Nothing` is provided, the default formatting settings of the backend - will be used. `Nothing` is currently the only setting accepted by the + `""` is provided, the default formatting settings of the backend + will be used. `""` is currently the only setting accepted by the Database backends. - on_problems: Specifies how to handle if a problem occurs, raising as a warning by default. @@ -1725,16 +1725,13 @@ type Column example_contains = Examples.text_column_1.parse Boolean 'Yes|No' @type Widget_Helpers.parse_type_selector - parse : Value_Type | Auto -> Text | Data_Formatter | Nothing -> Problem_Behavior -> Column - parse self type=Auto format=Data_Formatter.Value on_problems=Report_Warning = + @format (make_format_chooser include_number=False) + parse : Value_Type | Auto -> Text | Data_Formatter -> Problem_Behavior -> Column + parse self type=Auto format:(Text|Data_Formatter)="" on_problems=Report_Warning = Value_Type.expect_text self <| formatter = case format of - _ : Text -> - Data_Formatter.Value.with_format type format + _ : Text -> if format == "" then Data_Formatter.Value else Data_Formatter.Value.with_format type format _ : Data_Formatter -> format - Nothing -> Data_Formatter.Value - _ -> Error.throw (Illegal_Argument.Error "Invalid format type. Expected Text or Data_Formatter or Nothing.") - parser = formatter.make_value_type_parser type storage = self.java_column.getStorage @@ -1749,9 +1746,8 @@ type Column Arguments: - format: The type-dependent format string to use to format the values. - If `format` is `""` or `Nothing`, .to_text is used to format the value. - In case of date/time columns, the format can also be a - `Date_Time_Formatter`. + If `format` is `""`, .to_text is used to format the value. + In case of date/time columns, a `Date_Time_Formatter` can be used. - locale: The locale in which the format should be interpreted. If a `Date_Time_Formatter` is provided for `format` and the `locale` is set to anything else than `Locale.default`, then that locale will @@ -1823,18 +1819,17 @@ type Column input.format "#,##0.00" locale=(Locale.new "fr") # ==> ["100 000 000,00", "2 222,00", "3,00"] @locale Locale.default_widget - format : Text | Date_Time_Formatter | Column | Nothing -> Locale -> Column ! Illegal_Argument - format self format:(Text | Date_Time_Formatter | Column | Nothing)=Nothing locale=Locale.default = - new_column = case format of - format_column : Column -> Value_Type.expect_text format_column <| - formatter = make_value_formatter_for_value_type self.value_type locale - formatter.if_not_error <| - formatter_flipped value format = formatter format value - Column_Ops.map_2_over_storage self format_column formatter_flipped make_string_builder - _ -> - formatter = make_value_formatter_for_value_type self.value_type locale format - Column_Ops.map_over_storage self formatter make_string_builder on_problems=Problem_Behavior.Report_Error - new_column + @format (self-> Widget_Helpers.make_format_chooser_for_type self.value_type) + format : Text | Date_Time_Formatter | Column -> Locale -> Column ! Illegal_Argument + format self format:(Text | Date_Time_Formatter | Column)="" locale=Locale.default = case format of + format_column : Column -> Value_Type.expect_text format_column <| + formatter = make_value_formatter_for_value_type self.value_type locale + formatter.if_not_error <| + formatter_flipped value format = formatter format value + Column_Ops.map_2_over_storage self format_column formatter_flipped make_string_builder + _ -> + formatter = make_value_formatter_for_value_type self.value_type locale format + Column_Ops.map_over_storage self formatter make_string_builder on_problems=Problem_Behavior.Report_Error ## GROUP Standard.Base.Conversions ICON convert diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso index 8db22977ca52..f7dcff2813d2 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso @@ -171,7 +171,7 @@ type Data_Formatter if formats.length != 2 then Error.throw (Illegal_Argument.Error "The `format` for Booleans must be a string with two values separated by `|`, for example: 'Yes|No'.") else self.with_boolean_values true_values=[formats.at 0] false_values=[formats.at 1] Auto -> - Error.throw (Illegal_Argument.Error "Cannot specify a `format` with type `Auto`.") + Error.throw (Illegal_Argument.Error "Specify a `type` to use a custom format.") _ : Value_Type -> Error.throw (Illegal_Argument.Error "Cannot specify a `format` for type `"+type.to_text+"`.") diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso index a6c3f55cb1aa..5b0bf3ca2dc0 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso @@ -17,7 +17,7 @@ import Standard.Base.Errors.Unimplemented.Unimplemented import Standard.Base.Runtime.Context import Standard.Base.System.File.Generic.Writable_File.Writable_File from Standard.Base.Metadata import make_single_choice -from Standard.Base.Widget_Helpers import make_delimiter_selector +from Standard.Base.Widget_Helpers import make_delimiter_selector, make_format_chooser import project.Data.Aggregate_Column.Aggregate_Column import project.Data.Blank_Selector.Blank_Selector @@ -965,14 +965,12 @@ type Table table.parse format=(Data_Formatter.Value.with_number_formatting decimal_point=',') @columns Widget_Helpers.make_column_name_vector_selector @type Widget_Helpers.parse_type_selector - parse : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Value_Type | Auto -> Text | Data_Formatter | Nothing -> Boolean -> Problem_Behavior -> Table - parse self columns=(self.columns . filter (c-> c.value_type.is_text) . map .name) type=Auto format=Data_Formatter.Value error_on_missing_columns=True on_problems=Report_Warning = + @format (make_format_chooser include_number=False) + parse : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Value_Type | Auto -> Text | Data_Formatter -> Boolean -> Problem_Behavior -> Table + parse self columns=(self.columns . filter (c-> c.value_type.is_text) . map .name) type=Auto format:(Text|Data_Formatter)='' error_on_missing_columns=True on_problems=Report_Warning = formatter = case format of - _ : Text -> - Data_Formatter.Value.with_format type format + _ : Text -> if format.is_empty then Data_Formatter.Value else Data_Formatter.Value.with_format type format _ : Data_Formatter -> format - Nothing -> Data_Formatter.Value - _ -> Error.throw (Illegal_Argument.Error "Invalid format type. Expected Text or Data_Formatter or Nothing.") parser = formatter.make_value_type_parser type @@ -1072,8 +1070,9 @@ type Table table.format @columns Widget_Helpers.make_column_name_vector_selector @locale Locale.default_widget - format : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Text | Date_Time_Formatter | Column | Nothing -> Locale -> Boolean -> Problem_Behavior -> Table ! Date_Time_Format_Parse_Error | Illegal_Argument - format self columns format:(Text | Date_Time_Formatter | Column | Nothing)=Nothing locale=Locale.default error_on_missing_columns=True on_problems=Report_Warning = + @format (make_format_chooser include_number=False) + format : Vector (Text | Integer | Regex) | Text | Integer | Regex -> Text | Date_Time_Formatter | Column -> Locale -> Boolean -> Problem_Behavior -> Table ! Date_Time_Format_Parse_Error | Illegal_Argument + format self columns format:(Text | Date_Time_Formatter | Column)="" locale=Locale.default error_on_missing_columns=True on_problems=Report_Warning = select_problem_builder = Problem_Builder.new error_on_missing_columns=error_on_missing_columns selected_columns = self.columns_helper.select_columns_helper columns Case_Sensitivity.Default True select_problem_builder select_problem_builder.attach_problems_before on_problems <| diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Type/Value_Type.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Type/Value_Type.enso index be5c04c365df..6e2e16c1ae3e 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Type/Value_Type.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Type/Value_Type.enso @@ -198,6 +198,13 @@ type Value_Type Value_Type.Decimal _ _ -> True _ -> False + ## GROUP Standard.Base.Metadata + Checks if the `Value_Type` represents a Date_Time type. + is_date_time : Boolean + is_date_time self = case self of + Value_Type.Date_Time _ -> True + _ -> False + ## GROUP Standard.Base.Metadata Checks if the `Value_Type` represents a type that holds a date. diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Widget_Helpers.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Widget_Helpers.enso index 4280430c42c1..24e562f55eb0 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Widget_Helpers.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Widget_Helpers.enso @@ -4,6 +4,7 @@ import Standard.Base.Metadata.Widget from Standard.Base.Metadata.Choice import Option from Standard.Base.Metadata.Widget import Numeric_Input, Single_Choice, Vector_Editor from Standard.Base.System.File_Format import format_types +from Standard.Base.Widget_Helpers import make_format_chooser import project.Data.Aggregate_Column.Aggregate_Column import project.Data.Join_Condition.Join_Condition @@ -197,3 +198,14 @@ write_table_selector = all_types = [Auto_Detect] + (format_types.filter can_write) make_name type_obj = type_obj.to_text.replace "_Format" "" . replace "_" " " Single_Choice display=Display.Always values=(all_types.map n->(Option (make_name n) (File_Format.constructor_code n))) + +## PRIVATE + Make format selector based off value type +make_format_chooser_for_type : Value_Type -> Widget +make_format_chooser_for_type value_type = + include_number = value_type.is_numeric || value_type==Value_Type.Mixed + include_date = value_type==Value_Type.Date || value_type==Value_Type.Mixed + include_date_time = value_type.is_date_time || value_type==Value_Type.Mixed + include_time = value_type==Value_Type.Time || value_type==Value_Type.Mixed + include_boolean = value_type.is_boolean || value_type==Value_Type.Mixed + make_format_chooser include_number include_date include_date_time include_time include_boolean diff --git a/test/Base_Tests/src/Data/Text/Parse_Spec.enso b/test/Base_Tests/src/Data/Text/Parse_Spec.enso index 0942283380b5..6e3f85ffd763 100644 --- a/test/Base_Tests/src/Data/Text/Parse_Spec.enso +++ b/test/Base_Tests/src/Data/Text/Parse_Spec.enso @@ -30,6 +30,7 @@ add_specs suite_builder = "1999 1 1".parse_date "yyyy M d" . should_equal <| Date.new 1999 1 1 "1999-01-01".parse_date "yyyy M d" . should_fail_with Time_Error "13 Jan 2023".parse_date "d MMM yyyy" . should_equal <| Date.new 2023 1 13 + "13 JAN 2023".parse_date "d MMM yyyy" . should_equal <| Date.new 2023 1 13 "13 January 2023".parse_date "d MMMM yyyy" . should_equal <| Date.new 2023 1 13 group_builder.specify "Date_Time" <| diff --git a/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso b/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso index 03b3d7692708..a1b5bf589d11 100644 --- a/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso +++ b/test/Table_Tests/src/In_Memory/Column_Format_Spec.enso @@ -32,12 +32,11 @@ add_specs suite_builder = input.format (Date_Time_Formatter.from "d. MMMM yyyy" Locale.uk) . should_equal expected_gb input.format (Date_Time_Formatter.from "d. MMMM yyyy" Locale.france) . should_equal expected_fr - group_builder.specify "Empty/Nothing format" <| + group_builder.specify "Empty format" <| input = Column.from_vector "values" [Date.new 2020 12 21, Date.new 2023 4 25] expected = Column.from_vector "values" ['2020-12-21', '2023-04-25'] input.format . should_equal expected input.format "" . should_equal expected - input.format Nothing . should_equal expected group_builder.specify "Bad format" <| input = Column.from_vector "values" [Date.new 2020 6 21, Date.new 2023 4 25] @@ -106,13 +105,12 @@ add_specs suite_builder = # If I provide a locale argument, it overrides what is already in the formatter: input.format formatter Locale.poland . should_equal expected_pl - group_builder.specify "Empty/Nothing format" <| + group_builder.specify "Empty format" <| zone = Time_Zone.parse "US/Hawaii" input = Column.from_vector "values" [Date_Time.new 2020 12 21 8 10 20 zone=zone, Date_Time.new 2023 4 25 14 25 2 zone=zone] expected = Column.from_vector "values" ['2020-12-21 08:10:20-10:00[US/Hawaii]', '2023-04-25 14:25:02-10:00[US/Hawaii]'] input.format . should_equal expected input.format "" . should_equal expected - input.format Nothing . should_equal expected group_builder.specify "Bad format" <| input = Column.from_vector "values" [Date_Time.new 2020 6 21 8 10 20, Date_Time.new 2023 4 25 14 25 2] @@ -170,12 +168,11 @@ add_specs suite_builder = input.format "HH.mm.ss" (Locale.default) . should_equal expected input.format "HH.mm.ss" (Locale.new "gb") . should_equal expected - group_builder.specify "Empty/Nothing format" <| + group_builder.specify "Empty format" <| input = Column.from_vector "values" [Time_Of_Day.new 8 10 20, Time_Of_Day.new 14 25 2] expected = Column.from_vector "values" ['08:10:20', '14:25:02'] input.format . should_equal expected input.format "" . should_equal expected - input.format Nothing . should_equal expected group_builder.specify "Bad format" <| input = Column.from_vector "values" [Time_Of_Day.new 8 10 20, Time_Of_Day.new 14 25 2] @@ -228,12 +225,11 @@ add_specs suite_builder = actual = input.format "t|f" actual . should_equal expected - group_builder.specify "Empty/Nothing format" <| + group_builder.specify "Empty format" <| input = Column.from_vector "values" [True, False] expected = Column.from_vector "values" ["True", "False"] input.format . should_equal expected input.format "" . should_equal expected - input.format Nothing . should_equal expected group_builder.specify "Bad format" <| input = Column.from_vector "values" [True, False] @@ -267,20 +263,18 @@ add_specs suite_builder = expected = Column.from_vector "values" ["100,000,000.00", "2,222.00", "3.00"] input.format "#,##0.00" . should_equal expected - suite_builder.group "Numeric, empty/Nothing" group_builder-> + suite_builder.group "Numeric format, empty" group_builder-> group_builder.specify "Integer" <| input = Column.from_vector "values" ["100000000", "2222", "3"] . parse (Value_Type.Integer Bits.Bits_64) expected = Column.from_vector "values" ["100000000", "2222", "3"] input.format . should_equal expected input.format "" . should_equal expected - input.format Nothing . should_equal expected group_builder.specify "Float" <| input = Column.from_vector "values" ["100000000", "2222", "3"] . parse (Value_Type.Float Bits.Bits_64) expected = Column.from_vector "values" ['1.0E8', '2222.0', '3.0'] input.format . should_equal expected input.format "" . should_equal expected - input.format Nothing . should_equal expected group_builder.specify "Integer, with format Column" <| input = Column.from_vector "values" ["100000000", "2222", "3"] . parse (Value_Type.Integer Bits.Bits_64)