[functional] Add tomap, Stream::toMap, Stream::anyMatch, Stream::allMatch, Stream::noneMatch

This commit is contained in:
aNNiMON 2024-01-01 14:10:08 +02:00 committed by Victor Melnik
parent 8ca3672204
commit 5a533cc6e1
22 changed files with 237 additions and 14 deletions

View File

@ -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.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}]} 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 - name: stream
args: data args: data
desc: creates stream from data and returns `StreamValue` desc: creates stream from data and returns `StreamValue`
@ -229,6 +240,26 @@ types:
args: "" args: ""
desc: returns array of elements desc: returns array of elements
desc_ru: возвращает массив элементов 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 - name: count
args: "" args: ""
desc: returns the elements count desc: returns the elements count

View File

@ -2,6 +2,7 @@ package com.annimon.ownlang.modules.functional;
import com.annimon.ownlang.exceptions.ArgumentsMismatchException; import com.annimon.ownlang.exceptions.ArgumentsMismatchException;
import com.annimon.ownlang.lib.*; import com.annimon.ownlang.lib.*;
import com.annimon.ownlang.modules.functional.functional_match.MatchType;
import java.util.Arrays; import java.util.Arrays;
class StreamValue extends MapValue { class StreamValue extends MapValue {
@ -33,6 +34,10 @@ class StreamValue extends MapValue {
set("forEachIndexed", wrapTerminal(new functional_forEachIndexed())); set("forEachIndexed", wrapTerminal(new functional_forEachIndexed()));
set("groupBy", wrapTerminal(new functional_groupBy())); set("groupBy", wrapTerminal(new functional_groupBy()));
set("toArray", args -> container); 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("joining", container::joinToString);
set("count", args -> NumberValue.of(container.size())); set("count", args -> NumberValue.of(container.size()));
} }

View File

@ -28,6 +28,7 @@ public final class functional implements Module {
result.put("takewhile", new functional_takeWhile()); result.put("takewhile", new functional_takeWhile());
result.put("dropwhile", new functional_dropWhile()); result.put("dropwhile", new functional_dropWhile());
result.put("groupby", new functional_groupBy()); result.put("groupby", new functional_groupBy());
result.put("tomap", new functional_toMap());
result.put("chain", new functional_chain()); result.put("chain", new functional_chain());
result.put("stream", new functional_stream()); result.put("stream", new functional_stream());

View File

@ -5,7 +5,7 @@ import com.annimon.ownlang.lib.Function;
import com.annimon.ownlang.lib.Value; import com.annimon.ownlang.lib.Value;
import com.annimon.ownlang.lib.ValueUtils; import com.annimon.ownlang.lib.ValueUtils;
public final class functional_chain implements Function { final class functional_chain implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -7,7 +7,7 @@ import com.annimon.ownlang.lib.FunctionValue;
import com.annimon.ownlang.lib.Types; import com.annimon.ownlang.lib.Types;
import com.annimon.ownlang.lib.Value; import com.annimon.ownlang.lib.Value;
public final class functional_combine implements Function { final class functional_combine implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -9,7 +9,7 @@ import com.annimon.ownlang.lib.Types;
import com.annimon.ownlang.lib.Value; import com.annimon.ownlang.lib.Value;
import com.annimon.ownlang.lib.ValueUtils; import com.annimon.ownlang.lib.ValueUtils;
public final class functional_dropWhile implements Function { final class functional_dropWhile implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -6,7 +6,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public final class functional_filter implements Function { final class functional_filter implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -2,7 +2,7 @@ package com.annimon.ownlang.modules.functional;
import com.annimon.ownlang.lib.*; import com.annimon.ownlang.lib.*;
public final class functional_filterNot implements Function { final class functional_filterNot implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.ValueUtils;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public final class functional_flatmap implements Function { final class functional_flatmap implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -4,7 +4,7 @@ import com.annimon.ownlang.exceptions.TypeException;
import com.annimon.ownlang.lib.*; import com.annimon.ownlang.lib.*;
import java.util.Map; import java.util.Map;
public final class functional_forEach implements Function { final class functional_forEach implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -3,7 +3,7 @@ package com.annimon.ownlang.modules.functional;
import com.annimon.ownlang.exceptions.TypeException; import com.annimon.ownlang.exceptions.TypeException;
import com.annimon.ownlang.lib.*; import com.annimon.ownlang.lib.*;
public final class functional_forEachIndexed implements Function { final class functional_forEachIndexed implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -7,7 +7,7 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public final class functional_groupBy implements Function { final class functional_groupBy implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.Value;
import com.annimon.ownlang.lib.ValueUtils; import com.annimon.ownlang.lib.ValueUtils;
import java.util.Map; import java.util.Map;
public final class functional_map implements Function { final class functional_map implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -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;
}
}

View File

@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.Value;
import com.annimon.ownlang.lib.ValueUtils; import com.annimon.ownlang.lib.ValueUtils;
import java.util.Map; import java.util.Map;
public final class functional_reduce implements Function { final class functional_reduce implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -10,7 +10,7 @@ import com.annimon.ownlang.lib.ValueUtils;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
public final class functional_sortBy implements Function { final class functional_sortBy implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -3,7 +3,7 @@ package com.annimon.ownlang.modules.functional;
import com.annimon.ownlang.exceptions.TypeException; import com.annimon.ownlang.exceptions.TypeException;
import com.annimon.ownlang.lib.*; import com.annimon.ownlang.lib.*;
public final class functional_stream implements Function { final class functional_stream implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -6,7 +6,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public final class functional_takeWhile implements Function { final class functional_takeWhile implements Function {
@Override @Override
public Value execute(Value[] args) { public Value execute(Value[] args) {

View File

@ -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<Value, Value> 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<Value, Value> result = new LinkedHashMap<>(map.size());
for (Map.Entry<Value, Value> 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);
}
}

View File

@ -54,6 +54,9 @@ public final class FunctionalExpression extends InterruptableNode
private Function consumeFunction(Node expr) { private Function consumeFunction(Node expr) {
final Value value = expr.eval(); final Value value = expr.eval();
if (value == null) {
throw new UnknownFunctionException(expr.toString(), getRange());
}
if (value.type() == Types.FUNCTION) { if (value.type() == Types.FUNCTION) {
return ((FunctionValue) value).getValue(); return ((FunctionValue) value).getValue();
} }

View File

@ -125,3 +125,32 @@ def testMapsGroupBy() {
assertEquals([["test1", 234], ["test2", 345], ["test3", 456]], sort(result[true])) assertEquals([["test1", 234], ["test2", 345], ["test3", 456]], sort(result[true]))
assertEquals([["abc", 123], ["def", 567]], sort(result[false])) 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))
}

View File

@ -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)
}