Repository: abs-lang/abs
Pull request #347 (2020-03-27 14:16:01)

array.union(other_array)
Computes the [union](https://en.wikipedia.org/wiki/Union_(set_theory))
between 2 arrays:

```bash
[1, 2, 3].union([1, 2, 3, 4]) # [1, 2, 3, 4]
[1, 2, 3].union([3]) # [1, 2, 3]
[].union([3, 1]) # [3, 1]
[1, 2].union([3, 4]) # [1, 2, 3, 4]
```


Patch:
diff --git a/docs/types/array.md b/docs/types/array.md
index f68458a..957f2e9 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -402,6 +402,18 @@ between 2 arrays (elements that are only on either of the 2):
 [1, 2, 3].diff([1, 2, 3, 4]) # [4]
 ```
 
+### union(array)
+
+Computes the [union](https://en.wikipedia.org/wiki/Union_(set_theory))
+between 2 arrays:
+
+```bash
+[1, 2, 3].union([1, 2, 3, 4]) # [1, 2, 3, 4]
+[1, 2, 3].union([3]) # [1, 2, 3]
+[].union([3, 1]) # [3, 1]
+[1, 2].union([3, 4]) # [1, 2, 3, 4]
+```
+
 ## Next
 
 That's about it for this section!
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 5516325..9ac4fb5 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -208,6 +208,11 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.ARRAY_OBJ},
 			Fn:    diffFn,
 		},
+		// union(array:[1, 2, 3], array:[1, 2, 3])
+		"union": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    unionFn,
+		},
 		// diff_symmetric(array:[1, 2, 3], array:[1, 2, 3])
 		"diff_symmetric": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
@@ -1200,11 +1205,11 @@ func intersectFn(tok token.Token, env *object.Environment, args ...object.Object
 	intersection := []object.Object{}
 
 	for _, o := range right {
-		found[string(o.Type())+"__"+o.Inspect()] = o
+		found[object.GenerateEqualityString(o)] = o
 	}
 
 	for _, o := range left {
-		element, ok := found[string(o.Type())+"__"+o.Inspect()]
+		element, ok := found[object.GenerateEqualityString(o)]
 
 		if ok {
 			intersection = append(intersection, element)
@@ -1215,8 +1220,8 @@ func intersectFn(tok token.Token, env *object.Environment, args ...object.Object
 }
 
 // diff(array:[1, 2, 3], array:[1, 2, 3])
-func diff(symmetric bool, tok token.Token, env *object.Environment, args ...object.Object) object.Object {
-	err := validateArgs(tok, "diff", args, 2, [][]string{{object.ARRAY_OBJ}, {object.ARRAY_OBJ}})
+func diff(symmetric bool, fnName string, tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, fnName, args, 2, [][]string{{object.ARRAY_OBJ}, {object.ARRAY_OBJ}})
 	if err != nil {
 		return err
 	}
@@ -1227,11 +1232,11 @@ func diff(symmetric bool, tok token.Token, env *object.Environment, args ...obje
 	difference := []object.Object{}
 
 	for _, o := range right {
-		foundRight[string(o.Type())+"__"+o.Inspect()] = o
+		foundRight[object.GenerateEqualityString(o)] = o
 	}
 
 	for _, o := range left {
-		_, ok := foundRight[string(o.Type())+"__"+o.Inspect()]
+		_, ok := foundRight[object.GenerateEqualityString(o)]
 
 		if !ok {
 			difference = append(difference, o)
@@ -1241,19 +1246,48 @@ func diff(symmetric bool, tok token.Token, env *object.Environment, args ...obje
 	if symmetric {
 		// If the did is symmetric, we simply re-run this function with the arrays swapped
 		// so diff_sym(a, b) = diff(a, b) + diff(b, a)
-		difference = append(difference, diff(false, tok, env, args[1], args[0]).(*object.Array).Elements...)
+		difference = append(difference, diff(false, fnName, tok, env, args[1], args[0]).(*object.Array).Elements...)
 	}
 
 	return &object.Array{Elements: difference}
 }
 
 func diffFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
-	return diff(false, tok, env, args...)
+	return diff(false, "diff", tok, env, args...)
 }
 
 // diff_symmetric(array:[1, 2, 3], array:[1, 2, 3])
 func diffSymmetricFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
-	return diff(true, tok, env, args...)
+	return diff(true, "diff_symmetric", tok, env, args...)
+}
+
+// union(array:[1, 2, 3], array:[1, 2, 3])
+func unionFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "union", args, 2, [][]string{{object.ARRAY_OBJ}, {object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	left := args[0].(*object.Array).Elements
+	right := args[1].(*object.Array).Elements
+
+	union := []object.Object{}
+
+	for _, v := range left {
+		union = append(union, v)
+	}
+
+	m := util.Mapify(left)
+
+	for _, v := range right {
+		_, found := m[object.GenerateEqualityString(v)]
+
+		if !found {
+			union = append(union, v)
+		}
+	}
+
+	return &object.Array{Elements: union}
 }
 
 // map(array:[1, 2, 3], function:f(x) { x + 1 })
@@ -1410,7 +1444,7 @@ func uniqueFn(tok token.Token, env *object.Environment, args ...object.Object) o
 	existingElements := map[string]bool{}
 
 	for _, v := range arr.Elements {
-		key := fmt.Sprintf("%s_%s", v.Type(), v.Inspect())
+		key := object.GenerateEqualityString(v)
 
 		if _, ok := existingElements[key]; !ok {
 			existingElements[key] = true
diff --git a/object/object.go b/object/object.go
index 92e5878..42f88ef 100644
--- a/object/object.go
+++ b/object/object.go
@@ -57,11 +57,23 @@ type Object interface {
 	Json() string
 }
 
+// GenerateEqualityString is used to compare
+// 2 objects, for example from different arrays.
+//
+// This function will generate a string that can
+// be used to compare the 2, including their type
+// and their string representation. Only using their
+// string representation wouldn't work as "1" would
+// be the same as 1.
+func GenerateEqualityString(o Object) string {
+	return fmt.Sprintf("%s:%s", o.Type(), o.Inspect())
+}
+
 // Equal compares 2 objects
 // and makes sure they represent
 // the same value.
 func Equal(obj1 Object, obj2 Object) bool {
-	return obj1.Type() == obj2.Type() && obj1.Inspect() == obj2.Inspect()
+	return GenerateEqualityString(obj1) == GenerateEqualityString(obj2)
 }
 
 type Iterable interface {
diff --git a/util/util.go b/util/util.go
index 74b12b7..7c81c6b 100644
--- a/util/util.go
+++ b/util/util.go
@@ -155,3 +155,19 @@ func appendIndexFile(path string) string {
 
 	return path
 }
+
+// Mapify converts a list of objects to a map.
+// This is useful when you want to test whether
+// elements of a list are present in another list:
+// You can mapify the first one and check whether
+// elements of the second one would occupy the same
+// key in the map.
+func Mapify(list []object.Object) map[string]object.Object {
+	m := make(map[string]object.Object)
+
+	for _, v := range list {
+		m[object.GenerateEqualityString(v)] = v
+	}
+
+	return m
+}


Test patch:
diff --git a/evaluator/builtin_functions_test.go b/evaluator/builtin_functions_test.go
index f2eced9..6e181eb 100644
--- a/evaluator/builtin_functions_test.go
+++ b/evaluator/builtin_functions_test.go
@@ -656,6 +656,17 @@ func TestDiffSymmetric(t *testing.T) {
 	testBuiltinFunction(tests, t)
 }
 
+func TestUnion(t *testing.T) {
+	tests := []Tests{
+		{`[1, 2, 3].union([1, 2, 3, 4])`, []int{1, 2, 3, 4}},
+		{`[1, 2, 3].union([3])`, []int{1, 2, 3}},
+		{`[].union([3, 1])`, []int{3, 1}},
+		{`[1, 2].union([3, 4])`, []int{1, 2, 3, 4}},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
 func testBuiltinFunction(tests []Tests, t *testing.T) {
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)
diff --git a/object/object_test.go b/object/object_test.go
index 34d8882..e03995b 100644
--- a/object/object_test.go
+++ b/object/object_test.go
@@ -20,3 +20,25 @@ func TestStringHashKey(t *testing.T) {
 		t.Errorf("strings with different content have same hash keys")
 	}
 }
+
+func TestGenerateEqualityString(t *testing.T) {
+	tests := []struct {
+		input    Object
+		expected string
+	}{
+		{&String{Value: "a"}, "STRING:a"},
+		{&Array{Elements: []Object{FALSE}}, "ARRAY:[false]"},
+		{&Array{Elements: []Object{&Number{Value: 1}}}, "ARRAY:[1]"},
+		{&Array{Elements: []Object{&String{Value: "1"}}}, "ARRAY:[\"1\"]"},
+		{&Hash{}, "HASH:{}"},
+		{&Hash{Pairs: map[HashKey]HashPair{}}, "HASH:{}"},
+		{&Hash{Pairs: map[HashKey]HashPair{HashKey{Value: "x", Type: STRING_OBJ}: HashPair{&String{Value: "x"}, &String{Value: "x"}}}}, "HASH:{\"x\": \"x\"}"},
+	}
+
+	for _, tt := range tests {
+		output := GenerateEqualityString(tt.input)
+		if tt.expected != output {
+			t.Fatalf("expected '%v', got '%v' (original: %s)", tt.expected, output, tt.input.Inspect())
+		}
+	}
+}
diff --git a/util/util_test.go b/util/util_test.go
index 18cda75..88f9a9c 100644
--- a/util/util_test.go
+++ b/util/util_test.go
@@ -109,3 +109,24 @@ func TestInterpolateStringVars(t *testing.T) {
 		}
 	}
 }
+
+func TestMapify(t *testing.T) {
+	elements := []object.Object{}
+	first := &object.String{Value: "x"}
+	second := &object.Number{Value: 10}
+	elements = append(elements, first, second)
+
+	m := Mapify(elements)
+
+	if len(m) != 2 {
+		t.Fatalf("expected len '%d', got '%d'", 2, len(m))
+	}
+
+	if m["STRING:x"] != first {
+		t.Fatalf("string element not found")
+	}
+
+	if m["NUMBER:10"] != second {
+		t.Fatalf("number element not found")
+	}
+}