Repository: abs-lang/abs
Pull request #343 (2020-03-27 00:16:37)

shortand syntax for array.find(...)
A shorthand syntax supports passing a hash and comparing
elements to the given hash:

```bash
[null, {"key": "val", "test": 123}].find({"key": "val"}) # {"key": "val", "test": 123}
```


Patch:
diff --git a/docs/types/array.md b/docs/types/array.md
index 18d0563..e2838b6 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -183,6 +183,13 @@ Returns the first element that returns `true` when applied to the function `f`:
 ["hello", 0, 1, 2].find(f(x){type(x) == "NUMBER"}) # 0
 ```
 
+A shorthand syntax supports passing a hash and comparing
+elements to the given hash:
+
+```bash
+[null, {"key": "val", "test": 123}].find({"key": "val"}) # {"key": "val", "test": 123}
+```
+
 ### len()
 
 Returns the length of the array:
@@ -358,6 +365,29 @@ Returns an array with unique values:
 [1, 1, 1, 2].unique() # [1, 2]
 ```
 
+### intersect(array)
+
+Computes the intersection between 2 arrays:
+
+```bash
+[1, 2, 3].intersect([]) # []
+[1, 2, 3].intersect([3]) # [3]
+[1, 2, 3].intersect([3, 1]) # [1, 3]
+[1, 2, 3].intersect([1, 2, 3, 4]) # [1, 2, 3]
+```
+
+### diff(array)
+
+Computes the difference between 2 arrays
+(elements that are only on either of the 2):
+
+```bash
+[1, 2, 3].diff([]) # [1, 2, 3]
+[1, 2, 3].diff([3]) # [1, 2]
+[1, 2, 3].diff([3, 1]) # [2]
+[1, 2, 3].diff([1, 2, 3, 4]) # [4]
+```
+
 ## Next
 
 That's about it for this section!
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 8174564..6a770d4 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -198,6 +198,16 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.ARRAY_OBJ},
 			Fn:    sortFn,
 		},
+		// intersect(array:[1, 2, 3], array:[1, 2, 3])
+		"intersect": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    intersectFn,
+		},
+		// diff(array:[1, 2, 3], array:[1, 2, 3])
+		"diff": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    diffFn,
+		},
 		// map(array:[1, 2, 3], function:f(x) { x + 1 })
 		"map": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
@@ -1172,6 +1182,73 @@ func sortFn(tok token.Token, env *object.Environment, args ...object.Object) obj
 	}
 }
 
+// intersect(array:[1, 2, 3], array:[1, 2, 3])
+func intersectFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "intersect", 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
+	found := map[string]object.Object{}
+	intersection := []object.Object{}
+
+	for _, o := range right {
+		found[string(o.Type())+"__"+o.Inspect()] = o
+	}
+
+	for _, o := range left {
+		element, ok := found[string(o.Type())+"__"+o.Inspect()]
+
+		if ok {
+			intersection = append(intersection, element)
+		}
+	}
+
+	return &object.Array{Elements: intersection}
+}
+
+// intersect(array:[1, 2, 3], array:[1, 2, 3])
+func diffFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "diff", 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
+	foundLeft := map[string]object.Object{}
+	foundRight := map[string]object.Object{}
+	difference := []object.Object{}
+
+	for _, o := range left {
+		foundLeft[string(o.Type())+"__"+o.Inspect()] = o
+	}
+
+	for _, o := range right {
+		foundRight[string(o.Type())+"__"+o.Inspect()] = o
+	}
+
+	for _, o := range left {
+		_, ok := foundRight[string(o.Type())+"__"+o.Inspect()]
+
+		if !ok {
+			difference = append(difference, o)
+		}
+	}
+
+	for _, o := range right {
+		_, ok := foundLeft[string(o.Type())+"__"+o.Inspect()]
+
+		if !ok {
+			difference = append(difference, o)
+		}
+	}
+
+	return &object.Array{Elements: difference}
+}
+
 // map(array:[1, 2, 3], function:f(x) { x + 1 })
 func mapFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
 	err := validateArgs(tok, "map", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
@@ -1243,18 +1320,46 @@ func everyFn(tok token.Token, env *object.Environment, args ...object.Object) ob
 
 // find(array:[1, 2, 3], function:f(x) { x == 2 })
 func findFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
-	err := validateArgs(tok, "find", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+	err := validateArgs(tok, "find", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ, object.HASH_OBJ}})
 	if err != nil {
 		return err
 	}
 
 	arr := args[0].(*object.Array)
 
