Skip to content

Commit

Permalink
Improving experience with format and parse. (#9045)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
jdunkerley authored Feb 14, 2024
1 parent d45f0fe commit 08584b0
Show file tree
Hide file tree
Showing 14 changed files with 122 additions and 77 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Metadata.enso
Original file line number Diff line number Diff line change
Expand Up @@ -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
45 changes: 40 additions & 5 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Widget_Helpers.enso
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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.
Expand All @@ -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 + ")"]
Expand All @@ -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 + ")"]
Expand All @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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."

Expand Down
12 changes: 7 additions & 5 deletions distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.")

Expand Down
Loading

0 comments on commit 08584b0

Please sign in to comment.