Repository: abs-lang/abs
Pull request #348 (2020-03-28 12:07:18)

array.flatten()
Flattens an array a single level deep:

```js
[[1, 2], 3, [4]].flatten() # [1, 2, 3, 4]
[[1, 2, 3, 4]].flatten() # [1, 2, 3, 4]
```


Patch:
diff --git a/docs/types/array.md b/docs/types/array.md
index 957f2e9..b25044c 100644
--- a/docs/types/array.md
+++ b/docs/types/array.md
@@ -141,7 +141,7 @@ Checks whether `e` is present in the array. `e` can only be
 a string or number and the array needs to be a homogeneous array
 of strings or numbers:
 
-``` bash
+``` py
 [1, 2, 3].contains(3) # true
 [1, 2, 3].contains(4) # false
 ```
@@ -150,7 +150,7 @@ of strings or numbers:
 
 Splits the array into chunks of the given size:
 
-```bash
+```py
 [1, 2, 3].chunk(2) # [[1, 2], [3]]
 [1, 2, 3].chunk(10) # [[1,2,3]]
 [1, 2, 3].chunk(1.2) # argument to chunk must be a positive integer, got '1.2'
@@ -161,7 +161,7 @@ Splits the array into chunks of the given size:
 Returns true when all elements in the array
 return `true` when applied to the function `f`:
 
-``` bash
+``` py
 [0, 1, 2].every(f(x){type(x) == "NUMBER"}) # true
 [0, 1, 2].every(f(x){x == 0}) # false
 ```
@@ -171,7 +171,7 @@ return `true` when applied to the function `f`:
 Returns a new array with only the elements that returned
 `true` when applied to the function `f`:
 
-``` bash
+``` py
 ["hello", 0, 1, 2].filter(f(x){type(x) == "NUMBER"}) # [0, 1, 2]
 ```
 
@@ -179,14 +179,14 @@ Returns a new array with only the elements that returned
 
 Returns the first element that returns `true` when applied to the function `f`:
 
-``` bash
+``` py
 ["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
+```py
 [null, {"key": "val", "test": 123}].find({"key": "val"}) # {"key": "val", "test": 123}
 ```
 
@@ -194,7 +194,7 @@ elements to the given hash:
 
 Returns the length of the array:
 
-``` bash
+``` py
 [1, 2].len() # 2
 ```
 
@@ -202,7 +202,7 @@ Returns the length of the array:
 
 Joins the elements of the array by `separator`, defaulting to an empty string:
 
-``` bash
+``` py
 [1, 2, 3].join("_") # "1_2_3"
 [1, 2, 3].join()    # "123"
 ```
@@ -211,7 +211,7 @@ Joins the elements of the array by `separator`, defaulting to an empty string:
 
 Returns an array of the keys in the original array:
 
-``` bash
+``` py
 (1..2).keys() # [0, 1]
 ```
 
@@ -219,7 +219,7 @@ Returns an array of the keys in the original array:
 
 Modifies the array by applying the function `f` to all its elements:
 
-``` bash
+``` py
 [0, 1, 2].map(f(x){x+1}) # [1, 2, 3]
 ```
 
@@ -227,7 +227,7 @@ Modifies the array by applying the function `f` to all its elements:
 
 Pops the last element from the array, returning it:
 
-``` bash
+``` py
 a = [1, 2, 3]
 a.shift() # 3
 a # [1, 2]
@@ -237,13 +237,13 @@ a # [1, 2]
 
 Pushes an element at the end of the array:
 
-``` bash
+``` py
 [1, 2].push(3) # [1, 2, 3]
 ```
 
 This is equivalent to summing 2 arrays:
 
-``` bash
+``` py
 [1, 2] + [3] # [1, 2, 3]
 ```
 
@@ -251,7 +251,7 @@ This is equivalent to summing 2 arrays:
 
 Reverses the order of the elements in the array:
 
-``` bash
+``` py
 [1, 2].reverse() # [2, 1]
 ```
 
