diff --git a/docs/src/modules/functional.yml b/docs/src/modules/functional.yml index c0b554f..3ab6bba 100644 --- a/docs/src/modules/functional.yml +++ b/docs/src/modules/functional.yml @@ -128,6 +128,17 @@ functions: ] println groupby(data, def(e) = e.k1) // {"2"=[{k1=2, k2=x}], "4"=[{k1=4, k2=z}], "5"=[{k2=p, k1=5}]} println groupby(data, def(e) = e.k2) // {"x"=[{k1=2, k2=x}], "z"=[{k1=4, k2=z}], "p"=[{k2=p, k1=5}]} + - name: tomap + args: "data, keyMapper, valueMapper = def(v) = v, merger = def(oldValue, newValue) = newValue" + desc: "converts elements of an array or a map to a map based on `keyMapper` and `valueMapper` functions result. `merger` function resolves collisions" + desc_ru: "преобразует элементы массива или объекта в объект, основываясь на результате функций `keyMapper` и `valueMapper`. Функция `merger` используется для разрешения коллизий" + since: 2.0.0 + example: |- + use functional + + data = ["apple", "banana"] + println tomap(data, def(str) = str.substring(0, 1)) // {"a": "apple", "b": "banana"} + println tomap(data, def(str) = str.substring(0, 1), ::toUpperCase) // {"a": "APPLE", "b": "BANANA"} - name: stream args: data desc: creates stream from data and returns `StreamValue` @@ -229,6 +240,26 @@ types: args: "" desc: returns array of elements desc_ru: возвращает массив элементов + - name: toMap + args: "keyMapper, valueMapper = def(v) = v, merger = def(oldValue, newValue) = newValue" + desc: "converts elements to a map based on `keyMapper` and `valueMapper` functions result. `merger` function resolves collisions" + desc_ru: "преобразует элементы в объект, основываясь на результате функций `keyMapper` и `valueMapper`. Функция `merger` используется для разрешения коллизий" + since: 2.0.0 + - name: anyMatch + args: predicate + desc: "returns `true` if there is any element matching the given `predicate`, otherwise returns `false`" + desc_ru: "возвращает `true`, если хотя бы один элемент удовлетворяет функции `predicate`, иначе возвращает `false`" + since: 2.0.0 + - name: allMatch + args: predicate + desc: "returns `true` if all elements match the given `predicate`, otherwise returns `false`" + desc_ru: "возвращает `true`, если все элементы удовлетворяют функции `predicate`, иначе возвращает `false`" + since: 2.0.0 + - name: noneMatch + args: predicate + desc: "returns `true` if no elements match the given `predicate`, otherwise returns `false`" + desc_ru: "возвращает `true`, если нет элементов, удовлетворяющих функции `predicate`, иначе возвращает `false`" + since: 2.0.0 - name: count args: "" desc: returns the elements count diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/StreamValue.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/StreamValue.java index ff26031..07defb5 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/StreamValue.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/StreamValue.java @@ -2,6 +2,7 @@ package com.annimon.ownlang.modules.functional; import com.annimon.ownlang.exceptions.ArgumentsMismatchException; import com.annimon.ownlang.lib.*; +import com.annimon.ownlang.modules.functional.functional_match.MatchType; import java.util.Arrays; class StreamValue extends MapValue { @@ -33,6 +34,10 @@ class StreamValue extends MapValue { set("forEachIndexed", wrapTerminal(new functional_forEachIndexed())); set("groupBy", wrapTerminal(new functional_groupBy())); set("toArray", args -> container); + set("toMap", wrapTerminal(new functional_toMap())); + set("anyMatch", wrapTerminal(functional_match.match(MatchType.ANY))); + set("allMatch", wrapTerminal(functional_match.match(MatchType.ALL))); + set("noneMatch", wrapTerminal(functional_match.match(MatchType.NONE))); set("joining", container::joinToString); set("count", args -> NumberValue.of(container.size())); } diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional.java index 711b170..d51bfe3 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional.java @@ -28,6 +28,7 @@ public final class functional implements Module { result.put("takewhile", new functional_takeWhile()); result.put("dropwhile", new functional_dropWhile()); result.put("groupby", new functional_groupBy()); + result.put("tomap", new functional_toMap()); result.put("chain", new functional_chain()); result.put("stream", new functional_stream()); diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_chain.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_chain.java index 9fd1f29..602999b 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_chain.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_chain.java @@ -5,7 +5,7 @@ import com.annimon.ownlang.lib.Function; import com.annimon.ownlang.lib.Value; import com.annimon.ownlang.lib.ValueUtils; -public final class functional_chain implements Function { +final class functional_chain implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_combine.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_combine.java index e5c9e28..d37432b 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_combine.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_combine.java @@ -7,7 +7,7 @@ import com.annimon.ownlang.lib.FunctionValue; import com.annimon.ownlang.lib.Types; import com.annimon.ownlang.lib.Value; -public final class functional_combine implements Function { +final class functional_combine implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_dropWhile.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_dropWhile.java index 11cbff3..5bd469b 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_dropWhile.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_dropWhile.java @@ -9,7 +9,7 @@ import com.annimon.ownlang.lib.Types; import com.annimon.ownlang.lib.Value; import com.annimon.ownlang.lib.ValueUtils; -public final class functional_dropWhile implements Function { +final class functional_dropWhile implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filter.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filter.java index 6f1c1ca..0600b43 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filter.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filter.java @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -public final class functional_filter implements Function { +final class functional_filter implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filterNot.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filterNot.java index 7ab6e20..1987c9b 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filterNot.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_filterNot.java @@ -2,7 +2,7 @@ package com.annimon.ownlang.modules.functional; import com.annimon.ownlang.lib.*; -public final class functional_filterNot implements Function { +final class functional_filterNot implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_flatmap.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_flatmap.java index 775be2b..e59302d 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_flatmap.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_flatmap.java @@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.ValueUtils; import java.util.ArrayList; import java.util.List; -public final class functional_flatmap implements Function { +final class functional_flatmap implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEach.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEach.java index a9b9b7a..d25daa1 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEach.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEach.java @@ -4,7 +4,7 @@ import com.annimon.ownlang.exceptions.TypeException; import com.annimon.ownlang.lib.*; import java.util.Map; -public final class functional_forEach implements Function { +final class functional_forEach implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEachIndexed.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEachIndexed.java index 4ab3ee7..bd86eb4 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEachIndexed.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_forEachIndexed.java @@ -3,7 +3,7 @@ package com.annimon.ownlang.modules.functional; import com.annimon.ownlang.exceptions.TypeException; import com.annimon.ownlang.lib.*; -public final class functional_forEachIndexed implements Function { +final class functional_forEachIndexed implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_groupBy.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_groupBy.java index 8b8c484..f90a2f1 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_groupBy.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_groupBy.java @@ -7,7 +7,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -public final class functional_groupBy implements Function { +final class functional_groupBy implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_map.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_map.java index 5ca801c..5d43a14 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_map.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_map.java @@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.Value; import com.annimon.ownlang.lib.ValueUtils; import java.util.Map; -public final class functional_map implements Function { +final class functional_map implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_match.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_match.java new file mode 100644 index 0000000..d76995a --- /dev/null +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_match.java @@ -0,0 +1,52 @@ +package com.annimon.ownlang.modules.functional; + +import com.annimon.ownlang.exceptions.TypeException; +import com.annimon.ownlang.lib.*; + +final class functional_match { + + enum MatchType { ALL, ANY, NONE } + + static Function match(MatchType matchType) { + return args -> { + Arguments.check(2, args.length); + final Value container = args[0]; + if (container.type() != Types.ARRAY) { + throw new TypeException("Invalid first argument. Array expected"); + } + final Function predicate = ValueUtils.consumeFunction(args[1], 1); + return switch (matchType) { + case ALL -> allMatch((ArrayValue) container, predicate); + case ANY -> anyMatch((ArrayValue) container, predicate); + case NONE -> noneMatch((ArrayValue) container, predicate); + }; + }; + } + + private static NumberValue allMatch(ArrayValue array, Function predicate) { + for (Value value : array) { + if (predicate.execute(value) == NumberValue.ZERO) { + return NumberValue.ZERO; + } + } + return NumberValue.ONE; + } + + private static NumberValue anyMatch(ArrayValue array, Function predicate) { + for (Value value : array) { + if (predicate.execute(value) != NumberValue.ZERO) { + return NumberValue.ONE; + } + } + return NumberValue.ZERO; + } + + private static NumberValue noneMatch(ArrayValue array, Function predicate) { + for (Value value : array) { + if (predicate.execute(value) != NumberValue.ZERO) { + return NumberValue.ZERO; + } + } + return NumberValue.ONE; + } +} diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_reduce.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_reduce.java index cc03dfd..b14b82b 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_reduce.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_reduce.java @@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.Value; import com.annimon.ownlang.lib.ValueUtils; import java.util.Map; -public final class functional_reduce implements Function { +final class functional_reduce implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_sortBy.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_sortBy.java index a395ee9..2292aec 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_sortBy.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_sortBy.java @@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.ValueUtils; import java.util.Arrays; import java.util.Comparator; -public final class functional_sortBy implements Function { +final class functional_sortBy implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_stream.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_stream.java index 2017326..5ae744b 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_stream.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_stream.java @@ -3,7 +3,7 @@ package com.annimon.ownlang.modules.functional; import com.annimon.ownlang.exceptions.TypeException; import com.annimon.ownlang.lib.*; -public final class functional_stream implements Function { +final class functional_stream implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_takeWhile.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_takeWhile.java index 3a0569d..33f7261 100644 --- a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_takeWhile.java +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_takeWhile.java @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; -public final class functional_takeWhile implements Function { +final class functional_takeWhile implements Function { @Override public Value execute(Value[] args) { diff --git a/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_toMap.java b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_toMap.java new file mode 100644 index 0000000..cb7c138 --- /dev/null +++ b/modules/main/src/main/java/com/annimon/ownlang/modules/functional/functional_toMap.java @@ -0,0 +1,63 @@ +package com.annimon.ownlang.modules.functional; + +import com.annimon.ownlang.exceptions.TypeException; +import com.annimon.ownlang.lib.*; +import java.util.LinkedHashMap; +import java.util.Map; + +final class functional_toMap implements Function { + + @Override + public Value execute(Value[] args) { + Arguments.checkRange(2, 4, args.length); + final Value container = args[0]; + final Function keyMapper = ValueUtils.consumeFunction(args[1], 1); + final Function valueMapper = args.length >= 3 + ? ValueUtils.consumeFunction(args[2], 2) + : null; + final Function merger = args.length >= 4 + ? ValueUtils.consumeFunction(args[3], 3) + : null; + return toMap(container, keyMapper, valueMapper, merger); + } + + static MapValue toMap(Value container, Function keyMapper, Function valueMapper, Function merger) { + return switch (container.type()) { + case Types.ARRAY -> toMap((ArrayValue) container, keyMapper, valueMapper, merger); + case Types.MAP -> toMap((MapValue) container, keyMapper, valueMapper, merger); + default -> throw new TypeException("Cannot iterate " + Types.typeToString(container.type())); + }; + } + + static MapValue toMap(ArrayValue array, Function keyMapper, Function valueMapper, Function merger) { + final Map result = new LinkedHashMap<>(array.size()); + for (Value element : array) { + final Value key = keyMapper.execute(element); + final Value value = valueMapper != null + ? valueMapper.execute(element) + : element; + final Value oldValue = result.get(key); + final Value newValue = (oldValue == null || merger == null) + ? value + : merger.execute(oldValue, value); + result.put(key, newValue); + } + return new MapValue(result); + } + + static MapValue toMap(MapValue map, Function keyMapper, Function valueMapper, Function merger) { + final Map result = new LinkedHashMap<>(map.size()); + for (Map.Entry element : map) { + final Value key = keyMapper.execute(element.getKey(), element.getValue()); + final Value value = valueMapper != null + ? valueMapper.execute(element.getKey(), element.getValue()) + : element.getValue(); + final Value oldValue = result.get(key); + final Value newValue = (oldValue == null || merger == null) + ? value + : merger.execute(oldValue, value); + result.put(key, newValue); + } + return new MapValue(result); + } +} diff --git a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java index 1475ef4..a97edfc 100644 --- a/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java +++ b/ownlang-parser/src/main/java/com/annimon/ownlang/parser/ast/FunctionalExpression.java @@ -54,6 +54,9 @@ public final class FunctionalExpression extends InterruptableNode private Function consumeFunction(Node expr) { final Value value = expr.eval(); + if (value == null) { + throw new UnknownFunctionException(expr.toString(), getRange()); + } if (value.type() == Types.FUNCTION) { return ((FunctionValue) value).getValue(); } diff --git a/ownlang-parser/src/test/resources/modules/functional/stream.own b/ownlang-parser/src/test/resources/modules/functional/stream.own index 394edc9..da98a29 100644 --- a/ownlang-parser/src/test/resources/modules/functional/stream.own +++ b/ownlang-parser/src/test/resources/modules/functional/stream.own @@ -125,3 +125,32 @@ def testMapsGroupBy() { assertEquals([["test1", 234], ["test2", 345], ["test3", 456]], sort(result[true])) assertEquals([["abc", 123], ["def", 567]], sort(result[false])) } + +def testToMap() { + data = ["apple", "banana", "cherry"] + result = stream(data) + .toMap(def(str) = str.substring(0, 1), ::toUpperCase) + assertEquals("APPLE", result.a) + assertEquals("BANANA", result.b) + assertEquals("CHERRY", result.c) +} + +def testAllMatch() { + data = [2, 4, 8, 20] + assertTrue(stream(data).allMatch(def(v) = v % 2 == 0)) + assertFalse(stream(data).allMatch(def(v) = v < 10)) +} + +def testAnyMatch() { + data = [2, 4, 8, 20] + assertTrue(stream(data).anyMatch(def(v) = v > 10)) + assertFalse(stream(data).anyMatch(def(v) = v % 2 == 1)) +} + +def testNoneMatch() { + data = [2, 4, 8, 20] + assertTrue(stream(data).noneMatch(def(v) = v % 2 == 1)) + assertFalse(stream(data).noneMatch(def(v) = v > 10)) +} + + diff --git a/ownlang-parser/src/test/resources/modules/functional/tomap.own b/ownlang-parser/src/test/resources/modules/functional/tomap.own new file mode 100644 index 0000000..a834571 --- /dev/null +++ b/ownlang-parser/src/test/resources/modules/functional/tomap.own @@ -0,0 +1,39 @@ +use std, functional + +def testArrayToMapByKeyMapper() { + data = ["apple", "banana", "cherry"] + result = tomap(data, def(str) = str.substring(0, 1)) + assertEquals("apple", result.a) + assertEquals("banana", result.b) + assertEquals("cherry", result.c) +} + +def testArrayToMapByKeyValueMapper() { + data = ["apple", "banana", "cherry"] + result = tomap(data, def(str) = str.substring(0, 1), ::toUpperCase) + assertEquals("APPLE", result.a) + assertEquals("BANANA", result.b) + assertEquals("CHERRY", result.c) +} + +def testArrayToMapByKeyValueMapperAndMerger() { + data = ["apple", "banana", "cherry", "apricot", "coconut"] + result = tomap(data, def(str) = str.substring(0, 1), ::toUpperCase, def(oldValue, newValue) = oldValue + ", " + newValue) + assertEquals("APPLE, APRICOT", result.a) + assertEquals("BANANA", result.b) + assertEquals("CHERRY, COCONUT", result.c) +} + +def testMapToMapByKeyMapper() { + data = {"k1": 1, "k2": 2} + result = tomap(data, def(k, v) = k + "" + v) + assertEquals(1, result.k11) + assertEquals(2, result.k22) +} + +def testMapToMapByKeyValueMapper() { + data = {"k1": 1, "k2": 2} + result = tomap(data, def(k, v) = k + "" + v, def(k, v) = v + 10) + assertEquals(11, result.k11) + assertEquals(12, result.k22) +}