Expand capabilities of Table.set and better dropdown support, (#8005)

- Adds the ability to use numbers, date/time and Boolean values as constants in `set`.
- `Table.set` can take a `Column_Operation`, allowing for deriving of a new column based on other columns.
- Added `Column_Ref` type to refer to a column in `filter`.
This commit is contained in:
James Dunkerley 2023-10-13 17:03:28 +01:00 committed by GitHub
parent a96f2d7aba
commit fac9e7a420
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 644 additions and 37 deletions

View File

@ -583,6 +583,8 @@
- [Implemented Text.substring to easily select part of a Text field][7913]
- [Implemented basic XML support][7947]
- [Implemented `Table.lookup_and_replace` for the in-memory backend.][7979]
- [Added `Column_Operation` to `Table.set` allowing for more streamlined flow of
deriving column values in the GUI.][8005]
[debug-shortcuts]:
https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug
@ -830,6 +832,7 @@
[7913]: https://github.com/enso-org/enso/pull/7913
[7947]: https://github.com/enso-org/enso/pull/7947
[7979]: https://github.com/enso-org/enso/pull/7979
[8005]: https://github.com/enso-org/enso/pull/8005
#### Enso Compiler

View File

@ -13,6 +13,7 @@ import Standard.Table.Internal.Java_Problems
import Standard.Table.Internal.Problem_Builder.Problem_Builder
import Standard.Table.Internal.Widget_Helpers
from Standard.Table import Auto, Data_Formatter, Sort_Column, Value_Type
from Standard.Table.Data.Column import default_date_period
from Standard.Table.Errors import Conversion_Failure, Floating_Point_Equality, Inexact_Type_Coercion, Invalid_Value_Type
from Standard.Table.Internal.Cast_Helpers import check_cast_compatibility
@ -1385,7 +1386,8 @@ type Column
shift each date.
- period: The period by which to shift. For `Date` columns it should be a
`Date_Period` and for `Time` columns it should be a `Time_Period`. For
`Date_Time` columns it can be either.
`Date_Time` columns it can be either. It defaults to `Day` where
possible and `Hour` for `Time` columns.
? Time Zone handling
@ -1395,7 +1397,7 @@ type Column
unusual events like DST.
@period Date_Time_Helpers.make_period_selector_for_column
date_add : (Column | Integer) -> Date_Period | Time_Period -> Column ! Invalid_Value_Type | Illegal_Argument
date_add self amount (period : Date_Period | Time_Period) =
date_add self amount (period : Date_Period|Time_Period = default_date_period self) =
Value_Type.expect_type self .is_date_or_time "date/time" <|
my_type = self.inferred_precise_value_type
Value_Type.expect_integer amount <|

View File

@ -11,6 +11,7 @@ import Standard.Base.Errors.Unimplemented.Unimplemented
from Standard.Base.Metadata import make_single_choice
from Standard.Base.Widget_Helpers import make_delimiter_selector
import Standard.Table.Data.Calculations.Column_Operation.Column_Operation
import Standard.Table.Data.Expression.Expression
import Standard.Table.Data.Expression.Expression_Error
import Standard.Table.Data.Join_Condition.Join_Condition
@ -21,9 +22,11 @@ import Standard.Table.Data.Report_Unmatched.Report_Unmatched
import Standard.Table.Data.Row.Row
import Standard.Table.Data.Table.Table as Materialized_Table
import Standard.Table.Data.Type.Value_Type_Helpers
import Standard.Table.Extensions.Table_Ref.Table_Ref
import Standard.Table.Internal.Add_Row_Number
import Standard.Table.Internal.Aggregate_Column_Helper
import Standard.Table.Internal.Column_Naming_Helper.Column_Naming_Helper
import Standard.Table.Internal.Constant_Column.Constant_Column
import Standard.Table.Internal.Lookup_Helpers
import Standard.Table.Internal.Problem_Builder.Problem_Builder
import Standard.Table.Internal.Table_Helpers
@ -549,6 +552,7 @@ type Table
people.filter "age" (age -> (age%10 == 0))
@column Widget_Helpers.make_column_name_selector
@filter Widget_Helpers.make_filter_condition_selector
filter : (Column | Text | Integer) -> (Filter_Condition|(Any->Boolean)) -> Problem_Behavior -> Table ! No_Such_Column | Index_Out_Of_Bounds | Invalid_Value_Type
filter self column (filter : Filter_Condition | Function = Filter_Condition.Equal True) on_problems=Report_Warning = case column of
_ : Column ->
@ -560,7 +564,9 @@ type Table
new_ctx = self.context.set_where_filters new_filters
self.updated_context new_ctx
case filter of
_ : Filter_Condition -> mask (make_filter_column column filter on_problems)
_ : Filter_Condition ->
resolved = (self:Table_Ref).resolve_condition filter
mask (make_filter_column column resolved on_problems)
## We check the `filter` predicate before reporting unsupported
operation, because it may just be a Filter_Condition with
missing arguments and then another error is more appropriate.
@ -825,22 +831,22 @@ type Table
table.set double_inventory new_name="total_stock"
table.set "2 * [total_stock]" new_name="total_stock_expr"
@new_name Widget_Helpers.make_column_name_selector
set : Column | Text | Array | Vector | Range | Date_Range -> Text | Nothing -> Set_Mode -> Problem_Behavior -> Table ! Existing_Column | Missing_Column | No_Such_Column | Expression_Error
set self column new_name=Nothing set_mode=Set_Mode.Add_Or_Update on_problems=Report_Warning =
set : Column | Text | Array | Vector | Range | Date_Range | Constant_Column | Column_Operation -> Text -> Set_Mode -> Problem_Behavior -> Table ! Existing_Column | Missing_Column | No_Such_Column | Expression_Error
set self column new_name="" set_mode=Set_Mode.Add_Or_Update on_problems=Report_Warning =
resolved = case column of
_ : Text -> self.evaluate_expression column on_problems
_ : Column ->
if Helpers.check_integrity self column then column else
Error.throw (Integrity_Error.Error "Column "+column.name)
_ : Constant_Column -> self.make_constant_column column
_ : Column_Operation -> column.evaluate self (set_mode==Set_Mode.Update && new_name=="") on_problems
_ : Vector -> Error.throw (Unsupported_Database_Operation.Error "Cannot use `Vector` for `set` in the database.")
_ : Array -> Error.throw (Unsupported_Database_Operation.Error "Cannot use `Array` for `set` in the database.")
_ : Range -> Error.throw (Unsupported_Database_Operation.Error "Cannot use `Range` for `set` in the database.")
_ : Date_Range -> Error.throw (Unsupported_Database_Operation.Error "Cannot use `Date_Range` for `set` in the database.")
_ -> Error.throw (Illegal_Argument.Error "Unsupported type for `Table.set`.")
renamed = case new_name of
Nothing -> resolved
_ : Text -> resolved.rename new_name
renamed = if new_name == "" then resolved else resolved.rename new_name
renamed.if_not_error <| self.column_naming_helper.check_ambiguity self.column_names new_name <|
index = self.internal_columns.index_of (c -> c.name == renamed.name)
check_add = case set_mode of
@ -2486,3 +2492,6 @@ default_join_condition table join_kind = case join_kind of
Materialized_Table.from (that:Table) =
_ = [that]
Error.throw (Illegal_Argument.Error "Currently cross-backend operations are not supported. Materialize the table using `.read` before mixing it with an in-memory Table.")
## PRIVATE
Table_Ref.from (that:Table) = Table_Ref.Value that

View File

@ -10,10 +10,10 @@ import project.Connection.SSL_Mode.SSL_Mode
import project.Data.Column_Description.Column_Description
import project.Data.SQL_Query.SQL_Query
import project.Data.Update_Action.Update_Action
import project.Extensions.Upload_Database_Table
import project.Extensions.Upload_In_Memory_Table
from project.Connection.Postgres_Details.Postgres_Details import Postgres
from project.Connection.SQLite_Details.SQLite_Details import SQLite
from project.Extensions.Upload_Database_Table import all
from project.Extensions.Upload_In_Memory_Table import all
export project.Connection.Client_Certificate.Client_Certificate
export project.Connection.Connection_Options.Connection_Options
@ -27,7 +27,7 @@ export project.Connection.SSL_Mode.SSL_Mode
export project.Data.Column_Description.Column_Description
export project.Data.SQL_Query.SQL_Query
export project.Data.Update_Action.Update_Action
export project.Extensions.Upload_Database_Table
export project.Extensions.Upload_In_Memory_Table
from project.Connection.Postgres_Details.Postgres_Details export Postgres
from project.Connection.SQLite_Details.SQLite_Details export SQLite
from project.Extensions.Upload_Database_Table export all
from project.Extensions.Upload_In_Memory_Table export all

View File

@ -0,0 +1,151 @@
from Standard.Base import all
import Standard.Base.Metadata.Display
import Standard.Base.Metadata.Widget
from Standard.Base.Metadata.Choice import Option
from Standard.Base.Metadata.Widget import Single_Choice
import project.Data.Column_Ref.Column_Ref
import project.Extensions.Table_Ref.Table_Ref
import project.Internal.Widget_Helpers
from project.Internal.Filter_Condition_Helpers import make_filter_column
## Defines a column operation generally acting on each row producing a new
column.
type Column_Operation
## Add two values/columns.
Add (input : Column_Ref|Number|Text) (rhs : Column_Ref|Number|Text)
## Subtract two values/columns.
Subtract (input : Column_Ref|Number) (rhs : Column_Ref|Number)
## Multiply two values/columns.
Multiply (input : Column_Ref|Number) (rhs : Column_Ref|Number)
## Divide a fixed value or column by another value or column.
Divide (input : Column_Ref|Number) (rhs : Column_Ref|Number)
## Compute the remainder of a fixed value or column divided by another
value or column.
Mod (input : Column_Ref|Number) (rhs : Column_Ref|Number)
## Raise a fixed value or column to the power of another value or column.
Power (input : Column_Ref|Number) (rhs : Column_Ref|Number)
## Rounds values in the column to the specified precision.
Round (input : Column_Ref|Number) (precision:Integer = 0) (use_bankers:Boolean = False)
## Rounds values in the column up to the nearest integer.
Ceil (input : Column_Ref|Number)
## Rounds values in the column down to the nearest integer.
Floor (input : Column_Ref|Number)
## Truncates the fractional part of values in the column.
If a Date_Time, returns the Date.
Truncate (input : Column_Ref|Number|Date_Time)
## Returns the minimum value of two columns.
Min (input : Column_Ref|Any) (rhs : Column_Ref|Any)
## Returns the maximum value of two columns.
Max (input : Column_Ref|Any) (rhs : Column_Ref|Any)
## Adds a period to a date/time column.
Date_Add (input : Column_Ref|Date_Time|Date|Time_Of_Day) (length : Column_Ref|Integer) (period : Date_Period|Time_Period = Date_Period.Day)
## Returns part of a date/time column.
Date_Part (input : Column_Ref|Date_Time|Date|Time_Of_Day) (period : Date_Period|Time_Period)
## Returns the difference between two date/time columns.
Date_Diff (input : Column_Ref|Date_Time|Date|Time_Of_Day) (end : Column_Ref|Date_Time|Date|Time_Of_Day) (period:Date_Period|Time_Period = Date_Period.Day)
## Negate a boolean column.
Not (input : Column_Ref|Boolean)
## Boolean AND on two boolean columns.
And (input : Column_Ref|Boolean) (rhs : Column_Ref|Boolean)
## Boolean OR on two boolean columns.
Or (input : Column_Ref|Boolean) (rhs : Column_Ref|Boolean)
## If input meets a condition return true value, otherwise false value.
The `true_value` and `false_value` can be either a constant or a column.
If (input : Column_Ref|Any) (condition:Filter_Condition) (true_value:Column_Ref|Any = True) (false_value:Column_Ref|Any = False)
## Removes the specified characters, by default any whitespace, from the
start, the end, or both ends of the input.
Trim (input : Column_Ref|Text) (where:Location = Location.Both) (what:Text|Column_Ref = "")
## PRIVATE
Interprets the `Column_Operation` as operation on columns of a provided
table, resolving the column references.
It creates a new column instance which can be added to the table.
evaluate : Table_Ref -> Boolean -> Problem_Behavior -> Any
evaluate self table:Table_Ref use_input_name:Boolean on_problems:Problem_Behavior =
input_column = table.resolve_as_column self.input
derived = case self of
Column_Operation.Add _ rhs -> input_column + (table.resolve rhs)
Column_Operation.Subtract _ rhs -> input_column - (table.resolve rhs)
Column_Operation.Multiply _ rhs -> input_column * (table.resolve rhs)
Column_Operation.Divide _ rhs -> input_column / (table.resolve rhs)
Column_Operation.Mod _ rhs -> input_column % (table.resolve rhs)
Column_Operation.Power _ rhs -> input_column ^ (table.resolve rhs)
Column_Operation.Round _ precision use_bankers ->
input_column.round precision use_bankers
Column_Operation.Ceil _ -> input_column.ceil
Column_Operation.Floor _ -> input_column.floor
Column_Operation.Truncate _ -> input_column.truncate
Column_Operation.Min _ rhs -> input_column.min (table.resolve rhs)
Column_Operation.Max _ rhs -> input_column.max (table.resolve rhs)
Column_Operation.Date_Add _ length period ->
input_column.date_add (table.resolve length) period
Column_Operation.Date_Part _ period ->
input_column.date_part period
Column_Operation.Date_Diff _ end period ->
input_column.date_diff (table.resolve end) period
Column_Operation.Not _ -> input_column.not
Column_Operation.And _ rhs -> input_column && (table.resolve rhs)
Column_Operation.Or _ rhs -> input_column || (table.resolve rhs)
Column_Operation.If _ condition true_value false_value ->
condition_column = make_filter_column input_column (table.resolve_condition condition) on_problems
condition_column.iif (table.resolve true_value) (table.resolve false_value)
Column_Operation.Trim _ where what ->
input_column.trim where (table.resolve what)
if use_input_name then derived.rename input_column.name else derived
## PRIVATE
Create a widget for operation
default_widget : Table_Ref -> Widget
default_widget table:Table_Ref display=Display.Always =
col_refs = Widget_Helpers.make_column_ref_by_name_selector table
filter_cond = Widget_Helpers.make_filter_condition_selector table
builder = Vector.new_builder
fqn = Meta.get_qualified_type_name Column_Operation
builder.append (Option "add" fqn+".Add" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "subtract" fqn+".Subtract" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "multiply" fqn+".Multiply" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "divide" fqn+".Divide" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "mod" fqn+".Mod" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "power" fqn+".Power" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "round" fqn+".Round" [["input", col_refs]])
builder.append (Option "ceil" fqn+".Ceil" [["input", col_refs]])
builder.append (Option "floor" fqn+".Floor" [["input", col_refs]])
builder.append (Option "truncate" fqn+".Truncate" [["input", col_refs]])
builder.append (Option "min" fqn+".Min" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "max" fqn+".Max" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "date add" fqn+".Date_Add" [["input", col_refs], ["length", col_refs]])
builder.append (Option "date part" fqn+".Date_Part" [["input", col_refs]])
builder.append (Option "date diff" fqn+".Date_Diff" [["start", col_refs], ["end", col_refs]])
builder.append (Option "not" fqn+".Not" [["input", col_refs]])
builder.append (Option "and" fqn+".And" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "or" fqn+".Or" [["input", col_refs], ["rhs", col_refs]])
builder.append (Option "if" fqn+".If" [["input", col_refs], ["condition", filter_cond], ["true_value", col_refs], ["false_value", col_refs]])
builder.append (Option "trim" fqn+".Trim" [["input", col_refs], ["what", col_refs]])
Single_Choice builder.to_vector display=display

View File

@ -1463,7 +1463,8 @@ type Column
shift each date.
- period: The period by which to shift. For `Date` columns it should be a
`Date_Period` and for `Time` columns it should be a `Time_Period`. For
`Date_Time` columns it can be either.
`Date_Time` columns it can be either. It defaults to `Day` where
possible and `Hour` for `Time` columns.
? Time Zone handling
@ -1473,7 +1474,7 @@ type Column
unusual events like DST.
@period Date_Time_Helpers.make_period_selector_for_column
date_add : (Column | Integer) -> Date_Period | Time_Period -> Column ! Invalid_Value_Type | Illegal_Argument
date_add self amount (period : Date_Period | Time_Period) =
date_add self amount (period : Date_Period|Time_Period = default_date_period self) =
Value_Type.expect_type self .is_date_or_time "date/time" <|
my_type = self.inferred_precise_value_type
Value_Type.expect_integer amount <|
@ -2529,6 +2530,10 @@ cast_if_needed column value_type = if column.value_type == value_type then colum
naming_helper : Column_Naming_Helper
naming_helper = Column_Naming_Helper.in_memory
## PRIVATE
Resolves the default date period for `date_add` depending on the source column value type.
default_date_period column = if column.value_type.has_date then Date_Period.Day else Time_Period.Hour
## PRIVATE
Conversion method to a Column from a Vector.
Column.from (that:Vector) (name:Text="Vector") = Column.from_vector name that

View File

@ -0,0 +1,9 @@
from Standard.Base import all
## Reference to a column in a table.
type Column_Ref
## Reference to a column by name in a table.
Name name:Text
## Reference to a column by index in a table.
Index index:Integer

View File

@ -14,6 +14,7 @@ from Standard.Base.Metadata import make_single_choice
from Standard.Base.Widget_Helpers import make_delimiter_selector
import project.Data.Aggregate_Column.Aggregate_Column
import project.Data.Calculations.Column_Operation.Column_Operation
import project.Data.Column as Column_Module
import project.Data.Column.Column
import project.Data.Data_Formatter.Data_Formatter
@ -28,11 +29,12 @@ import project.Data.Report_Unmatched.Report_Unmatched
import project.Data.Row.Row
import project.Data.Set_Mode.Set_Mode
import project.Data.Sort_Column.Sort_Column
import project.Data.Table_Conversions
import project.Delimited.Delimited_Format.Delimited_Format
import project.Extensions.Table_Ref.Table_Ref
import project.Internal.Add_Row_Number
import project.Internal.Aggregate_Column_Helper
import project.Internal.Column_Naming_Helper.Column_Naming_Helper
import project.Internal.Constant_Column.Constant_Column
import project.Internal.Delimited_Reader
import project.Internal.Delimited_Writer
import project.Internal.Expand_Objects_Helpers
@ -49,6 +51,7 @@ import project.Internal.Widget_Helpers
from project.Data.Column import get_item_string, normalize_string_for_display
from project.Data.Type.Value_Type import Auto, Value_Type
from project.Errors import all
from project.Extensions.Table_Conversions import all
from project.Internal.Filter_Condition_Helpers import make_filter_column
from project.Internal.Lookup_Helpers import make_java_lookup_column_description
from project.Internal.Rows_View import Rows_View
@ -1235,12 +1238,15 @@ type Table
people.filter "age" (age -> (age%10 == 0))
@column Widget_Helpers.make_column_name_selector
@filter Widget_Helpers.make_filter_condition_selector
filter : (Column | Text | Integer) -> (Filter_Condition|(Any->Boolean)) -> Problem_Behavior -> Table ! No_Such_Column | Index_Out_Of_Bounds | Invalid_Value_Type
filter self column (filter : Filter_Condition | Function = Filter_Condition.Equal True) on_problems=Report_Warning = case column of
_ : Column ->
mask filter_column = Table.Value (self.java_table.mask filter_column.java_column)
case filter of
_ : Filter_Condition -> mask (make_filter_column column filter on_problems)
_ : Filter_Condition ->
resolved = (self:Table_Ref).resolve_condition filter
mask (make_filter_column column resolved on_problems)
_ : Function -> Filter_Condition_Module.handle_constructor_missing_arguments filter <|
mask (column.map filter)
_ ->
@ -1429,17 +1435,17 @@ type Table
double_inventory = table.at "total_stock" * 2
table.set double_inventory new_name="total_stock"
table.set "2 * [total_stock]" new_name="total_stock_expr"
@new_name Widget_Helpers.make_column_name_selector
set : Text | Column -> Text | Nothing -> Set_Mode -> Problem_Behavior -> Table ! Existing_Column | Missing_Column | No_Such_Column | Expression_Error
set self column:(Text | Column) new_name=Nothing set_mode=Set_Mode.Add_Or_Update on_problems=Report_Warning =
@column Column_Operation.default_widget
set : Text | Column -> Text -> Set_Mode -> Problem_Behavior -> Table ! Existing_Column | Missing_Column | No_Such_Column | Expression_Error
set self column:(Text | Column | Constant_Column | Column_Operation) new_name="" set_mode=Set_Mode.Add_Or_Update on_problems=Report_Warning =
resolved = case column of
_ : Text -> self.evaluate_expression column on_problems
_ : Column -> column
_ : Constant_Column -> self.make_constant_column column.value
_ : Column_Operation -> column.evaluate self (set_mode==Set_Mode.Update && new_name=="") on_problems
_ -> Error.throw (Illegal_Argument.Error "Unsupported type for `Table.set`.")
renamed = case new_name of
Nothing -> resolved
_ : Text -> resolved.rename new_name
renamed = if new_name == "" then resolved else resolved.rename new_name
renamed.if_not_error <| self.column_naming_helper.check_ambiguity self.column_names new_name <|
check_add_mode = case set_mode of
Set_Mode.Add_Or_Update -> True

View File

@ -4,6 +4,7 @@ import Standard.Base.Errors.Common.Index_Out_Of_Bounds
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Errors.Unimplemented.Unimplemented
from Standard.Base.Metadata import make_single_choice
import project.Data.Match_Columns.Match_Columns
import project.Data.Table.Table

View File

@ -0,0 +1,90 @@
from Standard.Base import all
import Standard.Base.Errors.Common.Index_Out_Of_Bounds
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import project.Data.Column_Ref.Column_Ref
import project.Data.Expression.Expression_Error
import project.Data.Set_Mode.Set_Mode
import project.Data.Table.Table
from project.Errors import No_Such_Column, Existing_Column, Missing_Column
## PRIVATE
A helper type allowing to resolve column references in a context of an underlying table.
type Table_Ref
## PRIVATE
Value underlying
## PRIVATE
Get a column.
Column must implement all the expected calculations.
This returns a Column, but the type is not known statically because it
may be an in-memory or Database column.
at : Text | Integer -> Any ! No_Such_Column | Index_Out_Of_Bounds
at self selector=0 = self.underlying.at selector
## PRIVATE
Resolve a Column_Ref to a Column, keeping any other values as-is.
resolve : Any -> Any ! No_Such_Column | Index_Out_Of_Bounds
resolve self value = case value of
Column_Ref.Name name -> self.at name
Column_Ref.Index index -> self.at index
_ -> value
## PRIVATE
Resolve a Column_Ref to a Column, converting any other values into
a constant column.
resolve_as_column : Any -> Any ! No_Such_Column | Index_Out_Of_Bounds
resolve_as_column self value = case value of
Column_Ref.Name name -> self.at name
Column_Ref.Index index -> self.at index
_ -> self.underlying.make_constant_column value
## PRIVATE
Transforms a condition, changing any Column_Ref instances into Column instances resolved in this table.
resolve_condition : Filter_Condition -> Filter_Condition
resolve_condition self condition = case condition of
Filter_Condition.Equal value -> Filter_Condition.Equal (self.resolve value)
Filter_Condition.Not_Equal value -> Filter_Condition.Not_Equal (self.resolve value)
Filter_Condition.Less value -> Filter_Condition.Less (self.resolve value)
Filter_Condition.Equal_Or_Less value -> Filter_Condition.Equal_Or_Less (self.resolve value)
Filter_Condition.Greater value -> Filter_Condition.Greater (self.resolve value)
Filter_Condition.Equal_Or_Greater value -> Filter_Condition.Equal_Or_Greater (self.resolve value)
Filter_Condition.Between lower upper -> Filter_Condition.Between (self.resolve lower) (self.resolve upper)
Filter_Condition.Starts_With prefix case_sensitivity -> Filter_Condition.Starts_With (self.resolve prefix) case_sensitivity
Filter_Condition.Ends_With prefix case_sensitivity -> Filter_Condition.Ends_With (self.resolve prefix) case_sensitivity
Filter_Condition.Contains prefix case_sensitivity -> Filter_Condition.Contains (self.resolve prefix) case_sensitivity
Filter_Condition.Not_Contains prefix case_sensitivity -> Filter_Condition.Not_Contains (self.resolve prefix) case_sensitivity
Filter_Condition.Like pattern -> Filter_Condition.Like (self.resolve pattern)
Filter_Condition.Not_Like pattern -> Filter_Condition.Not_Like (self.resolve pattern)
Filter_Condition.Is_In values -> Filter_Condition.Is_In (check_is_in_values "Is_In" values)
Filter_Condition.Not_In values -> Filter_Condition.Not_In (check_is_in_values "Not_In" values)
_ -> condition
## PRIVATE
Set a column.
set : Any -> Set_Mode -> Problem_Behavior -> Table_Ref ! Existing_Column | Missing_Column | No_Such_Column | Expression_Error
set self column new_name set_mode=Set_Mode.Add_Or_Update on_problems=Report_Warning =
new_underlying = self.underlying.set column new_name set_mode=set_mode on_problems=on_problems
Table_Ref.from new_underlying
## PRIVATE
Gets a list of column names
column_names : Vector Text
column_names self = self.underlying.column_names
## PRIVATE
Table_Ref.from (that:Table) = Table_Ref.Value that
## PRIVATE
check_is_in_values : Text -> Vector -> Vector ! Illegal_Argument
check_is_in_values operation_name values =
check_value v = case v of
_ : Column_Ref ->
message = "Column_Ref is not allowed in "+operation_name+" to avoid unexpected behavior. As opposed to other operations, which operate on a row-by-row basis when passed a column, "+operation_name+" looks at the whole contents of the passed collection - thus usually expecting a Vector. If you want to filter the elements checking if they are present _anywhere_ in a passed column, you can pass a column by first getting it from the table using the `at` operator."
Error.throw (Illegal_Argument.Error message)
_ -> v
case values of
_ : Vector -> values.map check_value
_ : Array -> values.map check_value
_ -> check_value values

View File

@ -0,0 +1,25 @@
from Standard.Base import all
## PRIVATE
type Constant_Column
Value value:Any
## PRIVATE
Conversion method to a Table from a Number.
Constant_Column.from (that:Number) = Constant_Column.Value that
## PRIVATE
Conversion method to a Table from a Date_Time.
Constant_Column.from (that:Date_Time) = Constant_Column.Value that
## PRIVATE
Conversion method to a Table from a Date.
Constant_Column.from (that:Date) = Constant_Column.Value that
## PRIVATE
Conversion method to a Table from a Time_Of_Day.
Constant_Column.from (that:Time_Of_Day) = Constant_Column.Value that
## PRIVATE
Conversion method to a Table from a Boolean.
Constant_Column.from (that:Boolean) = Constant_Column.Value that

View File

@ -8,10 +8,10 @@ from Standard.Base.System.File_Format import format_types
import project.Data.Aggregate_Column.Aggregate_Column
import project.Data.Join_Condition.Join_Condition
import project.Data.Table.Table
import project.Data.Table_Conversions
import project.Data.Type.Value_Type.Auto
import project.Data.Type.Value_Type.Value_Type
import project.Internal.Parse_Values_Helper
from project.Extensions.Table_Conversions import all
## PRIVATE
Make an aggregate column selector.
@ -80,6 +80,43 @@ make_column_name_vector_selector table display=Display.Always =
item_editor = make_column_name_selector table display=Display.Always
Vector_Editor item_editor=item_editor item_default=item_editor.values.first.value display=display
## PRIVATE
Make a column reference by name selector.
make_column_ref_by_name_selector : Table -> Display -> Widget
make_column_ref_by_name_selector table display=Display.Always =
col_names_options = table.column_names.map (name -> Option name "(Column_Ref.Name "+name.pretty+")")
Single_Choice values=col_names_options display=display
## PRIVATE
Make a filter condition selector.
make_filter_condition_selector : Table -> Display -> Widget
make_filter_condition_selector table display=Display.Always =
col_names = make_column_ref_by_name_selector table
builder = Vector.new_builder
fqn = Meta.get_qualified_type_name Filter_Condition
builder.append (Option "Equals" fqn+".Equal" [["to", col_names]])
builder.append (Option "Not Equals" fqn+".Not_Equal" [["to", col_names]])
builder.append (Option "Less Than" fqn+".Less" [["than", col_names]])
builder.append (Option "Less Than Or Equal" fqn+".Equal_Or_Less" [["than", col_names]])
builder.append (Option "Greater Than" fqn+".Greater" [["than", col_names]])
builder.append (Option "Greater Than Or Equal" fqn+".Greater_Or_Less" [["than", col_names]])
builder.append (Option "Between" fqn+".Between" [["lower", col_names], ["upper", col_names]])
builder.append (Option "Starts With" fqn+".Starts_With" [["prefix", col_names]])
builder.append (Option "Ends With" fqn+".Ends_With" [["suffix", col_names]])
builder.append (Option "Contains" fqn+".Contains" [["substring", col_names]])
builder.append (Option "Not Contains" fqn+".Not_Contains" [["substring", col_names]])
builder.append (Option "Is Nothing" fqn+".Is_Nothing")
builder.append (Option "Is Not Nothing" fqn+".Not_Nothing")
builder.append (Option "Is True" fqn+".Is_True")
builder.append (Option "Is False" fqn+".Is_False")
builder.append (Option "Is Empty" fqn+".Is_Empty")
builder.append (Option "Is Not Empty" fqn+".Not_Empty")
builder.append (Option "Like" fqn+".Like" [["pattern", col_names]])
builder.append (Option "Not Like" fqn+".Not_Like" [["pattern", col_names]])
builder.append (Option "Is In" fqn+".Is_In")
builder.append (Option "Not In" fqn+".Not_In")
Single_Choice builder.to_vector display=display
## PRIVATE
Make a join condition selector.
make_join_condition_selector : Table -> Display -> Widget

View File

@ -1,7 +1,9 @@
from Standard.Base import all
import project.Data.Aggregate_Column.Aggregate_Column
import project.Data.Calculations.Column_Operation.Column_Operation
import project.Data.Column.Column
import project.Data.Column_Ref.Column_Ref
import project.Data.Column_Vector_Extensions
import project.Data.Data_Formatter.Data_Formatter
import project.Data.Join_Condition.Join_Condition
@ -12,7 +14,6 @@ import project.Data.Report_Unmatched.Report_Unmatched
import project.Data.Set_Mode.Set_Mode
import project.Data.Sort_Column.Sort_Column
import project.Data.Table.Table
import project.Data.Table_Conversions
import project.Data.Type.Value_Type.Auto
import project.Data.Type.Value_Type.Value_Type
import project.Delimited.Delimited_Format.Delimited_Format
@ -24,9 +25,12 @@ import project.Excel.Excel_Workbook.Excel_Workbook
from project.Delimited.Delimited_Format.Delimited_Format import Delimited
from project.Excel.Excel_Format.Excel_Format import Excel
from project.Excel.Excel_Section.Excel_Section import Cell_Range, Range_Names, Sheet_Names, Worksheet
from project.Extensions.Table_Conversions import all
export project.Data.Aggregate_Column.Aggregate_Column
export project.Data.Calculations.Column_Operation.Column_Operation
export project.Data.Column.Column
export project.Data.Column_Ref.Column_Ref
export project.Data.Column_Vector_Extensions
export project.Data.Data_Formatter.Data_Formatter
export project.Data.Join_Condition.Join_Condition
@ -45,7 +49,7 @@ export project.Excel.Excel_Format.Excel_Format
export project.Excel.Excel_Range.Excel_Range
export project.Excel.Excel_Section.Excel_Section
export project.Excel.Excel_Workbook.Excel_Workbook
from project.Data.Table_Conversions export all
from project.Delimited.Delimited_Format.Delimited_Format export Delimited
from project.Excel.Excel_Format.Excel_Format export Excel
from project.Excel.Excel_Section.Excel_Section export Cell_Range, Range_Names, Sheet_Names, Worksheet
from project.Extensions.Table_Conversions export all

View File

@ -108,6 +108,29 @@ type Test
if err.is_a matcher then Nothing else
Test.fail ("Expected a " + matcher.to_text + ", but " + err.to_text + " was thrown instead.")
## Expect a function to fail with the provided panic.
An alternative API to `expect_panic_with` where the order of arguments is
more natural - as it allows blocks without reordering the arguments.
Arguments:
- matcher: The expected type of the panic thrown by `action`.
- action: The action to evaluate that is expected to fail with a panic.
> Example
Expect that a computation should panic as part of a test.
import Standard.Examples
from Standard.Test import Test
example_expect_panic_with =
Test.expect_panic_with Examples.My_Error <|
IO.println 'hello'
Examples.throw_panic
IO.println 'this is not reached'
expect_panic : Any -> Any -> Test_Result
expect_panic matcher ~action = Test.expect_panic_with action matcher
## Checks that the provided action returns without any errors or warnings.

View File

@ -107,7 +107,6 @@ spec setup =
t3.column_names . should_equal ["foo", "bar", "Baz", "foo 1", "foo 2", "ab.+123", "abcd123", "bar2", "bar3"]
Test.specify "should not allow illegal column names" <|
table.set (table.get "bar") new_name="" . should_fail_with Invalid_Column_Names
table.set (table.get "bar") new_name='a\0b' . should_fail_with Invalid_Column_Names
Test.specify "should allow replacing a column" <|

View File

@ -421,8 +421,11 @@ spec setup =
(t3.at "X").date_add (t3.at "Y") Time_Period.Day . to_vector . should_fail_with Illegal_Argument
(t3.at "X").date_add 1 Date_Period.Month . to_vector . should_fail_with Illegal_Argument
# There is no default period.
(t1.at "X").date_add (t1.at "Y") . should_be_a Function
# Date period defaults to Day for date/date-time
(t1.at "X").date_add (t1.at "Y") . to_vector . should_equal [Date.new 2021 02 05, Date.new 2020 12 31, Date.new 2021 12 31]
(t2.at "X").date_add (t2.at "Y") . to_vector . should_equal_tz_agnostic [Date_Time.new 2021 02 05 12 30 0, Date_Time.new 2020 12 31 12 30 0, Date_Time.new 2021 12 31 12 30 0]
# and defaults to Hour for time-of-day
(t3.at "X").date_add (t3.at "Y") . to_vector . should_equal [Time_Of_Day.new 17 30 0, Time_Of_Day.new 22 45 0, Time_Of_Day.new 1 30 0]
Test.specify "should check shift_amount type in date_add" <|
t = table_builder [["X", [Date.new 2021 01 31]]]

View File

@ -0,0 +1,199 @@
from Standard.Base import all
import Standard.Base.Errors.Common.Index_Out_Of_Bounds
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
from Standard.Table import all
from Standard.Table.Errors import all
from Standard.Test import Test, Problems
import Standard.Test.Extensions
from project.Common_Table_Operations.Util import all
main = run_default_backend spec
spec setup =
prefix = setup.prefix
table_builder = setup.table_builder
pending_datetime = if setup.test_selection.date_time.not then "Date/Time operations are not supported by this backend."
Test.group prefix+"Table.set with Column_Operation" <|
Test.specify "arithmetics" <|
t = table_builder [["A", [1, 2]], ["B", [10, 40]]]
t.set (Column_Operation.Add (Column_Ref.Name "A") (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [11, 42]
t.set (Column_Operation.Add 100 (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [110, 140]
t.set (Column_Operation.Add (Column_Ref.Name "A") 100) "C" . at "C" . to_vector . should_equal [101, 102]
t.set (Column_Operation.Add 23 100) "C" . at "C" . to_vector . should_equal [123, 123]
t.set (Column_Operation.Subtract (Column_Ref.Name "A") (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [-9, -38]
t.set (Column_Operation.Subtract 100 (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [90, 60]
t.set (Column_Operation.Subtract (Column_Ref.Name "A") 100) "C" . at "C" . to_vector . should_equal [-99, -98]
t.set (Column_Operation.Subtract 23 100) "C" . at "C" . to_vector . should_equal [-77, -77]
t.set (Column_Operation.Multiply (Column_Ref.Name "A") (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [10, 80]
t.set (Column_Operation.Multiply 100 (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [1000, 4000]
t.set (Column_Operation.Multiply (Column_Ref.Name "A") 100) "C" . at "C" . to_vector . should_equal [100, 200]
t.set (Column_Operation.Multiply 23 100) "C" . at "C" . to_vector . should_equal [2300, 2300]
t.set (Column_Operation.Divide (Column_Ref.Name "B") (Column_Ref.Name "A")) "C" . at "C" . to_vector . should_equal [10, 20]
t.set (Column_Operation.Divide (Column_Ref.Name "A") (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [0.1, 0.05]
t.set (Column_Operation.Divide 1 (Column_Ref.Name "A")) "C" . at "C" . to_vector . should_equal [1, 0.5]
t.set (Column_Operation.Divide 1 2) "C" . at "C" . to_vector . should_equal [0.5, 0.5]
t2 = table_builder [["A", [23, 42]], ["B", [10, 3]]]
t2.set (Column_Operation.Mod (Column_Ref.Name "A") (Column_Ref.Name "B")) "C" . at "C" . to_vector . should_equal [3, 0]
t2.set (Column_Operation.Mod (Column_Ref.Name "A") 10) "C" . at "C" . to_vector . should_equal [3, 2]
t2.set (Column_Operation.Mod 7 5) "C" . at "C" . to_vector . should_equal [2, 2]
t.set (Column_Operation.Power (Column_Ref.Name "B") (Column_Ref.Name "A")) "C" . at "C" . to_vector . should_equal [10, 1600]
t.set (Column_Operation.Power (Column_Ref.Name "A") 3) "C" . at "C" . to_vector . should_equal [1, 8]
t.set (Column_Operation.Power 2 (Column_Ref.Name "A")) "C" . at "C" . to_vector . should_equal [2, 4]
t.set (Column_Operation.Power 3 4) "C" . at "C" . to_vector . should_equal [81, 81]
Test.expect_panic Type_Error <| t.set (Column_Operation.Subtract "x" "y")
t.set (Column_Operation.Add 42 "y") . should_fail_with Illegal_Argument
Test.specify "rounding" <|
t = table_builder [["A", [1.13333, 122.74463, 32.52424, -12.7]]]
t.set (Column_Operation.Round (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal [1, 123, 33, -13]
t.set (Column_Operation.Round (Column_Ref.Name "A") precision=1) "Z" . at "Z" . to_vector . should_equal [1.1, 122.7, 32.5, -12.7]
t.set (Column_Operation.Round (Column_Ref.Name "A") precision=-1) "Z" . at "Z" . to_vector . should_equal [0, 120, 30, -10]
t.set (Column_Operation.Ceil (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal [2, 123, 33, -12]
t.set (Column_Operation.Floor (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal [1, 122, 32, -13]
t.set (Column_Operation.Truncate (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal [1, 122, 32, -12]
Test.expect_panic Type_Error <| t.set (Column_Operation.Round "1.23")
Test.expect_panic Type_Error <| t.set (Column_Operation.Truncate "1.23")
Test.specify "date/time" pending=pending_datetime <|
t = table_builder [["A", [Date_Time.new 2023 1 12 12 45, Date_Time.new 2020 5 12 1 45]], ["B", [Date_Time.new 2023 1 15 18 45, Date_Time.new 2020 6 12 22 20]], ["x", [1, 3]]]
# TODO ticket for truncate for DB
if setup.is_database.not then
t.set (Column_Operation.Truncate (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal [Date.new 2023 1 12, Date.new 2020 5 12]
t.set (Column_Operation.Truncate (Date_Time.new 1999 12 10 14 55 11)) "Z" . at "Z" . to_vector . should_equal [Date.new 1999 12 10, Date.new 1999 12 10]
t.set (Column_Operation.Date_Add (Column_Ref.Name "A") (Column_Ref.Name "x")) "Z" . at "Z" . to_vector . should_equal_tz_agnostic [Date_Time.new 2023 1 13 12 45, Date_Time.new 2020 5 15 1 45]
t.set (Column_Operation.Date_Add (Column_Ref.Name "A") 10 Date_Period.Year) "Z" . at "Z" . to_vector . should_equal_tz_agnostic [Date_Time.new 2033 1 12 12 45, Date_Time.new 2030 5 12 1 45]
t.set (Column_Operation.Date_Add (Date_Time.new 2001 12 15 11 00) 10 Time_Period.Minute) "Z" . at "Z" . to_vector . should_equal_tz_agnostic [Date_Time.new 2001 12 15 11 10, Date_Time.new 2001 12 15 11 10]
t.set (Column_Operation.Date_Diff (Column_Ref.Name "A") (Column_Ref.Name "B") Date_Period.Day) "Z" . at "Z" . to_vector . should_equal [3, 31]
t.set (Column_Operation.Date_Part (Column_Ref.Name "A") Date_Period.Year) "Z" . at "Z" . to_vector . should_equal [2023, 2020]
t.set (Column_Operation.Date_Part (Column_Ref.Name "A") Time_Period.Minute) "Z" . at "Z" . to_vector . should_equal [45, 45]
t2 = table_builder [["C", [Date.new 2002 12 10, Date.new 2005 01 01]], ["D", [Time_Of_Day.new 12 45, Time_Of_Day.new 01 01]]]
t2.set (Column_Operation.Date_Add (Column_Ref.Name "C") 5 Date_Period.Month) "Z" . at "Z" . to_vector . should_equal [Date.new 2003 5 10, Date.new 2005 6 01]
t2.set (Column_Operation.Date_Add (Column_Ref.Name "D") 15 Time_Period.Hour) "Z" . at "Z" . to_vector . should_equal [Time_Of_Day.new 03 45, Time_Of_Day.new 16 01]
t2.set (Column_Operation.Date_Diff (Column_Ref.Name "C") (Date.new 2003) Date_Period.Year) "Z" . at "Z" . to_vector . should_equal [0, -2]
t2.set (Column_Operation.Date_Diff (Column_Ref.Name "D") (Time_Of_Day.new 13) Time_Period.Minute) "Z" . at "Z" . to_vector . should_equal [15, 59+(60*11)]
t2.set (Column_Operation.Date_Part (Column_Ref.Name "C") Date_Period.Year) "Z" . at "Z" . to_vector . should_equal [2002, 2005]
t2.set (Column_Operation.Date_Part (Column_Ref.Name "D") Time_Period.Minute) "Z" . at "Z" . to_vector . should_equal [45, 1]
# error handling
t2.set (Column_Operation.Date_Part (Column_Ref.Name "C") Time_Period.Second) . should_fail_with Illegal_Argument
t2.set (Column_Operation.Date_Add (Column_Ref.Name "D") 5 Date_Period.Year) . should_fail_with Illegal_Argument
t.set (Column_Operation.Date_Part (Column_Ref.Name "x") Date_Period.Year) . should_fail_with Invalid_Value_Type
Test.expect_panic Type_Error <| t2.set (Column_Operation.Date_Diff 42 "x" Date_Period.Year)
Test.specify "boolean" <|
t = table_builder [["A", [True, False]], ["T", [True, True]]]
t.set (Column_Operation.And (Column_Ref.Name "A") (Column_Ref.Name "T")) "Z" . at "Z" . to_vector . should_equal [True, False]
t.set (Column_Operation.And (Column_Ref.Name "A") False) "Z" . at "Z" . to_vector . should_equal [False, False]
t.set (Column_Operation.And True True) "Z" . at "Z" . to_vector . should_equal [True, True]
t.set (Column_Operation.Or (Column_Ref.Name "A") (Column_Ref.Name "T")) "Z" . at "Z" . to_vector . should_equal [True, True]
t.set (Column_Operation.Or False (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal [True, False]
t.set (Column_Operation.Or False False) "Z" . at "Z" . to_vector . should_equal [False, False]
t.set (Column_Operation.Not (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal [False, True]
t.set (Column_Operation.Not False) "Z" . at "Z" . to_vector . should_equal [True, True]
Test.expect_panic_with (t.set (Column_Operation.And 42 True)) Type_Error
Test.expect_panic_with (t.set (Column_Operation.Or (Column_Ref.Name "A") "x")) Type_Error
Test.specify "if" <|
t = table_builder [["A", [1, 100]], ["B", [10, 40]], ["C", [23, 55]]]
t.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Greater than=(Column_Ref.Name "B"))) "Z" . at "Z" . to_vector . should_equal [False, True]
t.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Greater than=20) "T" "F") "Z" . at "Z" . to_vector . should_equal ["F", "T"]
t.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Less than=20) (Column_Ref.Name "B") (Column_Ref.Name "C")) "Z" . at "Z" . to_vector . should_equal [10, 55]
t.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Greater than="X") "T" "F") . should_fail_with Invalid_Value_Type
t2 = table_builder [["A", ["a", "c"]], ["B", ["c", "b"]], ["C", [23, 55]]]
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Greater than=(Column_Ref.Name "B"))) "Z" . at "Z" . to_vector . should_equal [False, True]
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Greater than=(Column_Ref.Name "B")) (Column_Ref.Name "C") 0) "Z" . at "Z" . to_vector . should_equal [0, 55]
t2.set (Column_Operation.If "A" (Filter_Condition.Greater than="B") (Column_Ref.Name "C") 0) "Z" . at "Z" . to_vector . should_equal [0, 0]
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Is_In ["x", "a", "dd"]) "TT" "FF") "Z" . at "Z" . to_vector . should_equal ["TT", "FF"]
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Not_In ["x", "a", "dd"]) "TT" "FF") "Z" . at "Z" . to_vector . should_equal ["FF", "TT"]
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Is_In []) "TT" "FF") "Z" . at "Z" . to_vector . should_equal ["FF", "FF"]
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Not_In []) "TT" "FF") "Z" . at "Z" . to_vector . should_equal ["TT", "TT"]
# Passing a column does not work row-by-row, but looks at whole column contents.
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Is_In (t2.at "B")) "TT" "FF") "Z" . at "Z" . to_vector . should_equal ["FF", "TT"]
t3 = table_builder [["x", ["e", "e", "a"]]]
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Is_In (t3.at "x")) "TT" "FF") "Z" . at "Z" . to_vector . should_equal ["TT", "FF"]
# Thus, passing a Column_Ref into Is_In/Not_In is not allowed as it would be confusing.
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Is_In (Column_Ref.Name "B")) "TT" "FF") . should_fail_with Illegal_Argument
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Not_In (Column_Ref.Name "B")) "TT" "FF") . should_fail_with Illegal_Argument
t2.set (Column_Operation.If (Column_Ref.Name "A") (Filter_Condition.Not_In [Column_Ref.Name "B", "X"]) "TT" "FF") . should_fail_with Illegal_Argument
Test.specify "text" <|
t = table_builder [["A", [" a ", "b"]], ["B", ["c", " d "]]]
t.set (Column_Operation.Trim (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal ["a", "b"]
t.set (Column_Operation.Trim (Column_Ref.Name "A") Location.End) "Z" . at "Z" . to_vector . should_equal [" a", "b"]
t.set (Column_Operation.Trim (Column_Ref.Name "A") Location.Start) "Z" . at "Z" . to_vector . should_equal ["a ", "b"]
t.set (Column_Operation.Trim (Column_Ref.Name "A") Location.Both "abc") "Z" . at "Z" . to_vector . should_equal [" a ", ""]
t.set (Column_Operation.Trim "bb aaaa" Location.Both (Column_Ref.Name "A")) "Z" . at "Z" . to_vector . should_equal ["bb", " aaaa"]
t.set (Column_Operation.Add (Column_Ref.Name "A") (Column_Ref.Name "B")) "Z" . at "Z" . to_vector . should_equal [" a c", "b d "]
t.set (Column_Operation.Add "prefix_" (Column_Ref.Name "B")) "Z" . at "Z" . to_vector . should_equal ["prefix_c", "prefix_ d "]
t.set (Column_Operation.Add (Column_Ref.Name "A") "!") "Z" . at "Z" . to_vector . should_equal [" a !", "b!"]
t.set (Column_Operation.Add "O" "!") "Z" . at "Z" . to_vector . should_equal ["O!", "O!"]
t2 = table_builder [["A", [42]]]
t2.set (Column_Operation.Trim (Column_Ref.Name "A")) . should_fail_with Invalid_Value_Type
Test.specify "min/max" <|
t = table_builder [["A", [1, 20]], ["B", [10, 2]]]
t.set (Column_Operation.Min (Column_Ref.Name "A") (Column_Ref.Name "B")) "Z" . at "Z" . to_vector . should_equal [1, 2]
t.set (Column_Operation.Min (Column_Ref.Name "A") 5) "Z" . at "Z" . to_vector . should_equal [1, 5]
t.set (Column_Operation.Max (Column_Ref.Name "A") (Column_Ref.Name "B")) "Z" . at "Z" . to_vector . should_equal [10, 20]
t.set (Column_Operation.Max (Column_Ref.Name "A") 5) "Z" . at "Z" . to_vector . should_equal [5, 20]
t.set (Column_Operation.Max 2 5) "Z" . at "Z" . to_vector . should_equal [5, 5]
t.set (Column_Operation.Min 2 5) "Z" . at "Z" . to_vector . should_equal [2, 2]
t2 = table_builder [["A", ["aardvark", "zebra"]], ["B", ["cat", "dog"]], ["x", [1, 20]]]
t2.set (Column_Operation.Min (Column_Ref.Name "A") (Column_Ref.Name "B")) "Z" . at "Z" . to_vector . should_equal ["aardvark", "dog"]
t2.set (Column_Operation.Max (Column_Ref.Name "A") (Column_Ref.Name "B")) "Z" . at "Z" . to_vector . should_equal ["cat", "zebra"]
t2.set (Column_Operation.Min (Column_Ref.Name "A") "animal") "Z" . at "Z" . to_vector . should_equal ["aardvark", "animal"]
t2.set (Column_Operation.Max "coyote" (Column_Ref.Name "B")) "Z" . at "Z" . to_vector . should_equal ["coyote", "dog"]
# mixed types result in an error
t2.set (Column_Operation.Min (Column_Ref.Name "A") 42) . should_fail_with Invalid_Value_Type
t2.set (Column_Operation.Min (Column_Ref.Name "x") "x") . should_fail_with Invalid_Value_Type
t2.set (Column_Operation.Min (Column_Ref.Name "x") (Column_Ref.Name "A")) . should_fail_with Invalid_Value_Type
if pending_datetime.is_nothing then
t3 = table_builder [["A", [Date.new 2002 12 10, Date.new 2005 01 01]]]
t3.set (Column_Operation.Min (Column_Ref.Name "A") (Date.new 2003)) "Z" . at "Z" . to_vector . should_equal [Date.new 2002 12 10, Date.new 2003 01 01]
t3.set (Column_Operation.Max (Column_Ref.Name "A") (Date.new 2003)) "Z" . at "Z" . to_vector . should_equal [Date.new 2003 01 01, Date.new 2005 01 01]
Test.specify "allows also indexing columns numerically" <|
t = table_builder [["X", [1, 2]], ["Y", [3, 4]]]
t.set (Column_Operation.Add (Column_Ref.Index 0) (Column_Ref.Index 1)) "Z" . at "Z" . to_vector . should_equal [4, 6]
Test.specify "will forward column resolution errors" <|
t = table_builder [["X", [1, 2]], ["Y", [3, 4]]]
t.set (Column_Operation.Add (Column_Ref.Name "X") (Column_Ref.Name "Z")) . should_fail_with No_Such_Column
t.set (Column_Operation.Not (Column_Ref.Name "zzz")) . should_fail_with No_Such_Column
t.set (Column_Operation.Not (Column_Ref.Index 42)) . should_fail_with Index_Out_Of_Bounds

View File

@ -5,9 +5,9 @@ import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Base.Errors.Illegal_State.Illegal_State
import Standard.Table.Data.Type.Value_Type.Value_Type
import Standard.Table.Data.Expression.Expression_Error
from Standard.Table import all hiding Table
from Standard.Table.Errors import all
import Standard.Table.Data.Expression.Expression_Error
from Standard.Database.Errors import SQL_Error
@ -58,12 +58,20 @@ spec setup =
t.filter "X" (Filter_Condition.Greater than=(t.at "Y")) . at "X" . to_vector . should_equal [12]
t.filter "Y" (Filter_Condition.Between (t.at "ix") 100) . at "Y" . to_vector . should_equal [100, 4, 11]
t.filter "X" (Filter_Condition.Equal to=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal [100]
t.filter "X" (Filter_Condition.Less than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal [3]
t.filter "X" (Filter_Condition.Equal_Or_Less than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal [100, 3]
t.filter "X" (Filter_Condition.Equal_Or_Greater than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal [100, 12]
t.filter "X" (Filter_Condition.Greater than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal [12]
t.filter "Y" (Filter_Condition.Between (Column_Ref.Name "ix") 100) . at "Y" . to_vector . should_equal [100, 4, 11]
Test.specify "Not_Equal test cases" pending="Specification needs clarifying, see: https://github.com/enso-org/enso/issues/5241#issuecomment-1480167927" <|
t = table_builder [["ix", [1, 2, 3, 4, 5]], ["X", [100, 3, Nothing, 4, 12]], ["Y", [100, 4, 2, Nothing, 11]]]
t3 = t.filter "X" (Filter_Condition.Not_Equal to=100)
t3 . at "X" . to_vector . should_equal [3, Nothing, 4, 12]
t3 . at "ix" . to_vector . should_equal [2, 3, 4, 5]
t.filter "X" (Filter_Condition.Not_Equal to=(t.at "Y")) . at "X" . to_vector . should_equal [3, Nothing, 4, 12]
t.filter "X" (Filter_Condition.Not_Equal to=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal [3, Nothing, 4, 12]
Test.specify "by text comparisons" <|
t = table_builder [["ix", [1, 2, 3, 4, 5]], ["X", ["abb", "baca", "b", Nothing, "c"]], ["Y", ["a", "b", "b", "c", "c"]]]
@ -87,6 +95,13 @@ spec setup =
t.filter "X" (Filter_Condition.Equal to=(t.at "Y")) . at "X" . to_vector . should_equal ["b", "c"]
t.filter "X" (Filter_Condition.Between (t.at "Y") "bzzzz") . at "X" . to_vector . should_equal ["abb", "baca", "b"]
t.filter "X" (Filter_Condition.Greater than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal ["abb", "baca"]
t.filter "X" (Filter_Condition.Equal_Or_Greater than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal ["abb", "baca", "b", "c"]
t.filter "X" (Filter_Condition.Equal_Or_Less than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal ["b", "c"]
t.filter "X" (Filter_Condition.Less than=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal []
t.filter "X" (Filter_Condition.Equal to=(Column_Ref.Name "Y")) . at "X" . to_vector . should_equal ["b", "c"]
t.filter "X" (Filter_Condition.Between (Column_Ref.Name "Y") "bzzzz") . at "X" . to_vector . should_equal ["abb", "baca", "b"]
Test.specify "by text search (contains, starts_with, ends_with, not_contains)" <|
t = table_builder [["ix", [1, 2, 3, 4, 5]], ["X", ["abb", "bacb", "banana", Nothing, "nana"]], ["Y", ["a", "B", "d", "c", "a"]], ["Z", ["aaaaa", "bbbbb", "[ab]", "[ab]aaaa", "[ab]ccc"]]]
@ -116,6 +131,11 @@ spec setup =
t.filter "X" (Filter_Condition.Not_Contains (t.at "Y") Case_Sensitivity.Sensitive) . at "X" . to_vector . should_equal ["bacb", "banana"]
t.filter "X" (Filter_Condition.Not_Contains (t.at "Y") Case_Sensitivity.Insensitive) . at "X" . to_vector . should_equal ["banana"]
t.filter "X" (Filter_Condition.Starts_With (Column_Ref.Name "Y")) . at "X" . to_vector . should_equal ["abb"]
t.filter "X" (Filter_Condition.Ends_With (Column_Ref.Name "Y") Case_Sensitivity.Insensitive) . at "X" . to_vector . should_equal ["bacb", "nana"]
t.filter "X" (Filter_Condition.Contains (Column_Ref.Name "Y") Case_Sensitivity.Insensitive) . at "X" . to_vector . should_equal ["abb", "bacb", "nana"]
t.filter "X" (Filter_Condition.Not_Contains (Column_Ref.Name "Y") Case_Sensitivity.Insensitive) . at "X" . to_vector . should_equal ["banana"]
Test.specify "by text search (like, not_like)" <|
t = table_builder [["ix", [1, 2, 3, 4, 5]], ["X", ["abb", "bacb", "banana", Nothing, "nana"]], ["Y", ["a", "B", "d", "c", "a"]], ["Z", ["aaaaa", "bbbbb", "[ab]", "[ab]aaaa", "[ab]ccc"]]]
@ -175,6 +195,13 @@ spec setup =
check_problem (t.filter "X" (Filter_Condition.Not_Like (t.at "ix")))
check_problem (t.filter "X" (Filter_Condition.Not_Contains (t.at "ix")))
check_problem (t.filter "X" (Filter_Condition.Starts_With (Column_Ref.Name "ix")))
check_problem (t.filter "X" (Filter_Condition.Ends_With (Column_Ref.Name "ix")))
check_problem (t.filter "X" (Filter_Condition.Contains (Column_Ref.Name "ix")))
check_problem (t.filter "X" (Filter_Condition.Like (Column_Ref.Name "ix")))
check_problem (t.filter "X" (Filter_Condition.Not_Like (Column_Ref.Name "ix")))
check_problem (t.filter "X" (Filter_Condition.Not_Contains (Column_Ref.Name "ix")))
check_problem (t.filter "ix" (Filter_Condition.Starts_With "A"))
check_problem (t.filter "ix" (Filter_Condition.Ends_With "A"))
check_problem (t.filter "ix" (Filter_Condition.Contains "A"))
@ -247,6 +274,18 @@ spec setup =
t2.filter "B" (Filter_Condition.Is_In [False]) . at "B" . to_vector . should_equal [False, False, False]
t2.filter "C" (Filter_Condition.Is_In [False, False]) . at "C" . to_vector . should_equal [False]
Test.specify "does not allow Column_Ref in Is_In/Not_In because that would be confusing" <|
## Is In and Not In check if a value is contained anywhere in a provided collection (e.g. column),
NOT on a row-by-row basis like all other operations. Column_Ref is used with row-by-row ops,
so this would only cause confusion. Very rarely someone wants to filter a column by Is_In
within the same table - and that's the only approach Column_Ref would support.
t = table_builder [["A", [1, 2, 3]], ["B", [2, 3, 4]]]
t.filter "A" (Filter_Condition.Is_In (Column_Ref.Name "B")) . should_fail_with Illegal_Argument
# If the user really wants this, they pass it as a raw column:
t.filter "A" (Filter_Condition.Is_In (t.at "B")) . at "A" . to_vector . should_equal [2, 3]
Test.specify "by a boolean mask" <|
t = table_builder [["ix", [1, 2, 3, 4, 5]], ["b", [True, False, Nothing, True, True]]]
t.filter "b" . at "ix" . to_vector . should_equal [1, 4, 5]

View File

@ -10,6 +10,7 @@ import project.Common_Table_Operations.Core_Spec
import project.Common_Table_Operations.Cross_Tab_Spec
import project.Common_Table_Operations.Conversion_Spec
import project.Common_Table_Operations.Date_Time_Spec
import project.Common_Table_Operations.Derived_Columns_Spec
import project.Common_Table_Operations.Distinct_Spec
import project.Common_Table_Operations.Expression_Spec
import project.Common_Table_Operations.Filter_Spec
@ -117,6 +118,7 @@ spec setup =
Select_Columns_Spec.spec setup
Column_Name_Edge_Cases_Spec.spec setup
Column_Operations_Spec.spec setup
Derived_Columns_Spec.spec setup
Date_Time_Spec.spec setup
Conversion_Spec.spec setup
Aggregate_Spec.spec setup

View File

@ -3,8 +3,8 @@ import Standard.Base.Errors.Encoding_Error.Encoding_Error
import Standard.Base.Errors.File_Error.File_Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Table.Data.Table_Conversions
from Standard.Table import Table, Column, Data_Formatter, Quote_Style, Delimited
from Standard.Table.Extensions.Table_Conversions import all
from Standard.Table.Errors import all
from Standard.Test import Test, Test_Suite, Problems

View File

@ -1,6 +1,6 @@
from Standard.Base import all
from Standard.Table import Table
import Standard.Table.Data.Table_Conversions
from Standard.Table.Extensions.Table_Conversions import all
from Standard.Test import Test, Test_Suite
import Standard.Test.Extensions

View File

@ -3,12 +3,12 @@ from Standard.Base import all
import Standard.Base.Data.Text.Regex.Regex_Syntax_Error
import Standard.Base.Errors.Common.Type_Error
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Table.Data.Table_Conversions
import Standard.Test.Extensions
from Standard.Table import Table
from Standard.Table.Data.Type.Value_Type import Bits, Value_Type
from Standard.Table.Errors import Invalid_Value_Type, Column_Count_Exceeded, Duplicate_Output_Column_Names, Missing_Input_Columns
from Standard.Table.Extensions.Table_Conversions import all
from Standard.Test import Test, Test_Suite, Problems
from project.Util import all

View File

@ -2,7 +2,7 @@ from Standard.Base import all
import Standard.Base.Errors.Illegal_Argument.Illegal_Argument
import Standard.Table.Data.Table_Conversions
from Standard.Table.Extensions.Table_Conversions import all
from Standard.Table import Table, Column
from Standard.Test import Test, Test_Suite, Problems

View File

@ -1,8 +1,8 @@
from Standard.Base import all
from Standard.Table import Table, Column, Delimited, Data_Formatter
import Standard.Table.Data.Table_Conversions
import Standard.Table.Data.Type.Value_Type.Value_Type
from Standard.Table.Extensions.Table_Conversions import all
from Standard.Test import Test, Test_Suite
import Standard.Test.Extensions

View File

@ -1,8 +1,8 @@
from Standard.Base import all
from Standard.Table import Table, Delimited, Column, Data_Formatter
import Standard.Table.Data.Table_Conversions
import Standard.Table.Data.Type.Value_Type.Value_Type
from Standard.Table.Extensions.Table_Conversions import all
from Standard.Test import Test, Test_Suite
import Standard.Test.Extensions

View File

@ -1,8 +1,8 @@
from Standard.Base import all
from Standard.Table import Table, Delimited, Column, Data_Formatter
import Standard.Table.Data.Table_Conversions
import Standard.Table.Data.Type.Value_Type.Value_Type
from Standard.Table.Extensions.Table_Conversions import all
from Standard.Test import Test, Test_Suite
import Standard.Test.Extensions