-	for _, v := range arr.Elements {
-		r := applyFunction(tok, args[1], env, []object.Object{v})
+	switch predicate := args[1].(type) {
+	case *object.Hash:
+		for _, v := range arr.Elements {
+			v, ok := v.(*object.Hash)
 
-		if isTruthy(r) {
-			return v
+			if !ok {
+				continue
+			}
+
+			match := true
+			for k, pair := range predicate.Pairs {
+				toCompare, ok := v.GetPair(k.Value)
+				if !ok {
+					match = false
+					continue
+				}
+
+				if !object.Equal(pair.Value, toCompare.Value) {
+					match = false
+				}
+			}
+
+			if match {
+				return v
+			}
+		}
+	default:
+		for _, v := range arr.Elements {
+			r := applyFunction(tok, predicate, env, []object.Object{v})
+
+			if isTruthy(r) {
+				return v
+			}
 		}
 	}
 
diff --git a/object/object.go b/object/object.go
index 0fba0d2..92e5878 100644
--- a/object/object.go
+++ b/object/object.go
@@ -57,6 +57,13 @@ type Object interface {
 	Json() string
 }
 
+// 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()
+}
+
 type Iterable interface {
 	Next() (Object, Object)
 	Reset()


Test patch:
diff --git a/evaluator/builtin_functions_test.go b/evaluator/builtin_functions_test.go
index 07ce018..d5dab67 100644
--- a/evaluator/builtin_functions_test.go
+++ b/evaluator/builtin_functions_test.go
@@ -136,6 +136,11 @@ func TestFind(t *testing.T) {
 	tests := []Tests{
 		{`find([1,2,3,3], f(x) {x == 3})`, 3},
 		{`find([1,2], f(x) {x == "some"})`, nil},
+		{`find([{}, {}], f(x) {x.y == 1})`, nil},
+		{`x = find([{}, {"y": 1, "z": 10}, {}], f(x) {x.y == 1}); x.z`, 10},
+		{`x = find([{}, {"y": 1, "z": 10}, {}], {"y": 1}); x.z`, 10},
+		{`x = find([{}, {"y": {}, "z": 10}, {}], {"y": {}}); x.z`, 10},
+		{`find([{}, {"y": "1", "z": 10}, {}], {"y": 1})`, nil},
 	}
 
 	testBuiltinFunction(tests, t)
@@ -618,6 +623,28 @@ func TestKebab(t *testing.T) {
 	testBuiltinFunction(tests, t)
 }
 
+func TestIntersect(t *testing.T) {
+	tests := []Tests{
+		{`[1,2,3].intersect([])`, []int{}},
+		{`[1,2,3].intersect([3])`, []int{3}},
+		{`[1,2,3].intersect([3, 1])`, []int{1, 3}},
+		{`[1,2,3].intersect([1,2,3,4])`, []int{1, 2, 3}},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestDiff(t *testing.T) {
+	tests := []Tests{
+		{`[1,2,3].diff([])`, []int{1, 2, 3}},
+		{`[1,2,3].diff([3])`, []int{1, 2}},
+		{`[1,2,3].diff([3, 1])`, []int{2}},
+		{`[1,2,3].diff([1,2,3,4])`, []int{4}},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
 func testBuiltinFunction(tests []Tests, t *testing.T) {
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)