diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index f33a02c..9d8f2d0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,7 +6,7 @@
version: 2
updates:
- - package-ecosystem: "devcontainers"
- directory: "/"
- schedule:
- interval: weekly
+ - package-ecosystem: "devcontainers"
+ directory: "/"
+ schedule:
+ interval: weekly
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 688c711..d1b0e0a 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -29,7 +29,9 @@ jobs:
distribution: 'temurin'
cache: maven
- name: Test with Maven
- run: mvn -B test
+ run: mvn -B -ntp test
+ - name: Check coverage
+ run: mvn -B -ntp verify
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- name: Update dependency graph
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index e01daa3..a4e99ea 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -2,5 +2,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index ebb128c..7c5ad6f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -28,15 +28,59 @@
-
-
+
+
org.apache.maven.plugins
- maven-surefire-plugin
- 3.2.5
+ maven-surefire-plugin
+ 3.2.5
true
-
-
-
+
+
+ org.jacoco
+ jacoco-maven-plugin
+ 0.8.12
+
+
+ default-prepare-agent
+
+ prepare-agent
+
+
+
+ default-report
+
+ report
+
+
+
+ default-check
+
+ check
+
+
+
+
+ SOURCEFILE
+
+ net/marcellperger/mathexpr/util/rs/Result.java
+ net/marcellperger/mathexpr/util/rs/Option.java
+
+
+
+ INSTRUCTION
+ COVEREDRATIO
+
+ 100%
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/net/marcellperger/mathexpr/parser/ExprParseRtException.java b/src/main/java/net/marcellperger/mathexpr/parser/ExprParseRtException.java
deleted file mode 100644
index 1b4c25a..0000000
--- a/src/main/java/net/marcellperger/mathexpr/parser/ExprParseRtException.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package net.marcellperger.mathexpr.parser;
-
-@SuppressWarnings("unused") // I want all the constructors!
-public class ExprParseRtException extends RuntimeException {
- public ExprParseRtException() {
- super();
- }
-
- public ExprParseRtException(String message) {
- super(message);
- }
-
- public ExprParseRtException(String message, Throwable cause) {
- super(message, cause);
- }
-
- public ExprParseRtException(Throwable cause) {
- super(cause);
- }
-}
diff --git a/src/main/java/net/marcellperger/mathexpr/parser/Parser.java b/src/main/java/net/marcellperger/mathexpr/parser/Parser.java
index 2c13958..7c9ac2b 100644
--- a/src/main/java/net/marcellperger/mathexpr/parser/Parser.java
+++ b/src/main/java/net/marcellperger/mathexpr/parser/Parser.java
@@ -12,6 +12,7 @@
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
+import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -39,38 +40,23 @@ public MathSymbol parseExpr() throws ExprParseException {
// https://regex101.com/r/2EogTA/1
protected static final Pattern DOUBLE_RE = Pattern.compile("^([+-]?)(\\d*\\.\\d+|\\d+\\.?)(?:[eE]([+-]?\\d+))?");
- public @NotNull MathSymbol parseDoubleLiteral_exc() throws ExprParseException {
- return switch (parseDoubleLiteral_null()) {
- case null -> throw new ExprParseException("Couldn't parse double in expression");
- case MathSymbol sym -> sym;
- };
- }
- public @Nullable MathSymbol parseDoubleLiteral_null() {
+ public @NotNull MathSymbol parseDoubleLiteral() throws ExprParseException {
discardWhitespace();
- Matcher m = DOUBLE_RE.matcher(strFromHere());
- if (!m.lookingAt()) return null;
- String s = m.group();
- idx += s.length();
- double value;
+ String s = matchNextRegexString(DOUBLE_RE, "Invalid number (double)");
try {
- value = Double.parseDouble(s);
- } catch (NumberFormatException _exc) {
- // Technically this should never happen - assuming I've got that regex right
- assert false: "There is a problem with the regex, this should've been rejected earlier";
- return null;
+ return new BasicDoubleSymbol(Double.parseDouble(s));
+ } catch (NumberFormatException exc) {
+ throw new AssertionError("There is a problem with the regex," +
+ " this should've been rejected earlier", exc);
}
- return new BasicDoubleSymbol(value);
}
- public @Nullable MathSymbol parseParensOrLiteral() throws ExprParseException {
+ public @NotNull MathSymbol parseParensOrLiteral() throws ExprParseException {
discardWhitespace();
- if(peek() == '(') return parseParens();
- // TODO add a better error handling system - don't want to maintain 2 versions of each function
- // returning null: +easier to do unions, +no need for verbose try/catch, -no info about errors
- return parseDoubleLiteral_null();
+ return peek() == '(' ? parseParens() : parseDoubleLiteral();
}
- public @Nullable MathSymbol parseParens() throws ExprParseException {
+ public MathSymbol parseParens() throws ExprParseException {
advanceExpectNext_ignoreWs('(');
MathSymbol sym = parseExpr();
advanceExpectNext_ignoreWs(')');
@@ -152,8 +138,7 @@ boolean advanceIf(@NotNull Function predicate) {
* @param predicate Keep advancing while this returns true
* @return Amount of spaces advanced
*/
- @SuppressWarnings("UnusedReturnValue")
- int advanceWhile(@NotNull Function predicate) {
+ protected int advanceWhile(@NotNull Function predicate) {
int n = 0;
while (notEof() && advanceIf(predicate)) ++n;
return n;
@@ -167,19 +152,45 @@ protected void discardWhitespace() {
advanceWhile(Character::isWhitespace);
}
- protected void advanceExpectNext(char expected) {
+ protected void advanceExpectNext(char expected) throws ExprParseException {
char actual = advance();
- if(actual != expected) throw new ExprParseRtException("Expected '%c', got '%c'".formatted(expected, actual));
+ if(actual != expected) throw new ExprParseException("Expected '%c', got '%c'".formatted(expected, actual));
}
- protected void advanceExpectNext_ignoreWs(char expected) {
+ protected void advanceExpectNext_ignoreWs(char expected) throws ExprParseException {
discardWhitespace();
advanceExpectNext(expected);
}
+ protected MatchResult matchNextRegexResult(@NotNull Pattern pat, ExprParseException exc) throws ExprParseException {
+ Matcher m = pat.matcher(strFromHere());
+ if (!m.lookingAt()) throw exc;
+ String s = m.group();
+ idx += s.length();
+ return m.toMatchResult();
+ }
+ protected MatchResult matchNextRegexResult(@NotNull Pattern pat, String exc) throws ExprParseException {
+ return matchNextRegexResult(pat, new ExprParseException(exc));
+ }
+ protected MatchResult matchNextRegexResult(@NotNull Pattern pat) throws ExprParseException {
+ return matchNextRegexResult(pat, "Regex should've been matched");
+ }
+ @SuppressWarnings("unused")
+ protected String matchNextRegexString(@NotNull Pattern pat, ExprParseException exc) throws ExprParseException {
+ return matchNextRegexResult(pat, exc).group();
+ }
+ @SuppressWarnings("SameParameterValue")
+ protected String matchNextRegexString(@NotNull Pattern pat, String msg) throws ExprParseException {
+ return matchNextRegexResult(pat, msg).group();
+ }
+ @SuppressWarnings("unused")
+ protected String matchNextRegexString(@NotNull Pattern pat) throws ExprParseException {
+ return matchNextRegexResult(pat).group();
+ }
+
protected boolean matchesNext(@NotNull String expected) {
return src.startsWith(expected, /*start*/idx);
}
-
+
private @NotNull List<@NotNull String> sortedByLength(@NotNull List<@NotNull String> arr) {
return arr.stream().sorted(Comparator.comparingInt(String::length).reversed()).toList();
}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/JacocoIgnoreNotGenerated.java b/src/main/java/net/marcellperger/mathexpr/util/JacocoIgnoreNotGenerated.java
new file mode 100644
index 0000000..be5ece3
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/JacocoIgnoreNotGenerated.java
@@ -0,0 +1,4 @@
+package net.marcellperger.mathexpr.util;
+
+public @interface JacocoIgnoreNotGenerated {
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/ThrowingSupplier.java b/src/main/java/net/marcellperger/mathexpr/util/ThrowingSupplier.java
new file mode 100644
index 0000000..a4f378f
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/ThrowingSupplier.java
@@ -0,0 +1,6 @@
+package net.marcellperger.mathexpr.util;
+
+@FunctionalInterface
+public interface ThrowingSupplier {
+ T get() throws E;
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/Util.java b/src/main/java/net/marcellperger/mathexpr/util/Util.java
index 14f28cb..0e8ffe2 100644
--- a/src/main/java/net/marcellperger/mathexpr/util/Util.java
+++ b/src/main/java/net/marcellperger/mathexpr/util/Util.java
@@ -7,8 +7,11 @@
import org.jetbrains.annotations.Nullable;
import java.util.*;
+import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
@SuppressWarnings("unused")
public class Util {
@@ -56,13 +59,11 @@ protected Util() {}
return obj;
}
- @SuppressWarnings("UnusedReturnValue")
@Contract("_ -> param1")
public static > @NotNull C requireNonEmpty(@NotNull C collection) {
if(collection.isEmpty()) throw new CollectionSizeException("Argument must not be empty");
return collection;
}
- @SuppressWarnings("UnusedReturnValue")
@Contract("null -> fail; _ -> param1")
public static > @NotNull C requireNonEmptyNonNull(C collection) {
return requireNonEmpty(Objects.requireNonNull(collection));
@@ -72,7 +73,6 @@ protected Util() {}
if(collection.isEmpty()) throw new CollectionSizeException(msg);
return collection;
}
- @SuppressWarnings("UnusedReturnValue")
@Contract("null, _ -> fail; _, _ -> param1")
public static > @NotNull C requireNonEmptyNonNull(C collection, String msg) {
return requireNonEmpty(Objects.requireNonNull(collection), msg);
@@ -194,4 +194,91 @@ public static T getOnlyItem(@Flow(sourceIsContainer = true) @NotNull Sequence
public static @NotNull V getNotNull(@NotNull Map map, K key) {
return Objects.requireNonNull(map.get(key));
}
+
+ @Contract(value = "_ -> new", pure = true)
+ public static @NotNull Iterator singleItemIterator(T value) {
+ return new SingleItemIterator<>(value);
+ }
+
+ protected static class SingleItemIterator implements Iterator {
+ T value;
+ boolean isAtEnd;
+
+ public SingleItemIterator(T value) {
+ this.value = value;
+ isAtEnd = false;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !isAtEnd;
+ }
+
+ @Override
+ public T next() {
+ if(isAtEnd) throw new NoSuchElementException("End of SingleItemIterator reached");
+ return _moveToEndAndPop();
+ }
+
+ protected T _moveToEndAndPop() {
+ isAtEnd = true;
+ T val = value;
+ // Don't hold on to our copy - we don't need it anymore
+ // as iterators cannot go backwards and this lets JVM free it earlier.
+ value = null;
+ return val;
+ }
+
+ @Override
+ public void forEachRemaining(Consumer super T> action) {
+ if(isAtEnd) return;
+ action.accept(_moveToEndAndPop());
+ }
+ }
+
+ @Contract(pure = true)
+ public static @NotNull Function super T, VoidVal> consumerToFunction(Consumer super T> consumer) {
+ return v -> {
+ consumer.accept(v);
+ return VoidVal.val();
+ };
+ }
+ @Contract(pure = true)
+ public static @NotNull UnaryOperator consumerToIdentityFunc(Consumer super T> consumer) {
+ return v -> {
+ consumer.accept(v);
+ return v;
+ };
+ }
+
+ public static @NotNull Supplier runnableToSupplier(Runnable runnable) {
+ return () -> {
+ runnable.run();
+ return VoidVal.val();
+ };
+ }
+
+ // These 2 trick Java into throwing checked exceptions in an unchecked way
+ @Contract("_ -> fail")
+ public static T throwAsUnchecked(Throwable exc) {
+ throwAs(exc); // is inferred from no `throws` clause
+ // Java doesn't know that this always throws so this lets us
+ // do `return/throw throwAsUnchecked()` to make Java's flow control analyser happy
+ throw new AssertionError("Unreachable");
+ }
+ @SuppressWarnings("unchecked")
+ @Contract("_ -> fail")
+ public static void throwAs(Throwable exc) throws E {
+ // We do a little type erasure hack to trick Java:
+ // - E will be type-erased to Throwable so this will become
+ // throw (Throwable)exc;
+ // but exc is already Throwable due to the param type so
+ // this is like a runtime no-op.
+ // - But javac will see that we're throwing an E which is allowed!
+ // - The reason we need a separate method is so that there is a
+ // generic for javac to type-erase (otherwise JVM would check that
+ // it's actually that concrete type when it is thrown and this way
+ // it only checks for the base condition)
+ throw (E)exc;
+ }
}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/VoidVal.java b/src/main/java/net/marcellperger/mathexpr/util/VoidVal.java
new file mode 100644
index 0000000..ade1e71
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/VoidVal.java
@@ -0,0 +1,28 @@
+package net.marcellperger.mathexpr.util;
+
+import org.jetbrains.annotations.NotNull;
+
+
+/** The equivalent of Rust's {@code ()} or Python's {@code NoneType}/{@code None}
+ * or a {@code void} type in Java but can be passed as a type arg
+ * when e.g. you don't actually want to return anything but {@code Function} interface
+ * requires it to return instances of a type. Because 'no value' is a type that has
+ * EXACTLY ONE instance: nothing ({@code None} / {@code ()} / {@code undefined})*/
+public final class VoidVal {
+ static final @NotNull VoidVal INST = new VoidVal();
+
+ @SuppressWarnings("ConstantValue") // This is actually called to initialize it to non-null
+ private VoidVal() {
+ if(INST != null) throw new AssertionError(
+ "new VoidVal() should only be called once to create VoidVal.INST");
+ }
+
+ public static VoidVal val() {
+ return INST;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return this == obj; // there can only be one.
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java b/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java
new file mode 100644
index 0000000..aa6140d
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/rs/Option.java
@@ -0,0 +1,196 @@
+package net.marcellperger.mathexpr.util.rs;
+
+import net.marcellperger.mathexpr.util.Util;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+import java.util.stream.Stream;
+
+public sealed interface Option extends Iterable {
+ // hashCode, equals are automatically implemented for `record`s
+ record Some(T value) implements Option {
+ @Contract(pure = true)
+ @Override
+ public @NotNull String toString() {
+ return "Some(" + value + ')';
+ }
+ }
+
+ record None() implements Option {
+ @Contract(" -> new")
+ public @NotNull None cast() {
+ return new None<>();
+ }
+
+ @Contract(pure = true)
+ @Override
+ public @NotNull String toString() {
+ return "None";
+ }
+ }
+
+ // Same as new Some/None but return Option
+ @Contract("_ -> new")
+ static @NotNull Option newSome(T value) {
+ return new Some<>(value);
+ }
+
+ @Contract(" -> new")
+ static @NotNull Option newNone() {
+ return new None<>();
+ }
+
+ static Option<@NotNull T> ofNullable(@Nullable T value) {
+ return ofOptional(Optional.ofNullable(value));
+ }
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+ static Option ofOptional(Optional value) {
+ return value.map(Option::newSome).orElse(newNone());
+ }
+
+ default boolean isSome() {
+ return this instanceof Some;
+ }
+
+ default boolean isNone() {
+ return this instanceof None;
+ }
+
+ default boolean isSomeAnd(Predicate super T> predicate) {
+ return switch (this) {
+ case None() -> false;
+ case Some(T value) -> predicate.test(value);
+ };
+ }
+ // I don't think we need as_slice because .stream().toList() / .collect()
+ // and there are already easy ways of Option -> Collection using .map etc.
+
+ default Option map(Function super T, ? extends U> fn) {
+ return switch (this) {
+ case Some(T value) -> new Some<>(fn.apply(value));
+ case None n -> n.cast();
+ };
+ }
+ default U mapOr(U default_, Function super T, ? extends U> fn) {
+ return mapOrElse(() -> default_, fn);
+ }
+ default U mapOrElse(Supplier extends U> defaultFn, Function super T, ? extends U> fn) {
+ return switch (this) {
+ case Some(T value) -> fn.apply(value);
+ case None() -> defaultFn.get();
+ };
+ }
+ // there is nothing to map from or to (None -> None)! so this is Runnable
+ // so this is the same as inspectErr. It's here for consistency with Result
+ default Option mapErr(Runnable noneFn) {
+ if(isNone()) noneFn.run();
+ return this;
+ }
+
+ // not Rust functions, but provides more logical argument order
+ // (if/else vs else/if) than mapOrElse
+ default void ifThenElse_void(Consumer super T> someFn, Runnable noneFn) {
+ mapOrElse(Util.runnableToSupplier(noneFn), Util.consumerToFunction(someFn));
+ }
+ default U ifThenElse(Function super T, ? extends U> someFn, Supplier extends U> noneFn) {
+ return mapOrElse(noneFn, someFn);
+ }
+
+ default Option inspect(Consumer super T> fn) {
+ return map(Util.consumerToIdentityFunc(fn));
+ }
+ // not in Rust but could be useful (same as mapErr)
+ default Option inspectErr(Runnable noneFn) {
+ if(isNone()) noneFn.run();
+ return this;
+ }
+
+ // .iter()-esque methods: why does Java have SO MANY - one is enough.
+ default Stream stream() {
+ return mapOrElse(Stream::empty, Stream::of);
+ }
+ // Iterable automatically implements `spliterator()` for us
+ @NotNull
+ @Override
+ default Iterator iterator() {
+ return switch (this) {
+ case Some(T value) -> Util.singleItemIterator(value);
+ case None() -> Collections.emptyIterator();
+ };
+ }
+ @Override
+ default void forEach(Consumer super T> action) {
+ map(Util.consumerToFunction(action));
+ }
+
+ default T unwrapOr(T default_) {
+ return unwrapOrElse(() -> default_);
+ }
+ default T unwrapOrElse(Supplier extends T> ifNone) {
+ return mapOrElse(ifNone, Function.identity());
+ }
+ // no unwrap_or_default (see Result.java for big explanation)
+ default T expect(String msg) {
+ return unwrapOrElse(() -> {
+ throw new OptionPanicException(msg);
+ });
+ }
+ default T unwrap() {
+ return expect("Option.unwrap() got None value");
+ }
+
+ default Result okOr(E err) {
+ return ifThenElse(Result::newOk, () -> Result.newErr(err));
+ }
+ default Result okOrElse(Supplier extends E> errSupplier) {
+ return ifThenElse(Result::newOk, () -> Result.newErr(errSupplier.get()));
+ }
+
+ default Option and(Option right) {
+ return andThen((_v) -> right);
+ }
+ default Option andThen(Function super T, ? extends Option> fn) {
+ return switch (this) {
+ case Some(T value) -> fn.apply(value);
+ case None n -> n.cast();
+ };
+ }
+
+ default Option or(Option right) {
+ return orElse(() -> right);
+ }
+ default Option orElse(Supplier extends Option> orFn) {
+ return switch (this) {
+ case Some s -> s;
+ case None() -> orFn.get();
+ };
+ }
+
+ default Option filter(Predicate super T> predicate) {
+ return switch (this) {
+ case Some(T value) -> predicate.test(value) ? this : newNone();
+ case None n -> n;
+ };
+ }
+
+ default Option xor(Option right) {
+ return switch (this) { // I wish this implementation was more elegant
+ case Some left -> right.isNone() ? left : newNone();
+ case None() -> right;
+ };
+ }
+
+ // no insert / get_or_insert(_*) / take(_if) / replace
+ // (as record is immutable in Java)
+ // no (un)zip(_with) because no tuples in Java (will do if needed tho)
+ // rest are `impl`s on compound types which Java can't do
+ // (see Result.java for detailed explanation), can do static method if we need it so much
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/OptionPanicException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/OptionPanicException.java
new file mode 100644
index 0000000..b0fb89a
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/rs/OptionPanicException.java
@@ -0,0 +1,35 @@
+package net.marcellperger.mathexpr.util.rs;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+public class OptionPanicException extends PanicException {
+ public OptionPanicException() {
+ }
+
+ public OptionPanicException(String message) {
+ super(message);
+ }
+
+ public OptionPanicException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public OptionPanicException(Throwable cause) {
+ super(cause);
+ }
+
+ @SuppressWarnings("unused") // may be used later
+ @Contract(value = " -> new", pure = true)
+ public static @NotNull Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder extends PanicException.Builder {
+ @Override
+ public OptionPanicException build() {
+ return build(OptionPanicException::new, OptionPanicException::new,
+ OptionPanicException::new, OptionPanicException::new);
+ }
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/PanicException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/PanicException.java
new file mode 100644
index 0000000..e42439a
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/rs/PanicException.java
@@ -0,0 +1,66 @@
+package net.marcellperger.mathexpr.util.rs;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class PanicException extends RuntimeException {
+ public PanicException() {
+ }
+
+ public PanicException(String message) {
+ super(message);
+ }
+
+ public PanicException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public PanicException(Throwable cause) {
+ super(cause);
+ }
+
+ @SuppressWarnings("unused") // may be used later
+ @Contract(value = " -> new", pure = true)
+ public static @NotNull Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+ protected @Nullable String msg;
+ protected @Nullable Throwable cause;
+
+ protected Builder() {
+ msg = null;
+ cause = null;
+ }
+
+ @Contract("_ -> this")
+ public Builder msg(@Nullable String msg) {
+ this.msg = msg;
+ return this;
+ }
+
+ @Contract("_ -> this")
+ public Builder cause(@Nullable Throwable cause) {
+ this.cause = cause;
+ return this;
+ }
+
+ public PanicException build() {
+ return build(PanicException::new, PanicException::new, PanicException::new, PanicException::new);
+ }
+
+ public T build(Supplier ctor0, Function ctor1str,
+ Function super Throwable, T> ctor1exc,
+ BiFunction ctor2) {
+ return msg != null
+ ? cause != null ? ctor2.apply(msg, cause) : ctor1str.apply(msg)
+ : cause != null ? ctor1exc.apply(cause) : ctor0.get();
+ }
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/Result.java b/src/main/java/net/marcellperger/mathexpr/util/rs/Result.java
new file mode 100644
index 0000000..c0d73cb
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/rs/Result.java
@@ -0,0 +1,256 @@
+package net.marcellperger.mathexpr.util.rs;
+
+import net.marcellperger.mathexpr.util.JacocoIgnoreNotGenerated;
+import net.marcellperger.mathexpr.util.ThrowingSupplier;
+import net.marcellperger.mathexpr.util.Util;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Optional;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+
+/**
+ * An implementation of Rust's immutable {@code Result} type that stores either:
+ * - A value of type {@link T} representing success
+ * - An error value of type {@link E} representing an error
+ * No restrictions are placed on any of these types for convenience and flexibility.
+ * @see Rust's std::result::Result
+ * @param Type of the {@link Ok} value
+ * @param Type of the {@link Err} error value
+ */
+@SuppressWarnings({"unused", "InnerClassOfInterface"}) // should be an abstract class but then the `record Ok/Err` cannot extend it
+public sealed interface Result extends Iterable {
+ // hashCode / equals is automatically implemented for these record types, but we customize toString
+ record Ok(T value) implements Result {
+ public Ok cast() { return new Ok<>(value); }
+
+ @Override
+ public String toString() {
+ return "Ok(" + value + ')';
+ }
+ }
+
+ record Err(E exc) implements Result {
+ public Err cast() { return new Err<>(exc); }
+
+ @Override
+ public String toString() {
+ return "Err(" + exc + ')';
+ }
+ }
+
+ // same as new Ok/Err but return Option
+ static Result newOk(T value) {
+ return new Ok<>(value);
+ }
+ static Result newErr(E err) {
+ return new Err<>(err);
+ }
+
+ /** Same as fromExc but also fills in the stack trace using
+ * {@link Throwable#fillInStackTrace()} for more useful error messages */
+ static @NotNull Result fromExc(@NotNull E exc) {
+ exc.fillInStackTrace();
+ return newErr(exc);
+ }
+
+ static Result fromTry(ThrowingSupplier extends T, E> inner, Class catchThis) {
+ try {
+ return newOk(inner.get());
+ } catch (Throwable exc) {
+ return _makeErrTypeOrFailUnchecked(exc, catchThis);
+ }
+ }
+
+ @NotNull
+ @JacocoIgnoreNotGenerated
+ private static Result _makeErrTypeOrFailUnchecked(Throwable exc, Class catchThis) {
+ try {
+ return newErr(catchThis.cast(exc));
+ } catch (ClassCastException c) {
+ // We've handled E, so only unchecked exceptions should reach here
+ // so it's safe to throw them (but Java doesn't know that so we trick it)
+ return Util.throwAsUnchecked(exc); // Jacoco doesn't realize that the `return` is unreachable
+ }
+ }
+
+ default @Nullable Ok ok() {
+ return switch (this) {
+ case Ok ok -> ok;
+ case Err _e -> null;
+ };
+ }
+ default @Nullable Err err() {
+ return switch (this) {
+ case Ok _ok -> null;
+ case Err err -> err;
+ };
+ }
+
+ default Optional> okOpt() { return Optional.ofNullable(ok()); }
+ default Optional> errOpt() { return Optional.ofNullable(err()); }
+
+ default Option okOption() {
+ return mapOr(Option.newNone(), Option::newSome);
+ }
+ default Option errOption() {
+ return ifThenElse(_ok -> Option.newNone(), Option::newSome);
+ }
+
+ default boolean isOk() { return ok() != null; }
+ default boolean isErr() { return err() != null; }
+
+ default boolean isOkAnd(Predicate super T> f) {
+ return switch (this) {
+ case Ok(T value) -> f.test(value);
+ case Err e -> false;
+ };
+ }
+ default boolean isErrAnd(Predicate super E> f) {
+ return switch (this) {
+ case Ok o -> false;
+ case Err(E err) -> f.test(err);
+ };
+ }
+
+ default Result map(Function super T, ? extends U> op) {
+ return switch (this) {
+ case Ok(T value) -> new Ok<>(op.apply(value));
+ case Err e -> e.cast();
+ };
+ }
+ default U mapOr(U default_, Function super T, ? extends U> f) {
+ return mapOrElse((_e) -> default_, f);
+ }
+ default U mapOrElse(Function super E, ? extends U> defaultFn, Function super T, ? extends U> f) {
+ return switch (this) {
+ case Ok(T value) -> f.apply(value);
+ case Err(E err) -> defaultFn.apply(err);
+ };
+ }
+ default Result mapErr(Function super E, ? extends E2> op) {
+ return switch (this) {
+ case Ok o -> o.cast();
+ case Err(E err) -> new Err<>(op.apply(err));
+ };
+ }
+
+ // not a Rust function. Similar to mapOrElse but reversed arguments
+ default void ifThenElse_void(Consumer super T> okFn, Consumer super E> errFn) {
+ // This _void variant exists (instead of an overload) to avoid Java complaining
+ // about ambiguous arguments when passing method references
+ mapOrElse(Util.consumerToFunction(errFn), Util.consumerToFunction(okFn));
+ }
+ default U ifThenElse(Function super T, ? extends U> okFn, Function super E, ? extends U> errFn) {
+ return mapOrElse(errFn, okFn);
+ }
+
+ default Result inspect(Consumer super T> f) {
+ return map(Util.consumerToIdentityFunc(f));
+ }
+ default Result inspectErr(Consumer super E> f) {
+ return mapErr(Util.consumerToIdentityFunc(f));
+ }
+
+ default Result runIfOk(Consumer super T> f) {
+ return map(Util.consumerToIdentityFunc(f));
+ }
+ default Result runIfErr(Consumer super E> f) {
+ return mapErr(Util.consumerToIdentityFunc(f));
+ }
+
+ // .iter()-esque methods: why does Java have SO MANY - one is enough.
+ default Stream stream() {
+ return okOpt().map(Ok::value).stream();
+ }
+ // Iterable automatically implements `spliterator()` for us
+ @NotNull
+ @Override
+ default Iterator iterator() {
+ return switch (this) {
+ case Ok(T value) -> Util.singleItemIterator(value);
+ case Err(E _err) -> Collections.emptyIterator();
+ };
+ }
+ @Override
+ default void forEach(Consumer super T> action) {
+ map(Util.consumerToFunction(action));
+ }
+
+ default T unwrap() {
+ return expect("unwrap() got Err value");
+ }
+ default T expect(String msg) {
+ return unwrapOrElse((err) -> {
+ throw ResultPanicWithValueException.fromMaybeExcValue(err, msg);
+ });
+ }
+ // We CANNOT do unwrap_or_default because Java:
+ // - Static methods cannot be overloaded properly and cannot even be part of an interface
+ // (static methods are very much NOT first-class things in Java - unlike Rust)
+ // so the language that is the 'standard' Object-Oriented language cannot
+ // even do polymorphism for static methods! (without resorting to
+ // runtime non-compile-time-checked reflection)
+ // - My (very many) attempts to solve it using C++-style CRTP have all failed
+ // because Java handles generics using type erasure, making 1 general
+ // type-erased version of a method so any attempts to call methods on the passed subtype
+ // just results on the method being called on the type declared extended in the ``.
+ // Another problematic consequence of (I think) type erasure is that a class hierarchy
+ // cannot implement the same interface with 2 different types so something
+ // cannot be both Iterable and Iterable.
+ // IMO, handling generics using monomorphisation (like C++ and Rust) would solve these issues and would
+ // allow much greater flexibility and could open paths to a SFINAE-type
+ // system which would solve the first BP.
+ // - I really don't want to use reflection, because in strongly typed languages,
+ // my philosophy is "Check as much as possible at compile-time to reduce runtime failure".
+ default E expectErr(String msg) {
+ return ifThenElse(ok -> {
+ throw ResultPanicWithValueException.fromPlainValue(ok, msg);
+ }, Function.identity());
+ }
+ default E unwrapErr() {
+ return expectErr("unwrapErr() got Ok value");
+ }
+ // We can't implement into_ok/into_err as Java doesn't have that
+ // kind of infallible type and I don't think Java can be that rigorous about
+ // infallibility (Java's type system in proven to be unsound so...)
+ // or can't even reason about it in the first place.
+
+ default Result and(Result other) {
+ return andThen((_ok) -> other);
+ }
+ default Result andThen(Function super T, ? extends Result> then) {
+ return switch (this) {
+ case Ok(T value) -> then.apply(value); // everything normal with `this`, do next
+ case Err err -> err.cast();
+ };
+ }
+
+ default Result or(Result other) {
+ return orElse((_err) -> other);
+ }
+ default Result orElse(Function super E, ? extends Result> elseFn) {
+ return switch (this) {
+ case Ok ok -> ok.cast();
+ case Err(E err) -> elseFn.apply(err); // `this` failed so try `elseFn` - "You're my only hope"
+ };
+ }
+
+ default T unwrapOr(T defaultV) {
+ return mapOr(defaultV, Function.identity());
+ }
+ default T unwrapOrElse(Function super E, ? extends T> ifErr) {
+ return mapOrElse(ifErr, Function.identity());
+ }
+ // no unwrap_unchecked/unwrap_err_unchecked for obvious reasons (usually Java != UB/unsafe)
+
+ // Cannot have implementations for Result, E> and similar because Java
+ // has nothing that allows us to enable methods if certain conditions are met
+ // and no multiple implementation blocks with possibly-different conditions on the type parameters.
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicException.java
new file mode 100644
index 0000000..fcdb4e4
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicException.java
@@ -0,0 +1,35 @@
+package net.marcellperger.mathexpr.util.rs;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+
+public class ResultPanicException extends PanicException {
+ public ResultPanicException() {
+ }
+
+ public ResultPanicException(String message) {
+ super(message);
+ }
+
+ public ResultPanicException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ResultPanicException(Throwable cause) {
+ super(cause);
+ }
+
+ @SuppressWarnings("unused") // may be used later
+ @Contract(value = " -> new", pure = true)
+ public static @NotNull Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder extends PanicException.Builder {
+ @Override
+ public ResultPanicException build() {
+ return build(ResultPanicException::new, ResultPanicException::new,
+ ResultPanicException::new, ResultPanicException::new);
+ }
+ }
+}
diff --git a/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicWithValueException.java b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicWithValueException.java
new file mode 100644
index 0000000..d1c62f7
--- /dev/null
+++ b/src/main/java/net/marcellperger/mathexpr/util/rs/ResultPanicWithValueException.java
@@ -0,0 +1,97 @@
+package net.marcellperger.mathexpr.util.rs;
+
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+// Can't make `Throwable`s generic because java...
+public class ResultPanicWithValueException extends ResultPanicException {
+ protected @Nullable Object value;
+
+ /// To set the cause, use `.builder()`
+ public ResultPanicWithValueException() {
+ }
+
+ public ResultPanicWithValueException(String message) {
+ super(message);
+ }
+
+ public ResultPanicWithValueException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public ResultPanicWithValueException(String message, Throwable cause, @Nullable Object value) {
+ super(message, cause);
+ this.value = value;
+ }
+
+ public ResultPanicWithValueException(Throwable cause) {
+ super(cause);
+ }
+
+ public static @NotNull ResultPanicWithValueException fromMaybeExcValue(@Nullable Object value, String msg) {
+ return switch (value) {
+ case null -> new ResultPanicWithValueException(msg);
+ case Throwable excValue -> new ResultPanicWithValueException(msg, excValue, excValue);
+ default -> new ResultPanicWithValueException(msg, null, value);
+ };
+ }
+
+ public static @NotNull ResultPanicWithValueException fromPlainValue(@Nullable Object value, String msg) {
+ return new Builder().msg(msg).value(value).build();
+ }
+
+ @Override
+ public String getMessage() {
+ return switch (super.getMessage()) {
+ case null -> value != null ? "Panic value: " + value : "";
+ case "" -> value != null ? "Panic value: " + value : "";
+ case String msg -> msg + (value == null ? "" : ": " + value);
+ };
+ }
+
+ public @Nullable Object getValue() {
+ return value;
+ }
+ public void setValue(@Nullable Object value) {
+ this.value = value;
+ }
+
+ public static class Builder extends ResultPanicException.Builder {
+ protected @Nullable Object m_value;
+
+ public Builder() {
+ super();
+ m_value = null;
+ }
+
+ @Override
+ public Builder msg(@Nullable String msg) {
+ // I wish there was a better way (e.g. a Self) type - could actually do
+ // CRTP here but that could get messy for users of the base type
+ super.msg(msg);
+ return this;
+ }
+
+ @Override
+ public Builder cause(@Nullable Throwable cause) {
+ super.cause(cause);
+ return this;
+ }
+
+ @Contract("_ -> this")
+ public Builder value(Object value) {
+ this.m_value = value;
+ return this;
+ }
+
+ @Override
+ public ResultPanicWithValueException build() {
+ ResultPanicWithValueException ret = build(
+ ResultPanicWithValueException::new, ResultPanicWithValueException::new,
+ ResultPanicWithValueException::new, ResultPanicWithValueException::new);
+ ret.setValue(m_value);
+ return ret;
+ }
+ }
+}
diff --git a/src/test/java/net/marcellperger/mathexpr/MiniMock.java b/src/test/java/net/marcellperger/mathexpr/MiniMock.java
new file mode 100644
index 0000000..d53f34d
--- /dev/null
+++ b/src/test/java/net/marcellperger/mathexpr/MiniMock.java
@@ -0,0 +1,288 @@
+package net.marcellperger.mathexpr;
+
+import net.marcellperger.mathexpr.util.VoidVal;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Unmodifiable;
+import org.junit.jupiter.api.Assertions;
+
+import java.util.*;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.Supplier;
+
+public class MiniMock {
+ public static class BaseMockedCallable {
+ List calls;
+
+ protected void handleCall(A args) {
+ calls.add(args);
+ }
+
+ public void assertCalledOnce() {
+ Assertions.assertEquals(1, calls.size(), "Expected function to be called exactly once.");
+ }
+ public void assertCalledOnceWith(A args) {
+ assertCalledOnce();
+ Assertions.assertEquals(args, calls.getFirst(), "Expected function to called with these args");
+ }
+
+ public void assertNotCalled() {
+ Assertions.assertEquals(0, calls.size(), "Expected function not to be called.");
+ }
+
+ public void reset() {
+ calls.clear();
+ }
+ }
+
+ public static class MockedSupplier extends BaseMockedCallable implements Supplier {
+ Supplier returnSupplier;
+
+ public MockedSupplier(R returnValue_) {
+ this(() -> returnValue_);
+ }
+ public MockedSupplier(Supplier returnSupplier_) {
+ calls = new ZST_List();
+ returnSupplier = returnSupplier_;
+ }
+
+ @Override
+ public R get() {
+ handleCall(VoidVal.val());
+ return returnSupplier.get();
+ }
+ }
+
+ public static class MockedFunction extends BaseMockedCallable implements Function {
+ Function super T, ? extends R> returnFunc;
+
+ public MockedFunction(R returnValue_) {
+ this((_v) -> returnValue_);
+ }
+ public MockedFunction(Function super T, ? extends R> returnSupplier_) {
+ calls = new ArrayList<>();
+ returnFunc = returnSupplier_;
+ }
+
+ @Override
+ public R apply(T arg) {
+ handleCall(arg);
+ return returnFunc.apply(arg);
+ }
+ }
+
+ public static class MockedPredicate extends BaseMockedCallable implements Predicate {
+ Predicate super T> returnFunc;
+
+ public MockedPredicate(boolean returnValue_) {
+ this((_v) -> returnValue_);
+ }
+ public MockedPredicate(Predicate super T> returnSupplier_) {
+ calls = new ArrayList<>();
+ returnFunc = returnSupplier_;
+ }
+
+ @Override
+ public boolean test(T arg) {
+ handleCall(arg);
+ return returnFunc.test(arg);
+ }
+ }
+
+ public static class MockedConsumer extends BaseMockedCallable implements Consumer {
+ public MockedConsumer() {
+ calls = new ArrayList<>();
+ }
+
+ @Override
+ public void accept(T arg) {
+ handleCall(arg);
+ }
+ }
+
+ public static class MockedRunnable extends BaseMockedCallable implements Runnable {
+ public MockedRunnable() {
+ calls = new ZST_List();
+ }
+
+ @Override
+ public void run() { handleCall(VoidVal.val()); }
+ }
+
+ // oof, this List interface is monstrously big, with very few defaults!
+ static class ZST_List implements List {
+ private int m_size = 0; // I wish this could be long but size() requires int
+
+ public ZST_List() {}
+ public ZST_List(int size) { m_size = size; }
+ @SuppressWarnings("unused")
+ @Contract(pure = true)
+ public ZST_List(@NotNull Collection c) {
+ m_size = c.size();
+ }
+
+
+ private void wantInBounds(int index) {
+ if(index < 0 || index >= m_size) throw new IndexOutOfBoundsException(index);
+ }
+ private void wantInBounds_insert(int index) {
+ if(index < 0 || index > m_size) throw new IndexOutOfBoundsException(index);
+ }
+
+ @Contract(" -> new")
+ private @NotNull @Unmodifiable List immutableImpl() {
+ return Collections.nCopies(m_size, VoidVal.val());
+ }
+
+ @Override
+ public int size() {
+ return m_size;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return m_size == 0;
+ }
+
+ @Override
+ public boolean contains(Object o) {
+ return o == VoidVal.val(); // we contain all VoidVal and nothing else
+ }
+
+ @NotNull
+ @Override
+ public Iterator iterator() {
+ return immutableImpl().iterator();
+ }
+
+ @NotNull
+ @Override
+ public Object @NotNull [] toArray() {
+ return immutableImpl().toArray();
+ }
+
+ @NotNull
+ @Override
+ public T @NotNull [] toArray(@NotNull T @NotNull [] a) {
+ return immutableImpl().toArray(a);
+ }
+
+ @Override
+ public boolean add(VoidVal voidVal) {
+ ++m_size;
+ return true;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ if(m_size <= 0) return false;
+ --m_size;
+ return true;
+ }
+
+ @Override
+ public boolean containsAll(@NotNull Collection> c) {
+ return m_size != 0 && c.stream().allMatch(o -> o == VoidVal.val());
+ }
+
+ @Override
+ public boolean addAll(@NotNull Collection extends VoidVal> c) {
+ if(c.stream().anyMatch(o -> o != VoidVal.val())) throw new ClassCastException("ZST_List can only contain VoidVal");
+ m_size += c.size();
+ return true;
+ }
+
+ @Override
+ public boolean addAll(int index, @NotNull Collection extends VoidVal> c) {
+ wantInBounds_insert(index);
+ return addAll(c);
+ }
+
+ @Override
+ public boolean removeAll(@NotNull Collection> c) {
+ boolean removeValues = c.contains(VoidVal.val());
+ if(removeValues && m_size != 0) {
+ m_size = 0;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public boolean retainAll(@NotNull Collection> c) {
+ boolean retainValues = c.contains(VoidVal.val());
+ if (retainValues || m_size == 0) {
+ return false;
+ }
+ m_size = 0;
+ return true;
+ }
+
+ @Override
+ public void clear() {
+ m_size = 0;
+ }
+
+ @Override
+ public VoidVal get(int index) {
+ wantInBounds(index);
+ return VoidVal.val();
+ }
+
+ @Override
+ public VoidVal set(int index, VoidVal element) {
+ wantInBounds(index);
+ if(element != VoidVal.val()) throw new ClassCastException("ZST_List can only contain VoidVal");
+ return VoidVal.val();
+ }
+
+ @Override
+ public void add(int index, VoidVal element) {
+ wantInBounds_insert(index);
+ ++m_size;
+ }
+
+ @Override
+ public VoidVal remove(int index) {
+ wantInBounds(index);
+ --m_size;
+ return VoidVal.val();
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ if(m_size == 0) return -1;
+ return o == VoidVal.val() ? 0 : -1;
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ if(m_size == 0) return -1;
+ return o == VoidVal.val() ? m_size - 1 : -1;
+ }
+
+ // TODO Technically I should make a custom class for this and .iterator()
+ // as modification IS allowed but I'll do that later.
+ @NotNull
+ @Override
+ public ListIterator listIterator() {
+ return immutableImpl().listIterator();
+ }
+ @NotNull
+ @Override
+ public ListIterator listIterator(int index) {
+ return immutableImpl().listIterator(index);
+ }
+
+ @NotNull
+ @Override
+ public List subList(int fromIndex, int toIndex) {
+ // We just return a copy, because, for us, there ARE no
+ // 'non-structural' changes as only the count matters and there is
+ // only one possible value for each place so the only way to modify it is
+ return new ZST_List(toIndex - fromIndex);
+ }
+ }
+}
diff --git a/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java b/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java
index b32ae3c..b708b27 100644
--- a/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java
+++ b/src/test/java/net/marcellperger/mathexpr/ObjStringPair.java
@@ -1,5 +1,4 @@
package net.marcellperger.mathexpr;
-@SuppressWarnings("unused") // IntelliJ is dumb - it IS used in tests
public record ObjStringPair(MathSymbol obj, String str) {
}
diff --git a/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java b/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java
new file mode 100644
index 0000000..10fe2af
--- /dev/null
+++ b/src/test/java/net/marcellperger/mathexpr/util/rs/OptionTest.java
@@ -0,0 +1,359 @@
+package net.marcellperger.mathexpr.util.rs;
+
+import net.marcellperger.mathexpr.MiniMock.*;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class OptionTest {
+ Option getSome() {
+ return Option.newSome(314);
+ }
+ Option getNone() {
+ return Option.newNone();
+ }
+
+ @Test
+ void isSome() {
+ assertTrue(getSome().isSome());
+ assertFalse(getNone().isSome());
+ }
+
+ @Test
+ void isNone() {
+ assertFalse(getSome().isNone());
+ assertTrue(getNone().isNone());
+ }
+
+ @Test
+ void isSomeAnd() {
+ MockedPredicate mfFalse = new MockedPredicate<>(false);
+ MockedPredicate mfTrue = new MockedPredicate<>(true);
+ {
+ assertTrue(getSome().isSomeAnd(mfTrue));
+ mfTrue.assertCalledOnceWith(314);
+ assertFalse(getSome().isSomeAnd(mfFalse));
+ mfFalse.assertCalledOnceWith(314);
+ }
+ mfTrue.reset();
+ mfFalse.reset();
+ {
+ assertFalse(getNone().isSomeAnd(mfTrue));
+ mfTrue.assertNotCalled();
+ assertFalse(getNone().isSomeAnd(mfFalse));
+ mfFalse.assertNotCalled();
+ }
+ }
+
+ @Test
+ void map() {
+ MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1);
+ assertEquals(Option.newSome(7), Option.newSome(6).map(mfAdd1));
+ mfAdd1.assertCalledOnceWith(6);
+ mfAdd1.reset();
+ assertEquals(Option.newNone(), Option.newNone().map(mfAdd1));
+ mfAdd1.assertNotCalled();
+ }
+
+ @Test
+ void mapOr() {
+ MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1);
+ assertEquals(7, Option.newSome(6).mapOr(-1, mfAdd1));
+ mfAdd1.assertCalledOnceWith(6);
+ mfAdd1.reset();
+ assertEquals(-1, Option.newNone().mapOr(-1, mfAdd1));
+ mfAdd1.assertNotCalled();
+ }
+
+ @Test
+ void mapOrElse() {
+ MockedSupplier mSupplier = new MockedSupplier<>(-6);
+ MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1);
+ {
+ assertEquals(7, Option.newSome(6).mapOrElse(mSupplier, mfAdd1));
+ mfAdd1.assertCalledOnceWith(6);
+ mSupplier.assertNotCalled();
+ }
+ mfAdd1.reset();
+ mSupplier.reset();
+ {
+ assertEquals(-6, Option.newNone().mapOrElse(mSupplier, mfAdd1));
+ mfAdd1.assertNotCalled();
+ mSupplier.assertCalledOnce();
+ }
+ }
+
+ @Test
+ void mapErr() {
+ MockedRunnable mRunnable = new MockedRunnable();
+ assertEquals(getNone(), getNone().mapErr(mRunnable));
+ mRunnable.assertCalledOnce();
+ mRunnable.reset();
+ assertEquals(getSome(), getSome().mapErr(mRunnable));
+ mRunnable.assertNotCalled();
+ }
+
+ @Test
+ void ifThenElse() {
+ MockedSupplier mSupplier = new MockedSupplier<>(-6);
+ MockedFunction mfAdd1 = new MockedFunction<>(i -> i + 1);
+ {
+ assertEquals(7, Option.newSome(6).ifThenElse(mfAdd1, mSupplier));
+ mfAdd1.assertCalledOnceWith(6);
+ mSupplier.assertNotCalled();
+ }
+ mfAdd1.reset();
+ mSupplier.reset();
+ {
+ assertEquals(-6, Option.newNone().ifThenElse(mfAdd1, mSupplier));
+ mfAdd1.assertNotCalled();
+ mSupplier.assertCalledOnce();
+ }
+ }
+
+ @Test
+ void ifThenElse_void() {
+ MockedConsumer mCons = new MockedConsumer<>();
+ MockedRunnable mRunnable = new MockedRunnable();
+ {
+ getSome().ifThenElse_void(mCons, mRunnable);
+ mCons.assertCalledOnceWith(314);
+ mRunnable.assertNotCalled();
+ }
+ mCons.reset();
+ mRunnable.reset();
+ {
+ getNone().ifThenElse_void(mCons, mRunnable);
+ mCons.assertNotCalled();
+ mRunnable.assertCalledOnce();
+ }
+ }
+
+ @Test
+ void inspect() {
+ MockedConsumer intCons = new MockedConsumer<>();
+ assertEquals(getSome(), getSome().inspect(intCons));
+ intCons.assertCalledOnceWith(314);
+ intCons.reset();
+ assertEquals(getNone(), getNone().inspect(intCons));
+ intCons.assertNotCalled();
+ }
+
+ @Test
+ void inspectErr() {
+ MockedRunnable mRunnable = new MockedRunnable();
+ assertEquals(getNone(), getNone().inspectErr(mRunnable));
+ mRunnable.assertCalledOnce();
+ mRunnable.reset();
+ assertEquals(getSome(), getSome().inspectErr(mRunnable));
+ mRunnable.assertNotCalled();
+ }
+
+ @Test
+ void stream() {
+ assertEquals(List.of(314), getSome().stream().toList());
+ assertEquals(List.of(), getNone().stream().toList());
+ }
+
+ @Test
+ void iterator() {
+ {
+ Iterator oks = getSome().iterator();
+ assertTrue(oks.hasNext());
+ assertEquals(314, oks.next());
+ assertFalse(oks.hasNext());
+ }
+ {
+ List ls = new ArrayList<>();
+ getSome().iterator().forEachRemaining(ls::add);
+ assertEquals(List.of(314), ls);
+ }
+ {
+ Iterator oks = getNone().iterator();
+ assertFalse(oks.hasNext());
+ }
+ {
+ List ls = new ArrayList<>();
+ getNone().iterator().forEachRemaining(ls::add);
+ assertEquals(List.of(), ls);
+ }
+ }
+
+ @Test
+ void forEach() {
+ MockedConsumer intCons = new MockedConsumer<>();
+ {
+ getSome().forEach(intCons);
+ intCons.assertCalledOnceWith(314);
+ }
+ intCons.reset();
+ {
+ getNone().forEach(intCons);
+ intCons.assertNotCalled();
+ }
+ }
+
+ @Test
+ void unwrapOr() {
+ assertEquals(314, getSome().unwrapOr(-1));
+ assertEquals(-1, getNone().unwrapOr(-1));
+ }
+
+ @Test
+ void unwrapOrElse() {
+ MockedSupplier ms = new MockedSupplier<>(271);
+ {
+ assertEquals(314, getSome().unwrapOrElse(ms));
+ ms.assertNotCalled();
+ }
+ ms.reset();
+ {
+ assertEquals(271, getNone().unwrapOrElse(ms));
+ ms.assertCalledOnce();
+ }
+ }
+
+ @Test
+ void expect() {
+ assertEquals(314, assertDoesNotThrow(() -> getSome().expect("EXPECT_MSG")));
+ Option n = getNone();
+ OptionPanicException exc = assertThrows(OptionPanicException.class, () -> n.expect("EXPECT_MSG"));
+ assertEquals("EXPECT_MSG", exc.getMessage());
+ assertNull(exc.getCause(), "Expected no cause for Option.expect()");
+ }
+
+ @Test
+ void unwrap() {
+ assertEquals(314, assertDoesNotThrow(() -> getSome().unwrap()));
+ OptionPanicException exc = assertThrows(OptionPanicException.class, getNone()::unwrap);
+ assertEquals("Option.unwrap() got None value", exc.getMessage());
+ assertNull(exc.getCause(), "Expected no cause for Option.unwrap()");
+ }
+
+ @Test
+ void okOr() {
+ assertEquals(Result.newOk(314), getSome().okOr("ERR_ARG"));
+ assertEquals(Result.newErr("ERR_ARG"), getNone().okOr("ERR_ARG"));
+ }
+
+ @Test
+ void okOrElse() {
+ MockedSupplier ms = new MockedSupplier<>("ERR_FN_RETURN");
+ {
+ assertEquals(Result.newOk(314), getSome().okOrElse(ms));
+ ms.assertNotCalled();
+ }
+ ms.reset();
+ {
+ assertEquals(Result.newErr("ERR_FN_RETURN"), getNone().okOrElse(ms));
+ ms.assertCalledOnce();
+ }
+ }
+
+ @Test
+ void and() {
+ assertEquals(Option.newSome(91L), getSome().and(Option.newSome(91L)));
+ assertEquals(Option.newNone(), getSome().and(Option.newNone()));
+ assertEquals(Option.newNone(), getNone().and(Option.newSome(91L)));
+ assertEquals(Option.newNone(), getNone().and(getNone()));
+ }
+
+ @Test
+ void andThen() {
+ MockedFunction> mfOk = new MockedFunction<>(Option.newSome(91L));
+ MockedFunction> mfErr = new MockedFunction<>(Option.newNone());
+ {
+ assertEquals(Option.newSome(91L), getSome().andThen(mfOk));
+ mfOk.assertCalledOnceWith(314);
+ assertEquals(Option.newNone(), getSome().andThen(mfErr));
+ mfErr.assertCalledOnceWith(314);
+ }
+ mfOk.reset();
+ mfErr.reset();
+ {
+ assertEquals(Option.newNone(), getNone().andThen(mfOk));
+ mfOk.assertNotCalled();
+ assertEquals(Option.newNone(), getNone().andThen(mfErr));
+ mfErr.assertNotCalled();
+ }
+ }
+
+ @Test
+ void filter() {
+ MockedPredicate mTrue = new MockedPredicate<>(true);
+ MockedPredicate mFalse = new MockedPredicate<>(false);
+ {
+ assertEquals(getSome(), getSome().filter(mTrue));
+ mTrue.assertCalledOnceWith(314);
+ assertEquals(getNone(), getSome().filter(mFalse));
+ mFalse.assertCalledOnceWith(314);
+ }
+ mTrue.reset();
+ mFalse.reset();
+ {
+ assertEquals(getNone(), getNone().filter(mTrue));
+ mTrue.assertNotCalled();
+ assertEquals(getNone(), getNone().filter(mFalse));
+ mFalse.assertNotCalled();
+ }
+ }
+
+ @Test
+ void or() {
+ assertEquals(getSome(), getSome().or(Option.newSome(271)));
+ assertEquals(getSome(), getSome().or(getNone()));
+ assertEquals(getSome(), getNone().or(getSome()));
+ assertEquals(getNone(), getNone().or(getNone()));
+ }
+
+ @Test
+ void orElse() {
+ MockedSupplier