Skip to content

Commit 7da4411

Browse files
authored
Add reduce/fold Expression (SkriptLang#8353)
1 parent 1e6bf23 commit 7da4411

3 files changed

Lines changed: 295 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package ch.njol.skript.expressions;
2+
3+
import ch.njol.skript.Skript;
4+
import ch.njol.skript.doc.*;
5+
import ch.njol.skript.lang.*;
6+
import ch.njol.skript.lang.SkriptParser.ParseResult;
7+
import ch.njol.skript.lang.parser.ParserInstance;
8+
import ch.njol.skript.lang.util.SimpleExpression;
9+
import ch.njol.skript.util.LiteralUtils;
10+
import ch.njol.util.Kleenean;
11+
import org.bukkit.event.Event;
12+
import org.jetbrains.annotations.Nullable;
13+
import org.jetbrains.annotations.UnknownNullability;
14+
15+
import java.util.*;
16+
17+
@Name("Reduce")
18+
@Description({
19+
"Reduces lists to single values by repeatedly applying an operation.",
20+
"The reduce expression takes each element and combines it with an accumulator value.",
21+
"Use 'reduced value' to access the current accumulated value and 'input' for the current element.",
22+
})
23+
@Example("set {_sum} to {_numbers::*} reduced with [reduced value + input]")
24+
@Example("set {_product} to {_values::*} reduced with [reduced value * input]")
25+
@Example("set {_concatenated} to {_strings::*} reduced with [\"%reduced value%%input%\"]")
26+
@Since("INSERT VERSION")
27+
@Keywords({"input", "reduced value", "accumulator"})
28+
public class ExprReduce extends SimpleExpression<Object> implements InputSource {
29+
30+
static {
31+
Skript.registerExpression(ExprReduce.class, Object.class, ExpressionType.PATTERN_MATCHES_EVERYTHING,
32+
"%objects% (reduced|folded) (to|with|by) \\[<.+>\\]",
33+
"%objects% (reduced|folded) (to|with|by) \\(<.+>\\)"
34+
);
35+
if (!ParserInstance.isRegistered(InputData.class))
36+
ParserInstance.registerData(InputData.class, InputData::new);
37+
}
38+
39+
private boolean keyed;
40+
private Expression<?> reduceExpr;
41+
private Expression<?> unreducedObjects;
42+
43+
private final Set<ExprInput<?>> dependentInputs = new HashSet<>();
44+
45+
private @Nullable Object currentValue;
46+
private @Nullable Object reducedValue;
47+
private @UnknownNullability String currentIndex;
48+
49+
@Override
50+
public boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
51+
unreducedObjects = LiteralUtils.defendExpression(expressions[0]);
52+
if (unreducedObjects.isSingle()) {
53+
Skript.error("A single value cannot be reduced. Only lists can be reduced.");
54+
return false;
55+
}
56+
if ((!LiteralUtils.canInitSafely(unreducedObjects)) || parseResult.regexes.isEmpty()) {
57+
return false;
58+
}
59+
60+
keyed = KeyProviderExpression.canReturnKeys(unreducedObjects);
61+
62+
@Nullable String unparsedExpression = parseResult.regexes.getFirst().group();
63+
assert unparsedExpression != null;
64+
reduceExpr = parseExpression(unparsedExpression, getParser(), SkriptParser.ALL_FLAGS);
65+
66+
return reduceExpr != null;
67+
}
68+
69+
@Override
70+
protected Object @Nullable [] get(Event event) {
71+
try {
72+
boolean hadNullResult = false;
73+
74+
if (keyed) {
75+
Iterator<? extends KeyedValue<?>> keyedIterator = ((KeyProviderExpression<?>) unreducedObjects).keyedIterator(event);
76+
if (keyedIterator == null || !keyedIterator.hasNext())
77+
return new Object[0];
78+
79+
KeyedValue<?> first = keyedIterator.next();
80+
reducedValue = first.value();
81+
currentIndex = first.key();
82+
83+
while (keyedIterator.hasNext()) {
84+
KeyedValue<?> next = keyedIterator.next();
85+
currentValue = next.value();
86+
currentIndex = next.key();
87+
88+
Object result = reduceExpr.getSingle(event);
89+
if (result != null) {
90+
reducedValue = result;
91+
} else {
92+
hadNullResult = true;
93+
}
94+
}
95+
} else {
96+
currentIndex = null;
97+
98+
Iterator<?> iterator = unreducedObjects.iterator(event);
99+
if (iterator == null || !iterator.hasNext())
100+
return null;
101+
102+
reducedValue = iterator.next();
103+
104+
int index = 1;
105+
while (iterator.hasNext()) {
106+
currentValue = iterator.next();
107+
currentIndex = String.valueOf(index);
108+
109+
Object result = reduceExpr.getSingle(event);
110+
if (result != null) {
111+
reducedValue = result;
112+
} else {
113+
hadNullResult = true;
114+
}
115+
116+
index++;
117+
}
118+
}
119+
120+
if (hadNullResult) {
121+
warning("The reduce expression returned null for one or more elements, which were skipped.");
122+
}
123+
124+
return new Object[] { reducedValue };
125+
} finally {
126+
currentValue = null;
127+
reducedValue = null;
128+
currentIndex = null;
129+
}
130+
}
131+
132+
@Override
133+
public boolean isSingle() {
134+
return true;
135+
}
136+
137+
@Override
138+
public Class<?> getReturnType() {
139+
return reduceExpr.getReturnType();
140+
}
141+
142+
@Override
143+
public Class<?>[] possibleReturnTypes() {
144+
return reduceExpr.possibleReturnTypes();
145+
}
146+
147+
@Override
148+
public boolean canReturn(Class<?> returnType) {
149+
return reduceExpr.canReturn(returnType);
150+
}
151+
152+
@Override
153+
public Set<ExprInput<?>> getDependentInputs() {
154+
return dependentInputs;
155+
}
156+
157+
@Override
158+
public @Nullable Object getCurrentValue() {
159+
return currentValue;
160+
}
161+
162+
/**
163+
* Gets the current reduced/accumulated value.
164+
* This is accessible via ExprReducedValue.
165+
*/
166+
public @Nullable Object getReducedValue() {
167+
return reducedValue;
168+
}
169+
170+
@Override
171+
public boolean hasIndices() {
172+
return keyed;
173+
}
174+
175+
@Override
176+
public @UnknownNullability String getCurrentIndex() {
177+
return currentIndex;
178+
}
179+
180+
@Override
181+
public String toString(@Nullable Event event, boolean debug) {
182+
return unreducedObjects.toString(event, debug) + " reduced with [" + reduceExpr.toString(event, debug) + "]";
183+
}
184+
185+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package ch.njol.skript.expressions;
2+
3+
import ch.njol.skript.Skript;
4+
import ch.njol.skript.doc.*;
5+
import ch.njol.skript.lang.Expression;
6+
import ch.njol.skript.lang.ExpressionType;
7+
import ch.njol.skript.lang.InputSource;
8+
import ch.njol.skript.lang.InputSource.InputData;
9+
import ch.njol.skript.lang.SkriptParser.ParseResult;
10+
import ch.njol.skript.lang.util.SimpleExpression;
11+
import ch.njol.util.Kleenean;
12+
import org.bukkit.event.Event;
13+
import org.jetbrains.annotations.Nullable;
14+
15+
@Name("Reduced Value")
16+
@Description({
17+
"Returns the current accumulated/reduced value within a <a href='#ExprReduce'>reduce expression</a>.",
18+
"This represents the result of all previous reduction operations.",
19+
"Can only be used inside the reduce expression's operation block."
20+
})
21+
@Example("set {_sum} to {_numbers::*} reduced with [reduced value + input]")
22+
@Example("set {_max} to {_values::*} reduced with [reduced value if reduced value > input else input]")
23+
@Example("set {_combined} to {_items::*} reduced with (\"%reduced value%, %input%\")")
24+
@Since("INSERT VERSION")
25+
public class ExprReducedValue extends SimpleExpression<Object> {
26+
27+
static {
28+
Skript.registerExpression(ExprReducedValue.class, Object.class, ExpressionType.SIMPLE,
29+
"[the] reduced value",
30+
"[the] (accumulator|accumulated) [value]",
31+
"[the] folded value"
32+
);
33+
}
34+
35+
private ExprReduce reduce;
36+
37+
@Override
38+
public boolean init(Expression<?>[] exprs, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
39+
InputSource inputSource = getParser().getData(InputData.class).getSource();
40+
if (!(inputSource instanceof ExprReduce exprReduce)) {
41+
Skript.error("The 'reduced value' expression can only be used within a reduce operation");
42+
return false;
43+
}
44+
45+
this.reduce = exprReduce;
46+
return true;
47+
}
48+
49+
@Override
50+
protected Object @Nullable [] get(Event event) {
51+
Object reducedValue = reduce.getReducedValue();
52+
return reducedValue == null ? new Object[0] : new Object[] { reducedValue };
53+
}
54+
55+
@Override
56+
public boolean isSingle() {
57+
return true;
58+
}
59+
60+
@Override
61+
public Class<?> getReturnType() {
62+
return Object.class;
63+
}
64+
65+
@Override
66+
public String toString(@Nullable Event event, boolean debug) {
67+
return "reduced value";
68+
}
69+
70+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
using error catching
3+
4+
test "basic sum":
5+
set {_numbers::*} to 1, 2, 3, 4, and 5
6+
set {_sum} to {_numbers::*} reduced with [reduced value + input]
7+
assert {_sum} is 15 with "sum of 1+2+3+4+5 should be 15", expected 15, got {_sum}
8+
9+
test "string concatenation":
10+
set {_strings::*} to "Hello", " ", "World", and "!"
11+
set {_result} to {_strings::*} reduced with ["%reduced value%%input%"]
12+
assert {_result} is "Hello World!" with "concatenation should produce 'Hello World!'", expected "Hello World!", got {_result}
13+
14+
test "single element":
15+
set {_single::*} to 42
16+
set {_result} to {_single::*} reduced with [reduced value + input]
17+
assert {_result} is 42 with "reducing single element should return that element", expected 42, got {_result}
18+
19+
test "keyed list":
20+
set {_values::a} to 1
21+
set {_values::b} to 2
22+
set {_values::c} to 3
23+
set {_sum} to (keyed {_values::*}) reduced with [reduced value + input]
24+
assert {_sum} is 6 with "sum of keyed list (1+2+3) should be 6", expected 6, got {_sum}
25+
26+
test "reducing with input index with keyed list":
27+
set {_data::x} to 10
28+
set {_data::y} to 20
29+
set {_data::z} to 30
30+
set {_result} to {_data::*} reduced with ["%reduced value%|%input index%:%input%"]
31+
# First element: "10" (initial value)
32+
# Second: "10|y:20"
33+
# Third: "10|y:20|z:30"
34+
assert {_result} contains "|y:20" with "should contain y:20 from input index"
35+
assert {_result} contains "|z:30" with "should contain z:30 from input index"
36+
37+
test "null after reduction":
38+
catch runtime errors:
39+
set {_sum} to ("1", 2, stone) reduced with [reduced value + input]
40+
assert last caught runtime errors contains "The reduce expression returned null for one or more elements, which were skipped." with "should runtime error from null elements"

0 commit comments

Comments
 (0)