diff --git a/artifacts/FastReverseTest.jar b/artifacts/FastReverseTest.jar
new file mode 100644
index 0000000..f3d1384
Binary files /dev/null and b/artifacts/FastReverseTest.jar differ
diff --git a/artifacts/Md2HtmlTest.jar b/artifacts/Md2HtmlTest.jar
new file mode 100644
index 0000000..228be12
Binary files /dev/null and b/artifacts/Md2HtmlTest.jar differ
diff --git a/artifacts/ReverseTest.jar b/artifacts/ReverseTest.jar
new file mode 100644
index 0000000..86ec21b
Binary files /dev/null and b/artifacts/ReverseTest.jar differ
diff --git a/artifacts/SumTest.jar b/artifacts/SumTest.jar
new file mode 100644
index 0000000..1a0bb8d
Binary files /dev/null and b/artifacts/SumTest.jar differ
diff --git a/artifacts/WordStatTest.jar b/artifacts/WordStatTest.jar
new file mode 100644
index 0000000..a047faa
Binary files /dev/null and b/artifacts/WordStatTest.jar differ
diff --git a/artifacts/WsppTest.jar b/artifacts/WsppTest.jar
new file mode 100644
index 0000000..12a8e26
Binary files /dev/null and b/artifacts/WsppTest.jar differ
diff --git a/java/RunMe.java b/java/RunMe.java
new file mode 100644
index 0000000..d10b0b3
--- /dev/null
+++ b/java/RunMe.java
@@ -0,0 +1,550 @@
+import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.*;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/**
+ * Run this code with provided arguments.
+ *
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@SuppressWarnings("all")
+public final class RunMe {
+ private RunMe() {
+ // Utility class
+ }
+
+ public static void main(final String[] args) {
+ final byte[] password = parseArgs(args);
+
+ flag0(password);
+ System.out.println("The first flag was low-hanging fruit, can you find others?");
+ System.out.println("Try to read, understand and modify code in flagX(...) functions");
+
+ flag1(password);
+ flag2(password);
+ flag3(password);
+ flag4(password);
+ flag5(password);
+ flag6(password);
+ flag7(password);
+ flag8(password);
+ flag9(password);
+ flag10(password);
+ flag12(password);
+ flag13(password);
+ flag14(password);
+ flag15(password);
+ flag16(password);
+ flag17(password);
+ flag18(password);
+ flag19(password);
+ flag20(password);
+ }
+
+ private static void flag0(final byte[] password) {
+ // The result of print(...) function depends only on explicit arguments
+ print(0, 0, password);
+ }
+
+
+ private static void flag1(final byte[] password) {
+ while ("true".length() == 4) {
+ }
+
+ print(1, -5204358702485720348L, password);
+ }
+
+
+ private static void flag2(final byte[] password) {
+ int result = 0;
+ for (int i = 0; i < 300_000; i++) {
+ for (int j = 0; j < 300_000; j++) {
+ for (int k = 0; k < 300_000; k++) {
+ result ^= (i * 7) | (j + k);
+ result ^= result << 1;
+ }
+ }
+ }
+
+ print(2, -3458723408232943523L, password);
+ }
+
+
+ private static void flag3(final byte[] password) {
+ int result = 0;
+ for (int i = 0; i < 2025; i++) {
+ for (int j = 0; j < 2025; j++) {
+ for (int k = 0; k < 2025; k++) {
+ for (int p = 0; p < 12; p++) {
+ result ^= (i * 17) | (j + k * 7) & ~p;
+ result ^= result << 1;
+ }
+ }
+ }
+ }
+
+ print(3, result, password);
+ }
+
+
+ private static void flag4(final byte[] password) {
+ final long target = 8504327508437503432L + getInt(password);
+ for (long i = 0; i < Long.MAX_VALUE; i++) {
+ if ((i ^ (i >>> 32)) == target) {
+ print(4, i, password);
+ }
+ }
+ }
+
+ /* package-private */ static final long PRIME = 2025_2025_07;
+
+ private static void flag5(final byte[] password) {
+ final long n = 1_000_000_000_000_000L + getInt(password);
+
+ long result = 0;
+ for (long i = 0; i < n; i++) {
+ result = (result + i / 3 + i / 5 + i / 7 + i / 2025) % PRIME;
+ }
+
+ print(5, result, password);
+ }
+
+
+ private static void flag6(final byte[] password) {
+ /***
+ \u002a\u002f\u0077\u0068\u0069\u006c\u0065\u0020\u0028\u0022\u0031\u0022
+ \u002e\u006c\u0065\u006e\u0067\u0074\u0068\u0028\u0029\u0020\u003d\u003d
+ \u0020\u0031\u0029\u003b\u0020\u0020\u006c\u006f\u006e\u0067\u0020\u0009
+ \u0020\u0020\u0072\u0065\u0073\u0075\u006c\u0074\u0020\u003d\u0020\u000a
+ \u0035\u0037\u0034\u0038\u0035\u0037\u0030\u0032\u0034\u0038\u0033\u004c
+ \u002b\u0070\u0061\u0073\u0073\u0077\u006f\u0072\u0064\u005b\u0035\u005d
+ \u002b\u0070\u0061\u0073\u0073\u0077\u006f\u0072\u0064\u005b\u0032\u005d
+ \u003b\u002f\u002a
+ ***/
+ print(6, result, password);
+ }
+
+
+ private static void flag7(final byte[] password) {
+ // Count the number of occurrences of the most frequent noun at the following page:
+ // https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html
+
+ // The singular form of the most frequent noun
+ final String singular = "";
+ // The plural form of the most frequent noun
+ final String plural = "";
+ // The total number of occurrences
+ final int total = 0;
+ if (total != 0) {
+ print(7, (singular + ":" + plural + ":" + total).hashCode(), password);
+ }
+ }
+
+
+ private static void flag8(final byte[] password) {
+ // Count the number of bluish (#5984A1) pixels of this image:
+ // https://dev.java/assets/images/java-affinity-logo-icode-lg.png
+
+ final int number = 0;
+ if (number != 0) {
+ print(8, number, password);
+ }
+ }
+
+
+ private static final String PATTERN = "Reading the documentation can be surprisingly helpful!";
+ private static final int SMALL_REPEAT_COUNT = 10_000_000;
+
+ private static void flag9(final byte[] password) {
+ String repeated = "";
+ for (int i = 0; i < SMALL_REPEAT_COUNT; i++) {
+ repeated += PATTERN;
+ }
+
+ print(9, repeated.hashCode(), password);
+ }
+
+
+ private static final long LARGE_REPEAT_SHIFT = 29;
+ private static final long LARGE_REPEAT_COUNT = 1L << LARGE_REPEAT_SHIFT;
+
+ private static void flag10(final byte[] password) {
+ String repeated = "";
+ for (long i = 0; i < LARGE_REPEAT_COUNT; i++) {
+ repeated += PATTERN;
+ }
+
+ print(10, repeated.hashCode(), password);
+ }
+
+
+ private static void flag11(final byte[] password) {
+ print(11, 5823470598324780581L, password);
+ }
+
+
+ private static void flag12(final byte[] password) {
+ final BigInteger year = BigInteger.valueOf(-2025);
+ final BigInteger term = BigInteger.valueOf(PRIME + Math.abs(getInt(password)) % PRIME);
+
+ final long result = Stream.iterate(BigInteger.ZERO, BigInteger.ONE::add)
+ .filter(i -> year.multiply(i).add(term).multiply(i).compareTo(BigInteger.TEN) > 0)
+ .mapToLong(i -> i.longValue() * password[i.intValue() % password.length])
+ .sum();
+
+ print(12, result, password);
+ }
+
+
+ private static final long MAX_DEPTH = 100_000_000L;
+
+ private static void flag13(final byte[] password) {
+ try {
+ flag13(password, 0, 0);
+ } catch (final StackOverflowError e) {
+ System.err.println("Stack overflow :((");
+ }
+ }
+
+ private static void flag13(final byte[] password, final long depth, final long result) {
+ if (depth < MAX_DEPTH) {
+ flag13(password, depth + 1, (result ^ PRIME) | (result << 2) + depth * 17);
+ } else {
+ print(13, result, password);
+ }
+ }
+
+
+ private static void flag14(final byte[] password) {
+ final Instant today = Instant.parse("2025-09-09T12:00:00Z");
+ final BigInteger hours = BigInteger.valueOf(Duration.between(Instant.EPOCH, today).toHours() + password[1] + password[3]);
+
+ final long result = Stream.iterate(BigInteger.ZERO, hours::add)
+ .reduce(BigInteger.ZERO, BigInteger::add)
+ .longValue();
+
+ print(14, result, password);
+ }
+
+ private static void flag15(final byte[] password) {
+ // REDACTED
+ }
+
+ private static void flag16(final byte[] password) {
+ byte[] a = {
+ (byte) (password[0] + password[3]),
+ (byte) (password[1] + password[4]),
+ (byte) (password[2] + password[5])
+ };
+
+ for (long i = 1_000_000_000_000_000_000L + getInt(password); i >= 0; i--) {
+ flag16Update(a);
+ }
+
+ print(16, flag16Result(a), password);
+ }
+
+ /* package-private */ static void flag16Update(byte[] a) {
+ a[0] ^= a[1];
+ a[1] -= a[2] | a[0];
+ a[2] *= a[0];
+ }
+
+ /* package-private */ static int flag16Result(byte[] a) {
+ return (a[0] + " " + a[1] + " " + a[2]).hashCode();
+ }
+
+ /**
+ * Original idea by Alexei Shishkin.
+ */
+ private static void flag17(final byte[] password) {
+ final int n = Math.abs(getInt(password) % 2025) + 2025;
+ print(17, calc17(n), password);
+ }
+
+ /**
+ * Write me
+ *
+ * 0: iconst_0
+ * 1: istore_1
+ * 2: iload_1
+ * 3: iload_1
+ * 4: imul
+ * 5: sipush 2025
+ * 8: idiv
+ * 9: iload_0
+ * 10: isub
+ * 11: ifge 20
+ * 14: iinc 1, 1
+ * 17: goto 2
+ * 20: iload_1
+ * 21: ireturn
+ *
+ */
+ private static int calc17(final int n) {
+ return n;
+ }
+
+
+ private static void flag18(final byte[] password) {
+ final int n = 2025 + getInt(password) % 2025;
+ // Find the number of factors of n! modulo PRIME
+ final int factors = 0;
+ if (factors != 0) {
+ print(18, factors, password);
+ }
+ }
+
+
+ private static void flag19(final byte[] password) {
+ // Let n = 2025e25 + abs(getInt(password)).
+ // Consider the sequence of numbers (n + i) ** 2.
+ // Instead of each number, we write the number that is obtained from it by discarding the last 25 digits.
+ // How many of the first numbers of the resulting sequence will form an arithmetic progression?
+ final long result = 0;
+ if (result != 0) {
+ print(19, result, password);
+ }
+ }
+
+ /**
+ * Original idea by Dmitrii Liapin.
+ */
+ private static void flag20(final byte[] password) {
+ final Collection longs = new Random(getInt(password)).longs(2025_000)
+ .map(n -> n % 1000)
+ .boxed()
+ .collect(Collectors.toCollection(LinkedList::new));
+
+ // Calculate the number of objects (recursively) accessible by "longs" reference.
+ final int result = 0;
+
+ if (result != 0) {
+ print(20, result, password);
+ }
+ }
+
+ /**
+ * Original idea and implementation Igor Panasyuk.
+ */
+ private static void flag21(final byte[] password) {
+ record Pair(int x, int y) {
+ }
+
+ final List items = new ArrayList<>(Arrays.asList(
+ Byte.toUnsignedInt(password[0]),
+ (long) getInt(password),
+ new Pair(password[1], password[2]),
+ "Java SE 21 " + Arrays.toString(password)
+ ));
+
+ for (int round = 0; round < 10; round++) {
+ for (final Object item : List.copyOf(items)) {
+ // TODO: complete the switch expression using Java 21 features:
+ // items.add(
+ // case Integer i -> square of i as long
+ // case Long l and l is even -> l ^ 0x21L
+ // case Long l and l is odd -> -l
+ // case Pair(int x, int y) -> x << 8 ^ y
+ // case String s -> s.hashCode()
+ // default -> 0
+ // );
+ }
+ }
+
+ long result = 0;
+ for (final Object item : items) {
+ result = result * 17 + item.toString().hashCode();
+ }
+
+ print(21, result, password);
+ }
+
+
+ // ---------------------------------------------------------------------------------------------------------------
+ // You may ignore all code below this line.
+ // It is not required to get any of the flags.
+ // ---------------------------------------------------------------------------------------------------------------
+
+ private static void print(final int no, long result, final byte[] password) {
+ System.out.format("flag %d: https://www.kgeorgiy.info/courses/prog-intro/hw1/%s%n", no, flag(result, password));
+ }
+
+ /* package-private */ static String flag(long result, byte[] password) {
+ final byte[] flag = password.clone();
+ for (int i = 0; i < 6; i++) {
+ flag[i] ^= result;
+ result >>>= 8;
+ }
+
+ return flag(flag);
+ }
+
+ /* package-private */ static String flag(final byte[] data) {
+ final MessageDigest messageDigest = RunMe.DIGEST.get();
+ messageDigest.update(SALT);
+ messageDigest.update(data);
+ messageDigest.update(SALT);
+ final byte[] digest = messageDigest.digest();
+
+ return IntStream.range(0, 6)
+ .map(i -> (((digest[i * 2] & 255) << 8) + (digest[i * 2 + 1] & 255)) % KEYWORDS.size())
+ .mapToObj(KEYWORDS::get)
+ .collect(Collectors.joining("-"));
+ }
+
+ /* package-private */ static byte[] parseArgs(final String[] args) {
+ if (args.length != 6) {
+ throw error("Expected 6 command line arguments, found: %d", args.length);
+ }
+
+ final byte[] bytes = new byte[args.length];
+ for (int i = 0; i < args.length; i++) {
+ final Byte value = VALUES.get(args[i].toLowerCase(Locale.US));
+ if (value == null) {
+ throw error("Expected keyword, found: %s", args[i]);
+ }
+ bytes[i] = value;
+ }
+ return bytes;
+ }
+
+ private static AssertionError error(final String format, final Object... args) {
+ System.err.format(format, args);
+ System.err.println();
+ System.exit(1);
+ throw new AssertionError();
+ }
+
+ /* package-private */ static int getInt(byte[] password) {
+ return IntStream.range(0, password.length)
+ .map(i -> password[i])
+ .reduce((a, b) -> a * KEYWORDS.size() + b)
+ .getAsInt();
+ }
+
+ private static final ThreadLocal DIGEST = ThreadLocal.withInitial(() -> {
+ try {
+ return MessageDigest.getInstance("SHA-256");
+ } catch (final NoSuchAlgorithmException e) {
+ throw new AssertionError("Cannot create SHA-256 digest", e);
+ }
+ });
+
+ public static final byte[] SALT = "fathdufimmonJiajFik3JeccafdaihoFrusthys9".getBytes(StandardCharsets.US_ASCII);
+
+ private static final List KEYWORDS = List.of(
+ "abstract",
+ "assert",
+ "boolean",
+ "break",
+ "byte",
+ "case",
+ "catch",
+ "char",
+ "class",
+ "const",
+ "new",
+ "package",
+ "private",
+ "protected",
+ "public",
+ "return",
+ "short",
+ "static",
+ "strictfp",
+ "super",
+ "for",
+ "goto",
+ "if",
+ "implements",
+ "import",
+ "instanceof",
+ "int",
+ "interface",
+ "long",
+ "native",
+ "continue",
+ "default",
+ "do",
+ "double",
+ "else",
+ "enum",
+ "extends",
+ "final",
+ "finally",
+ "float",
+ "switch",
+ "synchronized",
+ "this",
+ "throw",
+ "throws",
+ "transient",
+ "try",
+ "void",
+ "volatile",
+ "while",
+ "record",
+ "Error",
+ "AssertionError",
+ "OutOfMemoryError",
+ "StackOverflowError",
+ "ArrayIndexOutOfBoundsException",
+ "ArrayStoreException",
+ "AutoCloseable",
+ "Character",
+ "CharSequence",
+ "ClassCastException",
+ "Comparable",
+ "Exception",
+ "IllegalArgumentException",
+ "IllegalStateException",
+ "IndexOutOfBoundsException",
+ "Integer",
+ "Iterable",
+ "Math",
+ "Module",
+ "NegativeArraySizeException",
+ "NullPointerException",
+ "Number",
+ "NumberFormatException",
+ "Object",
+ "Override",
+ "RuntimeException",
+ "StrictMath",
+ "String",
+ "StringBuilder",
+ "StringIndexOutOfBoundsException",
+ "SuppressWarnings",
+ "System",
+ "Thread",
+ "Throwable",
+ "ArithmeticException",
+ "ClassLoader",
+ "ClassNotFoundException",
+ "Cloneable",
+ "Deprecated",
+ "FunctionalInterface",
+ "InterruptedException",
+ "Process",
+ "ProcessBuilder",
+ "Runnable",
+ "SafeVarargs",
+ "StackTraceElement",
+ "Runtime",
+ "ThreadLocal",
+ "UnsupportedOperationException"
+ );
+
+ private static final Map VALUES = IntStream.range(0, KEYWORDS.size())
+ .boxed()
+ .collect(Collectors.toMap(index -> KEYWORDS.get(index).toLowerCase(Locale.US), Integer::byteValue));
+}
diff --git a/java/base/Asserts.java b/java/base/Asserts.java
new file mode 100644
index 0000000..17ea3e6
--- /dev/null
+++ b/java/base/Asserts.java
@@ -0,0 +1,84 @@
+package base;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@SuppressWarnings("StaticMethodOnlyUsedInOneClass")
+public final class Asserts {
+ static {
+ Locale.setDefault(Locale.US);
+ }
+
+ private Asserts() {
+ }
+
+ public static void assertEquals(final String message, final Object expected, final Object actual) {
+ final String reason = String.format("%s:%n expected `%s`,%n actual `%s`",
+ message, toString(expected), toString(actual));
+ assertTrue(reason, Objects.deepEquals(expected, actual));
+ }
+
+ public static String toString(final Object value) {
+ if (value != null && value.getClass().isArray()) {
+ final String result = Arrays.deepToString(new Object[]{value});
+ return result.substring(1, result.length() - 1);
+ } else {
+ return Objects.toString(value);
+ }
+ }
+
+ public static void assertEquals(final String message, final List expected, final List actual) {
+ for (int i = 0; i < Math.min(expected.size(), actual.size()); i++) {
+ assertEquals(message + ":" + (i + 1), expected.get(i), actual.get(i));
+ }
+ assertEquals(message + ": Number of items", expected.size(), actual.size());
+ }
+
+ public static void assertTrue(final String message, final boolean value) {
+ if (!value) {
+ throw error("%s", message);
+ }
+ }
+
+ public static void assertEquals(final String message, final double expected, final double actual, final double precision) {
+ assertTrue(
+ String.format("%s: Expected %.12f, found %.12f", message, expected, actual),
+ isEqual(expected, actual, precision)
+ );
+ }
+
+ public static boolean isEqual(final double expected, final double actual, final double precision) {
+ final double error = Math.abs(actual - expected);
+ return error <= precision
+ || error <= precision * Math.abs(expected)
+ || !Double.isFinite(expected)
+ || Math.abs(expected) > 1e100
+ || Math.abs(expected) < precision && !Double.isFinite(actual);
+ }
+
+ public static void assertSame(final String message, final Object expected, final Object actual) {
+ assertTrue(String.format("%s: expected same objects: %s and %s", message, expected, actual), expected == actual);
+ }
+
+ public static void checkAssert(final Class> c) {
+ if (!c.desiredAssertionStatus()) {
+ throw error("You should enable assertions by running 'java -ea %s'", c.getName());
+ }
+ }
+
+ public static AssertionError error(final String format, final Object... args) {
+ final String message = String.format(format, args);
+ return args.length > 0 && args[args.length - 1] instanceof Throwable
+ ? new AssertionError(message, (Throwable) args[args.length - 1])
+ : new AssertionError(message);
+ }
+
+ public static void printStackTrace(final String message) {
+ new Exception(message).printStackTrace(System.out);
+ }
+}
diff --git a/java/base/BaseChecker.java b/java/base/BaseChecker.java
new file mode 100644
index 0000000..67bd57c
--- /dev/null
+++ b/java/base/BaseChecker.java
@@ -0,0 +1,20 @@
+package base;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public abstract class BaseChecker {
+ protected final TestCounter counter;
+
+ protected BaseChecker(final TestCounter counter) {
+ this.counter = counter;
+ }
+
+ public ExtendedRandom random() {
+ return counter.random();
+ }
+
+ public int mode() {
+ return counter.mode();
+ }
+}
diff --git a/java/base/Either.java b/java/base/Either.java
new file mode 100644
index 0000000..8a3eca8
--- /dev/null
+++ b/java/base/Either.java
@@ -0,0 +1,95 @@
+package base;
+
+import java.util.function.Function;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public interface Either {
+ Either mapRight(final Function super R, NR> f);
+ Either flatMapRight(final Function super R, ? extends Either> f);
+ T either(Function super L, ? extends T> lf, Function super R, ? extends T> rf);
+
+ boolean isRight();
+
+ L getLeft();
+ R getRight();
+
+ static Either right(final R value) {
+ return new Either<>() {
+ @Override
+ public Either mapRight(final Function super R, NR> f) {
+ return right(f.apply(value));
+ }
+
+ @Override
+ public Either flatMapRight(final Function super R, ? extends Either> f) {
+ return f.apply(value);
+ }
+
+ @Override
+ public T either(final Function super L, ? extends T> lf, final Function super R, ? extends T> rf) {
+ return rf.apply(value);
+ }
+
+ @Override
+ public boolean isRight() {
+ return true;
+ }
+
+ @Override
+ public L getLeft() {
+ return null;
+ }
+
+ @Override
+ public R getRight() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Right(%s)", value);
+ }
+ };
+ }
+
+ static Either left(final L value) {
+ return new Either<>() {
+ @Override
+ public Either mapRight(final Function super R, NR> f) {
+ return left(value);
+ }
+
+ @Override
+ public Either flatMapRight(final Function super R, ? extends Either> f) {
+ return left(value);
+ }
+
+ @Override
+ public T either(final Function super L, ? extends T> lf, final Function super R, ? extends T> rf) {
+ return lf.apply(value);
+ }
+
+ @Override
+ public boolean isRight() {
+ return false;
+ }
+
+ @Override
+ public L getLeft() {
+ return value;
+ }
+
+ @Override
+ public R getRight() {
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Left(%s)", value);
+ }
+ };
+ }
+}
diff --git a/java/base/ExtendedRandom.java b/java/base/ExtendedRandom.java
new file mode 100644
index 0000000..ac2b059
--- /dev/null
+++ b/java/base/ExtendedRandom.java
@@ -0,0 +1,89 @@
+package base;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public final class ExtendedRandom {
+ public static final String ENGLISH = "abcdefghijklmnopqrstuvwxyz";
+ public static final String RUSSIAN = "абвгдеежзийклмнопрстуфхцчшщъыьэюя";
+ public static final String GREEK = "αβγŋδεζηθικλμνξοπρτυφχψω";
+ @SuppressWarnings("StaticMethodOnlyUsedInOneClass")
+ public static final String SPACES = " \t\n\u000B\u2029\f";
+
+ private final Random random;
+
+ public ExtendedRandom(final Random random) {
+ this.random = random;
+ }
+
+ public ExtendedRandom(final Class> owner) {
+ this(new Random(7912736473497634913L + owner.getName().hashCode()));
+ }
+
+ public String randomString(final String chars) {
+ return randomChar(chars) + (random.nextBoolean() ? "" : randomString(chars));
+ }
+
+ public char randomChar(final String chars) {
+ return chars.charAt(nextInt(chars.length()));
+ }
+
+ public String randomString(final String chars, final int length) {
+ final StringBuilder string = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ string.append(randomChar(chars));
+ }
+ return string.toString();
+ }
+
+ public String randomString(final String chars, final int minLength, final int maxLength) {
+ return randomString(chars, nextInt(minLength, maxLength));
+ }
+
+ public boolean nextBoolean() {
+ return random.nextBoolean();
+ }
+
+ public int nextInt() {
+ return random.nextInt();
+ }
+
+ public int nextInt(final int min, final int max) {
+ return nextInt(max - min + 1) + min;
+ }
+
+ public int nextInt(final int n) {
+ return random.nextInt(n);
+ }
+
+ @SafeVarargs
+ public final T randomItem(final T... items) {
+ return items[nextInt(items.length)];
+ }
+
+ public T randomItem(final List items) {
+ return items.get(nextInt(items.size()));
+ }
+
+ public Random getRandom() {
+ return random;
+ }
+
+ public List random(final int list, final Function generator) {
+ return Stream.generate(() -> generator.apply(this)).limit(list).toList();
+ }
+
+ public double nextDouble() {
+ return random.nextDouble();
+ }
+
+ public void shuffle(final List all) {
+ Collections.shuffle(all, random);
+ }
+}
diff --git a/java/base/Functional.java b/java/base/Functional.java
new file mode 100644
index 0000000..ef14dd5
--- /dev/null
+++ b/java/base/Functional.java
@@ -0,0 +1,92 @@
+package base;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public final class Functional {
+ private Functional() {}
+
+ public static List map(final Collection items, final Function super T, ? extends R> f) {
+ return items.stream().map(f).collect(Collectors.toUnmodifiableList());
+ }
+
+ public static List map(final List items, final BiFunction super Integer, ? super T, ? extends R> f) {
+ return IntStream.range(0, items.size())
+ .mapToObj(i -> f.apply(i, items.get(i)))
+ .collect(Collectors.toUnmodifiableList());
+ }
+
+ public static Map mapValues(final Map map, final Function f) {
+ return map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> f.apply(e.getValue())));
+ }
+
+ @SafeVarargs
+ public static Map mergeMaps(final Map... maps) {
+ return Stream.of(maps).flatMap(m -> m.entrySet().stream())
+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a));
+ }
+
+ @SafeVarargs
+ public static List concat(final Collection extends T>... items) {
+ final List result = new ArrayList<>();
+ for (final Collection extends T> item : items) {
+ result.addAll(item);
+ }
+ return result;
+ }
+
+ public static List append(final Collection collection, final T item) {
+ final List list = new ArrayList<>(collection);
+ list.add(item);
+ return list;
+ }
+
+ public static List> allValues(final List vals, final int length) {
+ return Stream.generate(() -> vals)
+ .limit(length)
+ .reduce(
+ List.of(List.of()),
+ (prev, next) -> next.stream()
+ .flatMap(value -> prev.stream().map(list -> append(list, value)))
+ .toList(),
+ (prev, next) -> next.stream()
+ .flatMap(suffix -> prev.stream().map(prefix -> concat(prefix, suffix)))
+ .toList()
+ );
+ }
+
+ public static V get(final Map map, final K key) {
+ final V result = map.get(key);
+ if (result == null) {
+ throw new NullPointerException(key.toString() + " in " + map(map.keySet(), Objects::toString));
+ }
+ return result;
+ }
+
+ public static void addRange(final List values, final int d, final int c) {
+ for (int i = -d; i <= d; i++) {
+ values.add(c + i);
+ }
+ }
+
+ public static void forEachPair(final T[] items, final BiConsumer super T, ? super T> consumer) {
+ assert items.length % 2 == 0;
+ IntStream.range(0, items.length / 2).forEach(i -> consumer.accept(items[i * 2], items[i * 2 + 1]));
+ }
+
+
+ public static List> toPairs(final T[] items) {
+ assert items.length % 2 == 0;
+ return IntStream.range(0, items.length / 2)
+ .mapToObj(i -> Pair.of(items[i * 2], items[i * 2 + 1]))
+ .toList();
+ }
+}
diff --git a/java/base/Log.java b/java/base/Log.java
new file mode 100644
index 0000000..00d9141
--- /dev/null
+++ b/java/base/Log.java
@@ -0,0 +1,56 @@
+package base;
+
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public class Log {
+ private final Pattern LINES = Pattern.compile("\n");
+ private int indent;
+
+ public static Supplier action(final Runnable action) {
+ return () -> {
+ action.run();
+ return null;
+ };
+ }
+
+ public void scope(final String name, final Runnable action) {
+ scope(name, action(action));
+ }
+
+ public T scope(final String name, final Supplier action) {
+ println(name);
+ indent++;
+ try {
+ return silentScope(name, action);
+ } finally {
+ indent--;
+ }
+ }
+
+ public T silentScope(final String ignoredName, final Supplier action) {
+ return action.get();
+ }
+
+ @SuppressWarnings("UseOfSystemOutOrSystemErr")
+ public void println(final Object value) {
+ for (final String line : LINES.split(String.valueOf(value))) {
+ System.out.println(indent() + line);
+ }
+ }
+
+ public void format(final String format, final Object... args) {
+ println(String.format(format,args));
+ }
+
+ private String indent() {
+ return " ".repeat(indent);
+ }
+
+ protected int getIndent() {
+ return indent;
+ }
+}
diff --git a/java/base/MainChecker.java b/java/base/MainChecker.java
new file mode 100644
index 0000000..e526e7e
--- /dev/null
+++ b/java/base/MainChecker.java
@@ -0,0 +1,28 @@
+package base;
+
+import java.util.List;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@SuppressWarnings("StaticMethodOnlyUsedInOneClass")
+public final class MainChecker {
+ private final Runner runner;
+
+ public MainChecker(final Runner runner) {
+ this.runner = runner;
+ }
+
+ public List run(final TestCounter counter, final String... input) {
+ return runner.run(counter, input);
+ }
+
+ public List run(final TestCounter counter, final List input) {
+ return runner.run(counter, input);
+ }
+
+ public void testEquals(final TestCounter counter, final List input, final List expected) {
+ runner.testEquals(counter, input, expected);
+ }
+
+}
diff --git a/java/base/Named.java b/java/base/Named.java
new file mode 100644
index 0000000..befb254
--- /dev/null
+++ b/java/base/Named.java
@@ -0,0 +1,15 @@
+package base;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public record Named(String name, T value) {
+ public static Named of(final String name, final T f) {
+ return new Named<>(name, f);
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+}
diff --git a/java/base/Pair.java b/java/base/Pair.java
new file mode 100644
index 0000000..8c27a31
--- /dev/null
+++ b/java/base/Pair.java
@@ -0,0 +1,44 @@
+package base;
+
+import java.util.Map;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.UnaryOperator;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@SuppressWarnings({"StaticMethodOnlyUsedInOneClass", "unused"})
+public record Pair(F first, S second) {
+ public static Pair of(final F first, final S second) {
+ return new Pair<>(first, second);
+ }
+
+ public static Pair of(final Map.Entry e) {
+ return of(e.getKey(), e.getValue());
+ }
+
+ public static UnaryOperator> lift(final UnaryOperator f, final UnaryOperator s) {
+ return p -> of(f.apply(p.first), s.apply(p.second));
+ }
+
+ public static BinaryOperator> lift(final BinaryOperator f, final BinaryOperator s) {
+ return (p1, p2) -> of(f.apply(p1.first, p2.first), s.apply(p1.second, p2.second));
+ }
+
+ public static Function> tee(
+ final Function super T, ? extends F> f,
+ final Function super T, ? extends S> s
+ ) {
+ return t -> of(f.apply(t), s.apply(t));
+ }
+
+ @Override
+ public String toString() {
+ return "(" + first + ", " + second + ")";
+ }
+
+ public Pair second(final R second) {
+ return new Pair<>(first, second);
+ }
+}
diff --git a/java/base/Runner.java b/java/base/Runner.java
new file mode 100644
index 0000000..c0a59bc
--- /dev/null
+++ b/java/base/Runner.java
@@ -0,0 +1,185 @@
+package base;
+
+import java.io.*;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@SuppressWarnings("unused")
+@FunctionalInterface
+public interface Runner {
+ List run(final TestCounter counter, final List input);
+
+ default List run(final TestCounter counter, final String... input) {
+ return run(counter, List.of(input));
+ }
+
+ default void testEquals(final TestCounter counter, final List input, final List expected) {
+ counter.test(() -> {
+ final List actual = run(counter, input);
+ for (int i = 0; i < Math.min(expected.size(), actual.size()); i++) {
+ final String exp = expected.get(i);
+ final String act = actual.get(i);
+ if (!exp.equalsIgnoreCase(act)) {
+ Asserts.assertEquals("Line " + (i + 1), exp, act);
+ return;
+ }
+ }
+ Asserts.assertEquals("Number of lines", expected.size(), actual.size());
+ });
+ }
+
+ static Packages packages(final String... packages) {
+ return new Packages(List.of(packages));
+ }
+
+ @FunctionalInterface
+ interface CommentRunner {
+ List run(String comment, TestCounter counter, List input);
+ }
+
+ final class Packages {
+ private final List packages;
+
+ private Packages(final List packages) {
+ this.packages = packages;
+ }
+
+ public Runner std(final String className) {
+ final CommentRunner main = main(className);
+ return (counter, input) -> counter.call("io", () -> {
+ final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ try (final PrintWriter writer = new PrintWriter(baos, false, StandardCharsets.UTF_8)) {
+ input.forEach(writer::println);
+ }
+
+ final InputStream oldIn = System.in;
+ try {
+ System.setIn(new ByteArrayInputStream(baos.toByteArray()));
+ return main.run(String.format("[%d input lines]", input.size()), counter, List.of());
+ } finally {
+ System.setIn(oldIn);
+ }
+ });
+ }
+
+ @SuppressWarnings("ConfusingMainMethod")
+ public CommentRunner main(final String className) {
+ final Method method = findMain(className);
+
+ return (comment, counter, input) -> {
+ counter.format("Running test %02d: java %s %s%n", counter.getTestNo(), method.getDeclaringClass().getName(), comment);
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ @SuppressWarnings("UseOfSystemOutOrSystemErr") final PrintStream oldOut = System.out;
+ try {
+ System.setOut(new PrintStream(out, false, StandardCharsets.UTF_8));
+ method.invoke(null, new Object[]{input.toArray(String[]::new)});
+ final BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(out.toByteArray()), StandardCharsets.UTF_8));
+ final List result = new ArrayList<>();
+ while (true) {
+ final String line = reader.readLine();
+ if (line == null) {
+ if (result.isEmpty()) {
+ result.add("");
+ }
+ return result;
+ }
+ result.add(line.trim());
+ }
+ } catch (final InvocationTargetException e) {
+ final Throwable cause = e.getCause();
+ throw Asserts.error("main thrown exception %s: %s", cause.getClass().getSimpleName(), cause);
+ } catch (final Exception e) {
+ throw Asserts.error("Cannot invoke main: %s: %s", e.getClass().getSimpleName(), e.getMessage());
+ } finally {
+ System.setOut(oldOut);
+ }
+ };
+ }
+
+ private Method findMain(final String className) {
+ try {
+ final URL url = new File(".").toURI().toURL();
+ final List candidates = packages.stream()
+ .flatMap(pkg -> {
+ final String prefix = pkg.isEmpty() ? pkg : pkg + ".";
+ return Stream.of(prefix + className, prefix + "$" + className);
+ })
+ .toList();
+
+ //noinspection ClassLoaderInstantiation,resource,IOResourceOpenedButNotSafelyClosed
+ final URLClassLoader classLoader = new URLClassLoader(new URL[]{url});
+ for (final String candidate : candidates) {
+ try {
+ final Class> loaded = classLoader.loadClass(candidate);
+ if (!Modifier.isPublic(loaded.getModifiers())) {
+ throw Asserts.error("Class %s is not public", candidate);
+ }
+ final Method main = loaded.getMethod("main", String[].class);
+ if (!Modifier.isPublic(main.getModifiers()) || !Modifier.isStatic(main.getModifiers())) {
+ throw Asserts.error("Method main of class %s should be public and static", candidate);
+ }
+ return main;
+ } catch (final ClassNotFoundException e) {
+ // Ignore
+ } catch (final NoSuchMethodException e) {
+ throw Asserts.error("Could not find method main(String[]) in class %s", candidate, e);
+ }
+ }
+ throw Asserts.error("Could not find neither of classes %s", candidates);
+ } catch (final MalformedURLException e) {
+ throw Asserts.error("Invalid path", e);
+ }
+ }
+
+ private static String getClassName(final String pkg, final String className) {
+ return pkg.isEmpty() ? className : pkg + "." + className;
+ }
+
+ public Runner args(final String className) {
+ final CommentRunner main = main(className);
+// final AtomicReference prev = new AtomicReference<>("");
+ return (counter, input) -> {
+ final int total = input.stream().mapToInt(String::length).sum() + input.size() * 3;
+ final String comment = total <= 300
+ ? input.stream().collect(Collectors.joining("\" \"", "\"", "\""))
+ : input.size() <= 100
+ ? String.format("[%d arguments, sizes: %s]", input.size(), input.stream()
+ .mapToInt(String::length)
+ .mapToObj(String::valueOf)
+ .collect(Collectors.joining(", ")))
+ : String.format("[%d arguments, total size: %d]", input.size(), total);
+// assert comment.length() <= 5 || !prev.get().equals(comment) : "Duplicate tests " + comment;
+// prev.set(comment);
+ return main.run(comment, counter, input);
+ };
+ }
+
+ public Runner files(final String className) {
+ final Runner args = args(className);
+ return (counter, input) -> counter.call("io", () -> {
+ final Path inf = counter.getFile("in");
+ final Path ouf = counter.getFile("out");
+ Files.write(inf, input);
+ args.run(counter, List.of(inf.toString(), ouf.toString()));
+ final List output = Files.readAllLines(ouf);
+ Files.delete(inf);
+ Files.delete(ouf);
+ return output;
+ });
+ }
+ }
+}
diff --git a/java/base/Selector.java b/java/base/Selector.java
new file mode 100644
index 0000000..dc119b9
--- /dev/null
+++ b/java/base/Selector.java
@@ -0,0 +1,143 @@
+package base;
+
+import java.util.*;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public final class Selector {
+ private final Class> owner;
+ private final List modes;
+ private final Set variantNames = new LinkedHashSet<>();
+ private final Map> variants = new LinkedHashMap<>();
+
+ public Selector(final Class> owner, final String... modes) {
+ this.owner = owner;
+ this.modes = List.of(modes);
+ }
+
+ public Selector variant(final String name, final Consumer operations) {
+ Asserts.assertTrue("Duplicate variant " + name, variants.put(name.toLowerCase(), operations) == null);
+ variantNames.add(name);
+ return this;
+ }
+
+ private static void check(final boolean condition, final String format, final Object... args) {
+ if (!condition) {
+ throw new IllegalArgumentException(String.format(format, args));
+ }
+ }
+
+ @SuppressWarnings("UseOfSystemOutOrSystemErr")
+ public void main(final String... args) {
+ try {
+ final String mode;
+ if (modes.isEmpty()) {
+ check(args.length >= 1, "At least one argument expected, found %s", args.length);
+ mode = "";
+ } else {
+ check(args.length >= 2, "At least two arguments expected, found %s", args.length);
+ mode = args[0];
+ }
+
+ final List vars = Arrays.stream(args).skip(modes.isEmpty() ? 0 : 1)
+ .flatMap(arg -> Arrays.stream(arg.split("[ +]+")))
+ .toList();
+
+ test(mode, vars);
+ } catch (final IllegalArgumentException e) {
+ System.err.println("ERROR: " + e.getMessage());
+ if (modes.isEmpty()) {
+ System.err.println("Usage: " + owner.getName() + " VARIANT...");
+ } else {
+ System.err.println("Usage: " + owner.getName() + " MODE VARIANT...");
+ System.err.println("Modes: " + String.join(", ", modes));
+ }
+ System.err.println("Variants: " + String.join(", ", variantNames));
+ System.exit(1);
+ }
+ }
+
+ public void test(final String mode, List vars) {
+ final int modeNo = modes.isEmpty() ? -1 : modes.indexOf(mode) ;
+ check(modes.isEmpty() || modeNo >= 0, "Unknown mode '%s'", mode);
+ if (variantNames.contains("Base") && !vars.contains("Base")) {
+ vars = new ArrayList<>(vars);
+ vars.add(0, "Base");
+ }
+
+ vars.forEach(variant -> check(variants.containsKey(variant.toLowerCase()), "Unknown variant '%s'", variant));
+
+ final Map properties = modes.isEmpty()
+ ? Map.of("variant", String.join("+", vars))
+ : Map.of("variant", String.join("+", vars), "mode", mode);
+ final TestCounter counter = new TestCounter(owner, modeNo, properties);
+ counter.printHead();
+ vars.forEach(variant -> counter.scope("Testing " + variant, () -> variants.get(variant.toLowerCase()).accept(counter)));
+ counter.printStatus();
+ }
+
+ public static Composite composite(final Class> owner, final Function factory, final String... modes) {
+ return new Composite<>(owner, factory, (tester, counter) -> tester.test(), modes);
+ }
+
+ public static Composite composite(final Class> owner, final Function factory, final BiConsumer tester, final String... modes) {
+ return new Composite<>(owner, factory, tester, modes);
+ }
+
+ public List getModes() {
+ return modes.isEmpty() ? List.of("~") : modes;
+ }
+
+ public List getVariants() {
+ return List.copyOf(variants.keySet());
+ }
+
+ /**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+ public static final class Composite {
+ private final Selector selector;
+ private final Function factory;
+ private final BiConsumer tester;
+ private List> base;
+
+ private Composite(final Class> owner, final Function factory, final BiConsumer tester, final String... modes) {
+ selector = new Selector(owner, modes);
+ this.factory = factory;
+ this.tester = tester;
+ }
+
+ @SafeVarargs
+ public final Composite variant(final String name, final Consumer super V>... parts) {
+ if ("Base".equalsIgnoreCase(name)) {
+ base = List.of(parts);
+ return v(name.toLowerCase());
+ } else {
+ return v(name, parts);
+ }
+ }
+
+ @SafeVarargs
+ private Composite v(final String name, final Consumer super V>... parts) {
+ selector.variant(name, counter -> {
+ final V variant = factory.apply(counter);
+ for (final Consumer super V> part : base) {
+ part.accept(variant);
+ }
+ for (final Consumer super V> part : parts) {
+ part.accept(variant);
+ }
+ tester.accept(variant, counter);
+ });
+ return this;
+ }
+
+ public Selector selector() {
+ return selector;
+ }
+ }
+}
diff --git a/java/base/TestCounter.java b/java/base/TestCounter.java
new file mode 100644
index 0000000..85fb9c9
--- /dev/null
+++ b/java/base/TestCounter.java
@@ -0,0 +1,184 @@
+package base;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public class TestCounter extends Log {
+ public static final int DENOMINATOR = Integer.getInteger("base.denominator", 1);
+ public static final int DENOMINATOR2 = (int) Math.round(Math.sqrt(DENOMINATOR));
+
+ private static final String JAR_EXT = ".jar";
+ private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");
+
+ private final Class> owner;
+ private final int mode;
+ private final Map properties;
+ private final ExtendedRandom random;
+
+ private final long start = System.currentTimeMillis();
+ private int passed;
+
+ public TestCounter(final Class> owner, final int mode, final Map properties) {
+ Locale.setDefault(Locale.US);
+ Asserts.checkAssert(getClass());
+
+ this.owner = owner;
+ this.mode = mode;
+ this.properties = properties;
+ random = new ExtendedRandom(owner);
+ }
+
+ public int mode() {
+ return mode;
+ }
+
+ public int getTestNo() {
+ return passed + 1;
+ }
+
+ public void test(final Runnable action) {
+ testV(() -> {
+ action.run();
+ return null;
+ });
+ }
+
+ public void testForEach(final Iterable extends T> items, final Consumer super T> action) {
+ for (final T item : items) {
+ test(() -> action.accept(item));
+ }
+ }
+
+ public T testV(final Supplier action) {
+ return silentScope("Test " + getTestNo(), () -> {
+ final T result = action.get();
+ passed++;
+ return result;
+ });
+ }
+
+ private String getLine() {
+ return getIndent() == 0 ? "=" : "-";
+ }
+
+ public void printHead() {
+ println("=== " + getTitle());
+ }
+
+ public void printStatus() {
+ format("%s%n%s%n", getLine().repeat(30), getTitle());
+ format("%d tests passed in %dms%n", passed, System.currentTimeMillis() - start);
+ println("Version: " + getVersion(owner));
+ println("");
+ }
+
+ private String getTitle() {
+ return String.format("%s %s", owner.getSimpleName(), properties.isEmpty() ? "" : properties);
+ }
+
+ private static String getVersion(final Class> clazz) {
+ try {
+ final ClassLoader cl = clazz.getClassLoader();
+ final URL url = cl.getResource(clazz.getName().replace('.', '/') + ".class");
+ if (url == null) {
+ return "(no manifest)";
+ }
+
+ final String path = url.getPath();
+ final int index = path.indexOf(JAR_EXT);
+ if (index == -1) {
+ return DATE_FORMAT.format(new Date(new File(path).lastModified()));
+ }
+
+ final String jarPath = path.substring(0, index + JAR_EXT.length());
+ try (final JarFile jarFile = new JarFile(new File(new URI(jarPath)))) {
+ final JarEntry entry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
+ return DATE_FORMAT.format(new Date(entry.getTime()));
+ }
+ } catch (final IOException | URISyntaxException e) {
+ return "error: " + e;
+ }
+ }
+
+ public T call(final String message, final SupplierE supplier) {
+ return get(supplier).either(e -> fail(e, "%s", message), Function.identity());
+ }
+
+ public void shouldFail(final String message, @SuppressWarnings("TypeMayBeWeakened") final RunnableE action) {
+ test(() -> get(action).either(e -> null, v -> fail("%s", message)));
+ }
+
+ public T fail(final String format, final Object... args) {
+ return fail(Asserts.error(format, args));
+ }
+
+ public T fail(final Throwable throwable) {
+ return fail(throwable, "%s: %s", throwable.getClass().getSimpleName(), throwable.getMessage());
+ }
+
+ public T fail(final Throwable throwable, final String format, final Object... args) {
+ final String message = String.format(format, args);
+ println("ERROR: " + message);
+ throw throwable instanceof Error ? (Error) throwable : new AssertionError(throwable);
+ }
+
+ public void checkTrue(final boolean condition, final String message, final Object... args) {
+ if (!condition) {
+ fail(message, args);
+ }
+ }
+
+ public static Either get(final SupplierE supplier) {
+ return supplier.get();
+ }
+
+ public Path getFile(final String suffix) {
+ return Paths.get(String.format("test%d.%s", getTestNo(), suffix));
+ }
+
+ public ExtendedRandom random() {
+ return random;
+ }
+
+ @FunctionalInterface
+ public interface SupplierE extends Supplier> {
+ T getE() throws Exception;
+
+ @Override
+ default Either get() {
+ try {
+ return Either.right(getE());
+ } catch (final Exception e) {
+ return Either.left(e);
+ }
+ }
+ }
+
+ @FunctionalInterface
+ public interface RunnableE extends SupplierE {
+ void run() throws Exception;
+
+ @Override
+ default Void getE() throws Exception {
+ run();
+ return null;
+ }
+ }
+}
diff --git a/java/base/Tester.java b/java/base/Tester.java
new file mode 100644
index 0000000..d30260d
--- /dev/null
+++ b/java/base/Tester.java
@@ -0,0 +1,18 @@
+package base;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public abstract class Tester extends BaseChecker {
+ protected Tester(final TestCounter counter) {
+ super(counter);
+ }
+
+ public abstract void test();
+
+ public void run(final Class> test, final String... args) {
+ System.out.println("=== Testing " + test.getSimpleName() + " " + String.join(" ", args));
+ test();
+ counter.printStatus();
+ }
+}
diff --git a/java/base/Unit.java b/java/base/Unit.java
new file mode 100644
index 0000000..290febf
--- /dev/null
+++ b/java/base/Unit.java
@@ -0,0 +1,15 @@
+package base;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public final class Unit {
+ public static final Unit INSTANCE = new Unit();
+
+ private Unit() { }
+
+ @Override
+ public String toString() {
+ return "unit";
+ }
+}
diff --git a/java/base/package-info.java b/java/base/package-info.java
new file mode 100644
index 0000000..0055441
--- /dev/null
+++ b/java/base/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * Common homeworks test classes
+ * of Introduction to Programming course.
+ *
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+package base;
\ No newline at end of file
diff --git a/java/expression/BigDecimalListExpression.java b/java/expression/BigDecimalListExpression.java
new file mode 100644
index 0000000..67841c3
--- /dev/null
+++ b/java/expression/BigDecimalListExpression.java
@@ -0,0 +1,202 @@
+package expression;
+
+import base.Asserts;
+import base.Pair;
+import base.TestCounter;
+import expression.common.ExpressionKind;
+import expression.common.Type;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.stream.IntStream;
+
+/**
+ * One-argument arithmetic expression over {@link BigDecimal}s.
+ *
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@FunctionalInterface
+@SuppressWarnings("ClassReferencesSubclass")
+public interface BigDecimalListExpression extends ToMiniString {
+ BigDecimal evaluateBd(List variables);
+
+ // Tests follow. You may temporarily remove everything til the end.
+
+ Add EXAMPLE = new Add(
+ new Subtract(new Variable(0), new Const(BigDecimal.ONE)),
+ new Multiply(new Variable(1), new Const(BigDecimal.TEN))
+ );
+
+ Type TYPE = new Type<>(
+ v -> new BigDecimal(v + ".000"),
+ random -> BigDecimal.valueOf(random.getRandom().nextGaussian()),
+ BigDecimal.class
+ );
+ ExpressionKind KIND = new ExpressionKind<>(
+ TYPE,
+ BigDecimalListExpression.class,
+ (r, c) -> IntStream.range(0, c)
+ .mapToObj(name -> Pair.of("$" + name, new Variable(name)))
+ .toList(),
+ (expr, variables, values) -> expr.evaluateBd(values)
+ );
+
+ @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled")
+ static ExpressionTester, ?> tester(final TestCounter counter) {
+ Asserts.assertEquals("Example toString()", "(($0 - 1) + ($1 * 10))", EXAMPLE.toString());
+ Asserts.assertEquals(
+ EXAMPLE + " at (2, 3)",
+ BigDecimal.valueOf(31),
+ EXAMPLE.evaluateBd(List.of(BigDecimal.valueOf(2), BigDecimal.valueOf(3)))
+ );
+
+ final Variable vx = new Variable(0);
+ final Variable vy = new Variable(1);
+
+ return new ExpressionTester<>(
+ counter, KIND, c -> v -> c,
+ (op, a, b) -> v -> op.apply(a.evaluateBd(v), b.evaluateBd(v)),
+ BigDecimal::add, BigDecimal::subtract, BigDecimal::multiply, BigDecimal::divide
+ )
+ .basic("10", "10", v -> v(10), c(10))
+ .basic("$x", "$x", BigDecimalListExpression::x, vx)
+ .basic("$y", "$y", BigDecimalListExpression::y, vy)
+ .basic("($x + $y)", "$x + $y", v -> x(v).add(y(v)), new Add(vx, vy))
+ .basic("($x + 2)", "$x + 2", v -> x(v).add(v(2)), new Add(vx, c(2)))
+ .basic("(2 - $x)", "2 - $x", v -> v(2).subtract(x(v)), new Subtract(c(2), vx))
+ .basic("(3 * $x)", "3 * $x", v -> v(3).multiply(x(v)), new Multiply(c(3), vx))
+ .basic("($x + $x)", "$x + $x", v -> x(v).add(x(v)), new Add(vx, vx))
+ .basic("($x / -2)", "$x / -2", v -> x(v).divide(v(-2)), new Divide(vx, c(-2)))
+ .basic("(2 + $x)", "2 + $x", v -> v(2).add(x(v)), new Add(c(2), vx))
+ .basic("((1 + 2) + 3)", "1 + 2 + 3", v -> v(6), new Add(new Add(c(1), c(2)), c(3)))
+ .basic("(1 + (2 * 3))", "1 + 2 * 3", v -> v(7), new Add(c(1), new Multiply(c(2), c(3))))
+ .basic("(1 - (2 * 3))", "1 - 2 * 3", v -> v(-5), new Subtract(c(1), new Multiply(c(2), c(3))))
+ .basic("(1 + (2 + 3))", "1 + 2 + 3", v -> v(6), new Add(c(1), new Add(c(2), c(3))))
+ .basic("((1 - 2) - 3)", "1 - 2 - 3", v -> v(-4), new Subtract(new Subtract(c(1), c(2)), c(3)))
+ .basic("(1 - (2 - 3))", "1 - (2 - 3)", v -> v(2), new Subtract(c(1), new Subtract(c(2), c(3))))
+ .basic("((1 * 2) * 3)", "1 * 2 * 3", v -> v(6), new Multiply(new Multiply(c(1), c(2)), c(3)))
+ .basic("(1 * (2 * 3))", "1 * 2 * 3", v -> v(6), new Multiply(c(1), new Multiply(c(2), c(3))))
+ .basic("((10 / 2) / 3)", "10 / 2 / 3", v -> v(10).divide(v(2)).divide(v(3)), new Divide(new Divide(c(10), c(2)), c(3)))
+ .basic("(10 / (3 / 2))", "10 / (3 / 2)", v -> v(10).divide(v(3).divide(v(2))), new Divide(c(10), new Divide(c(3), c(2))))
+ .basic("(($x * $x) + (($x - 1) / 10))",
+ "$x * $x + ($x - 1) / 10",
+ v -> x(v).multiply(x(v)).add(x(v).subtract(v(1)).divide(v(10))),
+ new Add(new Multiply(vx, vx), new Divide(new Subtract(vx, c(1)), c(10)))
+ )
+ .basic("($x * -1000000000)", "$x * -1000000000", v -> x(v).multiply(v(-1_000_000_000)), new Multiply(vx, c(-1_000_000_000)))
+ .basic("($x * -1000000000000000)", "$x * -1000000000000000", v -> x(v).multiply(v(-1_000_000_000_000_000L)), new Multiply(vx, c(-1_000_000_000_000_000L)))
+ .basic("(10 / $x)", "10 / $x", v -> v(10).divide(x(v)), new Divide(c(10), vx))
+ .basic("($x / $x)", "$x / $x", v -> x(v).divide(x(v)), new Divide(vx, vx))
+
+ .advanced("(2 + 1)", "2 + 1", v -> v(2 + 1), new Add(c(2), c(1)))
+ .advanced("($x - 1)", "$x - 1", v -> x(v).subtract(v(1)), new Subtract(vx, c(1)))
+ .advanced("(1 * 2)", "1 * 2", v -> v(1 * 2), new Multiply(c(1), c(2)))
+ .advanced("($x / 1)", "$x / 1", v -> x(v).divide(v(1)), new Divide(vx, c(1)))
+ .advanced("(1 + (2 + 1))", "1 + 2 + 1", v -> v(1 + 2 + 1), new Add(c(1), new Add(c(2), c(1))))
+ .advanced("($x - ($x - 1))", "$x - ($x - 1)", v -> x(v).subtract(x(v).subtract(v(1))), new Subtract(vx, new Subtract(vx, c(1))))
+ .advanced("(2 * ($x / 1))", "2 * ($x / 1)", v -> v(2).multiply(x(v).divide(v(1))), new Multiply(c(2), new Divide(vx, c(1))))
+ .advanced("(2 / ($x - 1))", "2 / ($x - 1)", v -> v(2).divide(x(v).subtract(v(1))), new Divide(c(2), new Subtract(vx, c(1))))
+ .advanced("((1 * 2) + $x)", "1 * 2 + $x", v -> v(1 * 2).add(x(v)), new Add(new Multiply(c(1), c(2)), vx))
+ .advanced("(($x - 1) - 2)", "$x - 1 - 2", v -> x(v).subtract(v(3)), new Subtract(new Subtract(vx, c(1)), c(2)))
+ .advanced("(($x / 1) * 2)", "$x / 1 * 2", v -> x(v).multiply(v(2)), new Multiply(new Divide(vx, c(1)), c(2)))
+ .advanced("((2 + 1) / 1)", "(2 + 1) / 1", v -> v(3), new Divide(new Add(c(2), c(1)), c(1)))
+ .advanced(
+ "(1 + (1 + (2 + 1)))",
+ "1 + 1 + 2 + 1",
+ v -> v(1 + 1 + 2 + 1),
+ new Add(c(1), new Add(c(1), new Add(c(2), c(1))))
+ )
+ .advanced(
+ "($x - ((1 * 2) + $x))",
+ "$x - (1 * 2 + $x)",
+ v -> x(v).subtract(v(1 * 2).add(x(v))),
+ new Subtract(vx, new Add(new Multiply(c(1), c(2)), vx))
+ )
+ .advanced(
+ "($x * (2 / ($x - 1)))",
+ "$x * (2 / ($x - 1))",
+ v -> x(v).multiply(v(2).divide(x(v).subtract(v(1)))),
+ new Multiply(vx, new Divide(c(2), new Subtract(vx, c(1))))
+ )
+ .advanced(
+ "($x / (1 + (2 + 1)))",
+ "$x / (1 + 2 + 1)",
+ v -> x(v).divide(v(1 + 2 + 1)),
+ new Divide(vx, new Add(c(1), new Add(c(2), c(1))))
+ )
+ .advanced(
+ "((1 * 2) + (2 + 1))",
+ "1 * 2 + 2 + 1",
+ v -> v(1 * 2 + 2 + 1),
+ new Add(new Multiply(c(1), c(2)), new Add(c(2), c(1)))
+ )
+ .advanced(
+ "((2 + 1) - (2 + 1))",
+ "2 + 1 - (2 + 1)",
+ v -> v(2 + 1 - (2 + 1)),
+ new Subtract(new Add(c(2), c(1)), new Add(c(2), c(1)))
+ )
+ .advanced(
+ "(($x - 1) * ($x / 1))",
+ "($x - 1) * ($x / 1)",
+ v -> x(v).subtract(v(1)).multiply(x(v).divide(v(1))),
+ new Multiply(new Subtract(vx, c(1)), new Divide(vx, c(1)))
+ )
+ .advanced(
+ "(($x - 1) / (1 * 2))",
+ "($x - 1) / (1 * 2)",
+ v -> x(v).subtract(v(1)).divide(v(2)),
+ new Divide(new Subtract(vx, c(1)), new Multiply(c(1), c(2)))
+ )
+ .advanced(
+ "((($x - 1) - 2) + $x)",
+ "$x - 1 - 2 + $x",
+ v -> x(v).subtract(v(3)).add(x(v)),
+ new Add(new Subtract(new Subtract(vx, c(1)), c(2)), vx)
+ )
+ .advanced(
+ "(((1 * 2) + $x) - 1)",
+ "1 * 2 + $x - 1",
+ v -> v(1).add(x(v)),
+ new Subtract(new Add(new Multiply(c(1), c(2)), vx), c(1))
+ )
+ .advanced(
+ "(((2 + 1) / 1) * $x)",
+ "(2 + 1) / 1 * $x",
+ v -> v(3).multiply(x(v)),
+ new Multiply(new Divide(new Add(c(2), c(1)), c(1)), vx)
+ )
+ .advanced(
+ "((2 / ($x - 1)) / 2)",
+ "2 / ($x - 1) / 2",
+ v -> v(2).divide(x(v).subtract(v(1))).divide(v(2)),
+ new Divide(new Divide(c(2), new Subtract(vx, c(1))), c(2))
+ );
+ }
+
+ private static BigDecimal x(final List vars) {
+ return vars.get(0);
+ }
+
+ private static BigDecimal y(final List vars) {
+ return vars.get(1);
+ }
+
+ private static Const c(final BigDecimal v) {
+ return TYPE.constant(v);
+ }
+
+ private static Const c(final long v) {
+ return TYPE.constant(v(v));
+ }
+
+ private static BigDecimal v(final long v) {
+ return BigDecimal.valueOf(v);
+ }
+
+ static void main(final String... args) {
+ TripleExpression.SELECTOR
+ .variant("BigDecimalList", ExpressionTest.v(BigDecimalListExpression::tester))
+ .main(args);
+ }
+}
diff --git a/java/expression/BigIntegerListExpression.java b/java/expression/BigIntegerListExpression.java
new file mode 100644
index 0000000..4f74521
--- /dev/null
+++ b/java/expression/BigIntegerListExpression.java
@@ -0,0 +1,192 @@
+package expression;
+
+import base.Asserts;
+import base.Pair;
+import base.TestCounter;
+import expression.common.ExpressionKind;
+import expression.common.Type;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.stream.IntStream;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@FunctionalInterface
+@SuppressWarnings("ClassReferencesSubclass")
+public interface BigIntegerListExpression extends ToMiniString {
+ BigInteger evaluateBi(List variables);
+
+ // Tests follow. You may temporarily remove everything til the end.
+
+ Add EXAMPLE = new Add(
+ new Subtract(new Variable(0), new Const(BigInteger.ONE)),
+ new Multiply(new Variable(1), new Const(BigInteger.TEN))
+ );
+
+ Type TYPE = new Type<>(BigInteger::valueOf, random -> v(random.getRandom().nextLong()), BigInteger.class);
+ ExpressionKind KIND = new ExpressionKind<>(
+ TYPE,
+ BigIntegerListExpression.class,
+ (r, c) -> IntStream.range(0, c)
+ .mapToObj(name -> Pair.of("$" + name, new Variable(name)))
+ .toList(),
+ (expr, variables, values) -> expr.evaluateBi(values)
+ );
+
+ @SuppressWarnings("PointlessArithmeticExpression")
+ static ExpressionTester, ?> tester(final TestCounter counter) {
+ Asserts.assertEquals("Example toString()", "(($0 - 1) + ($1 * 10))", EXAMPLE.toString());
+ Asserts.assertEquals(
+ EXAMPLE + " at (2, 3)",
+ BigInteger.valueOf(31),
+ EXAMPLE.evaluateBi(List.of(BigInteger.valueOf(2), BigInteger.valueOf(3)))
+ );
+
+ final Variable vx = new Variable(0);
+ final Variable vy = new Variable(1);
+
+ return new ExpressionTester<>(
+ counter, KIND, c -> v -> c,
+ (op, a, b) -> v -> op.apply(a.evaluateBi(v), b.evaluateBi(v)),
+ BigInteger::add, BigInteger::subtract, BigInteger::multiply, BigInteger::divide
+ )
+ .basic("10", "10", v -> v(10), c(10))
+ .basic("$x", "$x", BigIntegerListExpression::x, vx)
+ .basic("$y", "$y", BigIntegerListExpression::y, vy)
+ .basic("($x + $y)", "$x + $y", v -> x(v).add(y(v)), new Add(vx, vy))
+ .basic("($x + 2)", "$x + 2", v -> x(v).add(v(2)), new Add(vx, c(2)))
+ .basic("(2 - $x)", "2 - $x", v -> v(2).subtract(x(v)), new Subtract(c(2), vx))
+ .basic("(3 * $x)", "3 * $x", v -> v(3).multiply(x(v)), new Multiply(c(3), vx))
+ .basic("($x + $x)", "$x + $x", v -> x(v).add(x(v)), new Add(vx, vx))
+ .basic("($x / -2)", "$x / -2", v -> x(v).divide(v(-2)), new Divide(vx, c(-2)))
+ .basic("(2 + $x)", "2 + $x", v -> v(2).add(x(v)), new Add(c(2), vx))
+ .basic("((1 + 2) + 3)", "1 + 2 + 3", v -> v(6), new Add(new Add(c(1), c(2)), c(3)))
+ .basic("(1 + (2 * 3))", "1 + 2 * 3", v -> v(7), new Add(c(1), new Multiply(c(2), c(3))))
+ .basic("(1 - (2 * 3))", "1 - 2 * 3", v -> v(-5), new Subtract(c(1), new Multiply(c(2), c(3))))
+ .basic("(1 + (2 + 3))", "1 + 2 + 3", v -> v(6), new Add(c(1), new Add(c(2), c(3))))
+ .basic("((1 - 2) - 3)", "1 - 2 - 3", v -> v(-4), new Subtract(new Subtract(c(1), c(2)), c(3)))
+ .basic("(1 - (2 - 3))", "1 - (2 - 3)", v -> v(2), new Subtract(c(1), new Subtract(c(2), c(3))))
+ .basic("((1 * 2) * 3)", "1 * 2 * 3", v -> v(6), new Multiply(new Multiply(c(1), c(2)), c(3)))
+ .basic("(1 * (2 * 3))", "1 * 2 * 3", v -> v(6), new Multiply(c(1), new Multiply(c(2), c(3))))
+ .basic("((10 / 2) / 3)", "10 / 2 / 3", v -> v(10 / 2 / 3), new Divide(new Divide(c(10), c(2)), c(3)))
+ .basic("(10 / (3 / 2))", "10 / (3 / 2)", v -> v(10 / (3 / 2)), new Divide(c(10), new Divide(c(3), c(2))))
+ .basic("(($x * $x) + (($x - 1) / 10))",
+ "$x * $x + ($x - 1) / 10",
+ v -> x(v).multiply(x(v)).add(x(v).subtract(v(1)).divide(v(10))),
+ new Add(new Multiply(vx, vx), new Divide(new Subtract(vx, c(1)), c(10)))
+ )
+ .basic("($x * -1000000000)", "$x * -1000000000", v -> x(v).multiply(v(-1_000_000_000)), new Multiply(vx, c(-1_000_000_000)))
+ .basic("($x * -1000000000000000)", "$x * -1000000000000000", v -> x(v).multiply(v(-1_000_000_000_000_000L)), new Multiply(vx, c(-1_000_000_000_000_000L)))
+ .basic("(10 / $x)", "10 / $x", v -> v(10).divide(x(v)), new Divide(c(10), vx))
+ .basic("($x / $x)", "$x / $x", v -> x(v).divide(x(v)), new Divide(vx, vx))
+
+ .advanced("(2 + 1)", "2 + 1", v -> v(2 + 1), new Add(c(2), c(1)))
+ .advanced("($x - 1)", "$x - 1", v -> x(v).subtract(v(1)), new Subtract(vx, c(1)))
+ .advanced("(1 * 2)", "1 * 2", v -> v(1 * 2), new Multiply(c(1), c(2)))
+ .advanced("($x / 1)", "$x / 1", v -> x(v).divide(v(1)), new Divide(vx, c(1)))
+ .advanced("(1 + (2 + 1))", "1 + 2 + 1", v -> v(1 + 2 + 1), new Add(c(1), new Add(c(2), c(1))))
+ .advanced("($x - ($x - 1))", "$x - ($x - 1)", v -> x(v).subtract(x(v).subtract(v(1))), new Subtract(vx, new Subtract(vx, c(1))))
+ .advanced("(2 * ($x / 1))", "2 * ($x / 1)", v -> v(2).multiply(x(v).divide(v(1))), new Multiply(c(2), new Divide(vx, c(1))))
+ .advanced("(2 / ($x - 1))", "2 / ($x - 1)", v -> v(2).divide(x(v).subtract(v(1))), new Divide(c(2), new Subtract(vx, c(1))))
+ .advanced("((1 * 2) + $x)", "1 * 2 + $x", v -> v(1 * 2).add(x(v)), new Add(new Multiply(c(1), c(2)), vx))
+ .advanced("(($x - 1) - 2)", "$x - 1 - 2", v -> x(v).subtract(v(3)), new Subtract(new Subtract(vx, c(1)), c(2)))
+ .advanced("(($x / 1) * 2)", "$x / 1 * 2", v -> x(v).multiply(v(2)), new Multiply(new Divide(vx, c(1)), c(2)))
+ .advanced("((2 + 1) / 1)", "(2 + 1) / 1", v -> v(3), new Divide(new Add(c(2), c(1)), c(1)))
+ .advanced(
+ "(1 + (1 + (2 + 1)))",
+ "1 + 1 + 2 + 1",
+ v -> v(1 + 1 + 2 + 1),
+ new Add(c(1), new Add(c(1), new Add(c(2), c(1))))
+ )
+ .advanced(
+ "($x - ((1 * 2) + $x))",
+ "$x - (1 * 2 + $x)",
+ v -> x(v).subtract(v(1 * 2).add(x(v))),
+ new Subtract(vx, new Add(new Multiply(c(1), c(2)), vx))
+ )
+ .advanced(
+ "($x * (2 / ($x - 1)))",
+ "$x * (2 / ($x - 1))",
+ v -> x(v).multiply(v(2).divide(x(v).subtract(v(1)))),
+ new Multiply(vx, new Divide(c(2), new Subtract(vx, c(1))))
+ )
+ .advanced(
+ "($x / (1 + (2 + 1)))",
+ "$x / (1 + 2 + 1)",
+ v -> x(v).divide(v(1 + 2 + 1)),
+ new Divide(vx, new Add(c(1), new Add(c(2), c(1))))
+ )
+ .advanced(
+ "((1 * 2) + (2 + 1))",
+ "1 * 2 + 2 + 1",
+ v -> v(1 * 2 + 2 + 1),
+ new Add(new Multiply(c(1), c(2)), new Add(c(2), c(1)))
+ )
+ .advanced(
+ "((2 + 1) - (2 + 1))",
+ "2 + 1 - (2 + 1)",
+ v -> v(2 + 1 - (2 + 1)),
+ new Subtract(new Add(c(2), c(1)), new Add(c(2), c(1)))
+ )
+ .advanced(
+ "(($x - 1) * ($x / 1))",
+ "($x - 1) * ($x / 1)",
+ v -> x(v).subtract(v(1)).multiply(x(v).divide(v(1))),
+ new Multiply(new Subtract(vx, c(1)), new Divide(vx, c(1)))
+ )
+ .advanced(
+ "(($x - 1) / (1 * 2))",
+ "($x - 1) / (1 * 2)",
+ v -> x(v).subtract(v(1)).divide(v(2)),
+ new Divide(new Subtract(vx, c(1)), new Multiply(c(1), c(2)))
+ )
+ .advanced(
+ "((($x - 1) - 2) + $x)",
+ "$x - 1 - 2 + $x",
+ v -> x(v).subtract(v(3)).add(x(v)),
+ new Add(new Subtract(new Subtract(vx, c(1)), c(2)), vx)
+ )
+ .advanced(
+ "(((1 * 2) + $x) - 1)",
+ "1 * 2 + $x - 1",
+ v -> v(1).add(x(v)),
+ new Subtract(new Add(new Multiply(c(1), c(2)), vx), c(1))
+ )
+ .advanced(
+ "(((2 + 1) / 1) * $x)",
+ "(2 + 1) / 1 * $x",
+ v -> v(3).multiply(x(v)),
+ new Multiply(new Divide(new Add(c(2), c(1)), c(1)), vx)
+ )
+ .advanced(
+ "((2 / ($x - 1)) / 2)",
+ "2 / ($x - 1) / 2",
+ v -> v(2).divide(x(v).subtract(v(1))).divide(v(2)),
+ new Divide(new Divide(c(2), new Subtract(vx, c(1))), c(2))
+ );
+ }
+
+ private static BigInteger x(final List vars) {
+ return vars.get(0);
+ }
+
+ private static BigInteger y(final List vars) {
+ return vars.get(1);
+ }
+
+ private static Const c(final long v) {
+ return TYPE.constant(v(v));
+ }
+
+ private static BigInteger v(final long v) {
+ return BigInteger.valueOf(v);
+ }
+
+ static void main(final String... args) {
+ TripleExpression.SELECTOR
+ .variant("BigIntegerList", ExpressionTest.v(BigIntegerListExpression::tester))
+ .main(args);
+ }
+}
diff --git a/java/expression/Expression.java b/java/expression/Expression.java
new file mode 100644
index 0000000..d52c16b
--- /dev/null
+++ b/java/expression/Expression.java
@@ -0,0 +1,112 @@
+package expression;
+
+import base.Asserts;
+import base.ExtendedRandom;
+import base.Pair;
+import base.TestCounter;
+import expression.common.ExpressionKind;
+import expression.common.Type;
+
+import java.util.List;
+
+/**
+ * One-argument arithmetic expression over integers.
+ *
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@FunctionalInterface
+@SuppressWarnings("ClassReferencesSubclass")
+public interface Expression extends ToMiniString {
+ int evaluate(int x);
+
+ // Tests follow. You may temporarily remove everything til the end.
+
+ Subtract EXAMPLE = new Subtract(
+ new Multiply(new Const(2), new Variable("x")),
+ new Const(3)
+ );
+
+ Type TYPE = new Type<>(a -> a, ExtendedRandom::nextInt, int.class);
+ ExpressionKind KIND = new ExpressionKind<>(
+ TYPE,
+ Expression.class,
+ List.of(Pair.of("x", new Variable("x"))),
+ (expr, variables, values) -> expr.evaluate(values.get(0))
+ );
+
+ private static Const c(final int c) {
+ return new Const(c);
+ }
+
+ @SuppressWarnings({"PointlessArithmeticExpression", "Convert2MethodRef"})
+ static ExpressionTester, ?> tester(final TestCounter counter) {
+ Asserts.assertEquals("Example toString()", "((2 * x) - 3)", EXAMPLE.toString());
+ Asserts.assertEquals("Example at 5", 7, EXAMPLE.evaluate(5));
+ Asserts.assertTrue("Example equals 1",
+ new Multiply(new Const(2), new Variable("x"))
+ .equals(new Multiply(new Const(2), new Variable("x"))));
+ Asserts.assertTrue("Example equals 2",
+ !new Multiply(new Const(2), new Variable("x"))
+ .equals(new Multiply(new Variable("x"), new Const(2))));
+
+ final Variable vx = new Variable("x");
+ final Const c1 = c(1);
+ final Const c2 = c(2);
+
+ return new ExpressionTester<>(
+ counter, KIND, c -> x -> c,
+ (op, a, b) -> x -> op.apply(a.evaluate(x), b.evaluate(x)),
+ (a, b) -> a + b, (a, b) -> a - b, (a, b) -> a * b, (a, b) -> a / b
+ )
+ .basic("10", "10", x -> 10, c(10))
+ .basic("x", "x", x -> x, vx)
+ .basic("(x + 2)", "x + 2", x -> x + 2, new Add(vx, c(2)))
+ .basic("(2 - x)", "2 - x", x -> 2 - x, new Subtract(c(2), vx))
+ .basic("(3 * x)", "3 * x", x -> 3*x, new Multiply(c(3), vx))
+ .basic("(x + x)", "x + x", x -> x + x, new Add(vx, vx))
+ .basic("(x / -2)", "x / -2", x -> -x / 2, new Divide(vx, c(-2)))
+ .basic("(2 + x)", "2 + x", x -> 2 + x, new Add(c(2), vx))
+ .basic("((1 + 2) + 3)", "1 + 2 + 3", x -> 6, new Add(new Add(c(1), c(2)), c(3)))
+ .basic("(1 + (2 + 3))", "1 + 2 + 3", x -> 6, new Add(c(1), new Add(c(2), c(3))))
+ .basic("((1 - 2) - 3)", "1 - 2 - 3", x -> -4, new Subtract(new Subtract(c(1), c(2)), c(3)))
+ .basic("(1 - (2 - 3))", "1 - (2 - 3)", x -> 2, new Subtract(c(1), new Subtract(c(2), c(3))))
+ .basic("((1 * 2) * 3)", "1 * 2 * 3", x -> 6, new Multiply(new Multiply(c(1), c(2)), c(3)))
+ .basic("(1 * (2 * 3))", "1 * 2 * 3", x -> 6, new Multiply(c(1), new Multiply(c(2), c(3))))
+ .basic("((10 / 2) / 3)", "10 / 2 / 3", x -> 10 / 2 / 3, new Divide(new Divide(c(10), c(2)), c(3)))
+ .basic("(10 / (3 / 2))", "10 / (3 / 2)", x -> 10 / (3 / 2), new Divide(c(10), new Divide(c(3), c(2))))
+ .basic("(10 * (3 / 2))", "10 * (3 / 2)", x -> 10 * (3 / 2), new Multiply(c(10), new Divide(c(3), c(2))))
+ .basic("(10 + (3 - 2))", "10 + 3 - 2", x -> 10 + (3 - 2), new Add(c(10), new Subtract(c(3), c(2))))
+ .basic("((x * x) + ((x - 1) / 10))", "x * x + (x - 1) / 10", x -> x * x + (x - 1) / 10, new Add(
+ new Multiply(vx, vx),
+ new Divide(new Subtract(vx, c(1)), c(10))
+ ))
+ .basic("(x * -1000000000)", "x * -1000000000", x -> x * -1_000_000_000, new Multiply(vx, c(-1_000_000_000)))
+ .basic("(10 / x)", "10 / x", x -> 10 / x, new Divide(c(10), vx))
+ .basic("(x / x)", "x / x", x -> x / x, new Divide(vx, vx))
+
+ .advanced("(2 + 1)", "2 + 1", x -> 2 + 1, new Add(c2, c1))
+ .advanced("(x - 1)", "x - 1", x -> x - 1, new Subtract(vx, c1))
+ .advanced("(1 * 2)", "1 * 2", x -> 1 * 2, new Multiply(c1, c2))
+ .advanced("(x / 1)", "x / 1", x -> x / 1, new Divide(vx, c1))
+ .advanced("(1 + (2 + 1))", "1 + 2 + 1", x -> 1 + 2 + 1, new Add(c1, new Add(c2, c1)))
+ .advanced("(x - (x - 1))", "x - (x - 1)", x -> x - (x - 1), new Subtract(vx, new Subtract(vx, c1)))
+ .advanced("(2 * (x / 1))", "2 * (x / 1)", x -> 2 * (x / 1), new Multiply(c2, new Divide(vx, c1)))
+ .advanced("(2 / (x - 1))", "2 / (x - 1)", x -> 2 / (x - 1), new Divide(c2, new Subtract(vx, c1)))
+ .advanced("((1 * 2) + x)", "1 * 2 + x", x -> 1 * 2 + x, new Add(new Multiply(c1, c2), vx))
+ .advanced("((x - 1) - 2)", "x - 1 - 2", x -> x - 1 - 2, new Subtract(new Subtract(vx, c1), c2))
+ .advanced("((x / 1) * 2)", "x / 1 * 2", x -> x / 1 * 2, new Multiply(new Divide(vx, c1), c2))
+ .advanced("((2 + 1) / 1)", "(2 + 1) / 1", x -> (2 + 1) / 1, new Divide(new Add(c2, c1), c1))
+ .advanced("(1 + (1 + (2 + 1)))", "1 + 1 + 2 + 1", x -> 1 + 1 + 2 + 1, new Add(c1, new Add(c1, new Add(c2, c1))))
+ .advanced("(x - ((1 * 2) + x))", "x - (1 * 2 + x)", x -> x - (1 * 2 + x), new Subtract(vx, new Add(new Multiply(c1, c2), vx)))
+ .advanced("(x * (2 / (x - 1)))", "x * (2 / (x - 1))", x -> x * (2 / (x - 1)), new Multiply(vx, new Divide(c2, new Subtract(vx, c1))))
+ .advanced("(x / (1 + (2 + 1)))", "x / (1 + 2 + 1)", x -> x / (1 + 2 + 1), new Divide(vx, new Add(c1, new Add(c2, c1))))
+ .advanced("((1 * 2) + (2 + 1))", "1 * 2 + 2 + 1", x -> 1 * 2 + 2 + 1, new Add(new Multiply(c1, c2), new Add(c2, c1)))
+ .advanced("((2 + 1) - (2 + 1))", "2 + 1 - (2 + 1)", x -> 2 + 1 - (2 + 1), new Subtract(new Add(c2, c1), new Add(c2, c1)))
+ .advanced("((x - 1) * (x / 1))", "(x - 1) * (x / 1)", x -> (x - 1) * (x / 1), new Multiply(new Subtract(vx, c1), new Divide(vx, c1)))
+ .advanced("((x - 1) / (1 * 2))", "(x - 1) / (1 * 2)", x -> (x - 1) / (1 * 2), new Divide(new Subtract(vx, c1), new Multiply(c1, c2)))
+ .advanced("(((x - 1) - 2) + x)", "x - 1 - 2 + x", x -> x - 1 - 2 + x, new Add(new Subtract(new Subtract(vx, c1), c2), vx))
+ .advanced("(((1 * 2) + x) - 1)", "1 * 2 + x - 1", x -> 1 * 2 + x - 1, new Subtract(new Add(new Multiply(c1, c2), vx), c1))
+ .advanced("(((2 + 1) / 1) * x)", "(2 + 1) / 1 * x", x -> (2 + 1) / 1 * x, new Multiply(new Divide(new Add(c2, c1), c1), vx))
+ .advanced("((2 / (x - 1)) / 2)", "2 / (x - 1) / 2", x -> 2 / (x - 1) / 2, new Divide(new Divide(c2, new Subtract(vx, c1)), c2));
+ }
+}
diff --git a/java/expression/ExpressionTest.java b/java/expression/ExpressionTest.java
new file mode 100644
index 0000000..ce24173
--- /dev/null
+++ b/java/expression/ExpressionTest.java
@@ -0,0 +1,26 @@
+package expression;
+
+import base.Selector;
+import base.TestCounter;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public final class ExpressionTest {
+ public static final Selector SELECTOR = new Selector(ExpressionTest.class, "easy", "hard")
+ .variant("Base", v(Expression::tester));
+
+ private ExpressionTest() {
+ }
+
+ public static Consumer v(final Function> tester) {
+ return t -> tester.apply(t).test();
+ }
+
+ public static void main(final String... args) {
+ SELECTOR.main(args);
+ }
+}
diff --git a/java/expression/ExpressionTester.java b/java/expression/ExpressionTester.java
new file mode 100644
index 0000000..28e789f
--- /dev/null
+++ b/java/expression/ExpressionTester.java
@@ -0,0 +1,288 @@
+package expression;
+
+import base.*;
+import expression.common.*;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Modifier;
+import java.util.*;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static base.Asserts.assertTrue;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public class ExpressionTester extends Tester {
+ private final List VALUES = IntStream.rangeClosed(-10, 10).boxed().toList();
+ private final ExpressionKind kind;
+
+ private final List basic = new ArrayList<>();
+ private final List advanced = new ArrayList<>();
+ private final Set used = new HashSet<>();
+ private final GeneratorBuilder generator;
+
+ private final List> prev = new ArrayList<>();
+ private final Map mappings;
+
+ protected ExpressionTester(
+ final TestCounter counter,
+ final ExpressionKind kind,
+ final Function expectedConstant,
+ final Binary binary,
+ final BinaryOperator add,
+ final BinaryOperator sub,
+ final BinaryOperator mul,
+ final BinaryOperator div,
+ final Map mappings
+ ) {
+ super(counter);
+ this.kind = kind;
+ this.mappings = mappings;
+
+ generator = new GeneratorBuilder(expectedConstant, kind::constant, binary, kind::randomValue);
+ generator.binary("+", 1600, add, Add.class);
+ generator.binary("-", 1602, sub, Subtract.class);
+ generator.binary("*", 2001, mul, Multiply.class);
+ generator.binary("/", 2002, div, Divide.class);
+ }
+
+ protected ExpressionTester(
+ final TestCounter counter,
+ final ExpressionKind kind,
+ final Function expectedConstant,
+ final Binary binary,
+ final BinaryOperator add,
+ final BinaryOperator sub,
+ final BinaryOperator mul,
+ final BinaryOperator div
+ ) {
+ this(counter, kind, expectedConstant, binary, add, sub, mul, div, Map.of());
+ }
+
+ @Override
+ public String toString() {
+ return kind.getName();
+ }
+
+ @Override
+ public void test() {
+ counter.scope("Basic tests", () -> basic.forEach(Test::test));
+ counter.scope("Advanced tests", () -> advanced.forEach(Test::test));
+ counter.scope("Random tests", generator::testRandom);
+ }
+
+ @SuppressWarnings({"ConstantValue", "EqualsWithItself"})
+ private void checkEqualsAndToString(final String full, final String mini, final ToMiniString expression, final ToMiniString copy) {
+ checkToString("toString", full, expression.toString());
+ if (mode() > 0) {
+ checkToString("toMiniString", mini, expression.toMiniString());
+ }
+
+ counter.test(() -> {
+ assertTrue("Equals to this", expression.equals(expression));
+ assertTrue("Equals to copy", expression.equals(copy));
+ assertTrue("Equals to null", !expression.equals(null));
+ assertTrue("Copy equals to null", !copy.equals(null));
+ });
+
+ final String expressionToString = Objects.requireNonNull(expression.toString());
+ for (final Pair pair : prev) {
+ counter.test(() -> {
+ final ToMiniString prev = pair.first();
+ final String prevToString = pair.second();
+ final boolean equals = prevToString.equals(expressionToString);
+ assertTrue("Equals to " + prevToString, prev.equals(expression) == equals);
+ assertTrue("Equals to " + prevToString, expression.equals(prev) == equals);
+ assertTrue("Inconsistent hashCode for " + prev + " and " + expression, (prev.hashCode() == expression.hashCode()) == equals);
+ });
+ }
+ }
+
+ private void checkToString(final String method, final String expected, final String actual) {
+ counter.test(() -> assertTrue(String.format("Invalid %s\n expected: %s\n actual: %s", method, expected, actual), expected.equals(actual)));
+ }
+
+ private void check(
+ final String full,
+ final E expected,
+ final E actual,
+ final List variables,
+ final List values
+ ) {
+ final String vars = IntStream.range(0, variables.size())
+ .mapToObj(i -> variables.get(i) + "=" + values.get(i))
+ .collect(Collectors.joining(","));
+ counter.test(() -> {
+ final Object expectedResult = evaluate(expected, variables, values);
+ final Object actualResult = evaluate(actual, variables, values);
+ final String reason = String.format(
+ "%s:%n expected `%s`,%n actual `%s`",
+ String.format("f(%s)\nwhere f is %s", vars, full),
+ Asserts.toString(expectedResult),
+ Asserts.toString(actualResult)
+ );
+ if (
+ expectedResult != null && actualResult != null &&
+ expectedResult.getClass() == actualResult.getClass()
+ && (expectedResult.getClass() == Double.class || expectedResult.getClass() == Float.class)
+ ) {
+ final double expectedValue = ((Number) expectedResult).doubleValue();
+ final double actualValue = ((Number) actualResult).doubleValue();
+ Asserts.assertEquals(reason, expectedValue, actualValue, 1e-6);
+ } else {
+ assertTrue(reason, Objects.deepEquals(expectedResult, actualResult));
+ }
+ });
+ }
+
+ private Object evaluate(final E expression, final List variables, final List values) {
+ try {
+ return kind.evaluate(expression, variables, values);
+ } catch (final Exception e) {
+ return e.getClass().getName();
+ }
+ }
+
+ protected ExpressionTester basic(final String full, final String mini, final E expected, final E actual) {
+ return basicF(full, mini, expected, vars -> actual);
+ }
+
+ protected ExpressionTester basicF(final String full, final String mini, final E expected, final Function, E> actual) {
+ return basic(new Test(full, mini, expected, actual));
+ }
+
+ private ExpressionTester basic(final Test test) {
+ Asserts.assertTrue(test.full, used.add(test.full));
+ basic.add(test);
+ return this;
+ }
+
+ protected ExpressionTester advanced(final String full, final String mini, final E expected, final E actual) {
+ return advancedF(full, mini, expected, vars -> actual);
+ }
+
+ protected ExpressionTester advancedF(final String full, final String mini, final E expected, final Function, E> actual) {
+ Asserts.assertTrue(full, used.add(full));
+ advanced.add(new Test(full, mini, expected, actual));
+ return this;
+ }
+
+ protected static Named variable(final String name, final E expected) {
+ return Named.of(name, expected);
+ }
+
+ @FunctionalInterface
+ public interface Binary {
+ E apply(BinaryOperator op, E a, E b);
+ }
+
+ private final class Test {
+ private final String full;
+ private final String mini;
+ private final E expected;
+ private final Function, E> actual;
+
+ private Test(final String full, final String mini, final E expected, final Function, E> actual) {
+ this.full = full;
+ this.mini = mini;
+ this.expected = expected;
+ this.actual = actual;
+ }
+
+ private void test() {
+ final List> variables = kind.variables().generate(random(), 3);
+ final List names = Functional.map(variables, Pair::first);
+ final E actual = kind.cast(this.actual.apply(names));
+ final String full = mangle(this.full, names);
+ final String mini = mangle(this.mini, names);
+
+ counter.test(() -> {
+ kind.allValues(variables.size(), VALUES).forEach(values -> check(mini, expected, actual, names, values));
+ checkEqualsAndToString(full, mini, actual, actual);
+ prev.add(Pair.of(actual, full));
+ });
+ }
+
+ private String mangle(String string, final List names) {
+ for (int i = 0; i < names.size(); i++) {
+ string = string.replace("$" + (char) ('x' + i), names.get(i));
+ }
+ for (final Map.Entry mapping : mappings.entrySet()) {
+ string = string.replace(mapping.getKey(), mapping.getValue().toString());
+ }
+ return string;
+ }
+ }
+
+ private final class GeneratorBuilder {
+ private final Generator.Builder generator;
+ private final NodeRendererBuilder renderer = new NodeRendererBuilder<>(random());
+ private final Renderer.Builder expected;
+ private final Renderer.Builder actual;
+ private final Renderer.Builder copy;
+ private final Binary binary;
+
+ private GeneratorBuilder(
+ final Function expectedConstant,
+ final Function super C, E> actualConstant,
+ final Binary binary,
+ final Function randomValue
+ ) {
+ generator = Generator.builder(() -> randomValue.apply(random()), random());
+ expected = Renderer.builder(expectedConstant::apply);
+ actual = Renderer.builder(actualConstant::apply);
+ copy = Renderer.builder(actualConstant::apply);
+
+ this.binary = binary;
+ }
+
+ private void binary(final String name, final int priority, final BinaryOperator op, final Class> type) {
+ generator.add(name, 2);
+ renderer.binary(name, priority);
+
+ expected.binary(name, (unit, a, b) -> binary.apply(op, a, b));
+
+ @SuppressWarnings("unchecked") final Constructor extends E> constructor = (Constructor extends E>) Arrays.stream(type.getConstructors())
+ .filter(cons -> Modifier.isPublic(cons.getModifiers()))
+ .filter(cons -> cons.getParameterCount() == 2)
+ .findFirst()
+ .orElseGet(() -> counter.fail("%s(..., ...) constructor not found", type.getSimpleName()));
+ final Renderer.BinaryOperator actual = (unit, a, b) -> {
+ try {
+ return constructor.newInstance(a, b);
+ } catch (final Exception e) {
+ return counter.fail(e);
+ }
+ };
+ this.actual.binary(name, actual);
+ copy.binary(name, actual);
+ }
+
+ private void testRandom() {
+ final NodeRenderer renderer = this.renderer.build();
+ final Renderer expectedRenderer = this.expected.build();
+ final Renderer actualRenderer = this.actual.build();
+ final expression.common.Generator generator = this.generator.build(kind.variables(), List.of());
+ generator.testRandom(counter, 1, expr -> {
+ final String full = renderer.render(expr, NodeRenderer.FULL);
+ final String mini = renderer.render(expr, NodeRenderer.MINI);
+ final E expected = expectedRenderer.render(expr, Unit.INSTANCE);
+ final E actual = actualRenderer.render(expr, Unit.INSTANCE);
+
+ final List> variables = expr.variables();
+ final List names = Functional.map(variables, Pair::first);
+ final List values = Stream.generate(() -> kind.randomValue(random()))
+ .limit(variables.size())
+ .toList();
+
+ checkEqualsAndToString(full, mini, actual, copy.build().render(expr, Unit.INSTANCE));
+ check(full, expected, actual, names, values);
+ });
+ }
+ }
+}
diff --git a/java/expression/ListExpression.java b/java/expression/ListExpression.java
new file mode 100644
index 0000000..7f5ff95
--- /dev/null
+++ b/java/expression/ListExpression.java
@@ -0,0 +1,211 @@
+package expression;
+
+import base.Asserts;
+import base.ExtendedRandom;
+import base.Pair;
+import base.TestCounter;
+import expression.common.ExpressionKind;
+import expression.common.Type;
+
+import java.util.List;
+import java.util.stream.IntStream;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@SuppressWarnings("ClassReferencesSubclass")
+@FunctionalInterface
+public interface ListExpression extends ToMiniString {
+ int evaluate(List variables);
+
+ // Tests follow. You may temporarily remove everything til the end.
+
+ Add EXAMPLE = new Add(
+ new Subtract(new Variable(0), new Const(1)),
+ new Multiply(new Variable(1), new Const(10))
+ );
+
+ Type TYPE = new Type<>(a -> a, ExtendedRandom::nextInt, int.class);
+ ExpressionKind KIND = new ExpressionKind<>(
+ TYPE,
+ ListExpression.class,
+ (r, c) -> IntStream.range(0, c)
+ .mapToObj(name -> Pair.of("$" + name, new Variable(name)))
+ .toList(),
+ (expr, variables, values) -> expr.evaluate(values)
+ );
+
+ private static Const c(final Integer c) {
+ return TYPE.constant(c);
+ }
+
+ @SuppressWarnings({"PointlessArithmeticExpression", "Convert2MethodRef"})
+ static ExpressionTester, ?> tester(final TestCounter counter) {
+ Asserts.assertEquals("Example toString()", "(($0 - 1) + ($1 * 10))", EXAMPLE.toString());
+ Asserts.assertEquals(EXAMPLE + " at (2, 3)", 31, EXAMPLE.evaluate(List.of(2, 3)));
+
+ final Variable vx = new Variable(0);
+ return new ExpressionTester<>(
+ counter, KIND, c -> vars -> c,
+ (op, a, b) -> vars -> op.apply(a.evaluate(vars), b.evaluate(vars)),
+ (a, b) -> a + b, (a, b) -> a - b, (a, b) -> a * b, (a, b) -> a / b
+ )
+ .basic("10", "10", vars -> 10, c(10))
+ .basic("$x", "$x", ListExpression::x, vx)
+ .basic("($x + 2)", "$x + 2", vars -> x(vars) + 2, new Add(vx, c(2)))
+ .basic("(2 - $x)", "2 - $x", vars -> 2 - x(vars), new Subtract(c(2), vx))
+ .basic("(3 * $x)", "3 * $x", vars -> 3 * x(vars), new Multiply(c(3), vx))
+ .basic("($x + $x)", "$x + $x", vars -> x(vars) + x(vars), new Add(vx, vx))
+ .basic("($x / -2)", "$x / -2", vars -> -x(vars) / 2, new Divide(vx, c(-2)))
+ .basic("(2 + $x)", "2 + $x", vars -> 2 + x(vars), new Add(c(2), vx))
+ .basic("((1 + 2) + 3)", "1 + 2 + 3", vars -> 6, new Add(new Add(c(1), c(2)), c(3)))
+ .basic("(1 + (2 + 3))", "1 + 2 + 3", vars -> 6, new Add(c(1), new Add(c(2), c(3))))
+ .basic("((1 - 2) - 3)", "1 - 2 - 3", vars -> -4, new Subtract(new Subtract(c(1), c(2)), c(3)))
+ .basic("(1 - (2 - 3))", "1 - (2 - 3)", vars -> 2, new Subtract(c(1), new Subtract(c(2), c(3))))
+ .basic("((1 * 2) * 3)", "1 * 2 * 3", vars -> 6, new Multiply(new Multiply(c(1), c(2)), c(3)))
+ .basic("(1 * (2 * 3))", "1 * 2 * 3", vars -> 6, new Multiply(c(1), new Multiply(c(2), c(3))))
+ .basic("((10 / 2) / 3)", "10 / 2 / 3", vars -> 10 / 2 / 3, new Divide(new Divide(c(10), c(2)), c(3)))
+ .basic("(10 / (3 / 2))", "10 / (3 / 2)", vars -> 10 / (3 / 2), new Divide(c(10), new Divide(c(3), c(2))))
+ .basic("(10 * (3 / 2))", "10 * (3 / 2)", vars -> 10 * (3 / 2), new Multiply(c(10), new Divide(c(3), c(2))))
+ .basic("(10 + (3 - 2))", "10 + 3 - 2", vars -> 10 + (3 - 2), new Add(c(10), new Subtract(c(3), c(2))))
+ .basic(
+ "(($x * $x) + (($x - 1) / 10))",
+ "$x * $x + ($x - 1) / 10",
+ vars -> x(vars) * x(vars) + (x(vars) - 1) / 10,
+ new Add(new Multiply(vx, vx), new Divide(new Subtract(vx, c(1)), c(10)))
+ )
+ .basic(
+ "($x * -1000000000)",
+ "$x * -1000000000",
+ vars -> x(vars) * -1_000_000_000,
+ new Multiply(vx, c(-1_000_000_000))
+ )
+ .basic("(10 / $x)", "10 / $x", vars -> 10 / x(vars), new Divide(c(10), vx))
+ .basic("($x / $x)", "$x / $x", vars -> x(vars) / x(vars), new Divide(vx, vx))
+
+ .advanced("(2 + 1)", "2 + 1", vars -> 2 + 1, new Add(c(2), c(1)))
+ .advanced("($x - 1)", "$x - 1", vars -> x(vars) - 1, new Subtract(vx, c(1)))
+ .advanced("(1 * 2)", "1 * 2", vars -> 1 * 2, new Multiply(c(1), c(2)))
+ .advanced("($x / 1)", "$x / 1", vars -> x(vars) / 1, new Divide(vx, c(1)))
+ .advanced("(1 + (2 + 1))", "1 + 2 + 1", vars -> 1 + 2 + 1, new Add(c(1), new Add(c(2), c(1))))
+ .advanced(
+ "($x - ($x - 1))",
+ "$x - ($x - 1)",
+ vars -> x(vars) - (x(vars) - 1),
+ new Subtract(vx, new Subtract(vx, c(1)))
+ )
+ .advanced(
+ "(2 * ($x / 1))",
+ "2 * ($x / 1)",
+ vars -> 2 * (x(vars) / 1),
+ new Multiply(c(2), new Divide(vx, c(1)))
+ )
+ .advanced(
+ "(2 / ($x - 1))",
+ "2 / ($x - 1)",
+ vars -> 2 / (x(vars) - 1),
+ new Divide(c(2), new Subtract(vx, c(1)))
+ )
+ .advanced(
+ "((1 * 2) + $x)",
+ "1 * 2 + $x",
+ vars -> 1 * 2 + x(vars),
+ new Add(new Multiply(c(1), c(2)), vx)
+ )
+ .advanced(
+ "(($x - 1) - 2)",
+ "$x - 1 - 2",
+ vars -> x(vars) - 1 - 2,
+ new Subtract(new Subtract(vx, c(1)), c(2))
+ )
+ .advanced(
+ "(($x / 1) * 2)",
+ "$x / 1 * 2",
+ vars -> x(vars) / 1 * 2,
+ new Multiply(new Divide(vx, c(1)), c(2))
+ )
+ .advanced("((2 + 1) / 1)", "(2 + 1) / 1", vars -> (2 + 1) / 1, new Divide(new Add(c(2), c(1)), c(1)))
+ .advanced(
+ "(1 + (1 + (2 + 1)))",
+ "1 + 1 + 2 + 1",
+ vars -> 1 + 1 + 2 + 1,
+ new Add(c(1), new Add(c(1), new Add(c(2), c(1))))
+ )
+ .advanced(
+ "($x - ((1 * 2) + $x))",
+ "$x - (1 * 2 + $x)",
+ vars -> x(vars) - (1 * 2 + x(vars)),
+ new Subtract(vx, new Add(new Multiply(c(1), c(2)), vx))
+ )
+ .advanced(
+ "($x * (2 / ($x - 1)))",
+ "$x * (2 / ($x - 1))",
+ vars -> x(vars) * (2 / (x(vars) - 1)),
+ new Multiply(vx, new Divide(c(2), new Subtract(vx, c(1))))
+ )
+ .advanced(
+ "($x / (1 + (2 + 1)))",
+ "$x / (1 + 2 + 1)",
+ vars -> x(vars) / (1 + 2 + 1),
+ new Divide(vx, new Add(c(1), new Add(c(2), c(1))))
+ )
+ .advanced(
+ "((1 * 2) + (2 + 1))",
+ "1 * 2 + 2 + 1",
+ vars -> 1 * 2 + 2 + 1,
+ new Add(new Multiply(c(1), c(2)), new Add(c(2), c(1)))
+ )
+ .advanced(
+ "((2 + 1) - (2 + 1))",
+ "2 + 1 - (2 + 1)",
+ vars -> 2 + 1 - (2 + 1),
+ new Subtract(new Add(c(2), c(1)), new Add(c(2), c(1)))
+ )
+ .advanced(
+ "(($x - 1) * ($x / 1))",
+ "($x - 1) * ($x / 1)",
+ vars -> (x(vars) - 1) * (x(vars) / 1),
+ new Multiply(new Subtract(vx, c(1)), new Divide(vx, c(1)))
+ )
+ .advanced(
+ "(($x - 1) / (1 * 2))",
+ "($x - 1) / (1 * 2)",
+ vars -> (x(vars) - 1) / (1 * 2),
+ new Divide(new Subtract(vx, c(1)), new Multiply(c(1), c(2)))
+ )
+ .advanced(
+ "((($x - 1) - 2) + $x)",
+ "$x - 1 - 2 + $x",
+ vars -> x(vars) - 1 - 2 + x(vars),
+ new Add(new Subtract(new Subtract(vx, c(1)), c(2)), vx)
+ )
+ .advanced(
+ "(((1 * 2) + $x) - 1)",
+ "1 * 2 + $x - 1",
+ vars -> 1 * 2 + x(vars) - 1,
+ new Subtract(new Add(new Multiply(c(1), c(2)), vx), c(1))
+ )
+ .advanced(
+ "(((2 + 1) / 1) * $x)",
+ "(2 + 1) / 1 * $x",
+ vars -> (2 + 1) / 1 * x(vars),
+ new Multiply(new Divide(new Add(c(2), c(1)), c(1)), vx)
+ )
+ .advanced(
+ "((2 / ($x - 1)) / 2)",
+ "2 / ($x - 1) / 2",
+ vars -> 2 / (x(vars) - 1) / 2,
+ new Divide(new Divide(c(2), new Subtract(vx, c(1))), c(2))
+ );
+ }
+
+ private static Integer x(final List vars) {
+ return vars.get(0);
+ }
+
+ static void main(final String... args) {
+ TripleExpression.SELECTOR
+ .variant("List", ExpressionTest.v(ListExpression::tester))
+ .main(args);
+ }
+}
diff --git a/java/expression/ToMiniString.java b/java/expression/ToMiniString.java
new file mode 100644
index 0000000..60a655c
--- /dev/null
+++ b/java/expression/ToMiniString.java
@@ -0,0 +1,10 @@
+package expression;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public interface ToMiniString {
+ default String toMiniString() {
+ return toString();
+ }
+}
diff --git a/java/expression/TripleExpression.java b/java/expression/TripleExpression.java
new file mode 100644
index 0000000..efc6705
--- /dev/null
+++ b/java/expression/TripleExpression.java
@@ -0,0 +1,190 @@
+package expression;
+
+import base.ExtendedRandom;
+import base.Pair;
+import base.Selector;
+import base.TestCounter;
+import expression.common.ExpressionKind;
+import expression.common.Type;
+
+import java.util.List;
+
+/**
+ * Three-argument arithmetic expression over integers.
+ *
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+@FunctionalInterface
+@SuppressWarnings("ClassReferencesSubclass")
+public interface TripleExpression extends ToMiniString {
+ int evaluate(int x, int y, int z);
+
+ // Tests follow. You may temporarily remove everything til the end.
+
+ Type TYPE = new Type<>(a -> a, ExtendedRandom::nextInt, int.class);
+ ExpressionKind KIND = new ExpressionKind<>(
+ TYPE,
+ TripleExpression.class,
+ List.of(
+ Pair.of("x", new Variable("x")),
+ Pair.of("y", new Variable("y")),
+ Pair.of("z", new Variable("z"))
+ ),
+ (expr, variables, values) -> expr.evaluate(values.get(0), values.get(1), values.get(2))
+ );
+
+ @SuppressWarnings("PointlessArithmeticExpression")
+ static ExpressionTester, ?> tester(final TestCounter counter) {
+ final Variable vx = new Variable("x");
+ final Variable vy = new Variable("y");
+ final Variable vz = new Variable("z");
+
+ return new ExpressionTester<>(
+ counter, KIND, c -> (x, y, z) -> c,
+ (op, a, b) -> (x, y, z) -> op.apply(a.evaluate(x, y, z), b.evaluate(x, y, z)),
+ Integer::sum, (a, b) -> a - b, (a, b) -> a * b, (a, b) -> a / b
+ )
+ .basic("10", "10", (x, y, z) -> 10, c(10))
+ .basic("x", "x", (x, y, z) -> x, vx)
+ .basic("y", "y", (x, y, z) -> y, vy)
+ .basic("z", "z", (x, y, z) -> z, vz)
+ .basic("(x + 2)", "x + 2", (x, y, z) -> x + 2, new Add(vx, c(2)))
+ .basic("(2 - y)", "2 - y", (x, y, z) -> 2 - y, new Subtract(c(2), vy))
+ .basic("(3 * z)", "3 * z", (x, y, z) -> 3 * z, new Multiply(c(3), vz))
+ .basic("(x / -2)", "x / -2", (x, y, z) -> -x / 2, new Divide(vx, c(-2)))
+ .basic("((1 + 2) + 3)", "1 + 2 + 3", (x, y, z) -> 6, new Add(new Add(c(1), c(2)), c(3)))
+ .basic("(1 + (2 + 3))", "1 + 2 + 3", (x, y, z) -> 6, new Add(c(1), new Add(c(2), c(3))))
+ .basic("((1 - 2) - 3)", "1 - 2 - 3", (x, y, z) -> -4, new Subtract(new Subtract(c(1), c(2)), c(3)))
+ .basic("(1 - (2 - 3))", "1 - (2 - 3)", (x, y, z) -> 2, new Subtract(c(1), new Subtract(c(2), c(3))))
+ .basic("((1 * 2) * 3)", "1 * 2 * 3", (x, y, z) -> 6, new Multiply(new Multiply(c(1), c(2)), c(3)))
+ .basic("(1 * (2 * 3))", "1 * 2 * 3", (x, y, z) -> 6, new Multiply(c(1), new Multiply(c(2), c(3))))
+ .basic("((10 / 2) / 3)", "10 / 2 / 3", (x, y, z) -> 10 / 2 / 3, new Divide(new Divide(c(10), c(2)), c(3)))
+ .basic("(10 / (3 / 2))", "10 / (3 / 2)", (x, y, z) -> 10, new Divide(c(10), new Divide(c(3), c(2))))
+ .basic("((x * y) + ((z - 1) / 10))", "x * y + (z - 1) / 10", (x, y, z) -> x * y + (z - 1) / 10, new Add(
+ new Multiply(vx, vy),
+ new Divide(new Subtract(vz, c(1)), c(10))
+ ))
+ .basic("(x + y)", "x + y", (x, y, z) -> x + y, new Add(vx, vy))
+ .basic("(y + x)", "y + x", (x, y, z) -> y + x, new Add(vy, vx))
+
+ .advanced("(1 + 1)", "1 + 1", (x, y, z) -> 1 + 1, new Add(c(1), c(1)))
+ .advanced("(y - x)", "y - x", (x, y, z) -> y - x, new Subtract(vy, vx))
+ .advanced("(2 * x)", "2 * x", (x, y, z) -> 2 * x, new Multiply(c(2), vx))
+ .advanced("(2 / x)", "2 / x", (x, y, z) -> 2 / x, new Divide(c(2), vx))
+ .advanced("(z + (1 + 1))", "z + 1 + 1", (x, y, z) -> z + 1 + 1, new Add(vz, new Add(c(1), c(1))))
+ .advanced(
+ "(2 - (y - x))",
+ "2 - (y - x)",
+ (x, y, z) -> 2 - (y - x),
+ new Subtract(c(2), new Subtract(vy, vx))
+ )
+ .advanced(
+ "(z * (2 / x))",
+ "z * (2 / x)",
+ (x, y, z) -> z * (2 / x),
+ new Multiply(vz, new Divide(c(2), vx))
+ )
+ .advanced(
+ "(z / (y - x))",
+ "z / (y - x)",
+ (x, y, z) -> z / (y - x),
+ new Divide(vz, new Subtract(vy, vx))
+ )
+ .advanced("((2 * x) + y)", "2 * x + y", (x, y, z) -> 2 * x + y, new Add(new Multiply(c(2), vx), vy))
+ .advanced(
+ "((y - x) - 2)",
+ "y - x - 2",
+ (x, y, z) -> y - x - 2,
+ new Subtract(new Subtract(vy, vx), c(2))
+ )
+ .advanced("((2 / x) * y)", "2 / x * y", (x, y, z) -> 2 / x * y, new Multiply(new Divide(c(2), vx), vy))
+ .advanced("((1 + 1) / x)", "(1 + 1) / x", (x, y, z) -> (1 + 1) / x, new Divide(new Add(c(1), c(1)), vx))
+ .advanced("(1 + (2 * 3))", "1 + 2 * 3", (x, y, z) -> 7, new Add(c(1), new Multiply(c(2), c(3))))
+ .advanced("(1 - (2 * 3))", "1 - 2 * 3", (x, y, z) -> -5, new Subtract(c(1), new Multiply(c(2), c(3))))
+ .advanced("(1 + (2 / 3))", "1 + 2 / 3", (x, y, z) -> 1, new Add(c(1), new Divide(c(2), c(3))))
+ .advanced("(1 - (2 / 3))", "1 - 2 / 3", (x, y, z) -> 1, new Subtract(c(1), new Divide(c(2), c(3))))
+ .advanced(
+ "(2 + (z + (1 + 1)))",
+ "2 + z + 1 + 1",
+ (x, y, z) -> 2 + z + 1 + 1,
+ new Add(c(2), new Add(vz, new Add(c(1), c(1))))
+ )
+ .advanced(
+ "(1 - ((2 * x) + y))",
+ "1 - (2 * x + y)",
+ (x, y, z) -> 1 - (2 * x + y),
+ new Subtract(c(1), new Add(new Multiply(c(2), vx), vy))
+ )
+ .advanced(
+ "(1 * (z / (y - x)))",
+ "1 * (z / (y - x))",
+ (x, y, z) -> 1 * (z / (y - x)),
+ new Multiply(c(1), new Divide(vz, new Subtract(vy, vx)))
+ )
+ .advanced(
+ "(z / (z + (1 + 1)))",
+ "z / (z + 1 + 1)",
+ (x, y, z) -> z / (z + 1 + 1),
+ new Divide(vz, new Add(vz, new Add(c(1), c(1))))
+ )
+ .advanced(
+ "((2 * x) + (1 + 1))",
+ "2 * x + 1 + 1",
+ (x, y, z) -> 2 * x + 1 + 1,
+ new Add(new Multiply(c(2), vx), new Add(c(1), c(1)))
+ )
+ .advanced(
+ "((1 + 1) - (1 + 1))",
+ "1 + 1 - (1 + 1)",
+ (x, y, z) -> 1 + 1 - (1 + 1),
+ new Subtract(new Add(c(1), c(1)), new Add(c(1), c(1)))
+ )
+ .advanced(
+ "((y - x) * (2 / x))",
+ "(y - x) * (2 / x)",
+ (x, y, z) -> (y - x) * (2 / x),
+ new Multiply(new Subtract(vy, vx), new Divide(c(2), vx))
+ )
+ .advanced(
+ "((y - x) / (2 * x))",
+ "(y - x) / (2 * x)",
+ (x, y, z) -> (y - x) / (2 * x),
+ new Divide(new Subtract(vy, vx), new Multiply(c(2), vx))
+ )
+ .advanced(
+ "(((y - x) - 2) + 1)",
+ "y - x - 2 + 1",
+ (x, y, z) -> y - x - 2 + 1,
+ new Add(new Subtract(new Subtract(vy, vx), c(2)), c(1))
+ )
+ .advanced(
+ "(((2 * x) + y) - z)",
+ "2 * x + y - z",
+ (x, y, z) -> 2 * x + y - z,
+ new Subtract(new Add(new Multiply(c(2), vx), vy), vz)
+ )
+ .advanced(
+ "(((1 + 1) / x) * 2)",
+ "(1 + 1) / x * 2",
+ (x, y, z) -> (1 + 1) / x * 2,
+ new Multiply(new Divide(new Add(c(1), c(1)), vx), c(2))
+ )
+ .advanced(
+ "((z / (y - x)) / x)",
+ "z / (y - x) / x",
+ (x, y, z) -> z / (y - x) / x,
+ new Divide(new Divide(vz, new Subtract(vy, vx)), vx)
+ );
+ }
+
+ private static Const c(final Integer c) {
+ return TYPE.constant(c);
+ }
+
+ Selector SELECTOR = ExpressionTest.SELECTOR
+ .variant("Triple", ExpressionTest.v(TripleExpression::tester));
+
+ static void main(final String... args) {
+ TripleExpression.SELECTOR.main(args);
+ }
+}
diff --git a/java/expression/common/Expr.java b/java/expression/common/Expr.java
new file mode 100644
index 0000000..b1dd418
--- /dev/null
+++ b/java/expression/common/Expr.java
@@ -0,0 +1,32 @@
+package expression.common;
+
+import base.Functional;
+import base.Pair;
+
+import java.util.List;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+
+/**
+ * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
+ */
+public record Expr(Node node, List> variables) {
+ public List> variables(final BiFunction f) {
+ return Functional.map(
+ variables,
+ variable -> variable.second(f.apply(variable.first(), variable.second()))
+ );
+ }
+
+ public Expr convert(final BiFunction f) {
+ return of(node, variables(f));
+ }
+
+ public Expr node(final Function, Node> f) {
+ return of(f.apply(node), variables);
+ }
+
+ public static Expr