@@ -259,7 +259,7 @@ Reverses the order of the elements in the array:
 
 Removes the first elements from the array, and returns it:
 
-``` bash
+``` py
 a = [1, 2, 3]
 a.shift() # 1
 a # [2, 3]
@@ -269,7 +269,7 @@ a # [2, 3]
 
 Returns a portion of the array, from `start` to `end`:
 
-``` bash
+``` py
 (1..10).slice(0, 3) # [1, 2, 3]"
 ```
 
@@ -285,7 +285,7 @@ back as many characters as the value of `start`:
 Returns true when at least one of the elements in the array
 returns `true` when applied to the function `f`:
 
-``` bash
+``` py
 [0, 1, 2].map(f(x){x == 1}) # true
 [0, 1, 2].map(f(x){x == 4}) # false
 ```
@@ -295,7 +295,7 @@ returns `true` when applied to the function `f`:
 Sorts the array. Only supported on arrays of only numbers
 or only strings:
 
-``` bash
+```py
 [3, 1, 2].sort() # [1, 2, 3]
 ["b", "a", "c"].sort() # ["a", "b", "c"]
 ```
@@ -304,7 +304,7 @@ or only strings:
 
 Returns the string representation of the array:
 
-``` bash
+```py
 [1, 2].str() # "[1, 2]"
 ```
 
@@ -312,7 +312,7 @@ Returns the string representation of the array:
 
 Sums the elements of the array. Only supported on arrays of numbers:
 
-``` bash
+```py
 [1, 1, 1].sum() # 3
 ```
 
@@ -361,7 +361,7 @@ James	Harden	null	null
 
 Returns an array with unique values:
 
-``` bash
+```py
 [1, 1, 1, 2].unique() # [1, 2]
 ```
 
@@ -369,7 +369,7 @@ Returns an array with unique values:
 
 Computes the intersection between 2 arrays:
 
-```bash
+```py
 [1, 2, 3].intersect([]) # []
 [1, 2, 3].intersect([3]) # [3]
 [1, 2, 3].intersect([3, 1]) # [1, 3]
@@ -381,7 +381,7 @@ Computes the intersection between 2 arrays:
 Computes the difference between 2 arrays,
 returning elements that are only on the first array:
 
-```bash
+```py
 [1, 2, 3].diff([]) # [1, 2, 3]
 [1, 2, 3].diff([3]) # [1, 2]
 [1, 2, 3].diff([3, 1]) # [2]
