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 f); + Either flatMapRight(final Function> f); + T either(Function lf, Function rf); + + boolean isRight(); + + L getLeft(); + R getRight(); + + static Either right(final R value) { + return new Either<>() { + @Override + public Either mapRight(final Function f) { + return right(f.apply(value)); + } + + @Override + public Either flatMapRight(final Function> f) { + return f.apply(value); + } + + @Override + public T either(final Function lf, final Function 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 f) { + return left(value); + } + + @Override + public Either flatMapRight(final Function> f) { + return left(value); + } + + @Override + public T either(final Function lf, final Function 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 f) { + return items.stream().map(f).collect(Collectors.toUnmodifiableList()); + } + + public static List map(final List items, final BiFunction 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... items) { + final List result = new ArrayList<>(); + for (final Collection 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 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 f, + final Function 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... 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... parts) { + selector.variant(name, counter -> { + final V variant = factory.apply(counter); + for (final Consumer part : base) { + part.accept(variant); + } + for (final Consumer 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 items, final Consumer 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 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 constructor = (Constructor) 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 of(final Node node, final List> variables) { + return new Expr<>(node, variables); + } +} diff --git a/java/expression/common/ExpressionKind.java b/java/expression/common/ExpressionKind.java new file mode 100644 index 0000000..d8da0aa --- /dev/null +++ b/java/expression/common/ExpressionKind.java @@ -0,0 +1,94 @@ +package expression.common; + +import base.ExtendedRandom; +import base.Functional; +import base.Pair; +import expression.ToMiniString; + +import java.util.List; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ExpressionKind { + private final Type type; + private final Class kind; + private final Variables variables; + private final Evaluator evaluator; + + public ExpressionKind( + final Type type, + final Class kind, + final Variables variables, + final Evaluator evaluator + ) { + this.type = type; + this.kind = kind; + this.variables = variables; + this.evaluator = evaluator; + } + + public ExpressionKind( + final Type type, + final Class kind, + final List> variables, + final Evaluator evaluator + ) { + this(type, kind, (r, c) -> variables, evaluator); + } + + public C evaluate(final E expression, final List variables, final List values) throws Exception { + return evaluator.evaluate(expression, variables, values); + } + + public E cast(final Object expression) { + return kind.cast(expression); + } + + public String getName() { + return kind.getSimpleName(); + } + + public E constant(final C value) { + return cast(type.constant(value)); + } + + public C randomValue(final ExtendedRandom random) { + return type.randomValue(random); + } + + public List> allValues(final int length, final List values) { + return Functional.allValues(fromInts(values), length); + } + + public List fromInts(final List values) { + return Functional.map(values, this::fromInt); + } + + public C fromInt(final int value) { + return type.fromInt(value); + } + + @Override + public String toString() { + return kind.getName(); + } + + public ExpressionKind withVariables(final Variables variables) { + return new ExpressionKind<>(type, kind, variables, evaluator); + } + + public Variables variables() { + return variables; + } + + @FunctionalInterface + public interface Variables { + List> generate(final ExtendedRandom random, final int count); + } + + @FunctionalInterface + public interface Evaluator { + R evaluate(final E expression, final List vars, final List values) throws Exception; + } +} diff --git a/java/expression/common/Generator.java b/java/expression/common/Generator.java new file mode 100644 index 0000000..2b7a52d --- /dev/null +++ b/java/expression/common/Generator.java @@ -0,0 +1,173 @@ +package expression.common; + +import base.*; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public class Generator { + private final Supplier constant; + private final List> ops; + private final ExpressionKind.Variables variables; + private final Set forbidden; + private final ExtendedRandom random; + private final List>, Stream>>> basicTests; + + public Generator( + final Supplier constant, + final List> ops, + final ExpressionKind.Variables variables, + final Set forbidden, + final ExtendedRandom random, + final List>, Stream>>> basicTests + ) { + this.constant = constant; + this.ops = List.copyOf(ops); + this.variables = variables; + this.forbidden = Set.copyOf(forbidden); + this.random = random; + this.basicTests = List.copyOf(basicTests); + } + + public static Builder builder(final Supplier constant, final ExtendedRandom random) { + return new Builder<>(random, constant); + } + + public void testRandom( + final TestCounter counter, + final int denominator, + final Consumer> consumer + ) { + final int d = Math.max(TestCounter.DENOMINATOR, denominator); + testRandom(counter, consumer, 1, 100, 100 / d, (vars, depth) -> generateFullDepth(vars, Math.min(depth, 3))); + testRandom(counter, consumer, 2, 1000 / d, 1, this::generateSize); + testRandom(counter, consumer, 3, 12, 100 / d, this::generateFullDepth); + testRandom(counter, consumer, 4, 777 / d, 1, this::generatePartialDepth); + } + + private void testRandom( + final TestCounter counter, + final Consumer> consumer, + final int seq, + final int levels, + final int perLevel, + final BiFunction>, Integer, Node> generator + ) { + counter.scope("Random tests #" + seq, () -> { + final int total = levels * perLevel; + int generated = 0; + for (int level = 0; level < levels; level++) { + for (int j = 0; j < perLevel; j++) { + if (generated % 100 == 0) { + progress(counter, total, generated); + } + generated++; + + final List> vars = variables(random.nextInt(10) + 1); + consumer.accept(Expr.of(generator.apply(Functional.map(vars, v -> Node.op(v.first())), level), vars)); + } + } + progress(counter, generated, total); + }); + } + + private static void progress(final TestCounter counter, final int total, final int generated) { + counter.format("Completed %4d out of %d%n", generated, total); + } + + private Node generate( + final List> variables, + final boolean nullary, + final Supplier> unary, + final Supplier, Node>> binary + ) { + if (nullary || ops.isEmpty()) { + return random.nextBoolean() ? random.randomItem(variables) : Node.constant(constant.get()); + } else { + final Named op = random.randomItem(ops); + if (Math.abs(op.value()) == 1) { + return Node.op(op.name(), (op.value() + 1) >> 1, unary.get()); + } else { + final Pair, Node> pair = binary.get(); + return Node.op(op.name(), pair.first(), pair.second()); + } + } + } + + private Node generate(final List> variables, final boolean nullary, final Supplier> child) { + return generate(variables, nullary, child, () -> Pair.of(child.get(), child.get())); + } + + private Node generateFullDepth(final List> variables, final int depth) { + return generate(variables, depth == 0, () -> generateFullDepth(variables, depth - 1)); + } + + private Node generatePartialDepth(final List> variables, final int depth) { + return generate(variables, depth == 0, () -> generatePartialDepth(variables, random.nextInt(depth))); + } + + private Node generateSize(final List> variables, final int size) { + final int first = size <= 1 ? 0 : random.nextInt(size); + return generate( + variables, + size == 0, + () -> generateSize(variables, size - 1), + () -> Pair.of( + generateSize(variables, first), + generateSize(variables, size - 1 - first) + ) + ); + } + + public void testBasic(final Consumer> consumer) { + basicTests.forEach(test -> { + final List> vars = variables(random.nextInt(5) + 3); + test.apply(Functional.map(vars, v -> Node.op(v.first()))) + .map(node -> Expr.of(node, vars)) + .forEachOrdered(consumer); + }); + } + + public List> variables(final int count) { + List> vars; + do { + vars = variables.generate(random, count); + } while (vars.stream().map(Pair::first).anyMatch(forbidden::contains)); + return vars; + } + + /** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + public static final class Builder { + private final ExtendedRandom random; + private final Supplier constant; + + private final List> ops = new ArrayList<>(); + private final Set forbidden = new HashSet<>(); + + private Builder(final ExtendedRandom random, final Supplier constant) { + this.random = random; + this.constant = constant; + } + + public void add(final String name, final int arity) { + ops.add(Named.of(name, arity)); + forbidden.add(name); + } + + public Generator build( + final ExpressionKind.Variables variables, + final List>, Stream>>> basicTests + ) { + return new Generator<>(constant, ops, variables, forbidden, random, basicTests); + } + } +} diff --git a/java/expression/common/Node.java b/java/expression/common/Node.java new file mode 100644 index 0000000..df13ff8 --- /dev/null +++ b/java/expression/common/Node.java @@ -0,0 +1,106 @@ +package expression.common; + +import java.util.function.Function; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public abstract class Node { + private Node() { + } + + public abstract R get(Const con, Nullary nul, Unary, R> un, Binary, R> bin); + public abstract R cata(Const con, Nullary nul, Unary un, Binary bin); + + public final String toPolish() { + return cata( + T::toString, + name -> name, + (name, priority, a) -> a + " " + name + ":1", + (name, a1, a2) -> a1 + " " + a2 + " " + name + ":2" + ); + } + + @Override + public final String toString() { + return cata( + T::toString, + name -> name, + (name, priority, a) -> name.equals("[") ? "[" + a + "]" : + (priority & 1) == 1 ? "(" + name + " " + a + ")" : "(" + a + " " + name + ")", + (name, a1, a2) -> "(" + a1 + " " + name + " " + a2 + ")" + ); + } + + public static Node constant(final T value) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return con.apply(value); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return con.apply(value); + } + }; + } + + public static Node op(final String name) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return nul.apply(name); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return nul.apply(name); + } + }; + } + + public static Node op(final String name, final int priority, final Node arg) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return un.apply(name, priority, arg); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return un.apply(name, priority, arg.cata(con, nul, un, bin)); + } + }; + } + + public static Node op(final String name, final Node arg1, final Node arg2) { + return new Node<>() { + @Override + public R get(final Const con, final Nullary nul, final Unary, R> un, final Binary, R> bin) { + return bin.apply(name, arg1, arg2); + } + + @Override + public R cata(final Const con, final Nullary nul, final Unary un, final Binary bin) { + return bin.apply(name, arg1.cata(con, nul, un, bin), arg2.cata(con, nul, un, bin)); + } + }; + } + + @FunctionalInterface + public interface Const extends Function {} + + @FunctionalInterface + public interface Nullary extends Function {} + + @FunctionalInterface + public interface Unary { + R apply(String name, int priority, T arg); + } + + @FunctionalInterface + public interface Binary { + R apply(String name, T arg1, T arg2); + } +} diff --git a/java/expression/common/NodeRenderer.java b/java/expression/common/NodeRenderer.java new file mode 100644 index 0000000..d35e4da --- /dev/null +++ b/java/expression/common/NodeRenderer.java @@ -0,0 +1,96 @@ +package expression.common; + +import base.ExtendedRandom; + +import java.util.List; +import java.util.Map; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class NodeRenderer { + public static final String PAREN = "["; + public static final List DEFAULT_PARENS = List.of(paren("(", ")")); + + public static final Mode MINI_MODE = Mode.SIMPLE_MINI; // Replace by TRUE_MINI for some challenge; + public static final Settings FULL = Mode.FULL.settings(0); + public static final Settings FULL_EXTRA = Mode.FULL.settings(Integer.MAX_VALUE / 4); + public static final Settings SAME = Mode.SAME.settings(0); + public static final Settings MINI = MINI_MODE.settings(0); + public static final Settings TRUE_MINI = Mode.TRUE_MINI.settings(0); + + private final Renderer> renderer; + private final Map brackets; + private final ExtendedRandom random; + + public NodeRenderer( + final Renderer> renderer, + final Map brackets, + final ExtendedRandom random + ) { + this.renderer = renderer; + this.brackets = Map.copyOf(brackets); + this.random = random; + } + + public static Node paren(final boolean condition, final Node node) { + return condition ? Node.op(PAREN, 1, node) : node; + } + + public static Paren paren(final String open, final String close) { + return new Paren(open, close); + } + + public Node renderToNode(final Settings settings, final Expr expr) { + final Expr> convert = expr.convert((name, variable) -> Node.op(name)); + return renderer.render(convert, settings); + } + + public String render(final Node node, final List parens) { + return node.cata( + String::valueOf, + name -> name, + (name, priority, arg) -> + name == PAREN ? random.randomItem(parens).apply(arg) : + priority == Integer.MAX_VALUE ? name + arg + brackets.get(name) : + (priority & 1) == 1 ? name + arg : + arg + name, + (name, a, b) -> a + " " + name + " " + b + ); + } + + public String render(final Expr expr, final Settings settings) { + return render(renderToNode(settings, expr), settings.parens()); + } + + public enum Mode { + FULL, SAME, TRUE_MINI, SIMPLE_MINI; + + public Settings settings(final int limit) { + return new Settings(this, limit); + } + } + + public record Paren(String open, String close) { + String apply(final String expression) { + return open() + expression + close(); + } + } + + public record Settings(Mode mode, int limit, List parens) { + public Settings(final Mode mode, final int limit) { + this(mode, limit, DEFAULT_PARENS); + } + + public Node extra(Node node, final ExtendedRandom random) { + while (random.nextInt(Integer.MAX_VALUE) < limit) { + node = paren(true, node); + } + return node; + } + + public Settings withParens(final List parens) { + return this.parens.equals(parens) ? this : new Settings(mode, limit, List.copyOf(parens)); + } + } +} diff --git a/java/expression/common/NodeRendererBuilder.java b/java/expression/common/NodeRendererBuilder.java new file mode 100644 index 0000000..4e4032a --- /dev/null +++ b/java/expression/common/NodeRendererBuilder.java @@ -0,0 +1,145 @@ +package expression.common; + +import base.ExtendedRandom; +import base.Functional; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("StaticMethodOnlyUsedInOneClass") +public class NodeRendererBuilder { + private final Renderer.Builder> nodeRenderer = Renderer.builder(Node::constant); + private final Map priorities = new HashMap<>(); + private final Map brackets = new HashMap<>(); + private final ExtendedRandom random; + + public NodeRendererBuilder(final ExtendedRandom random) { + this.random = random; + nodeRenderer.unary(NodeRenderer.PAREN, (mode, arg) -> NodeRenderer.paren(true, arg)); + } + + public void unary(final String name, final int priority) { + final String space = name.equals("-") || Character.isLetter(name.charAt(0)) ? " " : ""; + nodeRenderer.unary( + name, + (settings, arg) -> settings.extra(Node.op(name, priority, inner(settings, priority, arg, space)), random) + ); + } + + public void unary(final String left, final String right) { + brackets.put(left, right); + nodeRenderer.unary( + left, + (settings, arg) -> settings.extra(Node.op(left, Integer.MAX_VALUE, arg), random) + ); + } + + private Node inner(final NodeRenderer.Settings settings, final int priority, final Node arg, final String space) { + if (settings.mode() == NodeRenderer.Mode.FULL) { + return NodeRenderer.paren(true, arg); + } else { + final String op = arg.get( + c -> space, + n -> space, + (n, p, a) -> + priority > unaryPriority(arg) ? NodeRenderer.PAREN : + NodeRenderer.PAREN.equals(n) ? "" : + space, + (n, a, b) -> NodeRenderer.PAREN + ); + return op.isEmpty() ? arg : Node.op(op, Priority.MAX.priority | 1, arg); + } + } + + private static Integer unaryPriority(final Node node) { + return node.get(c -> Integer.MAX_VALUE, n -> Integer.MAX_VALUE, (n, p, a) -> p, (n, a, b) -> Integer.MIN_VALUE); + } + + public void binary(final String name, final int priority) { + final Priority mp = new Priority(name, priority); + priorities.put(name, mp); + + nodeRenderer.binary(name, (settings, l, r) -> settings.extra(process(settings, mp, l, r), random)); + } + + private Node process(final NodeRenderer.Settings settings, final Priority mp, final Node l, final Node r) { + if (settings.mode() == NodeRenderer.Mode.FULL) { + return NodeRenderer.paren(true, op(mp, l, r)); + } + + final Priority lp = priority(l); + final Priority rp = priority(r); + + final int rc = rp.compareLevels(mp); + + // :NOTE: Especially ugly code, do not replicate + final boolean advanced = settings.mode() == NodeRenderer.Mode.SAME + || mp.has(2) + || mp.has(1) && (mp != rp || (settings.mode() == NodeRenderer.Mode.TRUE_MINI && hasOther(r, rp))); + + final Node al = NodeRenderer.paren(lp.compareLevels(mp) < 0, l); + if (rc == 0 && !advanced) { + return get(r, null, (n, a, b) -> rp.op(mp.op(al, a), b)); + } else { + return mp.op(al, NodeRenderer.paren(rc == 0 && advanced || rc < 0, r)); + } + } + + private boolean hasOther(final Node node, final Priority priority) { + return get(node, () -> false, (name, l, r) -> { + final Priority p = Functional.get(priorities, name); + if (p.compareLevels(priority) != 0) { + return false; + } + return p != priority || hasOther(l, priority); + }); + } + + private Node op(final Priority mp, final Node l, final Node r) { + return mp.op(l, r); + } + + private Priority priority(final Node node) { + return get(node, () -> Priority.MAX, (n, a, b) -> Functional.get(priorities, n)); + } + + private R get(final Node node, final Supplier common, final Node.Binary, R> binary) { + return node.get( + c -> common.get(), + n -> common.get(), + (n, p, a) -> common.get(), + binary + ); + } + + public NodeRenderer build() { + return new NodeRenderer<>(nodeRenderer.build(), brackets, random); + } + + // :NOTE: Especially ugly bit-fiddling, do not replicate + private record Priority(String op, int priority) { + private static final int Q = 3; + private static final Priority MAX = new Priority("MAX", Integer.MAX_VALUE - Q); + + private int compareLevels(final Priority that) { + return (priority | Q) - (that.priority | Q); + } + + @Override + public String toString() { + return String.format("Priority(%s, %d, %d)", op, priority | Q, priority & Q); + } + + public Node op(final Node l, final Node r) { + return Node.op(op, l, r); + } + + private boolean has(final int value) { + return (priority & Q) == value; + } + } +} diff --git a/java/expression/common/Reason.java b/java/expression/common/Reason.java new file mode 100644 index 0000000..9ad5b22 --- /dev/null +++ b/java/expression/common/Reason.java @@ -0,0 +1,60 @@ +package expression.common; + +import base.Either; + +import java.util.function.LongUnaryOperator; +import java.util.function.Supplier; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Reason { + public static final Reason OVERFLOW = new Reason("Overflow"); + public static final Reason DBZ = new Reason("Division by zero"); + + private final String description; + + public Reason(final String description) { + this.description = description; + } + + public static Either eval(final Supplier action) { + try { + return Either.right(action.get()); + } catch (final ReasonException e) { + return Either.left(e.reason); + } + } + + public static int overflow(final long value) { + return value < Integer.MIN_VALUE || Integer.MAX_VALUE < value + ? OVERFLOW.error() + : (int) value; + } + + public T error() { + throw new ReasonException(this); + } + + public LongUnaryOperator less(final long limit, final LongUnaryOperator op) { + return a -> a < limit ? error() : op.applyAsLong(a); + } + + public LongUnaryOperator greater(final int limit, final LongUnaryOperator op) { + return a -> a > limit ? error() : op.applyAsLong(a); + } + + private static class ReasonException extends RuntimeException { + private final Reason reason; + + public ReasonException(final Reason reason) { + super(reason.description); + this.reason = reason; + } + } + + @Override + public String toString() { + return String.format("Reason(%s)", description); + } +} diff --git a/java/expression/common/Renderer.java b/java/expression/common/Renderer.java new file mode 100644 index 0000000..1187f31 --- /dev/null +++ b/java/expression/common/Renderer.java @@ -0,0 +1,60 @@ +package expression.common; + +import base.Functional; +import base.Pair; + +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Renderer { + static Builder builder(final Node.Const constant) { + return new Builder<>(constant); + } + + R render(final Expr expr, final S settings); + + @FunctionalInterface + interface UnaryOperator { + R apply(S settings, R arg); + } + + @FunctionalInterface + interface BinaryOperator { + R apply(S settings, R arg1, R arg2); + } + + final class Builder { + private final Node.Const constant; + private final Map> unary = new HashMap<>(); + private final Map> binary = new HashMap<>(); + + private Builder(final Node.Const constant) { + this.constant = constant; + } + + public void unary(final String name, final UnaryOperator op) { + unary.put(name, op); + } + + public void binary(final String name, final BinaryOperator op) { + binary.put(name, op); + } + + public Renderer build() { + return (expr, settings) -> { + final Map vars = expr.variables().stream() + .collect(Collectors.toMap(Pair::first, Pair::second)); + return expr.node().cata( + constant, + name -> Functional.get(vars, name), + (name, p, arg) -> Functional.get(unary, name).apply(settings, arg), + (name, arg1, arg2) -> Functional.get(binary, name).apply(settings, arg1, arg2) + ); + }; + } + } +} diff --git a/java/expression/common/TestGenerator.java b/java/expression/common/TestGenerator.java new file mode 100644 index 0000000..fcf7288 --- /dev/null +++ b/java/expression/common/TestGenerator.java @@ -0,0 +1,56 @@ +package expression.common; + +import base.Pair; +import base.TestCounter; +import expression.ToMiniString; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +public class TestGenerator { + private final Generator generator; + private final NodeRenderer renderer; + + public TestGenerator(final Generator generator, final NodeRenderer renderer) { + this.generator = generator; + this.renderer = renderer; + } + + public void testBasic(final Consumer> test) { + generator.testBasic(consumer(test)); + } + + public void testRandom(final TestCounter counter, final int denominator, final Consumer> test) { + generator.testRandom(counter, denominator, consumer(test)); + } + + private Consumer> consumer(final Consumer> consumer) { + return expr -> consumer.accept(new TestGenerator.Test<>(expr, renderer)); + } + + + public List> variables(final int count) { + return generator.variables(count); + } + + public String render(final Expr expr, final NodeRenderer.Settings settings) { + return renderer.render(expr, settings); + } + + public static class Test { + public final Expr expr; + private final Map rendered = new HashMap<>(); + private final NodeRenderer renderer; + + public Test(final Expr expr, final NodeRenderer renderer) { + this.expr = expr; + this.renderer = renderer; + } + + public String render(final NodeRenderer.Settings settings) { + return rendered.computeIfAbsent(settings, s -> renderer.render(expr, s)); + } + } +} diff --git a/java/expression/common/TestGeneratorBuilder.java b/java/expression/common/TestGeneratorBuilder.java new file mode 100644 index 0000000..fa3a7b5 --- /dev/null +++ b/java/expression/common/TestGeneratorBuilder.java @@ -0,0 +1,145 @@ +package expression.common; + +import base.ExtendedRandom; +import base.Functional; +import expression.ToMiniString; +import expression.common.ExpressionKind.Variables; + +import java.util.*; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class TestGeneratorBuilder { + private final ExtendedRandom random; + + private final Generator.Builder generator; + private final NodeRendererBuilder renderer; + + private final List>, Stream>>> basicTests = new ArrayList<>(); + private final List> consts; + private final boolean verbose; + + public TestGeneratorBuilder( + final ExtendedRandom random, + final Supplier constant, + final List constants, + final boolean verbose + ) { + this.random = random; + this.verbose = verbose; + + generator = Generator.builder(constant, random); + renderer = new NodeRendererBuilder<>(random); + + consts = Functional.map(constants, Node::constant); + basicTests.add(vars -> consts.stream()); + basicTests.add(List::stream); + } + + private Node c() { + return random.randomItem(consts); + } + + private Node v(final List> variables) { + return random.randomItem(variables); + } + + private static Node f(final String name, final int priority, final Node arg) { + return Node.op(name, priority, arg); + } + + private static Node f(final String left, final Node arg) { + return Node.op(left, Integer.MAX_VALUE, arg); + } + + private static Node f(final String name, final Node arg1, final Node arg2) { + return Node.op(name, arg1, arg2); + } + + @SafeVarargs + private void basicTests(final Function>, Node>... tests) { + Arrays.stream(tests).map(test -> test.andThen(Stream::of)).forEachOrdered(basicTests::add); + } + + public void unary(final String name, final int priority) { + generator.add(name, (priority & 1) * 2 - 1); + renderer.unary(name, priority); + + if (verbose) { + basicTests.add(vars -> Stream.concat(consts.stream(), vars.stream()).map(a -> f(name, priority, a))); + } else { + basicTests(vars -> f(name, priority, c()), vars -> f(name, priority, v(vars))); + } + + final Function>, Node> p1 = vars -> f(name, priority, f(name, priority, f("+", v(vars), c()))); + final Function>, Node> p2 = vars -> f("*", v(vars), f("*", v(vars), f(name, priority, c()))); + basicTests( + vars -> f(name, priority, f("+", v(vars), v(vars))), + vars -> f(name, priority, f(name, priority, v(vars))), + vars -> f(name, priority, f("/", f(name, priority, v(vars)), f("+", v(vars), v(vars)))), + p1, + p2, + vars -> f("+", p1.apply(vars), p2.apply(vars)) + ); + } + + public void unary(final String left, final String right) { + generator.add(left, 1); + renderer.unary(left, right); + + if (verbose) { + basicTests.add(vars -> Stream.concat(consts.stream(), vars.stream()).map(a -> f(left, a))); + } else { + basicTests(vars -> f(left, c()), vars -> f(left, v(vars))); + } + + final Function>, Node> p1 = vars -> f(left, f(left, f("+", v(vars), c()))); + final Function>, Node> p2 = vars -> f("*", v(vars), f("*", v(vars), f(left, c()))); + basicTests( + vars -> f(left, f("+", v(vars), v(vars))), + vars -> f(left, f(left, v(vars))), + vars -> f(left, f("/", f(left, v(vars)), f("+", v(vars), v(vars)))), + p1, + p2, + vars -> f("+", p1.apply(vars), p2.apply(vars)) + ); + } + + public void binary(final String name, final int priority) { + generator.add(name, 2); + renderer.binary(name, priority); + + if (verbose) { + basicTests.add(vars -> Stream.concat(consts.stream(), vars.stream().limit(3)) + .flatMap(a -> consts.stream().map(b -> f(name, a, b)))); + } else { + basicTests( + vars -> f(name, c(), c()), + vars -> f(name, v(vars), c()), + vars -> f(name, c(), v(vars)), + vars -> f(name, v(vars), v(vars)) + ); + } + + final Function>, Node> p1 = vars -> f(name, f(name, f("+", v(vars), c()), v(vars)), v(vars)); + final Function>, Node> p2 = vars -> f("*", v(vars), f("*", v(vars), f(name, c(), v(vars)))); + + basicTests( + vars -> f(name, f(name, v(vars), v(vars)), v(vars)), + vars -> f(name, v(vars), f(name, v(vars), v(vars))), + vars -> f(name, f(name, v(vars), v(vars)), f(name, v(vars), v(vars))), + vars -> f(name, f("-", f(name, v(vars), v(vars)), c()), f("+", v(vars), v(vars))), + p1, + p2, + vars -> f("+", p1.apply(vars), p2.apply(vars)) + ); + } + + public TestGenerator build(final Variables variables) { + return new TestGenerator<>(generator.build(variables, basicTests), renderer.build()); + } +} diff --git a/java/expression/common/Type.java b/java/expression/common/Type.java new file mode 100644 index 0000000..3b396e9 --- /dev/null +++ b/java/expression/common/Type.java @@ -0,0 +1,51 @@ +package expression.common; + +import base.Asserts; +import base.ExtendedRandom; +import expression.Const; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.function.Function; +import java.util.function.IntFunction; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Type { + private final IntFunction fromInt; + private final Function random; + private final Function constant; + + public Type(final IntFunction fromInt, final Function random, final Class type) { + this.fromInt = fromInt; + this.random = random; + + try { + final MethodHandle constructor = MethodHandles.publicLookup() + .findConstructor(Const.class, MethodType.methodType(void.class, type)); + constant = c -> { + try { + return (Const) constructor.invoke(c); + } catch (final Throwable e) { + throw Asserts.error("Cannot create new Const(%s): %s", c, e); + } + }; + } catch (final IllegalAccessException | NoSuchMethodException e) { + throw Asserts.error("Cannot find constructor Const(%s): %s", type, e); + } + } + + public Const constant(final C value) { + return constant.apply(value); + } + + public C fromInt(final int value) { + return fromInt.apply(value); + } + + public C randomValue(final ExtendedRandom random) { + return this.random.apply(random); + } +} diff --git a/java/expression/common/package-info.java b/java/expression/common/package-info.java new file mode 100644 index 0000000..70d03f2 --- /dev/null +++ b/java/expression/common/package-info.java @@ -0,0 +1,7 @@ +/** + * Expressions generators for expression-based homeworks + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.common; \ No newline at end of file diff --git a/java/expression/exceptions/ExceptionsTest.java b/java/expression/exceptions/ExceptionsTest.java new file mode 100644 index 0000000..851f635 --- /dev/null +++ b/java/expression/exceptions/ExceptionsTest.java @@ -0,0 +1,30 @@ +package expression.exceptions; + +import base.Selector; +import expression.ListExpression; +import expression.parser.Operations; + +import static expression.parser.Operations.*; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ExceptionsTest { + private static final ExpressionParser PARSER = new ExpressionParser(); + private static final Operations.Operation LIST = kind(ListExpression.KIND, PARSER::parse); + + public static final Selector SELECTOR = Selector.composite(ExceptionsTest.class, ExceptionsTester::new, "easy", "hard") + .variant("Base", LIST, ADD, SUBTRACT, MULTIPLY, DIVIDE, NEGATE) + .variant("3637", POW, LOG) + .variant("3839", POW, LOG, POW_2, LOG_2) + .variant("3435", POW_2, LOG_2) + .variant("3233", HIGH, LOW) + .selector(); + + private ExceptionsTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/expression/exceptions/ExceptionsTestSet.java b/java/expression/exceptions/ExceptionsTestSet.java new file mode 100644 index 0000000..f19baa5 --- /dev/null +++ b/java/expression/exceptions/ExceptionsTestSet.java @@ -0,0 +1,162 @@ +package expression.exceptions; + +import base.Functional; +import base.Named; +import base.Pair; +import expression.ToMiniString; +import expression.Variable; +import expression.common.ExpressionKind; +import expression.parser.ParserTestSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.LongBinaryOperator; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ExceptionsTestSet extends ParserTestSet { + private static final int D = 5; + private static final List OVERFLOW_VALUES = new ArrayList<>(); + private final char[] CHARS = "AZ+-*%()[]<>".toCharArray(); + + static { + Functional.addRange(OVERFLOW_VALUES, D, Integer.MIN_VALUE + D); + Functional.addRange(OVERFLOW_VALUES, D, Integer.MIN_VALUE / 2); + Functional.addRange(OVERFLOW_VALUES, D, (int) -Math.sqrt(Integer.MAX_VALUE)); + Functional.addRange(OVERFLOW_VALUES, D, 0); + Functional.addRange(OVERFLOW_VALUES, D, (int) Math.sqrt(Integer.MAX_VALUE)); + Functional.addRange(OVERFLOW_VALUES, D, Integer.MAX_VALUE / 2); + Functional.addRange(OVERFLOW_VALUES, D, Integer.MAX_VALUE - D); + } + + private final List> parsingTest; + + public ExceptionsTestSet(final ExceptionsTester tester, final ParsedKind kind) { + super(tester, kind, false); + parsingTest = tester.parsingTest; + } + + private void testParsingErrors() { + counter.testForEach(parsingTest, op -> { + final List names = Functional.map(kind.kind().variables().generate(counter.random(), 3), Pair::first); + final String expr = mangle(op.value(), names); + try { + kind.parse(expr, names); + counter.fail("Successfully parsed '%s'", op.value()); + } catch (final Exception e) { + counter.format("%-30s %s%n", op.name(), e.getClass().getSimpleName() + ": " + e.getMessage()); + } + }); + } + + private void testOverflow() { + final List> variables = kind.kind().variables().generate(counter.random(), 3); + final List names = Functional.map(variables, Pair::first); + final Variable vx = (Variable) variables.get(0).second(); + final Variable vy = (Variable) variables.get(1).second(); + + //noinspection Convert2MethodRef + testOverflow(names, (a, b) -> a + b, "+", new CheckedAdd(vx, vy)); + testOverflow(names, (a, b) -> a - b, "-", new CheckedSubtract(vx, vy)); + testOverflow(names, (a, b) -> a * b, "*", new CheckedMultiply(vx, vy)); + testOverflow(names, (a, b) -> b == 0 ? Long.MAX_VALUE : a / b, "/", new CheckedDivide(vx, vy)); + testOverflow(names, (a, b) -> -b, "<- ignore first argument, unary -", new CheckedNegate(vy)); + } + + private void testOverflow(final List names, final LongBinaryOperator f, final String op, final Object expression) { + final ExpressionKind kind = this.kind.kind(); + for (final int a : OVERFLOW_VALUES) { + for (final int b : OVERFLOW_VALUES) { + final long expected = f.applyAsLong(a, b); + final boolean isInt = Integer.MIN_VALUE <= expected && expected <= Integer.MAX_VALUE; + try { + final C actual = kind.evaluate( + kind.cast(expression), + names, + kind.fromInts(List.of(a, b, 0)) + ); + counter.checkTrue( + isInt && kind.fromInt((int) expected).equals(actual), + "%d %s %d == %d", a, op, b, actual + ); + } catch (final Exception e) { + if (isInt) { + counter.fail(e, "Unexpected error in %d %s %d", a, op, b); + } + } + } + } + } + + @Override + protected void test() { + counter.scope("Overflow tests", (Runnable) this::testOverflow); + super.test(); + counter.scope("Parsing error tests", this::testParsingErrors); + } + + + @Override + protected E parse(final String expression, final List variables, final boolean reparse) { + final String expr = expression.strip(); + if (expr.length() > 10) { + for (final char ch : CHARS) { + for (int i = 0; i < 10; i++) { + final int index = 1 + tester.random().nextInt(expr.length() - 2); + int pi = index - 1; + while (Character.isWhitespace(expr.charAt(pi))) { + pi--; + } + int ni = index; + while (Character.isWhitespace(expr.charAt(ni))) { + ni++; + } + final char pc = expr.charAt(pi); + final char nc = expr.charAt(ni); + if ( + "-([{*∛√²³₂₃!‖⎵⎴⌊⌈=?".indexOf(nc) < 0 && + (!Character.isLetterOrDigit(pc) || !Character.isLetterOrDigit(ch)) && + nc != ch && pc != ch && + !Character.isLetterOrDigit(nc) && nc != '$' + ) { + shouldFail( + variables, + "Parsing error expected for " + insert(expr, index, "" + ch + "<-- ERROR_INSERTED>"), + insert(expr, index, String.valueOf(ch)) + ); + break; + } + } + } + parens(variables, expr, '[', ']'); + parens(variables, expr, '{', '}'); + } + + return counter.testV(() -> counter.call("parse", () -> kind.parse(expr, variables))); + } + + private static String insert(final String expr, final int index, final String value) { + return expr.substring(0, index) + value + expr.substring(index); + } + + private void parens(final List variables, final String expr, final char open, final char close) { + if (expr.indexOf(open) >= 0) { + replaces(variables, expr, open, '('); + replaces(variables, expr, close, ')'); + if (expr.indexOf('(') >= 0) { + replaces(variables, expr, '(', open); + replaces(variables, expr, ')', close); + } + } + } + + private void replaces(final List variables, final String expr, final char what, final char by) { + final String input = expr.replace(what, by); + shouldFail(variables, "Unmatched parentheses: " + input, input); + } + + private void shouldFail(final List variables, final String message, final String input) { + counter.shouldFail(message, () -> kind.parse(input, variables)); + } +} diff --git a/java/expression/exceptions/ExceptionsTester.java b/java/expression/exceptions/ExceptionsTester.java new file mode 100644 index 0000000..df9048b --- /dev/null +++ b/java/expression/exceptions/ExceptionsTester.java @@ -0,0 +1,100 @@ +package expression.exceptions; + +import base.Named; +import base.TestCounter; +import expression.common.Reason; +import expression.parser.ParserTestSet; +import expression.parser.ParserTester; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.LongBinaryOperator; +import java.util.function.LongToIntFunction; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ExceptionsTester extends ParserTester { + /* package-private */ final List> parsingTest = new ArrayList<>(List.of( + Named.of("No first argument", "* $y * $z"), + Named.of("No middle argument", "$x * * $z"), + Named.of("No last argument", "$x * $y * "), + Named.of("No first argument'", "1 + (* $y * $z) + 2"), + Named.of("No middle argument'", "1 + ($x * / 9) + 3"), + Named.of("No last argument'", "1 + ($x * $y - ) + 3"), + Named.of("No opening parenthesis", "$x * $y)"), + Named.of("No closing parenthesis", "($x * $y"), + Named.of("Mismatched closing parenthesis", "($x * $y]"), + Named.of("Mismatched open parenthesis", "[$x * $y)"), + Named.of("Start symbol", "@$x * $y"), + Named.of("Middle symbol", "$x @ * $y"), + Named.of("End symbol", "$x * $y@"), + Named.of("Constant overflow 1", Integer.MIN_VALUE - 1L + ""), + Named.of("Constant overflow 2", Integer.MAX_VALUE + 1L + ""), + Named.of("Bare +", "+"), + Named.of("Bare -", "-"), + Named.of("Bare a", "a"), + Named.of("(())", "(())"), + Named.of("Spaces in numbers", "10 20") + )); + + public ExceptionsTester(final TestCounter counter) { + super(counter); + } + + + private void parsingTests(final String... tests) { + for (final String test : tests) { + parsingTest.add(Named.of(test, test)); + } + } + + @Override + public void unary(final String name, final int priority, final BiFunction op) { + if (allowed(name)) { + parsingTests(name, "1 * " + name, name + " * 1"); + } + parsingTests(name + "()", name + "(1, 2)"); + if (name.length() > 1) { + parsingTests(name + "q"); + } + if (allLetterAndDigit(name)) { + parsingTests(name + "1", name + "q"); + } + super.unary(name, priority, op); + } + + private static boolean allowed(final String name) { + return !"xyz".contains(name.substring(0, 1)) && !"xyz".contains(name.substring(name.length() - 1)); + } + + @Override + public void binary(final String name, final int priority, final LongBinaryOperator op) { + if (allowed(name)) { + parsingTests(name); + } + parsingTests("1 " + name, "1 " + name + " * 3"); + if (!"-".equals(name)) { + parsingTests(name + " 1", "1 * " + name + " 2"); + } + if (allLetterAndDigit(name)) { + parsingTests("5" + name + "5", "5 " + name + "5", "5 " + name + "5 5", "1" + name + "x 1", "1 " + name + "x 1"); + } + super.binary(name, priority, op); + } + + private static boolean allLetterAndDigit(final String name) { + return name.chars().allMatch(Character::isLetterOrDigit); + } + + @Override + protected void test(final ParserTestSet.ParsedKind kind) { + new ExceptionsTestSet<>(this, kind).test(); + } + + @Override + protected int cast(final long value) { + return Reason.overflow(value); + } +} diff --git a/java/expression/exceptions/ListParser.java b/java/expression/exceptions/ListParser.java new file mode 100644 index 0000000..2370790 --- /dev/null +++ b/java/expression/exceptions/ListParser.java @@ -0,0 +1,13 @@ +package expression.exceptions; + +import expression.ListExpression; + +import java.util.List; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +public interface ListParser { + ListExpression parse(String expression, final List variables) throws Exception; +} diff --git a/java/expression/exceptions/package-info.java b/java/expression/exceptions/package-info.java new file mode 100644 index 0000000..5debca1 --- /dev/null +++ b/java/expression/exceptions/package-info.java @@ -0,0 +1,8 @@ +/** + * Tests for Expression Error Handling homework + * of Introduction to Programming course. + * + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.exceptions; \ No newline at end of file diff --git a/java/expression/package-info.java b/java/expression/package-info.java new file mode 100644 index 0000000..8d9e3c1 --- /dev/null +++ b/java/expression/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Expressions homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression; \ No newline at end of file diff --git a/java/expression/parser/ListParser.java b/java/expression/parser/ListParser.java new file mode 100644 index 0000000..0f331d9 --- /dev/null +++ b/java/expression/parser/ListParser.java @@ -0,0 +1,13 @@ +package expression.parser; + +import expression.ListExpression; + +import java.util.List; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@FunctionalInterface +public interface ListParser { + ListExpression parse(String expression, List variables); +} diff --git a/java/expression/parser/Operations.java b/java/expression/parser/Operations.java new file mode 100644 index 0000000..3266560 --- /dev/null +++ b/java/expression/parser/Operations.java @@ -0,0 +1,148 @@ +package expression.parser; + +import expression.ToMiniString; +import expression.common.ExpressionKind; +import expression.common.Reason; + +import java.math.BigInteger; +import java.util.function.*; +import java.util.stream.LongStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Operations { + // === Base + + public static final Operation NEGATE = unary("-", 1, a -> -a); + @SuppressWarnings("Convert2MethodRef") + public static final Operation ADD = binary("+", 1600, (a, b) -> a + b); + public static final Operation SUBTRACT = binary("-", 1602, (a, b) -> a - b); + public static final Operation MULTIPLY = binary("*", 2001, (a, b) -> a * b); + public static final Operation DIVIDE = binary("/", 2002, (a, b) -> b == 0 ? Reason.DBZ.error() : a / b); + + + // === MinMax + public static final Operation MIN = binary("min", 401, Math::min); + public static final Operation MAX = binary("max", 401, Math::max); + + + // === Reverse + + private static Operation digits(final String name, final boolean mask, final int r, final LongBinaryOperator q) { + return unary(name, 1, v -> LongStream.iterate(mask ? v & 0xffff_ffffL : v, n -> n != 0, n -> n / r) + .map(n -> n % r) + .reduce(0, q)); + } + + public static final Operation REVERSE = digits("reverse", false, 10, (a, b) -> a * 10 + b); + + + // === Digits + public static final Operation DIGITS = digits("digits", false, 10, Long::sum); + + + // === Floor and Ceiling + + private static long floor(final long a) { + return (a >= 0 ? a : a - FLOOR_CEILING_STEP + 1) / FLOOR_CEILING_STEP * FLOOR_CEILING_STEP; + } + + private static long ceiling(final long a) { + return (a >= 0 ? a + FLOOR_CEILING_STEP - 1: a) / FLOOR_CEILING_STEP * FLOOR_CEILING_STEP; + } + + public static final int FLOOR_CEILING_STEP = 1000; + public static final Operation FLOOR = unary("floor", 1, Operations::floor); + public static final Operation CEILING = unary("ceiling", 1, Operations::ceiling); + + // === Set, Clear + + @SuppressWarnings("IntegerMultiplicationImplicitCastToLong") + public static final Operation SET = binary("set", 202, (a, b) -> a | (1 << b)); + @SuppressWarnings("IntegerMultiplicationImplicitCastToLong") + public static final Operation CLEAR = binary("clear", 202, (a, b) -> a & ~(1 << b)); + + + // === Pow, Log + public static final Operation POW_O = binary("**", 2402, (a, b) -> + b < 0 ? 1 : BigInteger.valueOf(a).modPow(BigInteger.valueOf(b), BigInteger.valueOf(1L << 32)).intValue()); + public static final Operation LOG_O = binary("//", 2402, (a, b) -> + a == 0 && b > 0 ? Integer.MIN_VALUE : + a <= 0 || b <= 0 || a == 1 && b == 1 ? 0 : + a > 1 && b == 1 ? Integer.MAX_VALUE + : LongStream.iterate(b, v -> v <= a, v -> v * b).count() + ); + + private static final Reason INVALID_POW = new Reason("Invalid power"); + public static final Operation POW = binary("**", 2402, Operations::powC); + + private static long powC(final long a, final long b) { + if (b < 0 || a == 0 && b == 0) { + return INVALID_POW.error(); + } + if (Math.abs(a) > 1 && b > 32) { + return Reason.OVERFLOW.error(); + } + final BigInteger result = BigInteger.valueOf(a).pow((int) b); + if (result.compareTo(BigInteger.valueOf(Integer.MIN_VALUE)) < 0 || BigInteger.valueOf(Integer.MAX_VALUE).compareTo(result) < 0) { + return Reason.OVERFLOW.error(); + } + return result.intValue(); + } + + private static final Reason INVALID_LOG = new Reason("Invalid log"); + public static final Operation LOG = binary("//", 2402, (a, b) -> + a <= 0 || b <= 1 ? INVALID_LOG.error() : (int) (Math.log(a) / Math.log(b))); + + + // Pow2, Log2 + + private static final Reason NEG_LOG = new Reason("Logarithm of negative value"); + public static final Operation LOG_2 + = unary("log₂", 1, NEG_LOG.less(1, a-> (long) (Math.log(a) / Math.log(2)))); + + private static final Reason NEG_POW = new Reason("Exponentiation to negative power"); + public static final Operation POW_2 + = unary("pow₂", 1, NEG_POW.less(0, Reason.OVERFLOW.greater(31, a -> (long) Math.pow(2, a)))); + + + // === High, Low + public static final Operation HIGH = unary("high", 1, v -> Integer.highestOneBit((int) v)); + public static final Operation LOW = unary("low", 1, v -> Integer.lowestOneBit((int) v)); + + // === Common + + private Operations() { + } + + public static Operation unary(final String name, final int priority, final LongUnaryOperator op) { + return unary(name, priority, (a, c) -> op.applyAsLong(a)); + } + + public static Operation unary(final String left, final String right, final LongUnaryOperator op) { + return unary(left, right, (a, c) -> op.applyAsLong(a)); + } + + public static Operation unary(final String name, final int priority, final BiFunction op) { + return tests -> tests.unary(name, priority, op); + } + + public static Operation unary(final String left, final String right, final BiFunction op) { + return tests -> tests.unary(left, right, op); + } + + public static Operation binary(final String name, final int priority, final LongBinaryOperator op) { + return tests -> tests.binary(name, priority, op); + } + + public static Operation kind( + final ExpressionKind kind, + final ParserTestSet.Parser parser + ) { + return factory -> factory.kind(kind, parser); + } + + @FunctionalInterface + public interface Operation extends Consumer {} +} diff --git a/java/expression/parser/ParserTest.java b/java/expression/parser/ParserTest.java new file mode 100644 index 0000000..c7b20bd --- /dev/null +++ b/java/expression/parser/ParserTest.java @@ -0,0 +1,31 @@ +package expression.parser; + +import base.Selector; +import expression.ListExpression; + +import static expression.parser.Operations.*; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ParserTest { + private static final ExpressionParser PARSER = new ExpressionParser(); + private static final Operations.Operation LIST = kind(ListExpression.KIND, PARSER::parse); + + // === Common + + public static final Selector SELECTOR = Selector.composite(ParserTest.class, ParserTester::new, "easy", "hard") + .variant("Base", LIST, ADD, SUBTRACT, MULTIPLY, DIVIDE, NEGATE) + .variant("3637", MIN, MAX, REVERSE) + .variant("3839", MIN, MAX, REVERSE, DIGITS) + .variant("3435", FLOOR, CEILING, SET, CLEAR) + .variant("3233", FLOOR, CEILING) + .selector(); + + private ParserTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/expression/parser/ParserTestSet.java b/java/expression/parser/ParserTestSet.java new file mode 100644 index 0000000..c70f039 --- /dev/null +++ b/java/expression/parser/ParserTestSet.java @@ -0,0 +1,236 @@ +package expression.parser; + +import base.*; +import expression.ToMiniString; +import expression.common.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ParserTestSet { + private static final int D = 5; + + private static final List TEST_VALUES = new ArrayList<>(); + static { + Functional.addRange(TEST_VALUES, D, D); + Functional.addRange(TEST_VALUES, D, -D); + } + + public static final List CONSTS + = List.of(0, 1, -1, 4, -4, 10, -10, 30, -30, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE); + + protected final ParserTester tester; + protected final ParsedKind kind; + private final boolean safe; + + protected final TestCounter counter; + + public ParserTestSet(final ParserTester tester, final ParsedKind kind) { + this(tester, kind, true); + } + + protected ParserTestSet(final ParserTester tester, final ParsedKind kind, final boolean safe) { + this.tester = tester; + this.kind = kind; + this.safe = safe; + + counter = tester.getCounter(); + } + + private void examples(final TestGenerator generator) { + example(generator, "$x+2", (x, y, z) -> x + 2); + example(generator, "2-$y", (x, y, z) -> 2 - y); + example(generator, " 3* $z ", (x, y, z) -> 3 * z); + example(generator, "$x/ - 2", (x, y, z) -> -x / 2); + example(generator, "$x*$y+($z-1 )/10", (x, y, z) -> x * y + (int) (z - 1) / 10); + example(generator, "-(-(-\t\t-5 + 16 *$x*$y) + 1 * $z) -(((-11)))", (x, y, z) -> -(-(5 + 16 * x * y) + z) + 11); + example(generator, "" + Integer.MAX_VALUE, (x, y, z) -> (long) Integer.MAX_VALUE); + example(generator, "" + Integer.MIN_VALUE, (x, y, z) -> (long) Integer.MIN_VALUE); + example(generator, "$x--$y--$z", (x, y, z) -> x + y + z); + example(generator, "((2+2))-0/(--2)*555", (x, y, z) -> 4L); + example(generator, "$x-$x+$y-$y+$z-($z)", (x, y, z) -> 0L); + example(generator, "(".repeat(300) + "$x + $y + (-10*-$z)" + ")".repeat(300), (x, y, z) -> x + y + 10 * z); + example(generator, "$x / $y / $z", (x, y, z) -> y == 0 || z == 0 ? Reason.DBZ.error() : (int) x / (int) y / z); + } + + private void example(final TestGenerator generator, final String expr, final ExampleExpression expression) { + final List names = Functional.map(generator.variables(3), Pair::first); + final TExpression expected = vars -> expression.evaluate(vars.get(0), vars.get(1), vars.get(2)); + + counter.test(() -> { + final String mangled = mangle(expr, names); + final E parsed = parse(mangled, names, true); + Functional.allValues(TEST_VALUES, 3).forEach(values -> check(expected, parsed, names, values, mangled)); + }); + } + + protected static String mangle(final String expr, final List names) { + return expr + .replace("$x", names.get(0)) + .replace("$y", names.get(1)) + .replace("$z", names.get(2)); + } + + protected void test() { + final TestGenerator generator = tester.generator.build(kind.kind.variables()); + final Renderer renderer = tester.renderer.build(); + final Consumer> consumer = test -> test(renderer, test); + counter.scope("Basic tests", () -> generator.testBasic(consumer)); + counter.scope("Handmade tests", () -> examples(generator)); + counter.scope("Random tests", () -> generator.testRandom(counter, 1, consumer)); + } + + private void test(final Renderer renderer, final TestGenerator.Test test) { + final Expr expr = test.expr; + final List> vars = expr.variables(); + final List variables = Functional.map(vars, Pair::first); + final String full = test.render(NodeRenderer.FULL); + final String mini = test.render(NodeRenderer.MINI); + + final E fullParsed = parse(test, variables, NodeRenderer.FULL); + final E miniParsed = parse(test, variables, NodeRenderer.MINI); + final E safeParsed = parse(test, variables, NodeRenderer.SAME); + + checkToString(full, mini, "base", fullParsed); + if (tester.mode() > 0) { + counter.test(() -> Asserts.assertEquals("mini.toMiniString", mini, miniParsed.toMiniString())); + counter.test(() -> Asserts.assertEquals("safe.toMiniString", mini, safeParsed.toMiniString())); + } + checkToString(full, mini, "extraParentheses", parse(test, variables, NodeRenderer.FULL_EXTRA)); + checkToString(full, mini, "noSpaces", parse(removeSpaces(full), variables, false)); + checkToString(full, mini, "extraSpaces", parse(extraSpaces(full), variables, false)); + + final TExpression expected = renderer.render( + Expr.of( + expr.node(), + Functional.map(vars, (i, var) -> Pair.of(var.first(), args -> args.get(i))) + ), + Unit.INSTANCE + ); + + check(expected, fullParsed, variables, tester.random().random(variables.size(), ExtendedRandom::nextInt), full); + if (this.safe) { + final String safe = test.render(NodeRenderer.SAME); + check(expected, safeParsed, variables, tester.random().random(variables.size(), ExtendedRandom::nextInt), safe); + } + } + + private E parse( + final TestGenerator.Test test, + final List variables, + final NodeRenderer.Settings settings + ) { + return parse(test.render(settings.withParens(tester.parens)), variables, false); + } + + private static final String LOOKBEHIND = "(?*/+=!-])"; + private static final String LOOKAHEAD = "(?![a-zA-Z0-9<>*/])"; + private static final Pattern SPACES = Pattern.compile(LOOKBEHIND + " | " + LOOKAHEAD + "|" + LOOKAHEAD + LOOKBEHIND); + private String extraSpaces(final String expression) { + return SPACES.matcher(expression).replaceAll(r -> tester.random().randomString( + ExtendedRandom.SPACES, + tester.random().nextInt(5) + )); + } + + private static String removeSpaces(final String expression) { + return SPACES.matcher(expression).replaceAll(""); + } + + private void checkToString(final String full, final String mini, final String context, final ToMiniString parsed) { + counter.test(() -> { + assertEquals(context + ".toString", full, full, parsed.toString()); + if (tester.mode() > 0) { + assertEquals(context + ".toMiniString", full, mini, parsed.toMiniString()); + } + }); + } + + private static void assertEquals( + final String context, + final String original, + final String expected, + final String actual + ) { + final String message = String.format("%s:%n original `%s`,%n expected `%s`,%n actual `%s`", + context, original, expected, actual); + Asserts.assertTrue(message, Objects.equals(expected, actual)); + } + + private Either eval(final TExpression expression, final List vars) { + return Reason.eval(() -> tester.cast(expression.evaluate(vars))); + } + + protected E parse(final String expression, final List variables, final boolean reparse) { + return counter.testV(() -> { + final E parsed = counter.testV(() -> counter.call("parse", + () -> kind.parse(expression, variables))); + if (reparse) { + counter.testV(() -> counter.call("parse", () -> kind.parse(parsed.toString(), variables))); + } + return parsed; + }); + } + + private void check( + final TExpression expectedExpression, + final E expression, + final List variables, + final List values, + final String unparsed + ) { + counter.test(() -> { + final Either answer = eval(expectedExpression, values); + final String args = IntStream.range(0, variables.size()) + .mapToObj(i -> variables.get(i) + "=" + values.get(i)) + .collect(Collectors.joining(", ")); + final String message = String.format("f(%s)%n\twhere f=%s%n\tyour f=%s", args, unparsed, expression); + try { + final C actual = kind.kind.evaluate(expression, variables, kind.kind.fromInts(values)); + counter.checkTrue(answer.isRight(), "Error expected for f(%s)%n\twhere f=%s%n\tyour f=%s", args, unparsed, expression); + Asserts.assertEquals(message, answer.getRight(), actual); + } catch (final Exception e) { + if (answer.isRight()) { + counter.fail(e, "No error expected for %s", message); + } + } + }); + } + + @FunctionalInterface + public interface TExpression { + long evaluate(List vars); + } + + @FunctionalInterface + protected interface ExampleExpression { + long evaluate(long x, long y, long z); + } + + /** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + public record ParsedKind(ExpressionKind kind, Parser parser) { + public E parse(final String expression, final List variables) throws Exception { + return parser.parse(expression, variables); + } + + @Override + public String toString() { + return kind.toString(); + } + } + + @FunctionalInterface + public interface Parser { + E parse(final String expression, final List variables) throws Exception; + } +} diff --git a/java/expression/parser/ParserTester.java b/java/expression/parser/ParserTester.java new file mode 100644 index 0000000..059acf2 --- /dev/null +++ b/java/expression/parser/ParserTester.java @@ -0,0 +1,76 @@ +package expression.parser; + +import base.ExtendedRandom; +import base.TestCounter; +import base.Tester; +import base.Unit; +import expression.ToMiniString; +import expression.common.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.LongBinaryOperator; +import java.util.function.LongToIntFunction; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ParserTester extends Tester { + /* package-private */ final TestGeneratorBuilder generator; + /* package-private */ final Renderer.Builder renderer; + private final List> kinds = new ArrayList<>(); + /* package-private */ final List parens = new ArrayList<>(List.of(NodeRenderer.paren("(", ")"))); + + public ParserTester(final TestCounter counter) { + super(counter); + renderer = Renderer.builder(c -> vars -> c); + final ExtendedRandom random = counter.random(); + generator = new TestGeneratorBuilder<>(random, random::nextInt, ParserTestSet.CONSTS, true); + } + + public void unary(final String name, final int priority, final BiFunction op) { + generator.unary(name, priority); + renderer.unary(name, (unit, a) -> vars -> cast(op.apply(a.evaluate(vars), this::cast))); + } + + public void unary(final String left, final String right, final BiFunction op) { + generator.unary(left, right); + renderer.unary(left, (unit, a) -> vars -> cast(op.apply(a.evaluate(vars), this::cast))); + } + + public void binary(final String name, final int priority, final LongBinaryOperator op) { + generator.binary(name, priority); + renderer.binary(name, (unit, a, b) -> vars -> cast(op.applyAsLong(a.evaluate(vars), b.evaluate(vars)))); + } + + void kind(final ExpressionKind kind, final ParserTestSet.Parser parser) { + kinds.add(new ParserTestSet.ParsedKind<>(kind, parser)); + } + + @Override + public void test() { + for (final ParserTestSet.ParsedKind kind : kinds) { + counter.scope(kind.toString(), () -> test(kind)); + } + } + + protected void test(final ParserTestSet.ParsedKind kind) { + new ParserTestSet<>(this, kind).test(); + } + + public TestCounter getCounter() { + return counter; + } + + protected int cast(final long value) { + return (int) value; + } + + public void parens(final String... parens) { + assert parens.length % 2 == 0 : "Parens should come in pairs"; + for (int i = 0; i < parens.length; i += 2) { + this.parens.add(NodeRenderer.paren(parens[i], parens[i + 1])); + } + } +} diff --git a/java/expression/parser/package-info.java b/java/expression/parser/package-info.java new file mode 100644 index 0000000..1f7a1e9 --- /dev/null +++ b/java/expression/parser/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Expressions Parsing homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package expression.parser; \ No newline at end of file diff --git a/java/markup/MarkupListTest.java b/java/markup/MarkupListTest.java new file mode 100644 index 0000000..fbc8b79 --- /dev/null +++ b/java/markup/MarkupListTest.java @@ -0,0 +1,248 @@ +package markup; + +import base.Asserts; +import base.Selector; +import base.TestCounter; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class MarkupListTest { + public static final Consumer VARIANT = MarkupListTest.variant( + "Tex", Map.ofEntries( + Map.entry("

", "\\par{}"), Map.entry("

", ""), + Map.entry("", "\\emph{"), Map.entry("", "}"), + Map.entry("", "\\textbf{"), Map.entry("", "}"), + Map.entry("", "\\textst{"), Map.entry("", "}"), + Map.entry("
    ", "\\begin{itemize}"), Map.entry("
", "\\end{itemize}"), + Map.entry("
    ", "\\begin{enumerate}"), Map.entry("
", "\\end{enumerate}"), + Map.entry("
  • ", "\\item "), Map.entry("
  • ", "") + ) + ); + + + public static final Selector SELECTOR = new Selector(MarkupListTest.class) + .variant("3637", VARIANT) + .variant("3839", VARIANT) + .variant("4142", VARIANT) + .variant("4749", VARIANT) + + ; + + private MarkupListTest() { + } + + public static Consumer variant(final String name, final Map mapping) { + return MarkupTester.variant(MarkupListTest::test, name, mapping); + } + + private static void test(final MarkupTester.Checker checker) { + final Paragraph paragraph0 = new Paragraph(List.of(new Text("hello"))); + final String paragraph0Markup = "

    hello

    "; + + final Paragraph paragraph1 = new Paragraph(List.of( + new Strong(List.of( + new Text("1"), + new Strikeout(List.of( + new Text("2"), + new Emphasis(List.of( + new Text("3"), + new Text("4") + )), + new Text("5") + )), + new Text("6") + )) + )); + final String paragraph1Markup = "

    123456

    "; + + final Paragraph paragraph2 = new Paragraph(List.of(new Strong(List.of( + new Text("sdq"), + new Strikeout(List.of(new Emphasis(List.of(new Text("r"))), new Text("vavc"))), + new Text("zg"))) + )); + final String paragraph2Markup = "

    sdqrvavczg

    "; + + checker.test(paragraph0, paragraph0Markup); + checker.test(paragraph1, paragraph1Markup); + checker.test(paragraph2, paragraph2Markup); + + final ListItem li1 = new ListItem(List.of(new Paragraph(List.of(new Text("1.1"))), new Paragraph(List.of(new Text("1.2"))))); + final String li1Markup = "

    1.1

    1.2

    "; + final ListItem li2 = new ListItem(List.of(new Paragraph(List.of(new Text("2"))))); + final String li2Markup = "

    2

    "; + final ListItem pli1 = new ListItem(List.of(paragraph1)); + final ListItem pli2 = new ListItem(List.of(paragraph2)); + + final ListItem nestedUl = new ListItem(List.of(ul(li1, li2))); + final String nestedUlMarkup = ul(li1Markup, li2Markup); + + checker.test(ul(li1), ul(li1Markup)); + checker.test(ul(li2), ul(li2Markup)); + checker.test(ul(pli1), ul(paragraph1Markup)); + checker.test(ul(pli2), ul(paragraph2Markup)); + checker.test(ul(li1, li2), nestedUlMarkup); + checker.test(ul(pli1, pli2), ul(paragraph1Markup, paragraph2Markup)); + checker.test(ul(nestedUl), ul(nestedUlMarkup)); + + final ListItem nestedOl = new ListItem(List.of(ol(li1, li2))); + final String nestedOlMarkup = ol(li1Markup, li2Markup); + checker.test(ol(li1), ol(li1Markup)); + checker.test(ol(li2), ol(li2Markup)); + checker.test(ol(pli1), ol(paragraph1Markup)); + checker.test(ol(pli2), ol(paragraph2Markup)); + checker.test(ol(li1, li2), nestedOlMarkup); + checker.test(ol(pli1, pli2), ol(paragraph1Markup, paragraph2Markup)); + checker.test(ol(nestedOl), ol(nestedOlMarkup)); + + checker.test(ul(nestedUl, nestedOl), ul(nestedUlMarkup, nestedOlMarkup)); + checker.test(ol(nestedUl, nestedOl), ol(nestedUlMarkup, nestedOlMarkup)); + + checker.test( + ul(nestedUl, nestedOl, pli1, pli2), + ul(nestedUlMarkup, nestedOlMarkup, paragraph1Markup, paragraph2Markup) + ); + checker.test( + ol(nestedUl, nestedOl, pli1, pli2), + ol(nestedUlMarkup, nestedOlMarkup, paragraph1Markup, paragraph2Markup) + ); + + checker.test( + new Paragraph(List.of(new Strikeout(List.of(new Strong(List.of(new Strikeout(List.of(new Emphasis(List.of(new Strikeout(List.of(new Text("е"), new Text("г"), new Text("ц"))), new Strong(List.of(new Text("щэш"), new Text("игепы"), new Text("хм"))), new Strikeout(List.of(new Text("б"), new Text("е"))))), new Strong(List.of(new Strong(List.of(new Text("ю"), new Text("дърб"), new Text("еи"))), new Emphasis(List.of(new Text("зр"), new Text("дуаужш"), new Text("ш"))), new Strong(List.of(new Text("рб"), new Text("щ"))))), new Text("a"))), new Strikeout(List.of(new Text("no"), new Text("ddw"), new Strong(List.of(new Emphasis(List.of(new Text("щ"), new Text("ча"), new Text("эгфш"))), new Strikeout(List.of(new Text("фяи"), new Text("штел"), new Text("н"))), new Strikeout(List.of(new Text("ту"), new Text("ьъг"))))))), new Emphasis(List.of(new Emphasis(List.of(new Text("tc"), new Strong(List.of(new Text("щ"), new Text("э"), new Text("то"))), new Strong(List.of(new Text("а"), new Text("ц"))))), new Emphasis(List.of(new Text("hld"), new Emphasis(List.of(new Text("ыо"), new Text("яще"), new Text("лэ"))), new Text("i"))), new Text("tm"))))), new Emphasis(List.of(new Text("q"), new Emphasis(List.of(new Text("zn"), new Strong(List.of(new Text("mnphd"), new Strong(List.of(new Text("г"), new Text("вй"), new Text("шш"))), new Strong(List.of(new Text("з"), new Text("ввъ"))))), new Strikeout(List.of(new Emphasis(List.of(new Text("у"), new Text("в"), new Text("у"))), new Strikeout(List.of(new Text("лдяр"), new Text("зоъ"), new Text("эн"))), new Strikeout(List.of(new Text("в"), new Text("м"))))))), new Strikeout(List.of(new Text("cqqzbhtn"), new Text("i"), new Strong(List.of(new Text("i"), new Strikeout(List.of(new Text("э"), new Text("як"))), new Text("i"))))))), new Text("ef"))), new Strikeout(List.of(new Strikeout(List.of(new Strong(List.of(new Emphasis(List.of(new Strong(List.of(new Text("шец"), new Text("ю"), new Text("дрк"))), new Strikeout(List.of(new Text("е"), new Text("мь"), new Text("б"))), new Strong(List.of(new Text("еп"), new Text("ряэк"))))), new Strong(List.of(new Text("t"), new Emphasis(List.of(new Text("сы"), new Text("в"), new Text("к"))), new Text("rf"))), new Text("x"))), new Emphasis(List.of(new Emphasis(List.of(new Emphasis(List.of(new Text("юд"), new Text("чх"), new Text("яжюи"))), new Emphasis(List.of(new Text("и"), new Text("п"), new Text("вх"))), new Text("mf"))), new Emphasis(List.of(new Strong(List.of(new Text("шб"), new Text("вс"), new Text("е"))), new Strong(List.of(new Text("т"), new Text("шж"), new Text("ину"))), new Strong(List.of(new Text("ыа"), new Text("ьскю"))))), new Text("x"))), new Strikeout(List.of(new Emphasis(List.of(new Strong(List.of(new Text("в"), new Text("зыйгг"), new Text("о"))), new Strikeout(List.of(new Text("ок"), new Text("уч"), new Text("л"))), new Text("v"))), new Emphasis(List.of(new Strong(List.of(new Text("н"), new Text("ъчжфзтодг"), new Text("кыч"))), new Strikeout(List.of(new Text("вд"), new Text("лпбзс"), new Text("гщ"))), new Emphasis(List.of(new Text("ъ"), new Text("й"))))), new Text("n"))))), new Strong(List.of(new Strong(List.of(new Emphasis(List.of(new Strong(List.of(new Text("ю"), new Text("сдям"), new Text("ш"))), new Strong(List.of(new Text("ц"), new Text("еящж"), new Text("шн"))), new Text("upg"))), new Text("d"), new Strikeout(List.of(new Text("xu"), new Strikeout(List.of(new Text("кл"), new Text("еок"), new Text("с"))), new Strong(List.of(new Text("а"), new Text("ь"))))))), new Strong(List.of(new Strikeout(List.of(new Text("zn"), new Text("syb"), new Strong(List.of(new Text("ъзюкмц"), new Text("ндюз"))))), new Strong(List.of(new Strikeout(List.of(new Text("н"), new Text("с"), new Text("ь"))), new Strikeout(List.of(new Text("зьуес"), new Text("к"), new Text("и"))), new Strong(List.of(new Text("тв"), new Text("у"))))), new Strikeout(List.of(new Strong(List.of(new Text("ы"), new Text("г"), new Text("гм"))), new Strong(List.of(new Text("сыр"), new Text("я"), new Text("т"))), new Emphasis(List.of(new Text("ь"), new Text("махыы"))))))), new Text("k"))), new Text("q"))), new Strikeout(List.of(new Text("b"), new Text("o"), new Emphasis(List.of(new Strong(List.of(new Strikeout(List.of(new Strong(List.of(new Text("х"), new Text("йз"), new Text("ж"))), new Text("udlh"), new Strikeout(List.of(new Text("чъ"), new Text("с"))))), new Strong(List.of(new Strong(List.of(new Text("ю"), new Text("т"), new Text("яъайл"))), new Strong(List.of(new Text("х"), new Text("ри"), new Text("в"))), new Strong(List.of(new Text("щ"), new Text("вт"))))), new Text("m"))), new Text("vzb"), new Strong(List.of(new Text("oi"), new Text("r"), new Text("inpz"))))))))), + "

    егцщэшигепыхмбеюдърбеизрдуаужшшрбщanoddwщчаэгфшфяиштелнтуьъгtcщэтоацhldыоящелэitmqznmnphdгвйшшзввъувулдярзоъэнвмcqqzbhtniiэякiefшецюдркемьбепряэкtсывкrfxюдчхяжюиипвхmfшбвсетшжинуыаьскюxвзыйггоокучлvнъчжфзтодгкычвдлпбзсгщъйnюсдямшцеящжшнupgdxuклеоксаьznsybъзюкмцндюзнсьзьуескитвуыггмсырятьмахыыkqboхйзжudlhчъсютяъайлхривщвтmvzboirinpz

    " + ); + + checker.test( + new OrderedList(List.of(new ListItem(List.of(new OrderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("е"))), new Paragraph(List.of(new Text("х"))))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()), new Paragraph(List.of(new Text("эш"))))), new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("цць"))))), new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("м"))))))), new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("ю"))), new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))))), new Paragraph(List.of(new Emphasis(List.of(new Emphasis(List.of(new Text("узр"))), new Text("i"), new Emphasis(List.of(new Text("аужш"))), new Text("ш"))), new Strong(List.of(new Text("c"), new Strikeout(List.of(new Text("щ"))), new Text("a"), new Text("з"))), new Strong(List.of(new Emphasis(List.of(new Text("ь"))), new Text("ddw"), new Text("зщ"), new Text("ча"))), new Emphasis(List.of(new Strong(List.of(new Text("гфш"))), new Strikeout(List.of(new Text("фяи"))), new Text("штел"), new Text("н"))))), new OrderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("юцщ"))), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("э"))))))))), new ListItem(List.of(new OrderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()), new Paragraph(List.of(new Text("ж"))))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("ыеж"))), new Paragraph(List.of(new Text("ыо"))))), new ListItem(List.of(new Paragraph(List.of(new Text("ще"))), new Paragraph(List.of(new Text("щш"))))), new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()))))), new OrderedList(List.of(new ListItem(List.of(new Paragraph(List.of(new Text("щосз"))), new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("сс"))), new UnorderedList(List.of()))))), new Paragraph(List.of(new Text("yu"), new Text("w"), new Text("ghtry"), new Emphasis(List.of(new Strikeout(List.of(new Text("прф"))), new Emphasis(List.of(new Text("р"))), new Text("я"), new Text("я"))))), new Paragraph(List.of(new Text("w"), new Strong(List.of(new Text("k"), new Emphasis(List.of(new Text("н"))), new Strikeout(List.of(new Text("в"))), new Text("м"))), new Strikeout(List.of(new Text("cqqzbhtn"), new Text("i"), new Text("м"), new Text("ю"))), new Strikeout(List.of(new Strong(List.of(new Text("ш"))), new Strong(List.of(new Text("к"))), new Text("ж"), new Text("б"))))))), new ListItem(List.of(new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))))), new UnorderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("е"))), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("ед"))), new UnorderedList(List.of()))))), new OrderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()), new Paragraph(List.of(new Text("п"))))), new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("э"))), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("к"))))))), new Paragraph(List.of(new Strong(List.of(new Strong(List.of(new Text("с"))), new Text("x"), new Emphasis(List.of(new Text("йюд"))), new Text("чх"))), new Strikeout(List.of(new Strong(List.of(new Text("жюи"))), new Emphasis(List.of(new Text("и"))), new Strong(List.of(new Text("ьмт"))), new Text("йц"))), new Emphasis(List.of(new Strong(List.of(new Text("шб"))), new Strong(List.of(new Text("еф"))), new Text("ут"), new Text("шж"))), new Emphasis(List.of(new Emphasis(List.of(new Text("ну"))), new Strong(List.of(new Text("ыа"))), new Text("ьскю"), new Text("чз"))))))), new ListItem(List.of(new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()), new Paragraph(List.of(new Text("ыйгг"))))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()), new Paragraph(List.of(new Text("ф"))))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("ч"))))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))))), new Paragraph(List.of(new Strikeout(List.of(new Emphasis(List.of(new Text("э"))), new Text("amqcfdzrg"), new Emphasis(List.of(new Text("т"))), new Text("з"))), new Text("b"), new Emphasis(List.of(new Strikeout(List.of(new Text("энфны"))), new Strikeout(List.of(new Text("гщ"))), new Text("ы"), new Text("шя"))), new Text("uvpqzhn"))), new UnorderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("ящж"))), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("цлл"))))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("ъ"))))))), new Paragraph(List.of(new Strong(List.of(new Strong(List.of(new Text("ъ"))), new Strikeout(List.of(new Text("кл"))), new Strikeout(List.of(new Text("счи"))), new Text("ра"))), new Strong(List.of(new Strikeout(List.of(new Text("ь"))), new Text("zn"), new Text("ъ"), new Text("умъъзюкмц"))), new Strikeout(List.of(new Emphasis(List.of(new Text("дюз"))), new Strong(List.of(new Text("эы"))), new Text("и"), new Text("р"))), new Emphasis(List.of(new Strong(List.of(new Text("ьуес"))), new Strikeout(List.of(new Text("йгтв"))), new Text("у"), new Text("еы"))))))))), + "
        1. е

          х

            1. эш

              • цць

                • м

                      1. ю

                                  узрiаужшшcщьddwзщчагфшфяиштелн

                                          1. юцщ

                                                  1. э

                                                      1. ж

                                                        1. ыеж

                                                          ыо

                                                        2. ще

                                                          щш

                                                            1. щосз

                                                                          • сс

                                                                            yuwghtryпрфряя

                                                                            wkнвмcqqzbhtniмюшкжб

                                                                                                        • е

                                                                                                              • ед

                                                                                                                    1. п

                                                                                                                      • э

                                                                                                                              1. к

                                                                                                                              сxйюдчхжюииьмтйцшбефутшжнуыаьскючз

                                                                                                                                  1. ыйгг

                                                                                                                                      • ф

                                                                                                                                        1. ч

                                                                                                                                            эamqcfdzrgтзbэнфныгщышяuvpqzhn

                                                                                                                                                    1. ящж

                                                                                                                                                        1. цлл

                                                                                                                                                          1. ъ

                                                                                                                                                          ъклсчираьznъумъъзюкмцдюзэыирьуесйгтвуеы

                                                                                                                                                        " + ); + + checker.test( + new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("е"))))), new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("нцйцць"))), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("м"))))))), new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("ю"))))), new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("щ"))))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))))), new Paragraph(List.of(new Strikeout(List.of(new Emphasis(List.of(new Text("зр"))), new Text("i"), new Text("и"), new Text("г"), new Text("с"))), new Strong(List.of(new Strong(List.of(new Text("шмрб"))), new Strong(List.of(new Text("ь"))), new Text("з"), new Text("з"), new Text("фь"))), new Text("ddw"), new Strong(List.of(new Emphasis(List.of(new Text("щ"))), new Strong(List.of(new Text("втъп"))), new Text("ш"), new Text("ч"), new Text("фяи"))), new Strong(List.of(new Emphasis(List.of(new Text("тел"))), new Text("н"), new Text("ь"), new Text("ддзюцщ"), new Text("пт"))))), new Paragraph(List.of(new Text("n"), new Text("zi"), new Strong(List.of(new Emphasis(List.of(new Text("ж"))), new Text("t"), new Text("ыеж"), new Text("ч"), new Text("г"))), new Text("kwt"), new Strong(List.of(new Strong(List.of(new Text("э"))), new Text("нх"), new Text("уи"), new Text("о"), new Text("п"))))), new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("ж"))), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("сс"))))), new ListItem(List.of(new Paragraph(List.of(new Text("т"))))))))), new ListItem(List.of(new UnorderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("щу"))))))), new OrderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("ир"))))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("зоъ"))), new Paragraph(List.of(new Text("е"))))), new ListItem(List.of(new Paragraph(List.of(new Text("в"))), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))))), new OrderedList(List.of(new ListItem(List.of(new Paragraph(List.of(new Text("сснюпия"))), new Paragraph(List.of(new Text("щ"))))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("э"))), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()))))), new UnorderedList(List.of(new ListItem(List.of(new Paragraph(List.of(new Text("м"))), new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))))), new UnorderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("е"))), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))))))), new ListItem(List.of(new Paragraph(List.of(new Strong(List.of(new Emphasis(List.of(new Text("п"))), new Text("l"), new Text("р"), new Text("п"), new Text("уерсы"))), new Strikeout(List.of(new Strikeout(List.of(new Text("к"))), new Text("rf"), new Text("екйюд"), new Text("чх"), new Text("яжюи"))), new Emphasis(List.of(new Strikeout(List.of(new Text("кьмт"))), new Strikeout(List.of(new Text("рщюереф"))), new Text("ут"), new Text("шж"), new Text("ину"))), new Strong(List.of(new Strong(List.of(new Text("дгб"))), new Emphasis(List.of(new Text("кю"))), new Text("чз"), new Text("мв"), new Text("зыйгг"))), new Strong(List.of(new Strikeout(List.of(new Text("ш"))), new Text("ф"), new Text("я"), new Text("ч"), new Text("ме"))))), new Paragraph(List.of(new Strikeout(List.of(new Emphasis(List.of(new Text("э"))), new Text("amqcfdzrg"), new Text("кыч"), new Text("к"), new Text("я"))), new Strikeout(List.of(new Strong(List.of(new Text("нфны"))), new Strikeout(List.of(new Text("гщ"))), new Text("ы"), new Text("шя"), new Text("е"))), new Strong(List.of(new Strong(List.of(new Text("ъю"))), new Emphasis(List.of(new Text("яхе"))), new Text("б"), new Text("бц"), new Text("еящж"))), new Text("cn"), new Emphasis(List.of(new Strong(List.of(new Text("як"))), new Text("въ"), new Text("оде"), new Text("кл"), new Text("еок"))))), new Paragraph(List.of(new Strikeout(List.of(new Strong(List.of(new Text("а"))), new Strong(List.of(new Text("иь"))), new Text("аш"), new Text("ъ"), new Text("умъъзюкмц"))), new Strikeout(List.of(new Emphasis(List.of(new Text("дюз"))), new Strong(List.of(new Text("эы"))), new Text("и"), new Text("р"), new Text("зьуес"))), new Strikeout(List.of(new Strikeout(List.of(new Text("и"))), new Strong(List.of(new Text("тв"))), new Text("у"), new Text("еы"), new Text("г"))), new Text("atsui"), new Strikeout(List.of(new Text("y"), new Text("щз"), new Text("н"), new Text("е"), new Text("э"))))), new Paragraph(List.of(new Emphasis(List.of(new Text("o"), new Text("rz"), new Text("к"), new Text("к"), new Text("б"))), new Emphasis(List.of(new Strong(List.of(new Text("ьх"))), new Emphasis(List.of(new Text("ил"))), new Text("ф"), new Text("пмгр"), new Text("и"))), new Emphasis(List.of(new Text("lhovy"), new Emphasis(List.of(new Text("ъайл"))), new Text("ь"), new Text("э"), new Text("п"))), new Strikeout(List.of(new Strong(List.of(new Text("щщ"))), new Strong(List.of(new Text("х"))), new Text("б"), new Text("е"), new Text("к"))), new Emphasis(List.of(new Strikeout(List.of(new Text("чяя"))), new Text("х"), new Text("я"), new Text("р"), new Text("ю"))))), new Paragraph(List.of(new Strikeout(List.of(new Emphasis(List.of(new Text("йл"))), new Emphasis(List.of(new Text("змл"))), new Text("б"), new Text("аж"), new Text("ъ"))), new Strong(List.of(new Strong(List.of(new Text("энян"))), new Emphasis(List.of(new Text("ю"))), new Text("п"), new Text("ымы"), new Text("ешьи"))), new Emphasis(List.of(new Strong(List.of(new Text("к"))), new Strikeout(List.of(new Text("яэ"))), new Text("п"), new Text("юзщ"), new Text("я"))), new Text("w"), new Emphasis(List.of(new Text("se"), new Text("о"), new Text("ъязе"), new Text("гзко"), new Text("ъ"))))))), new ListItem(List.of(new OrderedList(List.of(new ListItem(List.of(new Paragraph(List.of(new Text("ч"))), new Paragraph(List.of(new Text("пз"))))), new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("й"))))), new ListItem(List.of(new Paragraph(List.of(new Text("лчж"))), new Paragraph(List.of(new Text("чв"))))), new ListItem(List.of(new Paragraph(List.of(new Text("с"))), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()))))), new UnorderedList(List.of(new ListItem(List.of(new Paragraph(List.of(new Text("ь"))), new Paragraph(List.of(new Text("ъ"))))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("вп"))), new Paragraph(List.of(new Text("р"))))), new ListItem(List.of(new OrderedList(List.of()))))), new Paragraph(List.of(new Text("ds"), new Emphasis(List.of(new Strikeout(List.of(new Text("дйгып"))), new Emphasis(List.of(new Text("и"))), new Text("сэ"), new Text("е"), new Text("юо"))), new Emphasis(List.of(new Strikeout(List.of(new Text("бвщ"))), new Text("d"), new Text("ъ"), new Text("ит"), new Text("бщ"))), new Emphasis(List.of(new Text("w"), new Strikeout(List.of(new Text("гсщ"))), new Text("ъ"), new Text("срцч"), new Text("хе"))), new Text("m"))), new OrderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("е"))), new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))))), new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("оото"))), new OrderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))))))), new ListItem(List.of(new Paragraph(List.of(new Emphasis(List.of(new Emphasis(List.of(new Text("я"))), new Strong(List.of(new Text("сшъ"))), new Text("лм"), new Text("ы"), new Text("рц"))), new Emphasis(List.of(new Strikeout(List.of(new Text("я"))), new Strikeout(List.of(new Text("ъ"))), new Text("п"), new Text("дхдэ"), new Text("щэ"))), new Emphasis(List.of(new Text("dtt"), new Emphasis(List.of(new Text("дрм"))), new Text("в"), new Text("яешц"), new Text("йшй"))), new Strong(List.of(new Strong(List.of(new Text("мив"))), new Text("u"), new Text("у"), new Text("к"), new Text("б"))), new Strikeout(List.of(new Text("c"), new Text("э"), new Text("м"), new Text("п"), new Text("о"))))), new UnorderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("х"))), new Paragraph(List.of(new Text("й"))))), new ListItem(List.of(new Paragraph(List.of(new Text("эя"))), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("ф"))))), new ListItem(List.of(new OrderedList(List.of()))))), new OrderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new Paragraph(List.of(new Text("щ"))))), new ListItem(List.of(new Paragraph(List.of(new Text("чи"))), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("к"))), new OrderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()), new OrderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("ф"))))))), new OrderedList(List.of(new ListItem(List.of(new OrderedList(List.of()), new UnorderedList(List.of()))), new ListItem(List.of(new Paragraph(List.of(new Text("м"))), new Paragraph(List.of(new Text("щцс"))))), new ListItem(List.of(new Paragraph(List.of(new Text("вус"))), new Paragraph(List.of(new Text("я"))))), new ListItem(List.of(new Paragraph(List.of(new Text("кр"))))), new ListItem(List.of(new UnorderedList(List.of()))))), new UnorderedList(List.of(new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("я"))))), new ListItem(List.of(new UnorderedList(List.of()), new Paragraph(List.of(new Text("гр"))))), new ListItem(List.of(new Paragraph(List.of(new Text("ж"))), new UnorderedList(List.of()))), new ListItem(List.of(new UnorderedList(List.of()))), new ListItem(List.of(new OrderedList(List.of()))))))))), + "
                                                                                                                                                            1. е

                                                                                                                                                                    1. нцйцць

                                                                                                                                                                      1. м

                                                                                                                                                                            1. ю

                                                                                                                                                                              • щ

                                                                                                                                                                                    зрiигсшмрбьззфьddwщвтъпшчфяителньддзюцщпт

                                                                                                                                                                                    nziжtыежчгkwtэнхуиоп

                                                                                                                                                                                        1. ж

                                                                                                                                                                                              • сс

                                                                                                                                                                                              • т

                                                                                                                                                                                                              • щу

                                                                                                                                                                                                                1. ир

                                                                                                                                                                                                                    1. зоъ

                                                                                                                                                                                                                      е

                                                                                                                                                                                                                    2. в

                                                                                                                                                                                                                        1. сснюпия

                                                                                                                                                                                                                          щ

                                                                                                                                                                                                                            • э

                                                                                                                                                                                                                                    • м

                                                                                                                                                                                                                                                            1. е

                                                                                                                                                                                                                                                                1. пlрпуерсыкrfекйюдчхяжюикьмтрщюерефутшжинудгбкючзмвзыйггшфячме

                                                                                                                                                                                                                                                                  эamqcfdzrgкычкянфныгщышяеъюяхеббцеящжcnяквъодеклеок

                                                                                                                                                                                                                                                                  аиьашъумъъзюкмцдюзэыирзьуеситвуеыгatsuiyщзнеэ

                                                                                                                                                                                                                                                                  orzккбьхилфпмгриlhovyъайльэпщщхбекчяяхярю

                                                                                                                                                                                                                                                                  йлзмлбажъэнянюпымыешьикяэпюзщяwseоъязегзкоъ

                                                                                                                                                                                                                                                                  1. ч

                                                                                                                                                                                                                                                                    пз

                                                                                                                                                                                                                                                                    1. й

                                                                                                                                                                                                                                                                    2. лчж

                                                                                                                                                                                                                                                                      чв

                                                                                                                                                                                                                                                                    3. с

                                                                                                                                                                                                                                                                        • ь

                                                                                                                                                                                                                                                                          ъ

                                                                                                                                                                                                                                                                                • вп

                                                                                                                                                                                                                                                                                  р

                                                                                                                                                                                                                                                                                  dsдйгыписэеюобвщdъитбщwгсщъсрцчхеm

                                                                                                                                                                                                                                                                                              • е

                                                                                                                                                                                                                                                                                                      • оото

                                                                                                                                                                                                                                                                                                              • ясшълмырцяъпдхдэщэdttдрмвяешцйшймивuукбcэмпо

                                                                                                                                                                                                                                                                                                                    1. х

                                                                                                                                                                                                                                                                                                                      й

                                                                                                                                                                                                                                                                                                                    2. эя

                                                                                                                                                                                                                                                                                                                        • ф

                                                                                                                                                                                                                                                                                                                            1. щ

                                                                                                                                                                                                                                                                                                                            2. чи

                                                                                                                                                                                                                                                                                                                              1. к

                                                                                                                                                                                                                                                                                                                                    1. ф

                                                                                                                                                                                                                                                                                                                                        • м

                                                                                                                                                                                                                                                                                                                                          щцс

                                                                                                                                                                                                                                                                                                                                        • вус

                                                                                                                                                                                                                                                                                                                                          я

                                                                                                                                                                                                                                                                                                                                        • кр

                                                                                                                                                                                                                                                                                                                                            • я

                                                                                                                                                                                                                                                                                                                                              • гр

                                                                                                                                                                                                                                                                                                                                              • ж

                                                                                                                                                                                                                                                                                                                                                  " + ); + + checkTypes(); + } + + private static OrderedList ol(final ListItem... items) { + return new OrderedList(List.of(items)); + } + + private static String ol(final String... items) { + return list("ol", items); + } + + private static UnorderedList ul(final ListItem... items) { + return new UnorderedList(List.of(items)); + } + + private static String ul(final String... items) { + return list("ul", items); + } + + private static String list(final String type, final String[] items) { + return "<" + type + ">" + Stream.of(items).map(item -> "
                                                                                                                                                                                                                                                                                                                                                • " + item + "
                                                                                                                                                                                                                                                                                                                                                • ").collect(Collectors.joining()) + ""; + } + + private static Class loadClass(final String name) { + try { + return Class.forName(name); + } catch (final ClassNotFoundException e) { + throw Asserts.error("Cannot find class %s: %s", name, e); + } + } + + private static Map> loadClasses(final String... names) { + return Arrays.stream(names) + .collect(Collectors.toUnmodifiableMap(Function.identity(), name -> loadClass("markup." + name))); + } + + private static void checkTypes() { + final Map> classes = loadClasses("Text", "Emphasis", "Strikeout", "Strong", "Paragraph", "OrderedList", "UnorderedList", "ListItem"); + final String[] inlineClasses = {"Text", "Emphasis", "Strikeout", "Strong"}; + + checkConstructor(classes, "OrderedList", "ListItem"); + checkConstructor(classes, "UnorderedList", "ListItem"); + checkConstructor(classes, "ListItem", "OrderedList", "UnorderedList", "Paragraph"); + Stream.of("Paragraph", "Emphasis", "Strong", "Strikeout") + .forEach(parent -> checkConstructor(classes, parent, inlineClasses)); + } + + private static void checkConstructor(final Map> classes, final String parent, final String... children) { + new TypeChecker(classes, parent, children).checkConstructor(); + } + + private static class TypeChecker { + private final Map> classes; + private final Set> children; + private final Class parent; + + public TypeChecker(final Map> classes, final String parent, final String[] children) { + this.classes = classes; + this.children = Arrays.stream(children).map(classes::get).collect(Collectors.toUnmodifiableSet()); + this.parent = Objects.requireNonNull(classes.get(parent)); + } + + private void checkClassType(final Class classType) { + final Predicate> isAssignableFrom = classType::isAssignableFrom; + checkType(parent, Predicate.not(isAssignableFrom), "not ", children.stream()); + checkType(parent, isAssignableFrom, "", classes.values().stream().filter(Predicate.not(children::contains))); + } + + private static void checkType(final Class parent, final Predicate> predicate, final String not, final Stream> children) { + children.filter(predicate).findAny().ifPresent(child -> { + throw Asserts.error("%s is %scompatible with child of type %s", parent, not, child); + }); + } + + @SuppressWarnings("ChainOfInstanceofChecks") + private void checkParametrizedType(final ParameterizedType type) { + final Type actualType = type.getActualTypeArguments()[0]; + if (actualType instanceof Class) { + checkClassType((Class) actualType); + } else if (actualType instanceof WildcardType) { + for (final Type boundType : ((WildcardType) actualType).getUpperBounds()) { + if (boundType instanceof Class) { + checkClassType((Class) boundType); + } else { + throw Asserts.error("Unsupported wildcard bound type in %s(List<...>): %s", parent, boundType); + } + } + } else { + throw Asserts.error("Unsupported type argument type in %s(List<...>): %s", parent, actualType); + } + } + + @SuppressWarnings("ChainOfInstanceofChecks") + private void checkConstructor() { + try { + final Type argType = parent.getConstructor(List.class).getGenericParameterTypes()[0]; + if (argType instanceof ParameterizedType) { + checkParametrizedType((ParameterizedType) argType); + } else if (argType instanceof Class) { + throw Asserts.error("Raw List type in %s(List)", parent.getName()); + } else { + throw Asserts.error("Unsupported argument type in %s(List<...>): %s", parent.getName(), argType); + } + } catch (final NoSuchMethodException e) { + throw Asserts.error("Missing %s(List<...>) constructor: %s", parent.getName(), e); + } + } + } + + public static void main(final String... args) { + MarkupTest.main(args); + SELECTOR.main(args); + } +} diff --git a/java/markup/MarkupTest.java b/java/markup/MarkupTest.java new file mode 100644 index 0000000..9574b13 --- /dev/null +++ b/java/markup/MarkupTest.java @@ -0,0 +1,97 @@ +package markup; + +import base.Selector; +import base.TestCounter; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class MarkupTest { + private static final Consumer MARKDOWN = MarkupTest.variant( + "Markdown", Map.of( + "&[", "", "&]", "", + "<", "", ">", "" + ) + ); + + private static final Consumer HTML = MarkupTest.variant( + "Html", Map.of( + "&[", "

                                                                                                                                                                                                                                                                                                                                                  ", "&]", "

                                                                                                                                                                                                                                                                                                                                                  ", + "*<", "", "*>", "", + "__<", "", "__>", "", + "~<", "", "~>", "" + ) + ); + + public static final Selector SELECTOR = new Selector(MarkupTest.class) + .variant("Base", MARKDOWN) + .variant("3637", MARKDOWN) + .variant("3839", MARKDOWN) + .variant("3435", HTML) + .variant("3233", HTML) + .variant("4142", MARKDOWN) + .variant("4749", MARKDOWN) + + ; + + public static Consumer variant(final String name, final Map mapping) { + return MarkupTester.variant(MarkupTest::test, name, mapping); + } + + private MarkupTest() { + } + + public static void test(final MarkupTester.Checker checker) { + test(checker, new Paragraph(List.of(new Text("Hello"))), "Hello"); + test(checker, new Paragraph(List.of(new Emphasis(List.of(new Text("Hello"))))), "*"); + test(checker, new Paragraph(List.of(new Strong(List.of(new Text("Hello"))))), "__"); + test(checker, new Paragraph(List.of(new Strikeout(List.of(new Text("Hello"))))), "~"); + + final Paragraph paragraph = new Paragraph(List.of( + new Strong(List.of( + new Text("1"), + new Strikeout(List.of( + new Text("2"), + new Emphasis(List.of( + new Text("3"), + new Text("4") + )), + new Text("5") + )), + new Text("6") + )) + )); + test(checker, paragraph, "__<1~<2*<34*>5~>6__>"); + test( + checker, + new Paragraph(List.of(new Strong(List.of( + new Text("sdq"), + new Strikeout(List.of(new Emphasis(List.of(new Text("r"))), new Text("vavc"))), + new Text("zg") + )))), + "__vavc~>zg__>" + ); + test( + checker, + new Paragraph(List.of(new Strikeout(List.of(new Strong(List.of(new Strikeout(List.of(new Text("е"), new Text("е"), new Text("г"))), new Text("ftje"), new Strong(List.of(new Text("йцць"), new Text("р"))))), new Strong(List.of(new Strikeout(List.of(new Text("д"), new Text("б"), new Text("е"))), new Strong(List.of(new Text("лъ"), new Text("шщ"))), new Strong(List.of(new Text("б"), new Text("еи"))))), new Emphasis(List.of(new Emphasis(List.of(new Text("м"), new Text("к"))), new Emphasis(List.of(new Text("уаужш"), new Text("ш"))), new Strong(List.of(new Text("рб"), new Text("щ"))))))), new Text("a"), new Strikeout(List.of(new Text("no"), new Text("ddw"), new Strong(List.of(new Emphasis(List.of(new Text("щ"), new Text("ча"))), new Emphasis(List.of(new Text("ъп"), new Text("ш"))), new Text("psk"))))))), + "~<__<~<еег~>ftje__<йццьр__>__>__<~<дбе~>__<лъшщ__>__<беи__>__>*<*<мк*>*<уаужшш*>__<рбщ__>*>~>a~*<ъпш*>psk__>~>" + ); + test( + checker, + new Paragraph(List.of(new Strikeout(List.of(new Strong(List.of(new Strikeout(List.of(new Emphasis(List.of(new Text("об"))), new Strikeout(List.of(new Text("ц"))), new Text("зснцйцць"), new Text("р"), new Text("а"))), new Strikeout(List.of(new Strikeout(List.of(new Text("б"))), new Strikeout(List.of(new Text("ялъ"))), new Text("шщ"), new Text("ф"), new Text("м"))), new Emphasis(List.of(new Emphasis(List.of(new Text("узр"))), new Text("i"), new Text("и"), new Text("г"), new Text("с"))), new Strong(List.of(new Strong(List.of(new Text("шмрб"))), new Strong(List.of(new Text("ь"))), new Text("з"), new Text("з"), new Text("фь"))), new Text("ddw"))), new Strong(List.of(new Emphasis(List.of(new Emphasis(List.of(new Text("ввтъп"))), new Strong(List.of(new Text("ш"))), new Text("хте"), new Text("чюе"), new Text("х"))), new Text("g"), new Strikeout(List.of(new Strikeout(List.of(new Text("ддзюцщ"))), new Strong(List.of(new Text("к"))), new Text("йщ"), new Text("э"), new Text("то"))), new Strong(List.of(new Emphasis(List.of(new Text("ж"))), new Text("t"), new Text("ыеж"), new Text("ч"), new Text("г"))), new Text("kwt"))), new Strong(List.of(new Strong(List.of(new Emphasis(List.of(new Text("ш"))), new Strong(List.of(new Text("х"))), new Text("уи"), new Text("о"), new Text("п"))), new Emphasis(List.of(new Text("zn"), new Strong(List.of(new Text("нш"))), new Text("диуьг"), new Text("вй"), new Text("шш"))), new Strong(List.of(new Emphasis(List.of(new Text("ьмша"))), new Emphasis(List.of(new Text("у"))), new Text("в"), new Text("у"), new Text("ир"))), new Emphasis(List.of(new Strikeout(List.of(new Text("я"))), new Strikeout(List.of(new Text("зоъ"))), new Text("эн"), new Text("ъ"), new Text("ьо"))), new Text("cqqzbhtn"))), new Text("i"), new Strong(List.of(new Text("i"), new Strikeout(List.of(new Strong(List.of(new Text("ш"))), new Strong(List.of(new Text("к"))), new Text("ж"), new Text("б"), new Text("ащ"))), new Strikeout(List.of(new Strikeout(List.of(new Text("пян"))), new Emphasis(List.of(new Text("ц"))), new Text("ю"), new Text("дрк"), new Text("лщ"))), new Strong(List.of(new Text("xywa"), new Text("ряэк"), new Text("п"), new Text("э"), new Text("т"))), new Strong(List.of(new Strikeout(List.of(new Text("е"))), new Text("чб"), new Text("зс"), new Text("екйюд"), new Text("чх"))))))), new Strikeout(List.of(new Strong(List.of(new Strong(List.of(new Strong(List.of(new Text("юи"))), new Emphasis(List.of(new Text("и"))), new Text("п"), new Text("вх"), new Text("ф"))), new Strong(List.of(new Strong(List.of(new Text("щюереф"))), new Text("otvic"), new Text("ж"), new Text("уыа"), new Text("ьскю"))), new Text("x"), new Strikeout(List.of(new Emphasis(List.of(new Text("ж"))), new Strikeout(List.of(new Text("зыйгг"))), new Text("о"), new Text("ш"), new Text("ф"))), new Text("zf"))), new Emphasis(List.of(new Text("a"), new Strikeout(List.of(new Emphasis(List.of(new Text("э"))), new Text("amqcfdzrg"), new Text("кыч"), new Text("к"), new Text("я"))), new Strikeout(List.of(new Strong(List.of(new Text("нфны"))), new Strikeout(List.of(new Text("гщ"))), new Text("ы"), new Text("шя"), new Text("е"))), new Strong(List.of(new Strong(List.of(new Text("ъю"))), new Emphasis(List.of(new Text("яхе"))), new Text("б"), new Text("бц"), new Text("еящж"))), new Text("cn"))), new Emphasis(List.of(new Strong(List.of(new Strong(List.of(new Text("л"))), new Text("wl"), new Text("оде"), new Text("кл"), new Text("еок"))), new Strikeout(List.of(new Strikeout(List.of(new Text("яяиь"))), new Strong(List.of(new Text("ик"))), new Text("юью"), new Text("ь"), new Text("э"))), new Emphasis(List.of(new Strikeout(List.of(new Text("жп"))), new Emphasis(List.of(new Text("ц"))), new Text("ндюз"), new Text("ч"), new Text("н"))), new Text("r"), new Strikeout(List.of(new Strikeout(List.of(new Text("зьуес"))), new Text("к"), new Text("и"), new Text("к"), new Text("й"))))), new Strikeout(List.of(new Emphasis(List.of(new Strikeout(List.of(new Text("еы"))), new Emphasis(List.of(new Text("б"))), new Text("сйсыр"), new Text("я"), new Text("т"))), new Emphasis(List.of(new Emphasis(List.of(new Text("з"))), new Strong(List.of(new Text("ахыы"))), new Text("х"), new Text("м"), new Text("п"))), new Strikeout(List.of(new Text("b"), new Text("o"), new Text("шьх"), new Text("йз"), new Text("ж"))), new Text("udlh"), new Strikeout(List.of(new Strikeout(List.of(new Text("п"))), new Text("хъфоз"), new Text("е"), new Text("ыф"), new Text("ю"))))), new Text("z"))), new Text("hy"), new Strong(List.of(new Text("tyv"), new Text("x"), new Strikeout(List.of(new Text("vzb"), new Strong(List.of(new Text("oi"), new Text("r"), new Text("ю"), new Text("с"), new Text("еппзмл"))), new Text("r"), new Emphasis(List.of(new Strikeout(List.of(new Text("игс"))), new Emphasis(List.of(new Text("нян"))), new Text("ю"), new Text("с"), new Text("цлъ"))), new Text("rptq"))), new Emphasis(List.of(new Strong(List.of(new Text("u"), new Strong(List.of(new Text("кще"))), new Text("пхте"), new Text("у"), new Text("з"))), new Text("zbmflu"), new Strikeout(List.of(new Strong(List.of(new Text("л"))), new Emphasis(List.of(new Text("ко"))), new Text("ъ"), new Text("щ"), new Text("жч"))), new Strong(List.of(new Strong(List.of(new Text("ж"))), new Strikeout(List.of(new Text("еъ"))), new Text("в"), new Text("ф"), new Text("йб"))), new Text("kvuf"))), new Strikeout(List.of(new Text("azn"), new Strikeout(List.of(new Strong(List.of(new Text("ъ"))), new Emphasis(List.of(new Text("ре"))), new Text("йч"), new Text("н"), new Text("ир"))), new Emphasis(List.of(new Emphasis(List.of(new Text("с"))), new Strong(List.of(new Text("щ"))), new Text("ъсбчиюзи"), new Text("сэ"), new Text("е"))), new Strikeout(List.of(new Emphasis(List.of(new Text("о"))), new Text("г"), new Text("бвщ"), new Text("пр"), new Text("йвъч"))), new Text("c"))))), new Strong(List.of(new Strikeout(List.of(new Strikeout(List.of(new Emphasis(List.of(new Text("жбфц"))), new Strong(List.of(new Text("рцч"))), new Text("хе"), new Text("ж"), new Text("ы"))), new Strikeout(List.of(new Emphasis(List.of(new Text("я"))), new Emphasis(List.of(new Text("мн"))), new Text("яе"), new Text("е"), new Text("дхпг"))), new Emphasis(List.of(new Emphasis(List.of(new Text("нй"))), new Text("gf"), new Text("и"), new Text("хю"), new Text("ця"))), new Strong(List.of(new Emphasis(List.of(new Text("о"))), new Emphasis(List.of(new Text("ъ"))), new Text("лм"), new Text("ы"), new Text("рц"))), new Emphasis(List.of(new Strikeout(List.of(new Text("я"))), new Text("ыл"), new Text("г"), new Text("я"), new Text("эй"))))), new Text("qi"), new Emphasis(List.of(new Text("dtt"), new Emphasis(List.of(new Strong(List.of(new Text("пв"))), new Text("i"), new Text("яешц"), new Text("йшй"), new Text("щмив"))), new Text("u"), new Text("d"), new Strikeout(List.of(new Strikeout(List.of(new Text("о"))), new Text("иов"), new Text("к"), new Text("кои"), new Text("яс"))))), new Strikeout(List.of(new Emphasis(List.of(new Text("j"), new Strong(List.of(new Text("эя"))), new Text("шыф"), new Text("дрн"), new Text("щ"))), new Text("j"), new Strong(List.of(new Emphasis(List.of(new Text("ю"))), new Strikeout(List.of(new Text("чцин"))), new Text("сф"), new Text("з"), new Text("юэи"))), new Emphasis(List.of(new Emphasis(List.of(new Text("цс"))), new Text("ювус"), new Text("ъ"), new Text("щэны"), new Text("б"))), new Emphasis(List.of(new Text("cbogf"), new Text("э"), new Text("ж"), new Text("ш"), new Text("м"))))), new Strikeout(List.of(new Strong(List.of(new Strong(List.of(new Text("ф"))), new Text("w"), new Text("цеъ"), new Text("н"), new Text("ем"))), new Strikeout(List.of(new Strikeout(List.of(new Text("л"))), new Strong(List.of(new Text("э"))), new Text("лд"), new Text("эд"), new Text("л"))), new Emphasis(List.of(new Emphasis(List.of(new Text("уг"))), new Strikeout(List.of(new Text("зп"))), new Text("юб"), new Text("сгы"), new Text("шю"))), new Strikeout(List.of(new Emphasis(List.of(new Text("рйей"))), new Text("с"), new Text("зюй"), new Text("р"), new Text("в"))), new Emphasis(List.of(new Text("p"), new Text("у"), new Text("на"), new Text("б"), new Text("х"))))))))), + "~<__<~<*<об*>~<ц~>зснцйццьра~>~<~<б~>~<ялъ~>шщфм~>*<*<узр*>iигс*>__<__<шмрб__>__<ь__>ззфь__>ddw__>__<*<*<ввтъп*>__<ш__>хтечюех*>g~<~<ддзюцщ~>__<к__>йщэто~>__<*<ж*>tыежчг__>kwt__>__<__<*<ш*>__<х__>уиоп__>*диуьгвйшш*>__<*<ьмша*>*<у*>вуир__>*<~<я~>~<зоъ~>энъьо*>cqqzbhtn__>i____<к__>жбащ~>~<~<пян~>*<ц*>юдрклщ~>____<~<е~>чбзсекйюдчх__>__>~>~<__<__<__<юи__>*<и*>пвхф__>__<__<щюереф__>otvicжуыаьскю__>x~<*<ж*>~<зыйгг~>ошф~>zf__>*amqcfdzrgкычкя~>~<__<нфны__>~<гщ~>ышяе~>__<__<ъю__>*<яхе*>ббцеящж__>cn*>*<__<__<л__>wlодеклеок__>~<~<яяиь~>__<ик__>юьюьэ~>*<~<жп~>*<ц*>ндюзчн*>r~<~<зьуес~>кикй~>*>~<*<~<еы~>*<б*>сйсырят*>*<*<з*>__<ахыы__>хмп*>~udlh~<~<п~>хъфозеыфю~>~>z~>hy__r*<~<игс~>*<нян*>юсцлъ*>rptq~>*<__пхтеуз__>zbmflu~<__<л__>*<ко*>ъщжч~>__<__<ж__>~<еъ~>вфйб__>kvuf*>~*<ре*>йчнир~>*<*<с*>__<щ__>ъсбчиюзисэе*>~<*<о*>гбвщпрйвъч~>c~>__>__<~<~<*<жбфц*>__<рцч__>хежы~>~<*<я*>*<мн*>яеедхпг~>*<*<нй*>gfихюця*>__<*<о*>*<ъ*>лмырц__>*<~<я~>ылгяэй*>~>qi*iяешцйшйщмив*>ud~<~<о~>иовккоияс~>*>~<*шыфдрнщ*>j__<*<ю*>~<чцин~>сфзюэи__>*<*<цс*>ювусъщэныб*>*~>~<__<__<ф__>wцеънем__>~<~<л~>__<э__>лдэдл~>*<*<уг*>~<зп~>юбсгышю*>~<*<рйей*>сзюйрв~>*~>__>" + ); + } + + private static void test(final MarkupTester.Checker checker, final Paragraph paragraph, final String template) { + checker.test(paragraph, String.format("&[%s&]", template)); + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/markup/MarkupTester.java b/java/markup/MarkupTester.java new file mode 100644 index 0000000..4d93d97 --- /dev/null +++ b/java/markup/MarkupTester.java @@ -0,0 +1,71 @@ +package markup; + +import base.Asserts; +import base.BaseChecker; +import base.TestCounter; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class MarkupTester { + private final Map mapping; + private final String toString; + + private MarkupTester(final Map mapping, final String toString) { + this.mapping = mapping; + this.toString = toString; + } + + public static Consumer variant(final Consumer checker, final String name, final Map mapping) { + return counter -> test(checker).accept(new MarkupTester(mapping, "to" + name), counter); + } + + public static BiConsumer test(final Consumer tester) { + return (checker, counter) -> tester.accept(checker.new Checker(counter)); + } + + @Override + public String toString() { + return toString; + } + + public class Checker extends BaseChecker { + public Checker(final TestCounter counter) { + super(counter); + } + + private MethodHandle findMethod(final T value) { + try { + return MethodHandles.publicLookup().findVirtual(value.getClass(), toString, MethodType.methodType(void.class, StringBuilder.class)); + } catch (final NoSuchMethodException | IllegalAccessException e) { + throw Asserts.error("Cannot find method 'void %s(StringBuilder)' for %s", toString, value.getClass()); + } + } + + public void test(final T value, String expectedTemplate) { + final MethodHandle method = findMethod(value); + for (final Map.Entry entry : mapping.entrySet()) { + expectedTemplate = expectedTemplate.replace(entry.getKey(), entry.getValue()); + } + + final String expected = expectedTemplate; + counter.println("Test " + counter.getTestNo()); + counter.test(() -> { + final StringBuilder sb = new StringBuilder(); + try { + method.invoke(value, sb); + } catch (final Throwable e) { + throw Asserts.error("%s(StringBuilder) for %s thrown exception: %s", toString, value.getClass(), e); + } + Asserts.assertEquals("Result", expected, sb.toString()); + }); + } + } +} diff --git a/java/markup/package-info.java b/java/markup/package-info.java new file mode 100644 index 0000000..fdef1bc --- /dev/null +++ b/java/markup/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Markup homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package markup; \ No newline at end of file diff --git a/java/md2html/Md2HtmlTest.java b/java/md2html/Md2HtmlTest.java new file mode 100644 index 0000000..78b186f --- /dev/null +++ b/java/md2html/Md2HtmlTest.java @@ -0,0 +1,71 @@ +package md2html; + +import base.Selector; + +import java.util.function.Consumer; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Md2HtmlTest { + // === 3637 + private static final Consumer INS = tester -> tester + .test("<<вставка>>", "

                                                                                                                                                                                                                                                                                                                                                  вставка

                                                                                                                                                                                                                                                                                                                                                  ") + .test("Это <<вставка>>, вложенная в текст", "

                                                                                                                                                                                                                                                                                                                                                  Это вставка, вложенная в текст

                                                                                                                                                                                                                                                                                                                                                  ") + .spoiled("Это не <<вставка>>", "

                                                                                                                                                                                                                                                                                                                                                  Это не <<вставка>>

                                                                                                                                                                                                                                                                                                                                                  ", "<", ">") + .spoiled("Это не <<вставка>> 2", "

                                                                                                                                                                                                                                                                                                                                                  Это не <<вставка>> 2

                                                                                                                                                                                                                                                                                                                                                  ", "<", ">") + .addElement("ins", "<<", ">>"); + private static final Consumer DEL = tester -> tester + .test("}}удаление{{", "

                                                                                                                                                                                                                                                                                                                                                  удаление

                                                                                                                                                                                                                                                                                                                                                  ") + .test("Это }}удаление{{, вложенное в текст", "

                                                                                                                                                                                                                                                                                                                                                  Это удаление, вложенное в текст

                                                                                                                                                                                                                                                                                                                                                  ") + .spoiled("Это не }}удаление{{", "

                                                                                                                                                                                                                                                                                                                                                  Это не }}удаление{{

                                                                                                                                                                                                                                                                                                                                                  ", "{") + .spoiled("Это не }}удаление{{ 2", "

                                                                                                                                                                                                                                                                                                                                                  Это не }}удаление{{ 2

                                                                                                                                                                                                                                                                                                                                                  ", "{") + .addElement("del", "}}", "{{"); + + // === 3839 + private static final Consumer PRE = tester -> tester + .test("```код __без__ форматирования```", "

                                                                                                                                                                                                                                                                                                                                                  код __без__ форматирования

                                                                                                                                                                                                                                                                                                                                                  ") + .test( + "Это не `\\``код __без__ форматирования``\\`", + "

                                                                                                                                                                                                                                                                                                                                                  Это не `код без форматирования`

                                                                                                                                                                                                                                                                                                                                                  " + ) + .addElement("pre", "```", (checker, markup, input, output) -> { + final String contentS = checker.generateInput(markup).replace("`", ""); + + input.append("```").append(contentS).append("```"); + output.append("
                                                                                                                                                                                                                                                                                                                                                  ").append(contentS.replace("<", "<").replace(">", "")).append("
                                                                                                                                                                                                                                                                                                                                                  "); + }); + + // === 3435 + private static final Consumer SAMP = tester -> tester + .test("!!пример!!", "

                                                                                                                                                                                                                                                                                                                                                  пример

                                                                                                                                                                                                                                                                                                                                                  ") + .test("Это !!пример!!, вложенный в текст", "

                                                                                                                                                                                                                                                                                                                                                  Это пример, вложенный в текст

                                                                                                                                                                                                                                                                                                                                                  ") + .spoiled("Это не !!пример!!", "

                                                                                                                                                                                                                                                                                                                                                  Это не !!пример!!

                                                                                                                                                                                                                                                                                                                                                  ", "!") + .spoiled("Это не !!пример!! 2", "

                                                                                                                                                                                                                                                                                                                                                  Это не !!пример!! 2

                                                                                                                                                                                                                                                                                                                                                  ", "!") + .addElement("samp", "!!"); + + // === 3233 + private static final Consumer VAR = tester -> tester + .test("%переменная%", "

                                                                                                                                                                                                                                                                                                                                                  переменная

                                                                                                                                                                                                                                                                                                                                                  ") + .test("Это %переменная%, вложенная в текст", "

                                                                                                                                                                                                                                                                                                                                                  Это переменная, вложенная в текст

                                                                                                                                                                                                                                                                                                                                                  ") + .spoiled("Это не %переменная%", "

                                                                                                                                                                                                                                                                                                                                                  Это не %переменная%

                                                                                                                                                                                                                                                                                                                                                  ", "%") + .spoiled("Это не %переменная% 2", "

                                                                                                                                                                                                                                                                                                                                                  Это не %переменная% 2

                                                                                                                                                                                                                                                                                                                                                  ", "%") + .addElement("var", "%"); + + // === Common + + public static final Selector SELECTOR = Selector.composite(Md2HtmlTest.class, c -> new Md2HtmlTester(), Md2HtmlTester::test) + .variant("Base") + .variant("3637", INS, DEL) + .variant("3839", PRE) + .variant("3435", SAMP) + .variant("3233", VAR) + .selector(); + + private Md2HtmlTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/md2html/Md2HtmlTester.java b/java/md2html/Md2HtmlTester.java new file mode 100644 index 0000000..9538e81 --- /dev/null +++ b/java/md2html/Md2HtmlTester.java @@ -0,0 +1,355 @@ +package md2html; + +import base.*; + +import java.util.*; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Md2HtmlTester { + private static final Map ESCAPES = Map.of("<", "<", ">", ">"); + + private final Map elements = new HashMap<>(); + private final Map> tags = new LinkedHashMap<>(); + private final List> tests = new ArrayList<>(); + + public Md2HtmlTester() { + addElement("em", "*"); + addElement("em", "_"); + addElement("strong", "**"); + addElement("strong", "__"); + addElement("s", "--"); + addElement("code", "`"); + + test( + "# Заголовок первого уровня\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Заголовок первого уровня

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "## Второго\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Второго

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "### Третьего ## уровня\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Третьего ## уровня

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "#### Четвертого\n# Все еще четвертого\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Четвертого\n# Все еще четвертого

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "Этот абзац текста\nсодержит две строки.", + "

                                                                                                                                                                                                                                                                                                                                                  Этот абзац текста\nсодержит две строки.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + " # Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с `#`.\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  # Может показаться, что это заголовок.\nНо нет, это абзац, начинающийся с #.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "#И это не заголовок.\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  #И это не заголовок.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "###### Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)\n\n", + "
                                                                                                                                                                                                                                                                                                                                                  Заголовки могут быть многострочными\n(и с пропуском заголовков предыдущих уровней)
                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "Мы все любим *выделять* текст _разными_ способами.\n**Сильное выделение**, используется гораздо реже,\nно __почему бы и нет__?\nНемного --зачеркивания-- еще никому не вредило.\nКод представляется элементом `code`.\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Мы все любим выделять текст разными способами.\nСильное выделение, используется гораздо реже,\nно почему бы и нет?\nНемного зачеркивания еще никому не вредило.\nКод представляется элементом code.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "Обратите внимание, как экранируются специальные\nHTML-символы, такие как `<`, `>` и `&`.\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Обратите внимание, как экранируются специальные\nHTML-символы, такие как <, > и &.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "Экранирование должно работать во всех местах: <>&.\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Экранирование должно работать во всех местах: <>&.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "Знаете ли вы, что в Markdown, одиночные * и _\nне означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: \\*.", + "

                                                                                                                                                                                                                                                                                                                                                  Знаете ли вы, что в Markdown, одиночные * и _\nне означают выделение?\nОни так же могут быть заэкранированы\nпри помощи обратного слэша: *.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "\n\n\nЛишние пустые строки должны игнорироваться.\n\n\n\n", + "

                                                                                                                                                                                                                                                                                                                                                  Лишние пустые строки должны игнорироваться.

                                                                                                                                                                                                                                                                                                                                                  " + ); + test( + "Любите ли вы *вложенные __выделения__* так,\nкак __--люблю--__ их я?", + "

                                                                                                                                                                                                                                                                                                                                                  Любите ли вы вложенные выделения так,\nкак люблю их я?

                                                                                                                                                                                                                                                                                                                                                  " + ); + + test( + """ + # Заголовок первого уровня + + ## Второго + + ### Третьего ## уровня + + #### Четвертого + # Все еще четвертого + + Этот абзац текста + содержит две строки. + + # Может показаться, что это заголовок. + Но нет, это абзац, начинающийся с `#`. + + #И это не заголовок. + + ###### Заголовки могут быть многострочными + (и с пропуском заголовков предыдущих уровней) + + Мы все любим *выделять* текст _разными_ способами. + **Сильное выделение**, используется гораздо реже, + но __почему бы и нет__? + Немного --зачеркивания-- еще никому не вредило. + Код представляется элементом `code`. + + Обратите внимание, как экранируются специальные + HTML-символы, такие как `<`, `>` и `&`. + + Знаете ли вы, что в Markdown, одиночные * и _ + не означают выделение? + Они так же могут быть заэкранированы + при помощи обратного слэша: \\*. + + + + Лишние пустые строки должны игнорироваться. + + Любите ли вы *вложенные __выделения__* так, + как __--люблю--__ их я? + """, + """ +

                                                                                                                                                                                                                                                                                                                                                  Заголовок первого уровня

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Второго

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Третьего ## уровня

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Четвертого + # Все еще четвертого

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Этот абзац текста + содержит две строки.

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  # Может показаться, что это заголовок. + Но нет, это абзац, начинающийся с #.

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  #И это не заголовок.

                                                                                                                                                                                                                                                                                                                                                  +
                                                                                                                                                                                                                                                                                                                                                  Заголовки могут быть многострочными + (и с пропуском заголовков предыдущих уровней)
                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Мы все любим выделять текст разными способами. + Сильное выделение, используется гораздо реже, + но почему бы и нет? + Немного зачеркивания еще никому не вредило. + Код представляется элементом code.

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Обратите внимание, как экранируются специальные + HTML-символы, такие как <, > и &.

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Знаете ли вы, что в Markdown, одиночные * и _ + не означают выделение? + Они так же могут быть заэкранированы + при помощи обратного слэша: *.

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Лишние пустые строки должны игнорироваться.

                                                                                                                                                                                                                                                                                                                                                  +

                                                                                                                                                                                                                                                                                                                                                  Любите ли вы вложенные выделения так, + как люблю их я?

                                                                                                                                                                                                                                                                                                                                                  + """ + ); + + test("# Без перевода строки в конце", "

                                                                                                                                                                                                                                                                                                                                                  Без перевода строки в конце

                                                                                                                                                                                                                                                                                                                                                  "); + test("# Один перевод строки в конце\n", "

                                                                                                                                                                                                                                                                                                                                                  Один перевод строки в конце

                                                                                                                                                                                                                                                                                                                                                  "); + test("# Два перевода строки в конце\n\n", "

                                                                                                                                                                                                                                                                                                                                                  Два перевода строки в конце

                                                                                                                                                                                                                                                                                                                                                  "); + test( + "Выделение может *начинаться на одной строке,\n а заканчиваться* на другой", + "

                                                                                                                                                                                                                                                                                                                                                  Выделение может начинаться на одной строке,\n а заканчиваться на другой

                                                                                                                                                                                                                                                                                                                                                  " + ); + test("# *Выделение* и `код` в заголовках", "

                                                                                                                                                                                                                                                                                                                                                  Выделение и код в заголовках

                                                                                                                                                                                                                                                                                                                                                  "); + } + + protected void addElement(final String tag, final String markup) { + addElement(tag, markup, markup); + } + + protected void addElement(final String tag, final String begin, final String end) { + addElement(tag, begin, (checker, markup, input, output) -> { + checker.space(input, output); + input.append(begin); + open(output, tag); + + checker.word(input, output); + checker.generate(markup, input, output); + checker.word(input, output); + + input.append(end); + close(output, tag); + checker.space(input, output); + }); + } + + public void addElement(final String tag, final String begin, final Generator generator) { + Asserts.assertTrue("Duplicate element " + begin, elements.put(begin, generator) == null); + tags.computeIfAbsent(tag, k -> new ArrayList<>()).add(begin); + } + + private final Runner runner = Runner.packages("md2html").files("Md2Html"); + + protected Md2HtmlTester test(final String input, final String output) { + tests.add(Pair.of(input, output)); + return this; + } + + protected Md2HtmlTester spoiled(final String input, final String output, final String... spoilers) { + for (final String spoiler : spoilers) { + final Indexer in = new Indexer(input, spoiler); + final Indexer out = new Indexer(output, ESCAPES.getOrDefault(spoiler, spoiler)); + while (in.next() && out.next()) { + tests.add(Pair.of(in.cut(), out.cut())); + tests.add(Pair.of(in.escape(), output)); + } + } + return this; + } + + private static class Indexer { + private final String string; + private final String spoiler; + private int index = - 1; + + public Indexer(final String string, final String spoiler) { + this.string = string; + this.spoiler = spoiler; + } + + public boolean next() { + index = string.indexOf(spoiler, index + 1); + return index >= 0; + } + + public String cut() { + return string.substring(0, index) + string.substring(index + spoiler.length()); + } + + public String escape() { + return string.substring(0, index) + "\\" + string.substring(index); + } + } + + private static void open(final StringBuilder output, final String tag) { + output.append("<").append(tag).append(">"); + } + + private static void close(final StringBuilder output, final String tag) { + output.append(""); + } + + public void test(final TestCounter counter) { + counter.scope("Testing " + String.join(", ", tags.keySet()), () -> new Checker(counter).test()); + } + + public class Checker extends BaseChecker { + public Checker(final TestCounter counter) { + super(counter); + } + + protected void test() { + for (final Pair test : tests) { + test(test); + } + + for (final String markup : elements.keySet()) { + randomTest(3, 10, List.of(markup)); + } + + final int d = TestCounter.DENOMINATOR; + for (int i = 0; i < 10; i++) { + randomTest(100, 1000, randomMarkup()); + } + randomTest(100, 100_000 / d, randomMarkup()); + } + + private void test(final Pair test) { + runner.testEquals(counter, Arrays.asList(test.first().split("\n")), Arrays.asList(test.second().split("\n"))); + } + + private List randomMarkup() { + return Functional.map(tags.values(), random()::randomItem); + } + + private void randomTest(final int paragraphs, final int length, final List markup) { + final StringBuilder input = new StringBuilder(); + final StringBuilder output = new StringBuilder(); + emptyLines(input); + final List markupList = new ArrayList<>(markup); + for (int i = 0; i < paragraphs; i++) { + final StringBuilder inputSB = new StringBuilder(); + paragraph(length, inputSB, output, markupList); + input.append(inputSB); + emptyLines(input); + } + test(Pair.of(input.toString(), output.toString())); + } + + private void paragraph(final int length, final StringBuilder input, final StringBuilder output, final List markup) { + final int h = random().nextInt(0, 6); + final String tag = h == 0 ? "p" : "h" + h; + if (h > 0) { + input.append(new String(new char[h]).replace('\0', '#')).append(" "); + } + + open(output, tag); + while (input.length() < length) { + generate(markup, input, output); + final String middle = random().randomString(ExtendedRandom.ENGLISH); + input.append(middle).append("\n"); + output.append(middle).append("\n"); + } + output.setLength(output.length() - 1); + close(output, tag); + + output.append("\n"); + input.append("\n"); + } + + private void space(final StringBuilder input, final StringBuilder output) { + if (random().nextBoolean()) { + final String space = random().nextBoolean() ? " " : "\n"; + input.append(space); + output.append(space); + } + } + + public void generate(final List markup, final StringBuilder input, final StringBuilder output) { + word(input, output); + if (markup.isEmpty()) { + return; + } + final String type = random().randomItem(markup); + + markup.remove(type); + elements.get(type).generate(this, markup, input, output); + markup.add(type); + } + + protected void word(final StringBuilder input, final StringBuilder output) { + final String word = random().randomString(random().randomItem(ExtendedRandom.ENGLISH, ExtendedRandom.GREEK, ExtendedRandom.RUSSIAN)); + input.append(word); + output.append(word); + } + + private void emptyLines(final StringBuilder sb) { + while (random().nextBoolean()) { + sb.append('\n'); + } + } + + String generateInput(final List markup) { + final StringBuilder sb = new StringBuilder(); + generate(markup, sb, new StringBuilder()); + return sb.toString() + .replace("<", "") + .replace(">", "") + .replace("]", ""); + } + } + + @FunctionalInterface + public interface Generator { + void generate(Checker checker, List markup, StringBuilder input, StringBuilder output); + } +} diff --git a/java/md2html/package-info.java b/java/md2html/package-info.java new file mode 100644 index 0000000..05f3608 --- /dev/null +++ b/java/md2html/package-info.java @@ -0,0 +1,8 @@ +/** + * Tests for Markdown to HTML homework + * of Introduction to Programming course. + * + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package md2html; \ No newline at end of file diff --git a/java/reverse/FastReverseTest.java b/java/reverse/FastReverseTest.java new file mode 100644 index 0000000..1695d36 --- /dev/null +++ b/java/reverse/FastReverseTest.java @@ -0,0 +1,87 @@ +package reverse; + +import base.ExtendedRandom; +import base.Named; +import base.Selector; +import base.TestCounter; +import wordStat.WordStatTest; + +import java.util.Arrays; +import java.util.function.BiFunction; +import java.util.function.LongBinaryOperator; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class FastReverseTest { + // === 3637 + + private static final Named> OCT = Named.of("", + (r, i) -> r.nextBoolean() ? Integer.toString(i) : Integer.toOctalString(i) + (r.nextBoolean() ? "o" : "O") + ); + private static final Named> DEC = Named.of("", (r, i) -> Integer.toString(i)); + + private static final Named PUNCT = Named.of( + "", + IntStream.range(0, Character.MAX_VALUE) + .filter(ch -> ch == ' ' || Character.getType(ch) == Character.START_PUNCTUATION || Character.getType(ch) == Character.END_PUNCTUATION) + .filter(ch -> ch != 13 && ch != 10) + .mapToObj(Character::toString) + .collect(Collectors.joining()) + ); + + public static final Named MIN_C = Named.of("MinC", scan2((a, b) -> b)); + public static final Named MIN = Named.of("Min", scan2(Math::min)); + + private static ReverseTester.Op scan2(final LongBinaryOperator reduce) { + return ints -> { + // This code is intentionally obscure + final int length = Arrays.stream(ints).mapToInt(r -> r.length).max().orElse(0); + final long[] cs = new long[length]; + final long[] cc = new long[length + 1]; + Arrays.fill(cs, Integer.MAX_VALUE); + Arrays.fill(cc, Integer.MAX_VALUE); + //noinspection NestedAssignment + final long[][] rows = range(ints.length).mapToObj(i -> { + range(ints[i].length).forEachOrdered(j -> cc[j] = reduce.applyAsLong( + cc[j + 1], + cs[j] = Math.min(cs[j], ints[i][j]) + )); + return Arrays.copyOf(cc, ints[i].length); + }) + .toArray(long[][]::new); + return range(ints.length).mapToObj(i -> rows[i]).toArray(long[][]::new); + }; + } + + private static IntStream range(final int length) { + return IntStream.iterate(length - 1, i -> i >= 0, i -> i - 1); + } + + + // === Common + + public static final int MAX_SIZE = 1_000_000 / TestCounter.DENOMINATOR / TestCounter.DENOMINATOR; + + public static final Selector SELECTOR = new Selector(FastReverseTest.class) + .variant("Base", ReverseTester.variant(MAX_SIZE, ReverseTest.REVERSE)) + .variant("3637", ReverseTester.variant(MAX_SIZE, "", MIN_C, OCT, DEC, PUNCT)) + .variant("3839", ReverseTester.variant(MAX_SIZE, "", MIN, OCT, DEC, PUNCT)) + .variant("3435", ReverseTester.variant(MAX_SIZE, ReverseTest.ROTATE, PUNCT)) + .variant("3233", ReverseTester.variant(MAX_SIZE, ReverseTest.EVEN, PUNCT)) + .variant("4142", ReverseTester.variant(MAX_SIZE, ReverseTest.AVG, PUNCT)) + .variant("4749", ReverseTester.variant(MAX_SIZE, ReverseTest.SUM, PUNCT)) + + ; + + + private FastReverseTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + WordStatTest.main(args); + } +} diff --git a/java/reverse/ReverseTest.java b/java/reverse/ReverseTest.java new file mode 100644 index 0000000..de1bced --- /dev/null +++ b/java/reverse/ReverseTest.java @@ -0,0 +1,165 @@ +package reverse; + +import base.Named; +import base.Selector; +import base.TestCounter; +import reverse.ReverseTester.Op; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.IntToLongFunction; +import java.util.function.LongBinaryOperator; +import java.util.stream.IntStream; + +/** + * Tests for {@code Reverse} homework. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ReverseTest { + // === Base + public static final Named REVERSE = Named.of("", ReverseTester::transform); + + + // === Max + + public static final Named MAX_C = Named.of("MaxC", scan2((a, b) -> b)); + public static final Named MAX = Named.of("Max", scan2(Math::max)); + + private static Op scan2(final LongBinaryOperator reduce) { + return ints -> { + // This code is intentionally obscure + final int length = Arrays.stream(ints).mapToInt(r -> r.length).max().orElse(0); + final long[] cs = new long[length]; + final long[] cc = new long[length + 1]; + Arrays.fill(cs, Integer.MIN_VALUE); + Arrays.fill(cc, Integer.MIN_VALUE); + //noinspection NestedAssignment + final long[][] rows = range(ints.length).mapToObj(i -> { + range(ints[i].length).forEachOrdered(j -> cc[j] = reduce.applyAsLong( + cc[j + 1], + cs[j] = Math.max(cs[j], ints[i][j]) + )); + return Arrays.copyOf(cc, ints[i].length); + }) + .toArray(long[][]::new); + return range(ints.length).mapToObj(i -> rows[i]).toArray(long[][]::new); + }; + } + + private static IntStream range(final int length) { + return IntStream.iterate(length - 1, i -> i >= 0, i -> i - 1); + } + + + // === Rotate + public static final Named ROTATE = Named.of("Rotate", ints -> { + final List rows = new ArrayList<>(List.of(ints)); + return IntStream.range(0, Arrays.stream(ints).mapToInt(r -> r.length).max().orElse(0)) + .mapToObj(c -> { + rows.removeIf(r -> r.length <= c); + return range(rows.size()).mapToObj(rows::get).mapToLong(r -> r[c]).toArray(); + }) + .toArray(long[][]::new); + }); + + + // === Even + public static final Named EVEN = Named.of( + "Even", + ints -> ReverseTester.transform(IntStream.range(0, ints.length) + .mapToObj(i -> IntStream.range(0, ints[i].length) + .filter(j -> (i + j) % 2 == 0) + .map(j -> ints[i][j])) + .map(IntStream::toArray).toArray(int[][]::new)) + ); + + // Sum + @FunctionalInterface + interface LongTernaryOperator { + long applyAsLong(long a, long b, long c); + } + + public static final Named SUM = cross("Sum", 0, Long::sum, (r, c, v) -> r + c - v); + + private static long[][] cross( + final int[][] ints, + final IntToLongFunction map, + final LongBinaryOperator reduce, + final int zero, + final LongTernaryOperator get + ) { + // This code is intentionally obscure + final long[] rt = Arrays.stream(ints) + .map(Arrays::stream) + .mapToLong(row -> row.mapToLong(map).reduce(zero, reduce)) + .toArray(); + final long[] ct = new long[Arrays.stream(ints).mapToInt(r -> r.length).max().orElse(0)]; + Arrays.fill(ct, zero); + Arrays.stream(ints).forEach(r -> IntStream.range(0, r.length) + .forEach(i -> ct[i] = reduce.applyAsLong(ct[i], map.applyAsLong(r[i])))); + return IntStream.range(0, ints.length) + .mapToObj(r -> IntStream.range(0, ints[r].length) + .mapToLong(c -> get.applyAsLong(rt[r], ct[c], ints[r][c])) + .toArray()) + .toArray(long[][]::new); + } + + private static Named cross( + final String name, + final int zero, + final LongBinaryOperator reduce, + final LongTernaryOperator get + ) { + return Named.of(name, ints -> cross(ints, n -> n, reduce, zero, get)); + } + + public static final Named AVG = avg( + "Avg", + ints -> cross(ints, n -> n, Long::sum, 0, (r, c, v) -> r + c - v), + ints -> cross(ints, n -> 1, Long::sum, 0, (r1, c1, v1) -> r1 + c1 - 1) + ); + + private static Named avg( + final String name, + final Op fs, + final Op fc + ) { + return Named.of(name, ints -> avg(ints, fs.apply(ints), fc.apply(ints))); + } + + private static long[][] avg(final int[][] ints, final long[][] as, final long[][] ac) { + return IntStream.range(0, ints.length).mapToObj(i -> IntStream.range(0, ints[i].length) + .mapToLong(j -> as[i][j] / ac[i][j]) + .toArray()) + .toArray(long[][]::new); + } + + + // === Common + + public static final int MAX_SIZE = 10_000 / TestCounter.DENOMINATOR; + + public static final Selector SELECTOR = selector(ReverseTest.class, MAX_SIZE); + + private ReverseTest() { + // Utility class + } + + public static Selector selector(final Class owner, final int maxSize) { + return new Selector(owner) + .variant("Base", ReverseTester.variant(maxSize, REVERSE)) + .variant("3637", ReverseTester.variant(maxSize, MAX_C)) + .variant("3839", ReverseTester.variant(maxSize, MAX)) + .variant("3435", ReverseTester.variant(maxSize, ROTATE)) + .variant("3233", ReverseTester.variant(maxSize, EVEN)) + .variant("4142", ReverseTester.variant(maxSize, AVG)) + .variant("4749", ReverseTester.variant(maxSize, SUM)) + ; + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/reverse/ReverseTester.java b/java/reverse/ReverseTester.java new file mode 100644 index 0000000..134f0fd --- /dev/null +++ b/java/reverse/ReverseTester.java @@ -0,0 +1,299 @@ +package reverse; + +import base.*; + +import java.util.*; +import java.util.function.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ReverseTester { + + public static final Named TRANSFORM = Named.of("", ReverseTester::transform); + public static final Named SPACE = Named.of("", " "); + + @FunctionalInterface + public interface Op extends Function {} + + private static final int[] DIVISORS = {100, 10, 1}; + + private final Op transform; + private final BiFunction inputToString; + private final BiFunction outputToString; + private final String name; + private final String spaces; + + private ReverseTester(final String className, final Op transform, final String spaces) { + this(className, transform, spaces, (r, i) -> Integer.toString(i), (r, i) -> Long.toString(i)); + } + + private ReverseTester( + final String className, + final Op transform, + final String spaces, + final BiFunction inputToString, + final BiFunction outputToString + ) { + name = className; + this.transform = transform; + this.spaces = spaces; + this.inputToString = inputToString; + this.outputToString = outputToString; + } + + private static Consumer variant(final int maxSize, final Supplier tester) { + return counter -> tester.get().run(counter, maxSize); + } + + public static Consumer variant(final int maxSize, final Named transform) { + return variant(maxSize, transform, SPACE); + } + + + public static Consumer variant(final int maxSize, final Named transform, final Named spaces) { + Objects.requireNonNull(transform); + Objects.requireNonNull(spaces); + return variant( + maxSize, + () -> new ReverseTester("Reverse" + transform.name() + spaces.name(), transform.value(), spaces.value()) + ); + } + + public static Consumer variant( + final int maxSize, + final String suffix, + final Named> input, + final Named> output + ) { + return variant(maxSize, suffix, TRANSFORM, input, output); + } + + public static Consumer variant( + final int maxSize, + final String suffix, + final Named op, + final Named> input, + final Named> output + ) { + return variant(maxSize, suffix, op, input, output, SPACE); + } + + public static Consumer variant( + final int maxSize, + final String suffix, + final Named op, + final Named> input, + final Named> output, + final Named spaces + ) { + final String out = input.name().contains(output.name()) ? "" : output.name(); + return variant(maxSize, () -> new ReverseTester( + "Reverse" + op.name() + input.name() + out + suffix + spaces.name(), + op.value(), + spaces.value(), + input.value(), + output.value() + )); + } + + private void run(final TestCounter counter, final int maxSize) { + new Checker(counter, maxSize, Runner.packages("", "reverse").std(name), spaces).test(); + } + + @Override + public String toString() { + return name; + } + + public static long[][] transform(final int[][] ints) { + return IntStream.range(1, ints.length + 1) + .mapToObj(i -> ints[ints.length - i]) + .map(is -> IntStream.range(1, is.length + 1).mapToLong(i -> is[is.length - i]).toArray()) + .toArray(long[][]::new); + } + + /** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + private class Checker extends BaseChecker { + private final int maxSize; + private final Runner runner; + private final String spaces; + private final Set manualTests = new HashSet<>(); + + Checker(final TestCounter counter, final int maxSize, final Runner runner, final String spaces) { + super(counter); + this.maxSize = maxSize; + this.runner = runner; + this.spaces = spaces; + } + + public void manualTest(final int[][] ints) { + for (final List permutation : permutations(new ArrayList<>(Arrays.asList(ints)))) { + final int[][] input = permutation.toArray(int[][]::new); + final String[][] lines = toString(input, inputToString); + if (manualTests.add(Arrays.deepToString(lines))) { + test(lines, toString(transform.apply(input), outputToString)); + } + } + } + + public void test(final int[][] ints) { + test(toString(ints, inputToString), toString(transform.apply(ints), outputToString)); + } + + public void test(final String[][] input, final String[][] output) { + final List inputLines = toLines(input, random().randomString(spaces, 1, 10)); + final List outputLines = toLines(output, " "); + runner.testEquals(counter, inputLines, outputLines); + } + + private String[][] toString(final int[][] ints, final BiFunction toString) { + return Arrays.stream(ints) + .map(row -> Arrays.stream(row).mapToObj(i -> toString.apply(random(), i)).toArray(String[]::new)) + .toArray(String[][]::new); + } + + private String[][] toString(final long[][] ints, final BiFunction toString) { + return Arrays.stream(ints) + .map(row -> Arrays.stream(row).mapToObj(i -> toString.apply(random(), (int) i)).toArray(String[]::new)) + .toArray(String[][]::new); + } + + private List toLines(final String[][] data, final String delimiter) { + if (data.length == 0) { + return Collections.singletonList(""); + } + return Arrays.stream(data) + .map(row -> String.join(delimiter, row)) + .collect(Collectors.toList()); + } + + public int[][] random(final int[] profile) { + final int col = random().nextInt(Arrays.stream(profile).max().orElse(0)); + final int row = random().nextInt(profile.length); + final int m = random().nextInt(5) - 2; + final int[][] ints = Arrays.stream(profile).mapToObj(random().getRandom()::ints).map(IntStream::toArray).toArray(int[][]::new); + Arrays.stream(ints).filter(r -> col < r.length).forEach(r -> r[col] = Math.abs(r[col]) / 2 * m); + ints[row] = Arrays.stream(ints[row]).map(Math::abs).map(v -> v / 2 * m).toArray(); + return ints; + } + + public void test() { + manualTest(new int[][]{ + {1} + }); + manualTest(new int[][]{ + {1, 2}, + {3} + }); + manualTest(new int[][]{ + {1, 2, 3}, + {4, 5}, + {6} + }); + manualTest(new int[][]{ + {1, 2, 3}, + {}, + {4, 5}, + {6} + }); + manualTest(new int[][]{ + {1, 2, 3}, + {-4, -5}, + {6} + }); + manualTest(new int[][]{ + {1, -2, 3}, + {}, + {4, -5}, + {6} + }); + manualTest(new int[][]{ + {1, 2, 0}, + {1, 0}, + {0}, + }); + manualTest(new int[][]{ + {1}, + {1, 3}, + {1, 2, 3}, + }); + manualTest(new int[][]{ + {-1}, + {-1, -2}, + {-1, -2, -3}, + }); + manualTest(new int[][]{ + {}, + }); + manualTest(new int[][]{ + {}, + {}, + {}, + }); + testRandom(tweakProfile(constProfile(10, 10), new int[][]{})); + testRandom(tweakProfile(constProfile(100, 100), new int[][]{})); + testRandom(randomProfile(100, maxSize)); + testRandom(randomProfile(maxSize / 10, maxSize)); + testRandom(randomProfile(maxSize, maxSize)); + for (final int d : DIVISORS) { + final int size = maxSize / d; + testRandom(tweakProfile(constProfile(size / 2, 0), new int[][]{{size / 2, 0}})); + testRandom(tweakProfile(randomProfile(size, size / 2), new int[][]{{size / 2, 0}})); + testRandom(tweakProfile(constProfile(size / 2, 0), new int[][]{{size / 2, size / 2 - 1}})); + testRandom(tweakProfile(constProfile(size / 3, 1), new int[][]{{size / 3, size / 6, size / 3 - 1}})); + } + } + + private int[] randomProfile(final int length, final int values) { + final int[] profile = new int[length]; + for (int i = 0; i < values; i++) { + profile[random().nextInt(0, length - 1)]++; + } + return profile; + } + + private void testRandom(final int[] profile) { + test(random(profile)); + } + + private int[] constProfile(final int length, final int value) { + final int[] profile = new int[length]; + Arrays.fill(profile, value); + return profile; + } + + private int[] tweakProfile(final int[] profile, final int[][] mods) { + for (final int[] mod : mods) { + Arrays.stream(mod).skip(1).forEach(i -> profile[i] = mod[0]); + } + return profile; + } + } + + private static List> permutations(final List elements) { + final List> result = new ArrayList<>(); + permutations(new ArrayList<>(elements), result, elements.size() - 1); + return result; + } + + private static void permutations(final List elements, final List> result, final int n) { + if (n == 0) { + result.add(List.copyOf(elements)); + } else { + for (int i = 0; i < n; i++) { + permutations(elements, result, n - 1); + if (n % 2 == 1) { + Collections.swap(elements, i, n); + } else { + Collections.swap(elements, 0, n); + } + } + permutations(elements, result, n - 1); + } + } +} diff --git a/java/reverse/package-info.java b/java/reverse/package-info.java new file mode 100644 index 0000000..86e0e5d --- /dev/null +++ b/java/reverse/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Reverse homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package reverse; \ No newline at end of file diff --git a/java/sum/SumTest.java b/java/sum/SumTest.java new file mode 100644 index 0000000..bdd37b3 --- /dev/null +++ b/java/sum/SumTest.java @@ -0,0 +1,178 @@ +package sum; + +import base.*; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Locale; +import java.util.function.*; +import java.util.stream.Collectors; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class SumTest { + // === Base + + @FunctionalInterface + /* package-private */ interface Op extends UnaryOperator> {} + + private static final BiConsumer TO_STRING = (expected, out) -> Asserts.assertEquals("Sum", expected.toString(), out); + + private static final Named>> BASE = Named.of("", () -> new SumTester<>( + Integer::sum, n -> (int) n, (r, max) -> r.nextInt() % max, TO_STRING, + 10, 100, Integer.MAX_VALUE + )); + + /* package-private */ static Named> plain() { + return Named.of("", test -> test); + } + + + // === DoubleHex + + private static BiConsumer approximate(final Function parser, final double precision) { + return (expected, out) -> + Asserts.assertEquals("Sum", expected.doubleValue(), parser.apply(out).doubleValue(), precision); + } + + private static final Named>> DOUBLE = Named.of("Double", () -> new SumTester<>( + Double::sum, n -> (double) n, (r, max) -> (r.getRandom().nextDouble() - 0.5) * 2 * max, + approximate(Double::parseDouble, 1e-10), + 10.0, 0.01, 1e20, 1e100, Double.MAX_VALUE / 10000) + .test(5, "2.5 2.5") + .test(0, "1e100 -1e100") + .testT(2e100, "1.5e100 0.5e100")); + + private static Named> hexFull(final Function toHex) { + final Function toHexSpoiled = toHex.andThen(s ->s.chars() + .map(ch -> ((ch ^ s.hashCode()) & 1) == 0 ? Character.toLowerCase(ch) : Character.toUpperCase(ch)) + .mapToObj(Character::toString) + .collect(Collectors.joining())); + return Named.of("Hex", test -> test + .test(toHex, 1) + .test(toHex, 0x1a) + .test(toHexSpoiled, 0xA2) + .test(toHexSpoiled, 0X0, 0X1, 0XF, 0XF, 0x0, 0x1, 0xF, 0xf) + .test(toHexSpoiled, 0x12345678) + .test(toHexSpoiled, 0x09abcdef) + .test(toHexSpoiled, 0x3CafeBab) + .test(toHexSpoiled, 0x3DeadBee) + + .test(toHex, Integer.MAX_VALUE) + .test(toHex, Integer.MIN_VALUE) + .setToString(number -> { + final int hashCode = number.hashCode(); + if ((hashCode & 1) == 0) { + return number.toString(); + } + + return toHexSpoiled.apply(number); + }) + ); + } + + // === Octal + private static Named> octal(final Function toOctal) { + //noinspection OctalInteger,StringConcatenationMissingWhitespace + return Named.of("Octal", test -> test + .test(1, "1o") + .test(017, "17o") + .testSpaces(6, " 1o 2o 3O ") + .test(01234567, "1234567O") + + .test(Integer.MIN_VALUE, "-0" + String.valueOf(Integer.MIN_VALUE).substring(1)) + .test(Integer.MAX_VALUE, "0" + Integer.MAX_VALUE) + .test(Integer.MAX_VALUE, Integer.toOctalString(Integer.MAX_VALUE) + "o") + .test(Integer.MAX_VALUE, "0" + Integer.toOctalString(Integer.MAX_VALUE) + "O") + .setToString(number -> { + final int hashCode = number.hashCode(); + if ((hashCode & 1) == 0) { + return number.toString(); + } + + final String lower = toOctal.apply(number).toLowerCase(Locale.ROOT) + "o"; + return (hashCode & 2) == 0 ? lower : lower.toUpperCase(Locale.ROOT); + }) + ); + } + + // === Long + + private static final Named>> LONG = Named.of("Long", () -> new SumTester<>( + Long::sum, n -> n, (r, max) -> r.getRandom().nextLong() % max, TO_STRING, + 10L, 100L, (long) Integer.MAX_VALUE, Long.MAX_VALUE) + .test(12345678901234567L, " +12345678901234567 ") + .test(0L, " +12345678901234567 -12345678901234567") + .test(0L, " +12345678901234567 -12345678901234567")); + + // === BigInteger + + private static final Named>> BIG_INTEGER = Named.of("BigInteger", () -> new SumTester<>( + BigInteger::add, BigInteger::valueOf, (r, max) -> new BigInteger(max.bitLength(), r.getRandom()), TO_STRING, + BigInteger.TEN, BigInteger.TEN.pow(10), BigInteger.TEN.pow(100), BigInteger.TWO.pow(1000)) + .test(0, "10000000000000000000000000000000000000000 -10000000000000000000000000000000000000000")); + + + // === BigDecimalHex + + @SuppressWarnings("BigDecimalMethodWithoutRoundingCalled") + private static final Named>> BIG_DECIMAL = Named.of("BigDecimal", () -> new SumTester<>( + BigDecimal::add, BigDecimal::valueOf, + (r, max) -> { + final BigInteger unscaled = new BigInteger((max.precision() - max.scale() + 2) * 3, r.getRandom()); + return new BigDecimal(unscaled, 3); + }, + TO_STRING, + BigDecimal.TEN, BigDecimal.TEN.pow(10), BigDecimal.TEN.pow(100), BigDecimal.ONE.add(BigDecimal.ONE).pow(1000)) + .testT(BigDecimal.ZERO.setScale(3), "10000000000000000000000000000000000000000.123 -10000000000000000000000000000000000000000.123")); + + private static String bigDecimalToString(final BigDecimal number) { + final int scale = number.scale(); + return "0x" + number.unscaledValue().toString(16) + (scale == 0 ? "" : "s" + Integer.toString(scale, 16)); + } + + + // === Hex + + private static Named> hex(final Function toHex) { + return hexFull(v -> "0x" + toHex.apply(v)); + } + + + // === Common + + /* package-private */ static Consumer variant( + final Named> runner, + final Named>> test, + final Named, ? extends SumTester>> modifier + ) { + return counter -> modifier.value().apply(test.value().get()) + .test("Sum" + test.name() + modifier.name() + runner.name(), counter, runner.value()); + } + + /* package-private */ static final Named> RUNNER = + Named.of("", Runner.packages("", "sum")::args); + + public static final Selector SELECTOR = selector(SumTest.class, RUNNER); + + private SumTest() { + // Utility class + } + + public static Selector selector(final Class owner, final Named> runner) { + return new Selector(owner) + .variant("Base", variant(runner, BASE, plain())) + .variant("3637", variant(runner, DOUBLE, hexFull(value -> value == value.intValue() && value > 0 ? "0x" + Integer.toHexString(value.intValue()) : Double.toHexString(value)))) + .variant("3839", variant(runner, BIG_DECIMAL, hexFull(SumTest::bigDecimalToString))) + .variant("3435", variant(runner, BASE, hex(Integer::toHexString))) + .variant("3233", variant(runner, DOUBLE, plain())) + .variant("4749", variant(runner, LONG, octal(Long::toOctalString))) + .variant("4142", variant(runner, BIG_INTEGER, octal(number -> number.toString(8)))) + ; + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/sum/SumTester.java b/java/sum/SumTester.java new file mode 100644 index 0000000..404e4b3 --- /dev/null +++ b/java/sum/SumTester.java @@ -0,0 +1,189 @@ +package sum; + +import base.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.function.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.LongStream; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class SumTester { + private static final List SPACES = List.of( + " \t\n\u000B\u2029\f", + IntStream.rangeClosed(0, Character.MAX_VALUE) + .filter(Character::isWhitespace) + .mapToObj(Character::toString) + .collect(Collectors.joining()) + ); + + private final BinaryOperator add; + private final LongFunction fromLong; + private BiFunction toString; + private final BiFunction randomValue; + private final BiConsumer verifier; + private List spaces; + private final List limits; + + private final List> tests = new ArrayList<>(); + + @SafeVarargs + public SumTester( + final BinaryOperator add, + final LongFunction fromLong, + final BiFunction randomValue, + final BiConsumer verifier, + final T... limits + ) { + this.add = add; + this.fromLong = fromLong; + this.randomValue = randomValue; + this.verifier = verifier; + this.limits = List.of(limits); + + setSpaces(SPACES); + setToString(String::valueOf); + + test(1, "1"); + test(6, "1", "2", "3"); + test(1, " 1"); + test(1, "1 "); + test(1, " 1 "); + test(12345, " 12345 "); + test(60, "010", "020", "030"); + testSpaces(1368, " 123 456 789 "); + test(-1, "-1"); + test(-6, "-1", "-2", "-3"); + test(-12345, " -12345 "); + testSpaces(-1368, " -123 -456 -789 "); + test(1, "+1"); + test(6, "+1", "+2", "+3"); + test(12345, " +12345 "); + testSpaces(1368, " +123 +456 +789 "); + test(0); + testSpaces(0, " ", " "); + } + + protected SumTester setSpaces(final List spaces) { + this.spaces = spaces; + return this; + } + + protected SumTester addSpaces(final String... spaces) { + this.spaces = Stream.concat(this.spaces.stream(), Arrays.stream(spaces)).toList(); + return this; + } + + public SumTester setToString(final Function toString) { + return setToString((r, n) -> toString.apply(n)); + } + + public SumTester setToString(final BiFunction toString) { + this.toString = toString; + return this; + } + + protected SumTester test(final Function toString, final long... input) { + return testT( + fromLong.apply(LongStream.of(input).sum()), + LongStream.of(input).mapToObj(fromLong).map(toString).collect(Collectors.joining(" ")) + ); + } + + protected SumTester test(final long result, final String... input) { + return testT(fromLong.apply(result), input); + } + + protected SumTester testT(final T result, final String... input) { + return testT(result, Arrays.asList(input)); + } + + private SumTester testT(final T result, final List input) { + tests.add(checker -> checker.test(result, input)); + return this; + } + + public SumTester testSpaces(final long result, final String... input) { + final T res = fromLong.apply(result); + tests.add(checker -> spaces.stream() + .flatMapToInt(String::chars) + .forEach(space -> checker.test( + res, + Functional.map(Arrays.asList(input), s -> s.replace(' ', (char) space)) + )) + ); + return this; + } + + public void test(final String name, final TestCounter counter, final Function runner) { + new Checker(counter, runner.apply(name)).test(); + } + + private class Checker extends BaseChecker { + private final Runner runner; + + public Checker(final TestCounter counter, final Runner runner) { + super(counter); + this.runner = runner; + } + + public void test() { + tests.forEach(test -> test.accept(this)); + + for (final T limit : limits) { + for (int n = 10; n <= 10_000 / TestCounter.DENOMINATOR; n *= 10) { + randomTest(n, limit); + } + } + } + + private void test(final T result, final List input) { + counter.test(() -> { + final List out = runner.run(counter, input); + Asserts.assertEquals("Single line expected", 1, out.size()); + verifier.accept(result, out.get(0)); + }); + } + + private void randomTest(final int numbers, final T max) { + for (final String spaces : spaces) { + randomTest(numbers, max, spaces); + } + } + + private void randomTest(final int numbers, final T max, final String spaces) { + final List values = new ArrayList<>(); + for (int i = 0; i < numbers; i++) { + values.add(randomValue.apply(random(), max)); + } + testRandom(values.stream().reduce(fromLong.apply(0), add), values, spaces); + } + + private void testRandom(final T result, final List args, final String spaces) { + final List spaced = args.stream() + .map(n -> toString.apply(random(), n)) + .map(value -> randomSpace(spaces) + value + randomSpace(spaces)) + .toList(); + final List argsList = new ArrayList<>(); + for (final Iterator i = spaced.listIterator(); i.hasNext(); ) { + final StringBuilder next = new StringBuilder(i.next()); + while (i.hasNext() && random().nextBoolean()) { + next.append(randomSpace(spaces)).append(i.next()); + } + argsList.add(next.toString()); + } + test(result, argsList); + } + + private String randomSpace(final String spaces) { + return random().randomString(spaces); + } + } +} diff --git a/java/sum/package-info.java b/java/sum/package-info.java new file mode 100644 index 0000000..04ce7fb --- /dev/null +++ b/java/sum/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Sum homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package sum; \ No newline at end of file diff --git a/java/wordStat/WordStatChecker.java b/java/wordStat/WordStatChecker.java new file mode 100644 index 0000000..cc19c83 --- /dev/null +++ b/java/wordStat/WordStatChecker.java @@ -0,0 +1,127 @@ +package wordStat; + +import base.*; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class WordStatChecker extends BaseChecker { + public static final String DASH = "-֊־‒–—―⸗⸚⸺〰゠︱︲﹘﹣-'"; + public static final String SIMPLE_DELIMITERS = " \t"; + public static final String ADVANCED_DELIMITERS = " \t!\"#%&()*+,./:;<=>?@[\\]^`{|}~ ¡¦§¨©«¬\u00AD®¯°±²³´¶·¸¹»¼½¾¿×÷˂˃˄˅˒˓˔˕˖˗˘˙˚˛˜˝"; + public static final String ALL = ExtendedRandom.RUSSIAN + ExtendedRandom.ENGLISH + ExtendedRandom.GREEK + DASH; + private static final Pattern PATTERN = Pattern.compile("[^\\p{IsLetter}'\\p{Pd}]+"); + public static final Runner.Packages RUNNER = Runner.packages("", "wordstat", "wspp"); + + private final Function>> processor; + + private final MainChecker main; + + private WordStatChecker( + final String className, + final Function>> processor, + final TestCounter counter + ) { + super(counter); + main = new MainChecker(RUNNER.files(className)); + this.processor = processor; + } + + public static void test( + final TestCounter counter, + final String className, + final Function>> processor, + final Consumer tests + ) { + tests.accept(new WordStatChecker(className, processor, counter)); + } + + public void test(final String... lines) { + test(PATTERN, lines); + } + + public void test(final Pattern pattern, final String... lines) { + final String[][] data = Arrays.stream(lines) + .map(line -> Arrays.stream(pattern.split(line)).filter(Predicate.not(String::isEmpty)).toArray(String[]::new)) + .toArray(String[][]::new); + test(lines, processor.apply(data)); + } + + private void randomTest( + final int wordLength, + final int totalWords, + final int wordsPerLine, + final int lines, + final String chars, + final String delimiters, + final Function>> processor + ) { + final String[] words = generateWords(wordLength, totalWords, chars); + final String[][] text = generateTest(lines, words, wordsPerLine); + test(input(text, delimiters), processor.apply(text)); + } + + public void randomTest( + final int wordLength, + final int totalWords, + final int wordsPerLine, + final int lines, + final String chars, + final String delimiters + ) { + randomTest(wordLength, totalWords, wordsPerLine, lines, chars, delimiters, processor::apply); + } + + private void test(final String[] text, final List> expected) { + final List expectedList = expected.stream() + .map(p -> p.first() + " " + p.second()) + .collect(Collectors.toList()); + main.testEquals(counter, Arrays.asList(text), expectedList); + } + + public void test(final String[][] text, final String delimiters, final List> answer) { + test(input(text, delimiters), answer); + } + + private String[] generateWords(final int wordLength, final int totalWords, final String chars) { + final String allChars = chars.chars().anyMatch(Character::isUpperCase) + ? chars : chars + chars.toUpperCase(Locale.ROOT); + return IntStream.range(0, totalWords) + .mapToObj(i -> random().randomString(allChars, wordLength / 2, wordLength)) + .toArray(String[]::new); + } + + private String[][] generateTest(final int lines, final String[] words, final int wordsPerLine) { + final String[][] text = new String[lines][]; + for (int i = 0; i < text.length; i++) { + text[i] = new String[random().nextInt(wordsPerLine / 2, wordsPerLine)]; + for (int j = 0; j < text[i].length; j++) { + text[i][j] = random().randomItem(words); + } + } + return text; + } + + private String[] input(final String[][] text, final String delimiters) { + final String[] input = new String[text.length]; + for (int i = 0; i < text.length; i++) { + final String[] line = text[i]; + final StringBuilder sb = new StringBuilder(random().randomString(delimiters)); + for (final String word : line) { + sb.append(word).append(random().randomString(delimiters)); + } + input[i] = sb.toString(); + } + return input; + } +} diff --git a/java/wordStat/WordStatTest.java b/java/wordStat/WordStatTest.java new file mode 100644 index 0000000..151991e --- /dev/null +++ b/java/wordStat/WordStatTest.java @@ -0,0 +1,70 @@ +package wordStat; + +import base.Named; +import base.Selector; + +import java.util.Comparator; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Tests for Word Statistics homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class WordStatTest { + // === Base + private static final Named>> ID = Named.of("", Stream::of); + private static final WordStatTester.Variant BASE = new WordStatTester.Variant("", false, Comparator.comparingInt(p -> 0)); + + + // === 3637 + public static final int SIZE = 3; + private static final WordStatTester.Variant LENGTH = new WordStatTester.Variant("Length", false, Comparator.comparingInt(p -> p.first().length())); + private static final Named>> MIDDLE = + size("Middle", SIZE * 2 + 1, s -> Stream.of(s.substring(SIZE, s.length() - SIZE))); + + static Named>> size( + final String name, + final int length, + final Function> f + ) { + return Named.of(name, s -> s.length() >= length ? f.apply(s) : Stream.empty()); + } + + // === 3839 + private static final Named>> AFFIX = size( + "Affix", + 2, + s -> Stream.of(s.substring(0, s.length() / 2), s.substring(s.length() - s.length() / 2)) + ); + + // === 3536 + private static final Named>> SUFFIX = + size("Suffix", 2, s -> Stream.of(s.substring(s.length() - s.length() / 2))); + + // === 4749 + private static final Named>> PREFIX = + size("Prefix", 2, s -> Stream.of(s.substring(0, s.length() / 2))); + + // === Common + public static final Selector SELECTOR = new Selector(WordStatTester.class) + .variant("Base", BASE.with(ID)) + .variant("3637", LENGTH.with(MIDDLE)) + .variant("3839", LENGTH.with(AFFIX)) + .variant("3435", LENGTH.with(SUFFIX)) + .variant("3233", LENGTH.with(ID)) + .variant("4142", LENGTH.with(MIDDLE)) + .variant("4749", LENGTH.with(PREFIX)) + + ; + + private WordStatTest() { + // Utility class + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/wordStat/WordStatTester.java b/java/wordStat/WordStatTester.java new file mode 100644 index 0000000..5534c14 --- /dev/null +++ b/java/wordStat/WordStatTester.java @@ -0,0 +1,100 @@ +package wordStat; + +import base.ExtendedRandom; +import base.Named; +import base.Pair; +import base.TestCounter; + +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class WordStatTester { + public static final String PRE_LOWER = chars() + .filter(s -> s.toLowerCase(Locale.ROOT).length() == 1) + .collect(Collectors.joining()); + public static final String POST_LOWER = chars() + .collect(Collectors.joining()) + .toLowerCase(); + + private WordStatTester() { + } + + private static Stream chars() { + return IntStream.range(' ', Character.MAX_VALUE) + .filter(ch -> !Character.isSurrogate((char) ch)) + .filter(ch -> Character.getType(ch) != Character.NON_SPACING_MARK) + .filter(ch -> Character.getType(ch) != Character.DIRECTIONALITY_NONSPACING_MARK) + .mapToObj(Character::toString); + } + + /* package-private */ record Variant(String name, boolean reverse, Comparator> c) { + public Consumer with(final Named>> split) { + return counter -> WordStatChecker.test( + counter, + "WordStat" + name + split.name(), + text -> answer(split.value(), text), + checker -> { + checker.test("To be, or not to be, that is the question:"); + checker.test("Monday's child is fair of face.", "Tuesday's child is full of grace."); + checker.test("Шалтай-Болтай", "Сидел на стене.", "Шалтай-Болтай", "Свалился во сне."); + checker.test( + "27 октября — 300-й день григорианскому календарю. До конца года остаётся 65 дней.", + "До 15 октября 1582 года — 27 октября по юлианскому календарю, с 15 октября 1582 года — 27 октября по григорианскому календарю.", + "В XX и XXI веках соответствует 14 октября по юлианскому календарю[1].", + "(c) Wikipedia" + ); + checker.test("23 октября — Всемирный день психического здоровья", "Тема 2025 года: Психическое здоровье на рабочем месте"); + + checker.randomTest(3, 10, 10, 3, ExtendedRandom.ENGLISH, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(10, 3, 5, 5, ExtendedRandom.RUSSIAN, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(4, 10, 10, 3, ExtendedRandom.GREEK, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(4, 10, 10, 3, WordStatChecker.DASH, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(3, 10, 10, 3, ExtendedRandom.ENGLISH, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(10, 3, 5, 5, ExtendedRandom.RUSSIAN, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(3, 10, 10, 3, ExtendedRandom.GREEK, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(3, 10, 10, 3, WordStatChecker.DASH, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(3, 10, 10, 10, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + + final int d = TestCounter.DENOMINATOR; + final int d2 = TestCounter.DENOMINATOR; + checker.randomTest(10, 10000 / d, 10, 10, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(10, 1, 10, 10, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(10, 1000 / d, 100 / d2, 100 / d2, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(4, 1000 / d, 10, 3000 / d, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(4, 1000 / d, 3000 / d, 10, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(10000 / d, 20, 10, 5, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(1000000 / d, 2, 2, 1, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + + checker.test(PRE_LOWER); + checker.test(POST_LOWER); + } + ); + } + + private List> answer(final Function> split, final String[][] text) { + final List parts = Arrays.stream(text) + .flatMap(Arrays::stream) + .filter(Predicate.not(String::isEmpty)) + .flatMap(split) + .peek(s -> {assert !s.isBlank();}) + .collect(Collectors.toList()); + if (reverse()) { + Collections.reverse(parts); + } + return parts.stream() + .collect(Collectors.toMap(String::toLowerCase, v -> 1, Integer::sum, LinkedHashMap::new)) + .entrySet().stream() + .map(Pair::of) + .sorted(c) + .toList(); + } + } +} diff --git a/java/wordStat/package-info.java b/java/wordStat/package-info.java new file mode 100644 index 0000000..b54b5f6 --- /dev/null +++ b/java/wordStat/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Word Statistics homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package wordStat; \ No newline at end of file diff --git a/java/wspp/WsppTest.java b/java/wspp/WsppTest.java new file mode 100644 index 0000000..16cdebc --- /dev/null +++ b/java/wspp/WsppTest.java @@ -0,0 +1,51 @@ +package wspp; + +import base.Named; +import base.Selector; + +import java.util.Comparator; +import java.util.Map; +import java.util.function.IntFunction; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class WsppTest { + // === Base + private static final Named>> INPUT = Named.of("", Comparator.comparingInt(e -> 0)); + private static final Named> ALL = Named.of("", size -> IntStream.range(0, size)); + private static final Named> WSPP = Named.of("", (r, l, L, g, G) -> g); + private static final Named NONE = Named.of("", ""); + + // === 3637 + private static final Named>> LENGTH = Named.of("", + Map.Entry.comparingByKey(Comparator.comparingInt(String::length))); + private static final Named> LAST = Named.of("Last", size -> IntStream.of(size - 1)); + private static final Named JAVA = Named.of("", "XHB7TmR9JF8="); + + // === 3839 + private static final Named> MIDDLE = Named.of("Middle", size -> IntStream.of(size / 2)); + + // === 3435 + public static final WsppTester.Extractor POSITION = (r, l, L, g, G) -> r + ":" + (G - g + 1); + + + // === Common + public static final Selector SELECTOR = new Selector(WsppTester.class) + .variant("Base", WsppTester.variant(INPUT, ALL, WSPP, NONE)) + .variant("3637", WsppTester.variant(LENGTH, LAST, WSPP, JAVA)) + .variant("3839", WsppTester.variant(LENGTH, MIDDLE, WSPP, JAVA)) + .variant("3435", WsppTester.variant(LENGTH, ALL, Named.of("Position", POSITION), JAVA)) + .variant("3233", WsppTester.variant(INPUT, ALL, Named.of("Pos", POSITION), JAVA)) + .variant("4142", WsppTester.variant(LENGTH, LAST, WSPP, JAVA)) + .variant("4749", WsppTester.variant(LENGTH, ALL, Named.of("Position", POSITION), JAVA)) + ; + + private WsppTest() { + } + + public static void main(final String... args) { + SELECTOR.main(args); + } +} diff --git a/java/wspp/WsppTester.java b/java/wspp/WsppTester.java new file mode 100644 index 0000000..ba571a5 --- /dev/null +++ b/java/wspp/WsppTester.java @@ -0,0 +1,139 @@ +package wspp; + +import base.ExtendedRandom; +import base.Named; +import base.Pair; +import base.TestCounter; +import wordStat.WordStatChecker; +import wordStat.WordStatTester; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class WsppTester { + private WsppTester() { + } + + public static Consumer variant( + final Named>> comparator, + final Named> selector, + final Named> extractor, + final Named extra + ) { + // Stream "magic" code. You do not expect to understand it + return counter -> WordStatChecker.test( + counter, + "Wspp" + comparator.name() + selector.name() + extractor.name() + extra.name(), + text -> { + final Map totals = Arrays.stream(text) + .flatMap(Arrays::stream) + .map(word -> word.toLowerCase(Locale.ROOT)) + .collect(Collectors.toMap(Function.identity(), k -> 1, Integer::sum, LinkedHashMap::new)); + final int[] lengths = Arrays.stream(text).mapToInt(a -> a.length).toArray(); + final int[] sizes = new int[lengths.length + 1]; + int start = 0; + for (int i = 0; i < lengths.length; i++) { + sizes[i] = start; + start += lengths[i]; + } + sizes[lengths.length] = start; + + final Map selected = IntStream.range(0, text.length).boxed() + .flatMap(r -> { + final String[] line = text[r]; + return IntStream.range(0, line.length).boxed() + .collect(Collectors.groupingBy( + w -> line[w].toLowerCase(Locale.ROOT), + Collectors.collectingAndThen( + Collectors.mapping( + w -> extractor.value().select( + r + 1, + w + 1, + line.length, + sizes[r] + w + 1, + sizes[lengths.length] + ), + Collectors.toUnmodifiableList() + ), + list -> selector.value() + .apply(list.size()) + .mapToObj(list::get) + .toList() + ) + )) + .entrySet().stream(); + } + ) + .collect(Collectors.groupingBy( + Map.Entry::getKey, + Collectors.flatMapping( + e -> e.getValue().stream(), + Collectors.mapping( + String::valueOf, + Collectors.mapping(" "::concat, Collectors.joining()) + ) + ) + )); + return totals.entrySet().stream() + .sorted(comparator.value()) + .map(e -> Pair.of(e.getKey(), e.getValue() + selected.get(e.getKey()))) + .collect(Collectors.toList()); + }, + checker -> { + final Pattern pattern = Pattern.compile(new String(Base64.getDecoder().decode("W15ccHtJc0xldHRlcn0nXHB7UGR9" + extra.value()), StandardCharsets.US_ASCII) + "]+"); + final String good = String.join("", pattern.split(WordStatTester.POST_LOWER)); + + checker.test(pattern, "To be, or not to be, that is the question:"); + checker.test( + pattern, + "Monday's child is fair of face.", + "Tuesday's child is full of grace." + ); + checker.test( + pattern, + "Шалтай-Болтай", + "Сидел на стене.", + "Шалтай-Болтай", + "Свалился во сне." + ); + + checker.randomTest(3, 10, 10, 3, ExtendedRandom.ENGLISH, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(10, 3, 5, 5, ExtendedRandom.RUSSIAN, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(3, 10, 10, 3, ExtendedRandom.GREEK, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(3, 10, 10, 3, WordStatChecker.DASH, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(3, 10, 10, 3, ExtendedRandom.ENGLISH, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(10, 3, 5, 5, ExtendedRandom.RUSSIAN, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(3, 10, 10, 3, ExtendedRandom.GREEK, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(3, 10, 10, 3, WordStatChecker.DASH, WordStatChecker.ADVANCED_DELIMITERS); + + checker.randomTest(10, 20, 10, 3, good, WordStatChecker.SIMPLE_DELIMITERS); + checker.randomTest(10, 20, 10, 3, good, WordStatChecker.ADVANCED_DELIMITERS); + + final int d = TestCounter.DENOMINATOR; + final int d2 = TestCounter.DENOMINATOR2; + checker.randomTest(100, 1000 / d, 1000 / d2, 1000 / d2, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(10, 1000 / d, 1000 / d2, 1000 / d2, good, WordStatChecker.ADVANCED_DELIMITERS); + + checker.randomTest(10000 / d, 20, 10, 5, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + checker.randomTest(1000000 / d, 2, 2, 1, WordStatChecker.ALL, WordStatChecker.ADVANCED_DELIMITERS); + + checker.test(pattern, WordStatTester.PRE_LOWER); + checker.test(pattern, WordStatTester.POST_LOWER); + } + ); + } + + @FunctionalInterface + public interface Extractor { + T select(int l, int li, int lt, int gi, int gt); + } +} diff --git a/java/wspp/package-info.java b/java/wspp/package-info.java new file mode 100644 index 0000000..117ede7 --- /dev/null +++ b/java/wspp/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Word Statistics++ homework + * of Introduction to Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package wspp; \ No newline at end of file diff --git a/test-rules.md b/test-rules.md new file mode 100644 index 0000000..6e0fdf5 --- /dev/null +++ b/test-rules.md @@ -0,0 +1,113 @@ +--- +gitea: none +include_toc: true +--- + +# Зачёт по дисциплине «Введение в программирование» + +## Расписание + +Досрочная сдача зачёта: + + * M3132-35: 30 декабря с 11:30, ауд. 2137 + * M3136-39: 30 декабря с 11:30, ауд. 2137 + +Сдача зачёта в сессию: + +* M3132-35: 10 января с 11:30, ауд. 1229 +* M3136-39: 20 января с 11:30, ауд. 2137 + + +Пересдача зачёта пройдёт: + +* M3132-39: TBA +* M3136-39: TBA + + +## Сдача зачёта + +Если вас устраивают ваши баллы, то зачёт можно не сдавать. +Вы можете сдавать зачёт либо досрочно, либо во время сессии, по вашему выбору. + +Для того, чтобы записаться на сдачу зачёта надо заполнить +[форму](https://docs.google.com/forms/d/e/1FAIpQLScjft8hZCjlfoeVicSJHnX_uMW7xpA5RxSMQwqhy6aXGZWCGw/viewform). +Для заполнения вам потребуются логин и пароль от репозитория. +Если не указано обратного, то на сдачу надо записаться до 9:00 дня сдачи. +Если вы не записались на сдачу вовремя, то вы можете прийти на зачёт, +но вы будете обработаны в ручном режиме после решения проблем всех записавшихся вовремя. +Время на обработку компенсировано не будет. + +Если у вас больше 59 баллов, то их округлят до 60 если вы заполните форму +и пообещаете не сдавать зачёт (это требуется отметить в форме). + + +## Формат заданий + +Вам будет выдано задание, связанное с пройденными темами. +В условии описано, что вам надо сделать и набор требований, +которым должно удовлетворять решение (примерно как в домашних заданиях). + +Некоторые задания рассчитаны на то, что вы адаптируте или скомпануете +код одного или нескольких ДЗ. +Если вы не сдали соответствующие ДЗ, то вы можете написать весь код с нуля, +но это будет сложнее. + +Если вам что-то не ясно в задании (например, оно кажется подозрительно простым), +то вы можете задать вопрос по условию. +Принимаются только вопросы о том, что надо сделать. +Как именно делать остаётся на ваше усмотрение. + +По решению должно быть очевидно как его запускать. +Например, назвать основной класс `Main` или по имени задания — +хорошая идея, а `FooBazFactoryProvider` — нет. + +Если вы хотете передать проверяющему какую-то дополнительную информацию +(например, о выбранном компромиссе между памятью и временем исполнения), +то напишите её либо в комментариях к коду, либо положите рядом с кодом +`README.md`. + +В решении вы можете использовать ваш код из ДЗ и код, +написанный преподавателями на лекциях. + + +## Порядок сдачи + + 1. Заранее проверьте, что у вас работает всё, необходимое для сдачи. + Претензии вида «у меня не работал компилятор/IDE/git/браузер/интернет» не принимаются. + 1. За 10 минут до начала зачёта сбор студентов в соответствующей аудитории. + Позаботьтесь, что бы вам хватило заряда ноутбука или принесите с собой удлинитель-тройник. + 1. Организационные вопросы. + В том числе, можно отказаться от участия, если записались по ошибке, + или вам больше не актуально. + 1. В момент *T* объявляется начало и выдаются билеты с заданиями. + 1. Ответы на вопросы по билетам до *T*+30 минут. + 1. Вы пишите решение и записываете его в каталог `java-solutions` зачётного репозитория + `https://www.kgeorgiy.info/git-students/year2025//prog-intro-exam`. + 1. В момент времени *T*+3 часа фиксируется состояние репозиториев. + 1. Проверяется код на состояние *T*+3 часа. Это может занять несколько дней. + 1. Результаты проверки отображаются в табличке, + комментарии по проверке загружаются в репозитории. + + +## Система оценки + +В репозитории есть код, решающий поставленную задачу (возможно не всю): +`20` баллов минус баллы за проблемы: + + * `#` — большая проблема (обычно не выполнено одно из требований задания): `−5` баллов + * `*` — средняя проблема (обычно нарушение неоднократно обсуждавшихся рекомендаций, + например, утечка ресурсов, если это не является основной целью задания): `−2` балла + * `-` — маленькая проблема (например, однократное нарушение правил оформления кода): `−1` балл + +Код (почти) отсутствует/не имеет отношения к заданию, преподаватель не нашёл ваш код: `−5` баллов + +Код не компилируется/синтаксически некорректен: `−10` баллов, дальнейшая проверка не осуществляется. + +Код списан: `−20` баллов + + +## Полезные ссылки + + 1. [Форма для записи на зачет](https://docs.google.com/forms/d/e/1FAIpQLScjft8hZCjlfoeVicSJHnX_uMW7xpA5RxSMQwqhy6aXGZWCGw/viewform) + 1. [Результаты записи на зачет](https://docs.google.com/spreadsheets/d/1g1XA_62KxWQHjXHsGoEAg0nisYQGZ_amLPVWXH_ftEA/edit?gid=1533773578#gid=1533773578) + 1. Экзаменационный репозиторий: `https://www.kgeorgiy.info/git-students/year2025//prog-intro-exam`