diff --git a/.gitignore b/.gitignore index 723ef36..1f1025f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.idea \ No newline at end of file +.idea +.DS_Store \ No newline at end of file diff --git a/gofp/setx/product.go b/gofp/setx/product.go new file mode 100644 index 0000000..6a25b9f --- /dev/null +++ b/gofp/setx/product.go @@ -0,0 +1,132 @@ +package setx + +import ( + "cmp" + "fmt" + + "github.com/msales/gox/gofp" +) + +type Product[Value cmp.Ordered] struct { + Result[Value] + immutable bool +} + +func NewProduct[Value cmp.Ordered]() Product[Value] { + return Product[Value]{ + Result: Result[Value]{ + Operator: All, + Values: []Value{}, + }, + } +} + +// Merge merges a condition to existing result - check package description for algorithm description +func (r *Product[Value]) Merge(operator Operator, values ...Value) (MergeResult, error) { + if r.immutable { + return Immutable, nil + } + + switch operator { + case All: + return r.mergeAll(values) + case None: + return r.mergeNone(values) + case In: + return r.mergeIn(values) + case NotIn: + return r.mergeNotIn(values) + default: + return Error, fmt.Errorf("unknown operator") + } +} + +func (r *Product[Value]) mergeNone(values []Value) (MergeResult, error) { + if len(values) > 0 { + return Error, fmt.Errorf("cannot merge any values when using none operator") + } + return r.makeImmutable() +} + +func (r *Product[Value]) mergeAll(values []Value) (MergeResult, error) { + if len(values) > 0 { + return Error, fmt.Errorf("cannot merge any values when using all operator") + } + // ToDo: implement when needed - the case we have will never use this + return r.makeImmutable() +} + +func (r *Product[Value]) mergeIn(values []Value) (MergeResult, error) { + switch r.Operator { + case In: + // operator was IN and merging IN => IN + // so the resulting list is just an intersection of both + r.Result.intersect(values...) + if len(r.Values) == 0 { + return r.makeImmutable() + } + return Continue, nil + case NotIn: + // operator was NOT_IN and merging IN => IN + r.Values = r.mergeInNotIn(r.Values, values) + r.Operator = In + if len(r.Values) == 0 { + // If resulting set is empty (IN is completely contained in NOT IN condition) + // then this pair of conditions makes the result always false, so NONE and immutable. + return r.makeImmutable() + } + return Continue, nil + case All: + // if the result is mutable and operator is ALL, this means it was not used yet + // with first usage, operator and values are set + r.Operator = In + r.Values = values + return Continue, nil + default: + return Error, fmt.Errorf("unknown operator in result") + } +} + +func (r *Product[Value]) mergeNotIn(values []Value) (MergeResult, error) { + switch r.Operator { + case In: + // operator was IN and merging NOT IN => IN + r.Values = r.mergeInNotIn(values, r.Values) + // the operator was already IN + if len(r.Values) == 0 { + // If resulting set is empty (IN is completely contained in NOT IN condition) + // then this pair of conditions makes the result always false, so NONE and immutable. + return r.makeImmutable() + } + return Continue, nil + case NotIn: + // operator was NOT IN and merging NOT IN => NOT IN + // Result is a sum of both sets. + r.Result.merge(values...) + return Continue, nil + case All: + // if the result is mutable and operator is ALL, this means it was not used yet + // with first usage, operator and values are set + r.Operator = NotIn + r.Values = values + return Continue, nil + default: + return Error, fmt.Errorf("unknown operator in result") + } +} + +// makeImmutable changes the result to immutable with all possible values. +// Once this state is reached it can't be changed. +// If conditions so far make the result completely open, no other condition can narrow it anymore +// because all conditions are connected with OR. ANY OR SOMETHING => ANY. +func (r *Product[Value]) makeImmutable() (MergeResult, error) { + r.Values = []Value{} + r.immutable = true + r.Operator = None + return Immutable, nil +} + +func (r *Product[Value]) mergeInNotIn(inValues, notInValues []Value) []Value { + diff := gofp.DiffRight(inValues, notInValues) + return diff +} diff --git a/gofp/setx/product_test.go b/gofp/setx/product_test.go new file mode 100644 index 0000000..2473b87 --- /dev/null +++ b/gofp/setx/product_test.go @@ -0,0 +1,383 @@ +package setx_test + +import ( + "testing" + + "github.com/msales/gox/gofp/setx" + "github.com/stretchr/testify/assert" +) + +type productConditions struct { + operator setx.Operator + values []string +} +type testCase struct { + name string + conditions []productConditions + wantResult setx.Product[string] + wantMergeResult setx.MergeResult + wantErr bool +} + +func TestResult_MergeProduct(t *testing.T) { + tests := productTestCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := setx.NewProduct[string]() + var mergeResult setx.MergeResult + var err error + for _, condition := range tt.conditions { + mergeResult, err = result.Merge(condition.operator, condition.values...) + } + assert.Equal(t, tt.wantMergeResult, mergeResult) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.Equal(t, tt.wantResult.Operator, result.Operator) + assert.ElementsMatch(t, tt.wantResult.Values, result.Values) + }) + } +} + +func TestResult_MergeProductWithNone(t *testing.T) { + tests := productTestCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // any test no error case if there is one more condition with all added to the conditions list, should result with + // immutable result with ALL operator + tt.conditions = append(tt.conditions, addToProductMerge(setx.None)) + tt.wantMergeResult = setx.Immutable + tt.wantResult.Operator = setx.None + tt.wantResult.Values = []string{} + if tt.wantErr { + return + } + + result := setx.NewProduct[string]() + var mergeResult setx.MergeResult + var err error + for _, condition := range tt.conditions { + mergeResult, err = result.Merge(condition.operator, condition.values...) + } + assert.Equal(t, tt.wantMergeResult, mergeResult) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.Equal(t, tt.wantResult.Operator, result.Operator) + assert.ElementsMatch(t, tt.wantResult.Values, result.Values) + }) + } +} + +// productTestCases is providing cases for 2 tests +// Unfortunately in goland it prevents running separate case test, but nevertheless it's better than +// copy-paste of all the cases. +func productTestCases() []testCase { + tests := []testCase{ + { + name: "One IN condition", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "ES"), + }, + wantResult: productResult(setx.In, "PL", "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "One NOT IN condition", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL", "ES"), + }, + wantResult: productResult(setx.NotIn, "PL", "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "Two IN conditions of disjoint sets", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "ES"), + addToProductMerge(setx.In, "DE", "US"), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "Two NOT IN conditions of disjoint sets", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL", "ES"), + addToProductMerge(setx.NotIn, "DE", "US"), + }, + wantResult: productResult(setx.NotIn, "PL", "ES", "DE", "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "Two IN conditions of intersecting sets", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "ES"), + addToProductMerge(setx.In, "PL", "US"), + }, + wantResult: productResult(setx.In, "PL"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "Two NOT IN conditions of intersecting sets", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL", "ES"), + addToProductMerge(setx.NotIn, "PL", "US"), + }, + wantResult: productResult(setx.NotIn, "PL", "ES", "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "Three NOT IN conditions of intersecting sets, but no value in all conditions", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL", "ES", "UK"), + addToProductMerge(setx.NotIn, "PL", "US", "UK"), + addToProductMerge(setx.NotIn, "IS", "US", "CZ"), + }, + wantResult: productResult(setx.NotIn, "PL", "ES", "UK", "US", "IS", "CZ"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "Three IN conditions of intersecting sets, but no value in all conditions", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "ES", "UK"), + addToProductMerge(setx.In, "PL", "US", "UK"), + addToProductMerge(setx.In, "IS", "US", "CZ"), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "Three NOT IN conditions of intersecting sets but at least one value in all conditions", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL", "CA", "ES", "UK", "SK"), + addToProductMerge(setx.NotIn, "PL", "SK", "US", "UK", "CA"), + addToProductMerge(setx.NotIn, "CA", "IS", "US", "SK", "CZ"), + }, + wantResult: productResult(setx.NotIn, "PL", "CA", "ES", "UK", "SK", "US", "IS", "CZ"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "Three IN conditions of intersecting sets but at least one value in all conditions", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "CA", "ES", "UK", "SK"), + addToProductMerge(setx.In, "PL", "SK", "US", "UK", "CA"), + addToProductMerge(setx.In, "CA", "IS", "US", "SK", "CZ"), + }, + wantResult: productResult(setx.In, "CA", "SK"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "IN then NOT IN intersecting conditions", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "ES"), + addToProductMerge(setx.NotIn, "PL", "US"), + }, + wantResult: productResult(setx.In, "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "NOT IN then IN intersecting conditions", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL", "US"), + addToProductMerge(setx.In, "PL", "ES"), + }, + wantResult: productResult(setx.In, "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "IN then NOT IN disjoint conditions", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "ES"), + addToProductMerge(setx.NotIn, "CZ", "US"), + }, + wantResult: productResult(setx.In, "PL", "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "NOT IN then IN disjoint conditions", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "CZ", "US"), + addToProductMerge(setx.In, "PL", "ES"), + }, + wantResult: productResult(setx.In, "PL", "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "IN then NOT IN identical conditions", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL"), + addToProductMerge(setx.NotIn, "PL"), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "NOT IN then IN identical conditions", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL"), + addToProductMerge(setx.In, "PL"), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "IN then NOT IN conditions, NOT IN set contained in IN set", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL", "US"), + addToProductMerge(setx.NotIn, "PL"), + }, + wantResult: productResult(setx.In, "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "NOT IN then IN conditions, NOT IN set contained in IN set", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL"), + addToProductMerge(setx.In, "PL", "US"), + }, + wantResult: productResult(setx.In, "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "wrong ALL condition with values", + conditions: []productConditions{ + addToProductMerge(setx.All, "PL"), + }, + wantErr: true, + }, + + { + name: "wrong NONE condition with values", + conditions: []productConditions{ + addToProductMerge(setx.None, "PL"), + }, + wantErr: true, + }, + + { + name: "only NONE", + conditions: []productConditions{ + addToProductMerge(setx.None), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "twice NONE", + conditions: []productConditions{ + addToProductMerge(setx.None), + addToProductMerge(setx.None), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "IN + NONE", + conditions: []productConditions{ + addToProductMerge(setx.In, "PL"), + addToProductMerge(setx.None), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "NONE + IN", + conditions: []productConditions{ + addToProductMerge(setx.None), + addToProductMerge(setx.In, "PL"), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "NOT IN + NONE", + conditions: []productConditions{ + addToProductMerge(setx.NotIn, "PL"), + addToProductMerge(setx.None), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "NONE + IN", + conditions: []productConditions{ + addToProductMerge(setx.None), + addToProductMerge(setx.NotIn, "PL"), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "ALL + NONE", + conditions: []productConditions{ + addToProductMerge(setx.All), + addToProductMerge(setx.None), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "NONE + ALL", + conditions: []productConditions{ + addToProductMerge(setx.None), + addToProductMerge(setx.All), + }, + wantResult: productResult(setx.None), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + } + return tests +} + +func productResult(operator setx.Operator, values ...string) setx.Product[string] { + return setx.Product[string]{ + Result: setx.Result[string]{ + Operator: operator, + Values: values, + }, + } +} + +func addToProductMerge(operator setx.Operator, values ...string) productConditions { + return productConditions{operator: operator, values: values} +} diff --git a/gofp/setx/result.go b/gofp/setx/result.go new file mode 100644 index 0000000..8c51315 --- /dev/null +++ b/gofp/setx/result.go @@ -0,0 +1,80 @@ +// Package setx calculates value for approximated condition for product (A AND B AND C) and sum (A OR B OR C) +// +// SUM +// Input: +// There is a list of conditions related to the same value type, where all conditions are summed up: +// A OR B OR C +// Each condition has an operator IN, NOT, ALL. +// Example expressions: +// country IN ("PL") +// country NOT IN ("PL") +// country IN ("PL") OR country IN ("US") +// country IN ("PL") OR country NOT IN ("US") +// country IN ("PL") OR ANY country // operator ALL +// The algorithm leverages the fact, that each time we can calculate a pair of conditions and use the result to +// calculate result with next one: +// A OR B OR C OR D -> ((A OR B) OR C) OR D +// Algorithm allows merging consecutive conditions to a result and each time calculates new operator and list of values. +// Each time Merge is called it returns MergeResult which may be used to optimise processing. It is enough that at +// any point of processing the list of values becomes ALL to ensure that no consecutive condition can change that +// because they are all connected with OR. Consecutive conditions can extend the possible list of resulting values +// but never limit them. +// If at any point of processing result becomes ALL values, MergeResult is set to Immutable and no additional Merge +// call will change that. In that case additional Merge calls may be skipped. +// +// PRODUCT +// ... +package setx + +import ( + "cmp" + "slices" + + "github.com/msales/gox/gofp" +) + +type MergeResult int +type Operator int + +const ( + Error MergeResult = iota + Continue + Immutable +) + +const ( + In Operator = iota + NotIn + All + None +) + +func (o Operator) String() string { + switch o { + case In: + return "IN" + case NotIn: + return "NOT_IN" + case All: + return "ALL" + default: + return "not implemented" + } +} + +type Result[Value cmp.Ordered] struct { + Operator Operator + Values []Value +} + +func (r *Result[Value]) merge(values ...Value) { + r.Values = append(r.Values, values...) + slices.Sort(r.Values) + r.Values = slices.Compact(r.Values) +} + +func (r *Result[Value]) intersect(values ...Value) { + r.Values = gofp.Intersection(r.Values, values, func(elementOne, elementTwo Value) bool { + return elementOne == elementTwo + }) +} diff --git a/gofp/setx/sum.go b/gofp/setx/sum.go new file mode 100644 index 0000000..9b90501 --- /dev/null +++ b/gofp/setx/sum.go @@ -0,0 +1,165 @@ +package setx + +import ( + "cmp" + "fmt" + + "github.com/msales/gox/gofp" +) + +type Sum[Value cmp.Ordered] struct { + Result[Value] + immutable bool +} + +func NewSum[Value cmp.Ordered]() Sum[Value] { + return Sum[Value]{ + Result: Result[Value]{ + Operator: All, + Values: []Value{}, + }, + } +} + +// Merge merges a condition to existing result - check package description for algorithm description +func (r *Sum[Value]) Merge(operator Operator, values ...Value) (MergeResult, error) { + if r.immutable { + return Immutable, nil + } + + switch operator { + case All: + return r.mergeAll(values) + case None: + return r.mergeNone(values) + case In: + return r.mergeIn(values) + case NotIn: + return r.mergeNotIn(values) + default: + return Error, fmt.Errorf("unknown operator") + } +} + +func (r *Sum[Value]) mergeAll(values []Value) (MergeResult, error) { + if len(values) > 0 { + return Error, fmt.Errorf("cannot merge any values when using all operator") + } + return r.makeImmutable() +} + +func (r *Sum[Value]) mergeNone(values []Value) (MergeResult, error) { + if len(values) > 0 { + return Error, fmt.Errorf("cannot merge any values when using none operator") + } + + switch r.Operator { + case In: + return Continue, nil + case NotIn: + return Continue, nil + case All: + // if the result is mutable and operator is ALL, this means it was not used yet + // with first usage, operator and values are set + r.Operator = None + return Continue, nil + case None: + return Continue, nil + default: + return Error, fmt.Errorf("unknown operator %d in result", r.Operator) + + } +} + +func (r *Sum[Value]) mergeIn(values []Value) (MergeResult, error) { + switch r.Operator { + case In: + // operator was IN and merging IN => IN + // so the resulting list is just a sum of both + r.Result.merge(values...) + return Continue, nil + case NotIn: + // operator was NOT_IN and merging IN => NOT IN + // since one of the operators is NOT in the result must be NOT IN too + // if there are any elements in the NOT IN set that are not in IN set then those + // elements are in resulting NOT IN set + r.Values = r.mergeInNotIn(values, r.Values) + // it is already NOT IN operator + if len(r.Values) == 0 { + // If resulting set is empty (NOT IN is completely contained in IN condition) + // then this pair of conditions makes the result always true, so ALL and immutable. + return r.makeImmutable() + } + return Continue, nil + case All: + // if the result is mutable and operator is ALL, this means it was not used yet + // with first usage, operator and values are set + r.Operator = In + r.Values = values + return Continue, nil + case None: + // so far result was none, but NONE result added to any result becomes that any result + r.Operator = In + r.Values = values + return Continue, nil + default: + return Error, fmt.Errorf("unknown operator %d in result", r.Operator) + } +} + +func (r *Sum[Value]) mergeNotIn(values []Value) (MergeResult, error) { + switch r.Operator { + case In: + // operator was NOT_IN and merging IN => NOT IN + // since one of the operators is NOT in the result must be NOT IN too + // if there are any elements in the NOT IN set that are not in IN set then those + // elements are in resulting NOT IN set + r.Values = r.mergeInNotIn(r.Values, values) + r.Operator = NotIn + if len(r.Values) == 0 { + // If resulting set is empty (NOT IN is completely contained in IN condition) + // then this pair of conditions makes the result always true, so ALL and immutable. + return r.makeImmutable() + } + return Continue, nil + case NotIn: + // operator was NOT IN and merging NOT IN => NOT IN + // so the resulting list is just an intersection of both, in case there is no intersection, all values + // are possible and result becomes immutable + r.Result.intersect(values...) + if len(r.Values) > 0 { + return Continue, nil + } + return r.makeImmutable() + case All: + // if the result is mutable and operator is ALL, this means it was not used yet + // with first usage, operator and values are set + r.Operator = NotIn + r.Values = values + return Continue, nil + case None: + // so far result was none, but NONE result added to any result becomes that any result + r.Operator = NotIn + r.Values = values + return Continue, nil + default: + return Error, fmt.Errorf("unknown operator %d in result", r.Operator) + + } +} + +// makeImmutable changes the result to immutable with all possible values. +// Once this state is reached it can't be changed. +// If conditions so far make the result completely open, no other condition can narrow it anymore +// because all conditions are connected with OR. ANY OR SOMETHING => ANY. +func (r *Sum[Value]) makeImmutable() (MergeResult, error) { + r.Values = []Value{} + r.immutable = true + r.Operator = All + return Immutable, nil +} + +func (r *Sum[Value]) mergeInNotIn(inValues, notInValues []Value) []Value { + diff := gofp.DiffRight(inValues, notInValues) + return diff +} diff --git a/gofp/setx/sum_test.go b/gofp/setx/sum_test.go new file mode 100644 index 0000000..a97fa07 --- /dev/null +++ b/gofp/setx/sum_test.go @@ -0,0 +1,382 @@ +package setx_test + +import ( + "testing" + + "github.com/msales/gox/gofp/setx" + "github.com/stretchr/testify/assert" +) + +type sumConditions struct { + operator setx.Operator + values []string +} +type sumTestCase struct { + name string + conditions []sumConditions + wantResult setx.Sum[string] + wantMergeResult setx.MergeResult + wantErr bool +} + +func TestResult_MergeSum(t *testing.T) { + tests := sumTestCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := setx.NewSum[string]() + var mergeResult setx.MergeResult + var err error + for _, condition := range tt.conditions { + mergeResult, err = result.Merge(condition.operator, condition.values...) + } + assert.Equal(t, tt.wantMergeResult, mergeResult) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.Equal(t, tt.wantResult.Operator, result.Operator) + assert.ElementsMatch(t, tt.wantResult.Values, result.Values) + }) + } +} + +func TestResult_MergeSumWithAll(t *testing.T) { + tests := sumTestCases() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // any test no error case if there is one more condition with all added to the conditions list, should result with + // immutable result with ALL operator + tt.conditions = append(tt.conditions, addToSumMerge(setx.All)) + tt.wantMergeResult = setx.Immutable + tt.wantResult.Operator = setx.All + tt.wantResult.Values = []string{} + if tt.wantErr { + return + } + + result := setx.NewSum[string]() + var mergeResult setx.MergeResult + var err error + for _, condition := range tt.conditions { + mergeResult, err = result.Merge(condition.operator, condition.values...) + } + assert.Equal(t, tt.wantMergeResult, mergeResult) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.Equal(t, tt.wantResult.Operator, result.Operator) + assert.ElementsMatch(t, tt.wantResult.Values, result.Values) + }) + } +} + +// sumTestCases is providing cases for 2 tests +// Unfortunately in goland it prevents running separate case test, but nevertheless it's better than +// copy-paste of all the cases. +func sumTestCases() []sumTestCase { + tests := []sumTestCase{ + { + name: "One IN condition", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "ES"), + }, + wantResult: sumResult(setx.In, "PL", "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "One NOT IN condition", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL", "ES"), + }, + wantResult: sumResult(setx.NotIn, "PL", "ES"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "Two IN conditions of disjoint sets", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "ES"), + addToSumMerge(setx.In, "DE", "US"), + }, + wantResult: sumResult(setx.In, "PL", "ES", "US", "DE"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "Two NOT IN conditions of disjoint sets", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL", "ES"), + addToSumMerge(setx.NotIn, "DE", "US"), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "Two IN conditions of intersecting sets", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "ES"), + addToSumMerge(setx.In, "PL", "US"), + }, + wantResult: sumResult(setx.In, "PL", "ES", "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "Two NOT IN conditions of intersecting sets", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL", "ES"), + addToSumMerge(setx.NotIn, "PL", "US"), + }, + wantResult: sumResult(setx.NotIn, "PL"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "Three NOT IN conditions of intersecting sets, but no value in all conditions", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL", "ES", "UK"), + addToSumMerge(setx.NotIn, "PL", "US", "UK"), + addToSumMerge(setx.NotIn, "IS", "US", "CZ"), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "Three IN conditions of intersecting sets, but no value in all conditions", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "ES", "UK"), + addToSumMerge(setx.In, "PL", "US", "UK"), + addToSumMerge(setx.In, "IS", "US", "CZ"), + }, + wantResult: sumResult(setx.In, "PL", "ES", "UK", "US", "IS", "CZ"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "Three NOT IN conditions of intersecting sets but at least one value in all conditions", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL", "CA", "ES", "UK", "SK"), + addToSumMerge(setx.NotIn, "PL", "SK", "US", "UK", "CA"), + addToSumMerge(setx.NotIn, "CA", "IS", "US", "SK", "CZ"), + }, + wantResult: sumResult(setx.NotIn, "CA", "SK"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "Three IN conditions of intersecting sets but at least one value in all conditions", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "CA", "ES", "UK", "SK"), + addToSumMerge(setx.In, "PL", "SK", "US", "UK", "CA"), + addToSumMerge(setx.In, "CA", "IS", "US", "SK", "CZ"), + }, + wantResult: sumResult(setx.In, "PL", "CA", "ES", "UK", "SK", "US", "IS", "CZ"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "IN then NOT IN intersecting conditions", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "ES"), + addToSumMerge(setx.NotIn, "PL", "US"), + }, + wantResult: sumResult(setx.NotIn, "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "NOT IN then IN intersecting conditions", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL", "US"), + addToSumMerge(setx.In, "PL", "ES"), + }, + wantResult: sumResult(setx.NotIn, "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "IN then NOT IN disjoint conditions", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "ES"), + addToSumMerge(setx.NotIn, "CZ", "US"), + }, + wantResult: sumResult(setx.NotIn, "CZ", "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "NOT IN then IN disjoint conditions", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "CZ", "US"), + addToSumMerge(setx.In, "PL", "ES"), + }, + wantResult: sumResult(setx.NotIn, "CZ", "US"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "IN then NOT IN identical conditions", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL"), + addToSumMerge(setx.NotIn, "PL"), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "NOT IN then IN identical conditions", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL"), + addToSumMerge(setx.In, "PL"), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "IN then NOT IN conditions, NOT IN set contained in IN set", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL", "US"), + addToSumMerge(setx.NotIn, "PL"), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "NOT IN then IN conditions, NOT IN set contained in IN set", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL"), + addToSumMerge(setx.In, "PL", "US"), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + + { + name: "wrong ALL condition with values", + conditions: []sumConditions{ + addToSumMerge(setx.All, "PL"), + }, + wantErr: true, + }, + { + name: "wrong NONE condition with values", + conditions: []sumConditions{ + addToSumMerge(setx.None, "PL"), + }, + wantErr: true, + }, + + { + name: "only NONE", + conditions: []sumConditions{ + addToSumMerge(setx.None), + }, + wantResult: sumResult(setx.None), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "twice NONE", + conditions: []sumConditions{ + addToSumMerge(setx.None), + addToSumMerge(setx.None), + }, + wantResult: sumResult(setx.None), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "IN + NONE", + conditions: []sumConditions{ + addToSumMerge(setx.In, "PL"), + addToSumMerge(setx.None), + }, + wantResult: sumResult(setx.In, "PL"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "NONE + IN", + conditions: []sumConditions{ + addToSumMerge(setx.None), + addToSumMerge(setx.In, "PL"), + }, + wantResult: sumResult(setx.In, "PL"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "NOT IN + NONE", + conditions: []sumConditions{ + addToSumMerge(setx.NotIn, "PL"), + addToSumMerge(setx.None), + }, + wantResult: sumResult(setx.NotIn, "PL"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + { + name: "NONE + IN", + conditions: []sumConditions{ + addToSumMerge(setx.None), + addToSumMerge(setx.NotIn, "PL"), + }, + wantResult: sumResult(setx.NotIn, "PL"), + wantMergeResult: setx.Continue, + wantErr: false, + }, + + { + name: "ALL + NONE", + conditions: []sumConditions{ + addToSumMerge(setx.All), + addToSumMerge(setx.None), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + { + name: "NONE + ALL", + conditions: []sumConditions{ + addToSumMerge(setx.None), + addToSumMerge(setx.All), + }, + wantResult: sumResult(setx.All), + wantMergeResult: setx.Immutable, + wantErr: false, + }, + } + return tests +} + +func sumResult(operator setx.Operator, values ...string) setx.Sum[string] { + return setx.Sum[string]{ + Result: setx.Result[string]{ + Operator: operator, + Values: values, + }, + } +} + +func addToSumMerge(operator setx.Operator, values ...string) sumConditions { + return sumConditions{operator: operator, values: values} +}