diff --git a/CHANGELOG.md b/CHANGELOG.md index ec035f6799..a176b759d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - [Implemented `.cast` to and from `Decimal` columns for the in-memory database.][10206] - [Implemented fallback to Windows-1252 encoding for `Encoding.Default`.][10190] +- [Added Table.duplicates component][10323] [debug-shortcuts]: @@ -50,6 +51,7 @@ [10130]: https://github.com/enso-org/enso/pull/10130 [10206]: https://github.com/enso-org/enso/pull/10206 [10190]: https://github.com/enso-org/enso/pull/10190 +[10323]: https://github.com/enso-org/enso/pull/10323
![Release Notes](/docs/assets/tags/release_notes.svg) diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso index 1906ac5f85..314f24e602 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso @@ -357,9 +357,9 @@ type Array first duplicate appeared in the input. > Example - Removing repeating entries. + Removing unique entries. - [1, 3, 1, 2, 2, 1].to_array . duplicates == [1, 2].to_array + [1, 3, 1, 2, 2, 1].to_array . duplicates == [1, 1, 2, 2, 1].to_array duplicates : (Any -> Any) -> Vector Any duplicates self (on = x->x) = Array_Like_Helpers.duplicates self on diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso index 77203cd05b..9cd47f4754 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso @@ -1227,9 +1227,9 @@ type Vector a first duplicate appeared in the input. > Example - Removing repeating entries. + Removing unique entries. - [1, 3, 1, 2, 2, 1] . duplicates == [1, 2] + [1, 3, 1, 2, 2, 1] . duplicates == [1, 1, 2, 2, 1] duplicates : (Any -> Any) -> Vector Any duplicates self (on = x->x) = Array_Like_Helpers.duplicates self on diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso index 599a5e4a66..8dd9975d08 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Internal/Array_Like_Helpers.enso @@ -165,11 +165,14 @@ distinct vector on = existing.insert key True duplicates vector on = Vector.build builder-> - vector.fold Map.empty current-> item-> + counts = vector.fold Map.empty current-> item-> key = on item count = current.get key 0 - if count == 1 then builder.append item current.insert key count+1 + vector.map item-> + key = on item + count = counts.get key 0 + if count != 1 then builder.append item take vector range = case range of ## We are using a specialized implementation for `take Sample`, because diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/DB_Table.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/DB_Table.enso index 0bd6bdfb8a..21ee6fd985 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/DB_Table.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/DB_Table.enso @@ -1323,9 +1323,7 @@ type DB_Table raised as an error regardless of the problem behavior, because it is not possible to create a table without any columns. - If a column in `columns` is not in the input table, a - `Missing_Input_Columns` is raised as an error, unless - `error_on_missing_columns` is set to `False`, in which case the - problem is reported according to the `on_problems` setting. + `Missing_Input_Columns` is raised as an error. - If no valid columns are selected, a `No_Input_Columns_Selected`, is reported as a dataflow error regardless of setting. - If floating points values are present in the distinct columns, a @@ -1333,13 +1331,46 @@ type DB_Table setting. @columns Widget_Helpers.make_column_name_multi_selector distinct : Vector (Integer | Text | Regex) | Text | Integer | Regex -> Case_Sensitivity -> Boolean -> Problem_Behavior -> DB_Table ! No_Output_Columns | Missing_Input_Columns | No_Input_Columns_Selected | Floating_Point_Equality - distinct self columns=self.column_names case_sensitivity:Case_Sensitivity=..Default error_on_missing_columns:Boolean=True on_problems:Problem_Behavior=Report_Warning = - key_columns = self.columns_helper.select_columns columns Case_Sensitivity.Default reorder=True error_on_missing_columns=error_on_missing_columns on_problems=on_problems . catch No_Output_Columns _-> + distinct self columns=self.column_names case_sensitivity:Case_Sensitivity=..Default on_problems:Problem_Behavior=Report_Warning = + key_columns = self.columns_helper.select_columns columns Case_Sensitivity.Default reorder=True error_on_missing_columns=True on_problems=on_problems . catch No_Output_Columns _-> Error.throw No_Input_Columns_Selected problem_builder = Problem_Builder.new new_table = self.connection.dialect.prepare_distinct self key_columns case_sensitivity problem_builder problem_builder.attach_problems_before on_problems new_table + ## GROUP Standard.Base.Selections + ICON preparation + Returns the set of rows which are duplicated within the specified columns from the + input table. + + When multiple rows have the same values within the specified columns all of those rows are + returned. Rows which are unique within the specified columns are removed. + + Arguments: + - columns: The columns of the table to use for distinguishing the rows. + - case_sensitivity: Specifies if the text values should be compared case + sensitively. + - on_problems: Specifies how to handle if a problem occurs, raising as a + warning by default. + + ! Error Conditions + + - If there are no columns in the output table, a `No_Output_Columns` is + raised as an error regardless of the problem behavior, because it is + not possible to create a table without any columns. + - If a column in `columns` is not in the input table, a + `Missing_Input_Columns` is raised as an error. + - If no valid columns are selected, a `No_Input_Columns_Selected`, is + reported as a dataflow error regardless of setting. + - If floating points values are present in the distinct columns, a + `Floating_Point_Equality` is reported according to the `on_problems` + setting. + @columns Widget_Helpers.make_column_name_multi_selector + duplicates : Vector (Integer | Text | Regex) | Text | Integer | Regex -> Case_Sensitivity -> Boolean -> Problem_Behavior -> DB_Table ! No_Output_Columns | Missing_Input_Columns | No_Input_Columns_Selected | Floating_Point_Equality + duplicates self columns=self.column_names case_sensitivity:Case_Sensitivity=..Default on_problems:Problem_Behavior=..Report_Warning = + _ = [columns, case_sensitivity, on_problems] + Error.throw (Unsupported_Database_Operation.Error "DB_Table.duplicates is not implemented yet for the Database backends.") + ## GROUP Standard.Base.Calculations ICON join Joins two tables according to the specified join conditions. diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Table_Helpers.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Table_Helpers.enso index 437e8a6ca2..9d78bbceef 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Table_Helpers.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Table_Helpers.enso @@ -336,7 +336,7 @@ rename_columns (naming_helper : Column_Naming_Helper) (internal_columns:Vector) ## Attempt to treat as Map map = Map.from_vector mapping error_on_duplicates=False if map.length == mapping.length then rename_columns naming_helper internal_columns map case_sensitivity error_on_missing_columns on_problems else - duplicates = mapping.duplicates on=_.first . map p->p.first.to_text + duplicates = mapping.duplicates on=_.first . map p->p.first.to_text . distinct duplicate_text = if duplicates.length < 5 then duplicates.to_vector . join ", " else duplicates.take 3 . to_vector . join ", " + (", ... " + (duplicates.length - 3).to_text + " others") Error.throw (Illegal_Argument.Error "duplicate old name mappings ("+duplicate_text+").") diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Table.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Table.enso index 916f066b09..292b337501 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Table.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Table.enso @@ -939,9 +939,7 @@ type Table raised as an error regardless of the problem behavior, because it is not possible to create a table without any columns. - If a column in `columns` is not in the input table, a - `Missing_Input_Columns` is raised as an error, unless - `error_on_missing_columns` is set to `False`, in which case the - problem is reported according to the `on_problems` setting. + `Missing_Input_Columns` is raised as an error. - If no valid columns are selected, a `No_Input_Columns_Selected`, is reported as a dataflow error regardless of setting. - If floating points values are present in the distinct columns, a @@ -949,8 +947,8 @@ type Table setting. @columns Widget_Helpers.make_column_name_multi_selector distinct : Vector (Integer | Text | Regex) | Text | Integer | Regex -> Case_Sensitivity -> Boolean -> Problem_Behavior -> Table ! No_Output_Columns | Missing_Input_Columns | No_Input_Columns_Selected | Floating_Point_Equality - distinct self (columns = self.column_names) case_sensitivity:Case_Sensitivity=Case_Sensitivity.Default error_on_missing_columns:Boolean=True on_problems:Problem_Behavior=..Report_Warning = - key_columns = self.columns_helper.select_columns columns Case_Sensitivity.Default reorder=True error_on_missing_columns=error_on_missing_columns on_problems=on_problems . catch No_Output_Columns _-> + distinct self (columns = self.column_names) case_sensitivity:Case_Sensitivity=Case_Sensitivity.Default on_problems:Problem_Behavior=..Report_Warning = + key_columns = self.columns_helper.select_columns columns Case_Sensitivity.Default reorder=True error_on_missing_columns=True on_problems=on_problems . catch No_Output_Columns _-> Error.throw No_Input_Columns_Selected java_columns = key_columns.map c->c.java_column text_folding_strategy = Case_Sensitivity.folding_strategy case_sensitivity @@ -959,6 +957,45 @@ type Table self.java_table.distinct java_columns text_folding_strategy java_aggregator Table.Value java_table + ## GROUP Standard.Base.Selections + ICON preparation + Returns the set of rows which are duplicated within the specified columns from the + input table. + + When multiple rows have the same values within the specified columns all of those rows are + returned. Rows which are unique within the specified columns are removed. + + Arguments: + - columns: The columns of the table to use for distinguishing the rows. + - case_sensitivity: Specifies if the text values should be compared case + sensitively. + - on_problems: Specifies how to handle if a problem occurs, raising as a + warning by default. + + ! Error Conditions + + - If there are no columns in the output table, a `No_Output_Columns` is + raised as an error regardless of the problem behavior, because it is + not possible to create a table without any columns. + - If a column in `columns` is not in the input table, a + `Missing_Input_Columns` is raised as an error. + - If no valid columns are selected, a `No_Input_Columns_Selected`, is + reported as a dataflow error regardless of setting. + - If floating points values are present in the distinct columns, a + `Floating_Point_Equality` is reported according to the `on_problems` + setting. + @columns Widget_Helpers.make_column_name_multi_selector + duplicates : Vector (Integer | Text | Regex) | Text | Integer | Regex -> Case_Sensitivity -> Boolean -> Problem_Behavior -> Table ! No_Output_Columns | Missing_Input_Columns | No_Input_Columns_Selected | Floating_Point_Equality + duplicates self (columns = self.column_names) case_sensitivity:Case_Sensitivity=..Default on_problems:Problem_Behavior=..Report_Warning = + key_columns = self.columns_helper.select_columns columns Case_Sensitivity.Default reorder=True error_on_missing_columns=True on_problems=on_problems . catch No_Output_Columns _-> + Error.throw No_Input_Columns_Selected + java_columns = key_columns.map c->c.java_column + text_folding_strategy = Case_Sensitivity.folding_strategy case_sensitivity + java_table = Illegal_Argument.handle_java_exception <| + Java_Problems.with_problem_aggregator on_problems java_aggregator-> + self.java_table.duplicates java_columns text_folding_strategy java_aggregator + Table.Value java_table + ## GROUP Standard.Base.Conversions ICON convert Parses columns within a `Table` to a specific value type. diff --git a/std-bits/table/src/main/java/org/enso/table/data/table/Table.java b/std-bits/table/src/main/java/org/enso/table/data/table/Table.java index 04d8ff3fef..695b1710c1 100644 --- a/std-bits/table/src/main/java/org/enso/table/data/table/Table.java +++ b/std-bits/table/src/main/java/org/enso/table/data/table/Table.java @@ -239,6 +239,30 @@ public class Table { return new Table(newColumns); } + /** + * Creates a new table keeping only rows with distinct key columns. + * + * @param keyColumns set of columns to use as an index + * @param textFoldingStrategy a strategy for folding text columns + * @param problemAggregator an aggregator for problems + * @return a table where duplicate rows with the same key are removed + */ + public Table duplicates( + Column[] keyColumns, + TextFoldingStrategy textFoldingStrategy, + ProblemAggregator problemAggregator) { + var rowsToKeep = + Distinct.buildDuplicatesRowsMask( + rowCount(), keyColumns, textFoldingStrategy, problemAggregator); + int cardinality = rowsToKeep.cardinality(); + Column[] newColumns = new Column[this.columns.length]; + for (int i = 0; i < this.columns.length; i++) { + newColumns[i] = this.columns[i].applyFilter(rowsToKeep, cardinality); + } + + return new Table(newColumns); + } + /** * Selects a subset of columns of this table, by names. * diff --git a/std-bits/table/src/main/java/org/enso/table/operations/Distinct.java b/std-bits/table/src/main/java/org/enso/table/operations/Distinct.java index ff6687e73f..2cf5f1066d 100644 --- a/std-bits/table/src/main/java/org/enso/table/operations/Distinct.java +++ b/std-bits/table/src/main/java/org/enso/table/operations/Distinct.java @@ -2,8 +2,10 @@ package org.enso.table.operations; import java.util.Arrays; import java.util.BitSet; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import org.enso.base.text.TextFoldingStrategy; import org.enso.table.data.column.storage.Storage; import org.enso.table.data.index.MultiValueKeyBase; @@ -15,6 +17,7 @@ import org.enso.table.util.ConstantList; import org.graalvm.polyglot.Context; public class Distinct { + /** Creates a row mask containing only the first row from sets of rows grouped by key columns. */ public static BitSet buildDistinctRowsMask( int tableSize, @@ -50,4 +53,42 @@ public class Distinct { return mask; } + + public static BitSet buildDuplicatesRowsMask( + int tableSize, + Column[] keyColumns, + TextFoldingStrategy textFoldingStrategy, + ProblemAggregator problemAggregator) { + ColumnAggregatedProblemAggregator groupingProblemAggregator = + new ColumnAggregatedProblemAggregator(problemAggregator); + Context context = Context.getCurrent(); + var mask = new BitSet(); + if (keyColumns.length != 0) { + Map visitedRows = new HashMap<>(); + int size = keyColumns[0].getSize(); + Storage[] storage = + Arrays.stream(keyColumns).map(Column::getStorage).toArray(Storage[]::new); + List strategies = ConstantList.make(textFoldingStrategy, storage.length); + for (int i = 0; i < size; i++) { + UnorderedMultiValueKey key = new UnorderedMultiValueKey(storage, i, strategies); + key.checkAndReportFloatingEquality( + groupingProblemAggregator, columnIx -> keyColumns[columnIx].getName()); + + var keyIndex = visitedRows.get(key); + if (keyIndex == null) { + visitedRows.put(key, i); + } else { + mask.set(i); + mask.set(keyIndex); + } + + context.safepoint(); + } + } else { + // If there are no columns to distinct-by we just return the whole table. + mask.set(0, tableSize); + } + + return mask; + } } diff --git a/test/Base_Tests/src/Data/Vector_Spec.enso b/test/Base_Tests/src/Data/Vector_Spec.enso index 9ea09770b0..33e8f692e4 100644 --- a/test/Base_Tests/src/Data/Vector_Spec.enso +++ b/test/Base_Tests/src/Data/Vector_Spec.enso @@ -858,11 +858,11 @@ type_spec suite_builder name alter = suite_builder.group name group_builder-> alter [1, 1.0, 2, 2.0] . distinct . should_equal [1, 2] alter [] . distinct . should_equal [] - group_builder.specify "should return a vector containing only duplicate elements" <| - alter [1, 3, 1, 2, 2, 1] . duplicates . should_equal [1, 2] - alter ["a", "a", "a"] . duplicates . should_equal ["a"] - alter ['ś', 's', 's\u0301'] . duplicates . should_equal ['s\u0301'] - alter [1, 1.0, 2, 2.0] . duplicates . should_equal [1.0, 2.0] + group_builder.specify "should return a vector containing duplicate elements" <| + alter [1, 3, 1, 2, 2, 1] . duplicates . should_equal [1, 1, 2, 2, 1] + alter ["a", "a", "a"] . duplicates . should_equal ["a", "a", "a"] + alter ['ś', 's', 's\u0301'] . duplicates . should_equal ['ś', 's\u0301'] + alter [1, 1.0, 2, 2.0] . duplicates . should_equal [1, 1.0, 2, 2.0] alter [] . duplicates . should_equal [] group_builder.specify "should be able to handle distinct on different primitive values" <| diff --git a/test/Table_Tests/src/Common_Table_Operations/Distinct_Spec.enso b/test/Table_Tests/src/Common_Table_Operations/Distinct_Spec.enso index 6c8750cce5..564208824a 100644 --- a/test/Table_Tests/src/Common_Table_Operations/Distinct_Spec.enso +++ b/test/Table_Tests/src/Common_Table_Operations/Distinct_Spec.enso @@ -128,21 +128,81 @@ add_specs suite_builder setup = t5 = t1.distinct [] on_problems=pb t5.should_fail_with No_Input_Columns_Selected - t6 = t1.distinct [] error_on_missing_columns=False on_problems=pb - t6.should_fail_with No_Input_Columns_Selected + db_todo = if setup.is_database.not then Nothing else "Table.duplicates is not implemented yet in Database." + suite_builder.group setup.prefix+"Table.duplicates" pending=db_todo group_builder-> + data = Data.setup create_connection_fn - # When not erroring about missing columns, No_Input_Columns_Selected will still yield an error. - t7 = t1.distinct ["Y", "Z"] error_on_missing_columns=False on_problems=Problem_Behavior.Report_Warning - t7.should_fail_with No_Input_Columns_Selected + group_builder.teardown <| + data.teardown - action2 = t1.distinct ["X", "Y"] error_on_missing_columns=False on_problems=_ - tester2 table = - table.at "X" . to_vector . should_equal [1, 2, 3] - problems2 = [Missing_Input_Columns.Error ["Y"]] - Problems.test_problem_handling action2 problems2 tester2 + table_builder cols = + setup.table_builder cols connection=data.connection - action3 = t1.distinct [0, 42] error_on_missing_columns=False on_problems=_ - tester3 table = - table.at "X" . to_vector . should_equal [1, 2, 3] - problems3 = [Missing_Input_Columns.Error [42]] - Problems.test_problem_handling action3 problems3 tester3 + group_builder.specify "should group by all columns by default" <| + a = ["A", ["a", "a", "b", "b", "c"]] + b = ["B", [1, 1, 1, 2, 1]] + t = table_builder [a, b] + r = t.duplicates on_problems=Report_Error |> materialize |> _.order_by ["A", "B"] + r.at "A" . to_vector . should_equal ["a", "a"] + r.at "B" . to_vector . should_equal [1, 1] + + group_builder.specify "should allow to select duplicates rows based on a subset of columns, returning any row from each group" <| + a = ["A", ["a", "a", "a", "a", "a", "a"]] + b = ["B", [1, 1, 2, 2, 1, 3]] + c = ["C", [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]] + t = table_builder [a, b, c] + + r1 = t.duplicates ["A"] on_problems=Report_Error |> materialize + r1.at "A" . to_vector . should_equal ["a", "a", "a", "a", "a", "a"] + r1.at "B" . to_vector . should_equal [1, 1, 2, 2, 1, 3] + r1.at "C" . to_vector . should_equal [0.1, 0.2, 0.3, 0.4, 0.5, 0.6] + + r2 = t.duplicates ["A", "B"] on_problems=Report_Error |> materialize + r2.at "A" . to_vector . should_equal ["a", "a", "a", "a", "a"] + r2.at "B" . to_vector . should_equal [1, 1, 2, 2, 1] + r2.at "C" . to_vector . should_equal [0.1, 0.2, 0.3, 0.4, 0.5] + + group_builder.specify "should allow to control case-sensitivity of keys" <| + x = ["X", ['A', 'a', 'enso', 'Enso', 'A']] + t1 = table_builder [x] + d1 = t1.duplicates ["X"] on_problems=Report_Error |> materialize |> _.order_by ["X"] + d1.at "X" . to_vector . should_equal ['A', 'A'] + + d2 = t1.duplicates ["X"] case_sensitivity=Case_Sensitivity.Insensitive on_problems=Report_Error |> materialize |> _.order_by ["X"] + d2.at "X" . to_vector . should_equal ['A', 'A', 'Enso', 'a', 'enso'] + + group_builder.specify "should report a warning if the key contains floating point values" <| + t1 = table_builder [["X", [3.0, 1.0, 2.0, 2.0, 1.0]]] + action1 = t1.duplicates on_problems=_ + tester1 table = + v = table.at "X" . to_vector + v.length . should_equal 4 + v.fold 0 (+) . should_equal 6.0 + problems1 = [Floating_Point_Equality.Error "X"] + Problems.test_problem_handling action1 problems1 tester1 + + group_builder.specify "should handle nulls correctly" <| + a = ["A", ["a", Nothing, "b", Nothing]] + b = ["B", [1, 2, 3, 4]] + t = table_builder [a, b] + r = t.duplicates ["A"] on_problems=Report_Error |> materialize |> _.order_by "B" + r.at "A" . to_vector . should_equal [Nothing, Nothing] + r.at "B" . to_vector . should_equal [2, 4] + + group_builder.specify "should report missing input columns" <| + t1 = table_builder [["X", [1, 2, 3, 2, 2]]] + [Problem_Behavior.Ignore, Problem_Behavior.Report_Warning, Problem_Behavior.Report_Error].each pb-> + t2 = t1.duplicates ["Y", "Z"] on_problems=pb + t2.should_fail_with Missing_Input_Columns + t2.catch . should_equal (Missing_Input_Columns.Error ["Y", "Z"]) + + t3 = t1.duplicates ["X", "Y"] on_problems=pb + t3.should_fail_with Missing_Input_Columns + t3.catch . should_equal (Missing_Input_Columns.Error ["Y"]) + + t4 = t1.duplicates [0, 42] on_problems=pb + t4.should_fail_with Missing_Input_Columns + t4.catch . should_equal (Missing_Input_Columns.Error [42]) + + t5 = t1.duplicates [] on_problems=pb + t5.should_fail_with No_Input_Columns_Selected diff --git a/test/Table_Tests/src/Common_Table_Operations/Select_Columns_Spec.enso b/test/Table_Tests/src/Common_Table_Operations/Select_Columns_Spec.enso index 1dbb97ae07..6225070d9d 100644 --- a/test/Table_Tests/src/Common_Table_Operations/Select_Columns_Spec.enso +++ b/test/Table_Tests/src/Common_Table_Operations/Select_Columns_Spec.enso @@ -584,10 +584,10 @@ add_specs suite_builder setup = result.catch Any . message . should_equal message test_duplicate_names [["Alpha", "1"], ["Alpha", "2"]] "duplicate old name mappings (Alpha)." - test_duplicate_names [["Alpha", "1"], ["Beta", "2"], ["Gamma", "3"], ["Beta", "4"], ["Alpha", "5"]] "duplicate old name mappings (Beta, Alpha)." + test_duplicate_names [["Alpha", "1"], ["Beta", "2"], ["Gamma", "3"], ["Beta", "4"], ["Alpha", "5"]] "duplicate old name mappings (Alpha, Beta)." test_duplicate_names [["Alpha", "1"], ["Alpha", "2"], ["Alpha", "3"]] "duplicate old name mappings (Alpha)." - test_duplicate_names [["Alpha", "1"], ["Beta", "2"], ["Gamma", "3"], ["Beta", "4"], ["Alpha", "5"], ["Gamma","6"], ["Delta","7"], ["Delta","8"]] "duplicate old name mappings (Beta, Alpha, Gamma, Delta)." - test_duplicate_names [["Alpha", "1"], ["Beta", "2"], ["Gamma", "3"], ["Beta", "4"], ["Alpha", "5"], ["Gamma","6"], ["Delta","7"], ["Delta","8"], ["Echo","9"], ["Echo","10"]] "duplicate old name mappings (Beta, Alpha, Gamma, ... 2 others)." + test_duplicate_names [["Alpha", "1"], ["Beta", "2"], ["Gamma", "3"], ["Beta", "4"], ["Alpha", "5"], ["Gamma","6"], ["Delta","7"], ["Delta","8"]] "duplicate old name mappings (Alpha, Beta, Gamma, Delta)." + test_duplicate_names [["Alpha", "1"], ["Beta", "2"], ["Gamma", "3"], ["Beta", "4"], ["Alpha", "5"], ["Gamma","6"], ["Delta","7"], ["Delta","8"], ["Echo","9"], ["Echo","10"]] "duplicate old name mappings (Alpha, Beta, Gamma, ... 2 others)." group_builder.specify "should correctly handle problems: unmatched names" <| weird_name = '.*?-!@#!"'