Skip to content

Commit 208609e

Browse files
refactor: Pre-compute PrecedenceLevelInfo (Big Commit)
1 parent 4ff1ed8 commit 208609e

File tree

6 files changed

+119
-45
lines changed

6 files changed

+119
-45
lines changed

src/main/java/net/marcellperger/mathexpr/BinaryOperation.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@ public interface BinaryOperation extends MathSymbol {
88
default String fmt() {
99
return "%s(%s, %s)".formatted(this.getClass().getSimpleName(), getLeft().fmt(), getRight().fmt());
1010
}
11+
12+
static MathSymbol construct(MathSymbol left, SymbolInfo op, MathSymbol right) {
13+
return op.getBiConstructor().construct(left, right);
14+
}
1115
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package net.marcellperger.mathexpr;
2+
3+
import net.marcellperger.mathexpr.util.Util;
4+
import net.marcellperger.mathexpr.util.UtilCollectors;
5+
import org.jetbrains.annotations.Contract;
6+
import org.jetbrains.annotations.NotNull;
7+
import org.jetbrains.annotations.Nullable;
8+
9+
import java.util.Comparator;
10+
import java.util.List;
11+
import java.util.Map;
12+
import java.util.Set;
13+
import java.util.stream.Collectors;
14+
15+
/**
16+
* Data for a specific precedence level
17+
*/
18+
public class PrecedenceLevelInfo {
19+
public Set<SymbolInfo> symbols;
20+
public Map<String, SymbolInfo> infixToSymbolMap;
21+
public List<String> sortedInfixes;
22+
public @Nullable GroupingDirection dirn;
23+
public int precedence;
24+
25+
/**
26+
* @param precedence_ The precedence level (gets data from SymbolInfo)
27+
*/
28+
public PrecedenceLevelInfo(int precedence_) {
29+
precedence = precedence_;
30+
symbols = SymbolInfo.PREC_TO_INFO_MAP.get(precedence);
31+
// Shouldn't be passed an empty level in the first place
32+
Util.requireNonEmptyNonNull(symbols, "Cannot create PrecedenceLevelInfo for a level with no items");
33+
dirn = symbols.stream()
34+
.map(sm -> sm.groupingDirection).collect(UtilCollectors.singleDistinctItem());
35+
try {
36+
infixToSymbolMap = symbols.stream().collect(
37+
Collectors.toMap(
38+
info -> Util.requireNonNull(info.infix, new NullInfixException()),
39+
info -> info));
40+
} catch (NullInfixException e) {
41+
// a precedence level should really be uniform so if any infix is null,
42+
// we set this to null. This may help avoid any subtle bugs later.
43+
infixToSymbolMap = null;
44+
sortedInfixes = null;
45+
}
46+
if(infixToSymbolMap != null) {
47+
sortedInfixes = infixToSymbolMap.keySet().stream()
48+
.sorted(Comparator.comparingInt(String::length).reversed()).toList();
49+
}
50+
}
51+
52+
@Contract("_ -> new")
53+
public static Map.@NotNull Entry<Integer, PrecedenceLevelInfo> newMapEntry(int precedence) {
54+
return Util.makeEntry(precedence, new PrecedenceLevelInfo(precedence));
55+
}
56+
57+
/** Marker exception that we throw to signal that there should be no infix info at all */
58+
private static class NullInfixException extends RuntimeException {}
59+
}

src/main/java/net/marcellperger/mathexpr/SymbolInfo.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package net.marcellperger.mathexpr;
22

33
import net.marcellperger.mathexpr.util.Util;
4+
import net.marcellperger.mathexpr.util.UtilCollectors;
45
import org.jetbrains.annotations.NotNull;
56
import org.jetbrains.annotations.Nullable;
67

@@ -30,6 +31,7 @@ public enum SymbolInfo {
3031
public static final Map<Integer, Set<SymbolInfo>> PREC_TO_INFO_MAP;
3132
public static final List<Entry<Integer, Set<SymbolInfo>>> PREC_SORTED_INFO;
3233
public static final int MAX_PRECEDENCE;
34+
public static final Map<Integer, PrecedenceLevelInfo> PREC_LEVELS_INFO;
3335

3436
public final int precedence; // TODO make this Integer
3537
public final Class<? extends MathSymbol> cls;
@@ -106,5 +108,6 @@ public enum SymbolInfo {
106108
PREC_TO_INFO_MAP = Arrays.stream(values()).collect(Collectors.groupingBy(s -> s.precedence, Collectors.toUnmodifiableSet()));
107109
PREC_SORTED_INFO = PREC_TO_INFO_MAP.entrySet().stream().sorted(Comparator.comparingInt(Entry::getKey)).toList();
108110
MAX_PRECEDENCE = PREC_SORTED_INFO.getLast().getKey();
111+
PREC_LEVELS_INFO = PREC_TO_INFO_MAP.keySet().stream().map(PrecedenceLevelInfo::newMapEntry).collect(UtilCollectors.entriesToMap());
109112
}
110113
}

src/main/java/net/marcellperger/mathexpr/parser/Parser.java

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
import net.marcellperger.mathexpr.*;
44
import net.marcellperger.mathexpr.util.Pair;
55
import net.marcellperger.mathexpr.util.Util;
6-
import net.marcellperger.mathexpr.util.UtilCollectors;
76
import org.jetbrains.annotations.NotNull;
87
import org.jetbrains.annotations.Nullable;
98
import org.jetbrains.annotations.Range;
109

1110
import java.nio.CharBuffer;
12-
import java.util.*;
11+
import java.util.ArrayList;
12+
import java.util.Comparator;
13+
import java.util.List;
1314
import java.util.function.Function;
1415
import java.util.regex.Matcher;
1516
import java.util.regex.Pattern;
16-
import java.util.stream.Collectors;
1717

1818

1919
public class Parser {
@@ -79,52 +79,39 @@ public MathSymbol parseExpr() throws ExprParseException {
7979

8080
public MathSymbol parseInfixPrecedenceLevel(int level) throws ExprParseException {
8181
discardWhitespace();
82-
if(level == 0) {
83-
return parseParensOrLiteral();
84-
}
85-
Set<SymbolInfo> symbols = SymbolInfo.PREC_TO_INFO_MAP.get(level);
86-
if(symbols == null || symbols.isEmpty()) return parseInfixPrecedenceLevel(level - 1);
87-
@Nullable GroupingDirection dirn = symbols.stream()
88-
.map(sm -> sm.groupingDirection).collect(UtilCollectors.singleDistinctItem());
89-
Map<String, SymbolInfo> infixToSymbolInfo = symbols.stream().collect( // TODO pre-compute/cache these
90-
Collectors.toUnmodifiableMap(
91-
si -> Objects.requireNonNull(si.infix, "null infix not allowed for parseInfixPrecedenceLevel"),
92-
Function.identity()));
93-
String[] infixesToFind = sortedByLength(infixToSymbolInfo.keySet().toArray(String[]::new));
82+
if(level == 0) return parseParensOrLiteral();
83+
PrecedenceLevelInfo precInfo = SymbolInfo.PREC_LEVELS_INFO.get(level);
84+
if (precInfo == null) return parseInfixPrecedenceLevel(level - 1);
9485
MathSymbol left = parseInfixPrecedenceLevel(level - 1);
95-
if(dirn == GroupingDirection.RightToLeft) {
96-
return parseInfixPrecedenceLevel_RTL(left, level, infixesToFind, infixToSymbolInfo);
86+
if(precInfo.dirn == GroupingDirection.RightToLeft) {
87+
return parseInfixPrecedenceLevel_RTL(left, precInfo);
9788
}
98-
return parseInfixPrecedenceLevel_LTR(left, level, infixesToFind, infixToSymbolInfo);
89+
return parseInfixPrecedenceLevel_LTR(left, precInfo);
9990
// TODO what if dirn == null? Maybe just disallow the ambiguous case of > 2 operands in same level
10091
}
10192

102-
private MathSymbol parseInfixPrecedenceLevel_RTL(MathSymbol left, int level, String[] infixesToFind,
103-
Map<String, SymbolInfo> infixToSymbolInfo) throws ExprParseException {
104-
// TODO: refactor this mess - 2 separate loops?
105-
// I feel like it should be doable w/ one loop but that may involve risking NullPointerException
106-
// by setting some members of LeftRightBinaryOperation to null
107-
// Actually, this first loop could be common between them if we make the other path 2 loops as well
93+
private MathSymbol parseInfixPrecedenceLevel_RTL(MathSymbol left, PrecedenceLevelInfo precInfo) throws ExprParseException {
10894
String op;
10995
List<Pair<SymbolInfo, MathSymbol>> otherOps = new ArrayList<>();
11096
discardWhitespace();
111-
while((op = discardMatchesNextAny_optionsSorted(infixesToFind)) != null) {
112-
otherOps.add(new Pair<>(Util.getNotNull(infixToSymbolInfo, op), parseInfixPrecedenceLevel(level - 1)));
97+
while((op = discardMatchesNextAny_optionsSorted(precInfo.sortedInfixes)) != null) {
98+
otherOps.add(new Pair<>(
99+
Util.getNotNull(precInfo.infixToSymbolMap, op),
100+
parseInfixPrecedenceLevel(precInfo.precedence - 1)));
113101
discardWhitespace();
114102
}
115103
return otherOps.reversed().stream().reduce((rightpair, leftpair) ->
116104
leftpair.asVars((preOp, argL) ->
117-
new Pair<>(preOp, rightpair.asVars((midOp, argR) -> midOp.getBiConstructor().construct(argL, argR))))
118-
).map(p -> p.asVars((midOp, argR) -> midOp.getBiConstructor().construct(left, argR))).orElse(left);
105+
new Pair<>(preOp, rightpair.asVars((midOp, argR) -> BinaryOperation.construct(argL, midOp, argR))))
106+
).map(p -> p.asVars((midOp, argR) -> BinaryOperation.construct(left, midOp, argR))).orElse(left);
119107
}
120108

121-
private MathSymbol parseInfixPrecedenceLevel_LTR(MathSymbol left, int level, String[] infixesToFind,
122-
Map<String, SymbolInfo> infixToSymbolInfo) throws ExprParseException {
109+
private MathSymbol parseInfixPrecedenceLevel_LTR(MathSymbol left, PrecedenceLevelInfo precInfo) throws ExprParseException {
123110
String op;
124111
discardWhitespace();
125-
while((op = discardMatchesNextAny_optionsSorted(infixesToFind)) != null) {
126-
left = Util.getNotNull(infixToSymbolInfo, op).getBiConstructor()
127-
.construct(left, parseInfixPrecedenceLevel(level - 1));
112+
while((op = discardMatchesNextAny_optionsSorted(precInfo.sortedInfixes)) != null) {
113+
left = BinaryOperation.construct(
114+
left, Util.getNotNull(precInfo.infixToSymbolMap, op), parseInfixPrecedenceLevel(precInfo.precedence - 1));
128115
discardWhitespace();
129116
}
130117
return left;
@@ -182,24 +169,24 @@ protected boolean matchesNext(@NotNull String expected) {
182169
return src.startsWith(expected, /*start*/idx);
183170
}
184171

185-
private @NotNull String @NotNull [] sortedByLength(@NotNull String @NotNull[] arr) {
186-
return Arrays.stream(arr).sorted(Comparator.comparingInt(String::length).reversed()).toArray(String[]::new);
172+
private @NotNull List<@NotNull String> sortedByLength(@NotNull List<@NotNull String> arr) {
173+
return arr.stream().sorted(Comparator.comparingInt(String::length).reversed()).toList();
187174
}
188-
private @Nullable String matchesNextAny_optionsSorted(@NotNull String... expected) {
189-
return Arrays.stream(expected).filter(this::matchesNext).findFirst().orElse(null);
175+
private @Nullable String matchesNextAny_optionsSorted(@NotNull List<@NotNull String> expected) {
176+
return expected.stream().filter(this::matchesNext).findFirst().orElse(null);
190177
}
191-
private @Nullable String discardMatchesNextAny_optionsSorted(@NotNull String... expected) {
178+
private @Nullable String discardMatchesNextAny_optionsSorted(@NotNull List<@NotNull String> expected) {
192179
String s = matchesNextAny_optionsSorted(expected);
193180
if(s != null) discardN(s.length());
194181
return s;
195182
}
196183
@SuppressWarnings("unused")
197-
protected @Nullable String matchesNextAny(@NotNull String... expected) {
184+
protected @Nullable String matchesNextAny(@NotNull List<@NotNull String> expected) {
198185
// Try to longer ones first, then shorter ones.
199186
return matchesNextAny_optionsSorted(sortedByLength(expected));
200187
}
201188
@SuppressWarnings("unused")
202-
protected @Nullable String discardMatchesNextAny(@NotNull String... expected) {
189+
protected @Nullable String discardMatchesNextAny(@NotNull List<@NotNull String> expected) {
203190
// Try to longer ones first, then shorter ones.
204191
return discardMatchesNextAny_optionsSorted(sortedByLength(expected));
205192
}

src/main/java/net/marcellperger/mathexpr/util/Util.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ protected Util() {}
4949
return chainNulls(chainNulls(value, f0, f1, f2, f3), f4);
5050
}
5151

52+
/** This exists solely to be able to provide a custom error type */
53+
@Contract(value = "null, _ -> fail; !null, _ -> param1", pure = true)
54+
public static <T> @NotNull T requireNonNull(T obj, RuntimeException exc) {
55+
if(obj == null) throw exc;
56+
return obj;
57+
}
58+
5259
@SuppressWarnings("UnusedReturnValue")
5360
@Contract("_ -> param1")
5461
public static <C extends Collection<?>> @NotNull C requireNonEmpty(@NotNull C collection) {
@@ -59,23 +66,29 @@ protected Util() {}
5966
@SuppressWarnings("UnusedReturnValue")
6067
@Contract("null -> fail; _ -> param1")
6168
public static <C extends Collection<?>> @NotNull C requireNonEmptyNonNull(C collection) {
62-
Objects.requireNonNull(collection);
63-
// IllegalArgumentException might not always be appropriate
64-
if(collection.isEmpty()) throw new IllegalArgumentException("Argument must not be empty");
65-
return collection;
69+
return requireNonEmpty(Objects.requireNonNull(collection));
6670
}
6771
@Contract("_,_ -> param1")
6872
public static <C extends Collection<?>> @NotNull C requireNonEmpty(@NotNull C collection, String msg) {
6973
// IllegalArgumentException might not always be appropriate
7074
if(collection.isEmpty()) throw new IllegalArgumentException(msg);
7175
return collection;
7276
}
77+
@SuppressWarnings("UnusedReturnValue")
78+
@Contract("null, _ -> fail; _, _ -> param1")
79+
public static <C extends Collection<?>> @NotNull C requireNonEmptyNonNull(C collection, String msg) {
80+
return requireNonEmpty(Objects.requireNonNull(collection), msg);
81+
}
7382
@Contract("_,_ -> param1")
7483
public static <C extends Collection<?>> @NotNull C requireNonEmpty(@NotNull C collection, RuntimeException exc) {
7584
// IllegalArgumentException might not always be appropriate
7685
if(collection.isEmpty()) throw exc;
7786
return collection;
7887
}
88+
@Contract("null, _ -> fail; _, _ -> param1")
89+
public static <C extends Collection<?>> @NotNull C requireNonEmptyNonNull(C collection, RuntimeException exc) {
90+
return requireNonEmpty(Objects.requireNonNull(collection), exc);
91+
}
7992

8093
@Contract("_, _ -> param1")
8194
public static<T> T expectOrFail(T value, @NotNull Predicate<T> predicate) {
@@ -167,6 +180,11 @@ public static void realAssert(boolean b, String msg, Throwable cause) {
167180
return new AbstractMap.SimpleImmutableEntry<>(k, v);
168181
}
169182

183+
@Contract(pure = true)
184+
public static <K, V, R> @NotNull Function<Map.Entry<K, V>, Map.Entry<K, R>> valueMapper(Function<V, R> mapper) {
185+
return en -> makeEntry(en.getKey(), mapper.apply(en.getValue()));
186+
}
187+
170188
public static<T> T getOnlyItem(@Flow(sourceIsContainer = true) @NotNull Collection<T> c) {
171189
if(c.size() != 1) throw new CollectionSizeException("Expected collection to have 1 item");
172190
return c.iterator().next();

src/main/java/net/marcellperger/mathexpr/util/UtilCollectors.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.jetbrains.annotations.Contract;
44
import org.jetbrains.annotations.NotNull;
55

6+
import java.util.HashSet;
67
import java.util.Map;
78
import java.util.Map.Entry;
89
import java.util.stream.Collector;
@@ -17,7 +18,9 @@ protected UtilCollectors() {} // prevent instantiation, allow subclassing
1718
return Collectors.collectingAndThen(Collectors.toList(), Util::getOnlyItem);
1819
}
1920
public static <T> @NotNull Collector<T, ?, T> singleDistinctItem() {
20-
return Collectors.collectingAndThen(Collectors.toSet(), Util::getOnlyItem);
21+
// We can't use toSet() as that doesn't guarantee null items are allowed
22+
// although https://stackoverflow.com/q/47212902/19115554
23+
return Collectors.collectingAndThen(Collectors.toCollection(HashSet::new), Util::getOnlyItem);
2124
}
2225

2326
public static <K, V> @NotNull Collector<Entry<K, V>, ?, Map<K, V>> entriesToUnmodifiableMap() {

0 commit comments

Comments
 (0)