@@ -395,7 +395,7 @@ For symmetric difference see [diff_symmetric(...)](#diff_symmetricarray)
 Computes the [symmetric difference](https://en.wikipedia.org/wiki/Symmetric_difference)
 between 2 arrays (elements that are only on either of the 2):
 
-```bash
+```py
 [1, 2, 3].diff([]) # [1, 2, 3]
 [1, 2, 3].diff([3]) # [1, 2]
 [1, 2, 3].diff([3, 1]) # [2]
@@ -407,13 +407,73 @@ between 2 arrays (elements that are only on either of the 2):
 Computes the [union](https://en.wikipedia.org/wiki/Union_(set_theory))
 between 2 arrays:
 
-```bash
+```py
 [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]
 ```
 
+### flatten()
+
+Flattens an array a single level deep:
+
+```py
+[[1, 2], 3, [4]].flatten() # [1, 2, 3, 4]
+[[1, 2, 3, 4]].flatten() # [1, 2, 3, 4]
+```
+
+### flatten_deep()
+
+Flattens an array recursively until no member is an array:
+
+```py
+[[[1, 2], [[[[3]]]], [4]]].flatten_deep() # [1, 2, 3, 4]
+[[1, [2, 3], 4]].flatten_deep() # [1, 2, 3, 4]
+```
+
+### max()
+
+Finds the highest number in an array:
+
+```py
+[].max() # NULL
+[0, 5, -10, 100].max() # 100
+```
+
+### min()
+
+Finds the lowest number in an array:
+
+```py
+[].min() # NULL
+[0, 5, -10, 100].min() # -10
+```
+
+### reduce(fn, accumulator)
+
+Reduces the array to a value by iterating through its elements and applying `fn` to them:
+
+```py
+[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 0) # 10
+[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 10) # 20
+```
+
+
+### partition(fn)
+
+Partitions the array by applying `fn` to all of its elements
+and using the result of the function invocation as the key to partition by:
+
+```py
+f odd(n) {
+  return !!(n % 2)
+}
+
+[0, 1, 2, 3, 4, 5].partition(odd) # [[0, 2, 4], [1, 3, 5]]
+[1, "1", {}].partition(str) # [[1, "1"], [{}]]
+```
+
 ## Next
 
 That's about it for this section!
diff --git a/evaluator/functions.go b/evaluator/functions.go
index 9ac4fb5..f2ca697 100644
--- a/evaluator/functions.go
+++ b/evaluator/functions.go
@@ -193,6 +193,21 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.ARRAY_OBJ},
 			Fn:    sumFn,
 		},
+		// max(array:[1, 2, 3])
+		"max": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    maxFn,
+		},
+		// min(array:[1, 2, 3])
+		"min": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    minFn,
+		},
+		// reduce(array:[1, 2, 3], f(){}, accumulator)
+		"reduce": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    reduceFn,
+		},
 		// sort(array:[1, 2, 3])
 		"sort": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
@@ -218,6 +233,21 @@ func getFns() map[string]*object.Builtin {
 			Types: []string{object.ARRAY_OBJ},
 			Fn:    diffSymmetricFn,
 		},
+		// flatten(array:[1, 2, 3])
+		"flatten": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    flattenFn,
+		},
+		// flatten(array:[1, 2, 3])
+		"flatten_deep": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    flattenDeepFn,
+		},
+		// partition(array:[1, 2, 3])
+		"partition": &object.Builtin{
+			Types: []string{object.ARRAY_OBJ},
+			Fn:    partitionFn,
+		},
 		// map(array:[1, 2, 3], function:f(x) { x + 1 })
 		"map": &object.Builtin{
 			Types: []string{object.ARRAY_OBJ},
@@ -414,7 +444,6 @@ func getFns() map[string]*object.Builtin {
 /*
 Here be the actual Builtin Functions
 */
-
 // Utility function that validates arguments passed to builtin functions.
 func validateArgs(tok token.Token, name string, args []object.Object, size int, types [][]string) object.Object {
 	if len(args) == 0 || len(args) > size || len(args) < size {
@@ -422,7 +451,7 @@ func validateArgs(tok token.Token, name string, args []object.Object, size int,
 	}
 
 	for i, t := range types {
-		if !util.Contains(t, string(args[i].Type())) {
+		if !util.Contains(t, string(args[i].Type())) && !util.Contains(t, object.ANY_OBJ) {
 			return newError(tok, "argument %d to %s(...) is not supported (got: %s, allowed: %s)", i, name, args[i].Inspect(), strings.Join(t, ", "))
 		}
 	}
@@ -1142,6 +1171,88 @@ func sumFn(tok token.Token, env *object.Environment, args ...object.Object) obje
 	return &object.Number{Token: tok, Value: sum}
 }
 
+// max(array:[1, 2, 3])
+func maxFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "max", args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	if arr.Empty() {
+		return object.NULL
+	}
+
+	if !arr.Homogeneous() {
+		return newError(tok, "max(...) can only be called on an homogeneous array, got %s", arr.Inspect())
+	}
+
+	if arr.Elements[0].Type() != object.NUMBER_OBJ {
+		return newError(tok, "max(...) can only be called on arrays of numbers, got %s", arr.Inspect())
+	}
+
+	max := arr.Elements[0].(*object.Number).Value
+
+	for _, v := range arr.Elements[1:] {
+		elem := v.(*object.Number)
+
+		if elem.Value > max {
+			max = elem.Value
+		}
+	}
+
+	return &object.Number{Token: tok, Value: max}
+}
+
+// min(array:[1, 2, 3])
+func minFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "min", args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	arr := args[0].(*object.Array)
+	if arr.Empty() {
+		return object.NULL
+	}
+
+	if !arr.Homogeneous() {
+		return newError(tok, "min(...) can only be called on an homogeneous array, got %s", arr.Inspect())
+	}
+
+	if arr.Elements[0].Type() != object.NUMBER_OBJ {
+		return newError(tok, "min(...) can only be called on arrays of numbers, got %s", arr.Inspect())
+	}
+
+	min := arr.Elements[0].(*object.Number).Value
+
+	for _, v := range arr.Elements[1:] {
+		elem := v.(*object.Number)
+
+		if elem.Value < min {
+			min = elem.Value
+		}
+	}
+
+	return &object.Number{Token: tok, Value: min}
+}
+
+// reduce(array:[1, 2, 3], f(){}, accumulator)
+func reduceFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "reduce", args, 3, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ}, {object.ANY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	accumulator := args[2]
+
+	for _, v := range args[0].(*object.Array).Elements {
+		accumulator = applyFunction(tok, args[1].(*object.Function), env, []object.Object{accumulator, v})
+	}
+
+	return accumulator
+}
+
 // sort(array:[1, 2, 3])
 func sortFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
 	err := validateArgs(tok, "sort", args, 1, [][]string{{object.ARRAY_OBJ}})
@@ -1290,6 +1401,86 @@ func unionFn(tok token.Token, env *object.Environment, args ...object.Object) ob
 	return &object.Array{Elements: union}
 }
 
+// flatten(array:[1, 2, 3])
+func flattenFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	return flatten("flatten", false, tok, env, args...)
+}
+
+// flatten_deep(array:[1, 2, 3])
+func flattenDeepFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	return flatten("flatten_deep", true, tok, env, args...)
+}
+
+func flatten(fnName string, deep bool, tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, fnName, args, 1, [][]string{{object.ARRAY_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	originalElements := args[0].(*object.Array).Elements
+	elements := []object.Object{}
+
+	for _, v := range originalElements {
+		switch e := v.(type) {
+		case *object.Array:
+			if deep {
+				elements = append(elements, flattenDeepFn(tok, env, e).(*object.Array).Elements...)
+			} else {
+				for _, x := range e.Elements {
+					elements = append(elements, x)
+				}
+			}
+		default:
+			elements = append(elements, e)
+		}
+	}
+
+	return &object.Array{Elements: elements}
+}
+
+func partitionFn(tok token.Token, env *object.Environment, args ...object.Object) object.Object {
+	err := validateArgs(tok, "partition", args, 2, [][]string{{object.ARRAY_OBJ}, {object.FUNCTION_OBJ, object.BUILTIN_OBJ}})
+	if err != nil {
+		return err
+	}
+
+	partitions := map[string][]object.Object{}
+	elements := args[0].(*object.Array).Elements
+	// This will allows us to preserve the order
+	// of partitions based on the order of elements.
+	//
+	// When we run the partitioning function, we store
+	// it's results in a map of result{list_of_values...}.
+	// When we loop over that map, Go doesn't guarantee
+	// order of results (https://nathanleclaire.com/blog/2014/04/27/a-surprising-feature-of-golang-that-colored-me-impressed/)
+	// but we want to, so
+	// we use the partitionOrder list to extract values
+	// from the map based on the order they were
+	// inserted in.
+	partitionOrder := []string{}
+	scanned := map[string]bool{}
+
+	for _, v := range elements {
+		res := applyFunction(tok, args[1], env, []object.Object{v})
+		eqs := object.GenerateEqualityString(res)
+
+		partitions[eqs] = append(partitions[eqs], v)
+
+		if _, ok := scanned[eqs]; !ok {
+			partitionOrder = append(partitionOrder, eqs)
+			scanned[eqs] = true
+		}
+	}
+
+	result := &object.Array{Elements: []object.Object{}}
+	for _, eqs := range partitionOrder {
+		partition := partitions[eqs]
+		result.Elements = append(result.Elements, &object.Array{Elements: partition})
+	}
+
+	return result
+}
+
 // 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}})
diff --git a/object/object.go b/object/object.go
index 42f88ef..f897fb0 100644
--- a/object/object.go
+++ b/object/object.go
@@ -27,6 +27,8 @@ const (
 
 	RETURN_VALUE_OBJ = "RETURN_VALUE"
 
+	// ANY_OBJ represents any ABS type
+	ANY_OBJ      = "ANY"
 	FUNCTION_OBJ = "FUNCTION"
 	BUILTIN_OBJ  = "BUILTIN"
 


Test patch:
diff --git a/evaluator/builtin_functions_test.go b/evaluator/builtin_functions_test.go
index 6e181eb..46b960c 100644
--- a/evaluator/builtin_functions_test.go
+++ b/evaluator/builtin_functions_test.go
@@ -667,6 +667,75 @@ func TestUnion(t *testing.T) {
 	testBuiltinFunction(tests, t)
 }
 
+func TestFlatten(t *testing.T) {
+	tests := []Tests{
+		{`[1, 2, 3].flatten()`, []int{1, 2, 3}},
+		{`[1, 2, [3]].flatten()`, []int{1, 2, 3}},
+		{`[1, 2, [3, 4]].flatten()`, []int{1, 2, 3, 4}},
+		{`[[1, 2], [3, 4]].flatten()`, []int{1, 2, 3, 4}},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestFlattenDeep(t *testing.T) {
+	tests := []Tests{
+		{`[1, 2, 3].flatten_deep()`, []int{1, 2, 3}},
+		{`[1, 2, [3]].flatten_deep()`, []int{1, 2, 3}},
+		{`[1, 2, [3, 4]].flatten_deep()`, []int{1, 2, 3, 4}},
+		{`[[1, 2], [3, 4]].flatten_deep()`, []int{1, 2, 3, 4}},
+		{`[[1, [2]], [3, 4]].flatten_deep()`, []int{1, 2, 3, 4}},
+		{`[[[1, [2]], [3, 4]]].flatten_deep()`, []int{1, 2, 3, 4}},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestMax(t *testing.T) {
+	tests := []Tests{
+		{`[].max()`, nil},
+		{`[-10].max()`, -10},
+		{`[-10, 0, 100, 9].max()`, 100},
+		{`[-10, 0, 100, 9, 100.1].max()`, 100.1},
+		{`[-10, {}, 100, 9].max()`, "max(...) can only be called on an homogeneous array, got [-10, {}, 100, 9]"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestMin(t *testing.T) {
+	tests := []Tests{
+		{`[].min()`, nil},
+		{`[-10].min()`, -10},
+		{`[-10, 0, 100, 9].min()`, -10},
+		{`[-10, 0, 100, 9, -10.5].min()`, -10.5},
+		{`[-10, {}, 100, 9].min()`, "min(...) can only be called on an homogeneous array, got [-10, {}, 100, 9]"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestReduce(t *testing.T) {
+	tests := []Tests{
+		{`[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 0)`, 10},
+		{`[1, 2, 3, 4].reduce(f(value, element) { return value + element }, 10)`, 20},
+		{`[1, 2, 3, 4].reduce(f(value, element) { return value + element })`, "wrong number of arguments to reduce(...)"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
+func TestPartition(t *testing.T) {
+	tests := []Tests{
+		{`[1, 1, 2, 2, 0].partition(f(x) { return x == 0 })[0]`, []int{1, 1, 2, 2}},
+		{`[1, 1, 2, 2, 0].partition(f(x) { return x == 0 })[1]`, []int{0}},
+		{`[1, "1"].partition(str)[0][0]`, 1},
+		{`[1, "1"].partition(str)[0][1]`, "1"},
+	}
+
+	testBuiltinFunction(tests, t)
+}
+
 func testBuiltinFunction(tests []Tests, t *testing.T) {
 	for _, tt := range tests {
 		evaluated := testEval(tt.input)