From c4eaad130f23808ddc817731ac2c64e9d96f9bf2 Mon Sep 17 00:00:00 2001 From: me Date: Tue, 17 Feb 2026 09:35:39 +0300 Subject: [PATCH] update --- .gitattributes | 1 + .gitea/workflows/search.yml | 17 + .gitignore | 3 + README.md | 49 +++ artifacts/search/BinarySearchTest.jar | Bin 0 -> 31747 bytes common/base/Asserts.java | 84 +++++ common/base/BaseChecker.java | 20 ++ common/base/Either.java | 95 +++++ common/base/ExtendedRandom.java | 89 +++++ common/base/Functional.java | 92 +++++ common/base/Log.java | 56 +++ common/base/MainChecker.java | 28 ++ common/base/Named.java | 15 + common/base/Pair.java | 44 +++ common/base/Runner.java | 185 ++++++++++ common/base/Selector.java | 143 ++++++++ common/base/TestCounter.java | 184 ++++++++++ common/base/Tester.java | 18 + common/base/Unit.java | 15 + common/base/package-info.java | 7 + common/common/Engine.java | 37 ++ common/common/EngineException.java | 12 + .../common/expression/ArithmeticBuilder.java | 268 ++++++++++++++ common/common/expression/BaseVariant.java | 201 +++++++++++ common/common/expression/Dialect.java | 57 +++ common/common/expression/Diff.java | 157 +++++++++ common/common/expression/Expr.java | 89 +++++ common/common/expression/ExprTester.java | 221 ++++++++++++ common/common/expression/Language.java | 64 ++++ common/common/expression/LanguageBuilder.java | 79 +++++ common/common/expression/Operation.java | 11 + common/common/expression/Operations.java | 326 ++++++++++++++++++ .../common/expression/OperationsBuilder.java | 29 ++ common/common/expression/Variant.java | 16 + java/search/BinarySearch.java | 65 ++++ java/search/BinarySearchTest.java | 137 ++++++++ java/search/IntList.java | 44 +++ java/search/package-info.java | 7 + lectures/README.md | 292 ++++++++++++++++ lectures/lec1/Magic.java | 19 + 40 files changed, 3276 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitea/workflows/search.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 artifacts/search/BinarySearchTest.jar create mode 100644 common/base/Asserts.java create mode 100644 common/base/BaseChecker.java create mode 100644 common/base/Either.java create mode 100644 common/base/ExtendedRandom.java create mode 100644 common/base/Functional.java create mode 100644 common/base/Log.java create mode 100644 common/base/MainChecker.java create mode 100644 common/base/Named.java create mode 100644 common/base/Pair.java create mode 100644 common/base/Runner.java create mode 100644 common/base/Selector.java create mode 100644 common/base/TestCounter.java create mode 100644 common/base/Tester.java create mode 100644 common/base/Unit.java create mode 100644 common/base/package-info.java create mode 100644 common/common/Engine.java create mode 100644 common/common/EngineException.java create mode 100644 common/common/expression/ArithmeticBuilder.java create mode 100644 common/common/expression/BaseVariant.java create mode 100644 common/common/expression/Dialect.java create mode 100644 common/common/expression/Diff.java create mode 100644 common/common/expression/Expr.java create mode 100644 common/common/expression/ExprTester.java create mode 100644 common/common/expression/Language.java create mode 100644 common/common/expression/LanguageBuilder.java create mode 100644 common/common/expression/Operation.java create mode 100644 common/common/expression/Operations.java create mode 100644 common/common/expression/OperationsBuilder.java create mode 100644 common/common/expression/Variant.java create mode 100644 java/search/BinarySearch.java create mode 100644 java/search/BinarySearchTest.java create mode 100644 java/search/IntList.java create mode 100644 java/search/package-info.java create mode 100644 lectures/README.md create mode 100644 lectures/lec1/Magic.java diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfdb8b7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf diff --git a/.gitea/workflows/search.yml b/.gitea/workflows/search.yml new file mode 100644 index 0000000..faeb097 --- /dev/null +++ b/.gitea/workflows/search.yml @@ -0,0 +1,17 @@ +name: Binary Search Test +on: + push: + pull_request: +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Compile Java + run: | + mkdir -p out + javac -d out $(find java common -name "*.java") + - name: Run Binary Search tests + run: | + java -ea -cp out search.BinarySearchTest Base diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a8a0461 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.class +*.iml +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce4ab42 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Тесты к курсу «Парадигмы программирования» + +[Условия домашних заданий](https://www.kgeorgiy.info/courses/paradigms/homeworks.html) + + +## Домашнее задание 2. Бинарный поиск [![BinarySearch Tests](https://git.fymio.us/me/paradigms-2026/actions/workflows/search.yml/badge.svg)](https://git.fymio.us/me/paradigms-2026/actions) + +Модификации + * *Базовая* ✅ + * Класс `BinarySearch` должен находиться в пакете `search` + * [Исходный код тестов](java/search/BinarySearchTest.java) + * [Откомпилированные тесты](artifacts/search/BinarySearchTest.jar) + + diff --git a/artifacts/search/BinarySearchTest.jar b/artifacts/search/BinarySearchTest.jar new file mode 100644 index 0000000000000000000000000000000000000000..47c113f53b26df92b0d43482db48299bb959cfaf GIT binary patch literal 31747 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 xnU3E?3+w+){9E4RCGzEq&u?Uz{r^P%bw#0{- 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/common/base/BaseChecker.java b/common/base/BaseChecker.java new file mode 100644 index 0000000..67bd57c --- /dev/null +++ b/common/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/common/base/Either.java b/common/base/Either.java new file mode 100644 index 0000000..8a3eca8 --- /dev/null +++ b/common/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/common/base/ExtendedRandom.java b/common/base/ExtendedRandom.java new file mode 100644 index 0000000..ac2b059 --- /dev/null +++ b/common/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/common/base/Functional.java b/common/base/Functional.java new file mode 100644 index 0000000..ef14dd5 --- /dev/null +++ b/common/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/common/base/Log.java b/common/base/Log.java new file mode 100644 index 0000000..00d9141 --- /dev/null +++ b/common/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/common/base/MainChecker.java b/common/base/MainChecker.java new file mode 100644 index 0000000..e526e7e --- /dev/null +++ b/common/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/common/base/Named.java b/common/base/Named.java new file mode 100644 index 0000000..befb254 --- /dev/null +++ b/common/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/common/base/Pair.java b/common/base/Pair.java new file mode 100644 index 0000000..8c27a31 --- /dev/null +++ b/common/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/common/base/Runner.java b/common/base/Runner.java new file mode 100644 index 0000000..7e30391 --- /dev/null +++ b/common/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)) { + 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/common/base/Selector.java b/common/base/Selector.java new file mode 100644 index 0000000..dc119b9 --- /dev/null +++ b/common/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/common/base/TestCounter.java b/common/base/TestCounter.java new file mode 100644 index 0000000..85fb9c9 --- /dev/null +++ b/common/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/common/base/Tester.java b/common/base/Tester.java new file mode 100644 index 0000000..d30260d --- /dev/null +++ b/common/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/common/base/Unit.java b/common/base/Unit.java new file mode 100644 index 0000000..290febf --- /dev/null +++ b/common/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/common/base/package-info.java b/common/base/package-info.java new file mode 100644 index 0000000..9ae2b5f --- /dev/null +++ b/common/base/package-info.java @@ -0,0 +1,7 @@ +/** + * Common homeworks test classes + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package base; \ No newline at end of file diff --git a/common/common/Engine.java b/common/common/Engine.java new file mode 100644 index 0000000..bb0b8c1 --- /dev/null +++ b/common/common/Engine.java @@ -0,0 +1,37 @@ +package common; + +import base.Asserts; + +import java.util.function.BiFunction; + +/** + * Test engine. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Engine { + Result prepare(String expression); + + Result evaluate(final Result prepared, double[] vars); + + Result toString(final Result prepared); + + default Result parse(final String expression) { + throw new UnsupportedOperationException(); + } + + record Result(String context, T value) { + public void assertEquals(final T expected) { + Asserts.assertEquals(context(), expected, value()); + } + + public Result cast(final BiFunction convert) { + return new Result<>(context(), convert.apply(value(), context())); + } + + @Override + public String toString() { + return context(); + } + } +} diff --git a/common/common/EngineException.java b/common/common/EngineException.java new file mode 100644 index 0000000..ffa0b68 --- /dev/null +++ b/common/common/EngineException.java @@ -0,0 +1,12 @@ +package common; + +/** + * Thrown on test engine error. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class EngineException extends RuntimeException { + public EngineException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/common/common/expression/ArithmeticBuilder.java b/common/common/expression/ArithmeticBuilder.java new file mode 100644 index 0000000..0532778 --- /dev/null +++ b/common/common/expression/ArithmeticBuilder.java @@ -0,0 +1,268 @@ +package common.expression; + +import java.util.List; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Basic arithmetics. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class ArithmeticBuilder implements OperationsBuilder { + private final BaseVariant variant; + private final F neg; + private final F add; + private final F sub; + private final F mul; + private final F div; + + public ArithmeticBuilder(final boolean varargs, final List variables) { + variant = new BaseVariant(varargs); + variables.forEach(this::variable); + + //noinspection Convert2MethodRef + variant.infix("+", 100, (a, b) -> a + b); + variant.infix("-", 100, (a, b) -> a - b); + variant.infix("*", 200, (a, b) -> a * b); + variant.infix("/", 200, (a, b) -> a / b); + variant.unary("negate", a -> -a); + + add = f("+", 2); + sub = f("-", 2); + mul = f("*", 2); + div = f("/", 2); + neg = f("negate", 1); + + basicTests(); + } + + public void basicTests() { + final List ops = List.of(neg, add, sub, mul, div); + variant.tests(() -> Stream.of( + Stream.of(variant.c()), + variant.getVariables().stream(), + ops.stream().map(F::c), + ops.stream().map(F::v), + ops.stream().map(F::r), + ops.stream().map(F::r), + Stream.of( + div.f(neg.r(), r()), + div.f(r(), mul.r()), + add.f(add.f(mul.r(), mul.r()), mul.r()), + sub.f(add.f(mul.r(), mul.f(r(), mul.f(r(), mul.r()))), mul.r()) + ) + ).flatMap(Function.identity())); + } + + @Override + public void constant(final String name, final String alias, final double value) { + alias(name, alias); + final ExprTester.Func expr = vars -> value; + variant.nullary(name, expr); + final Expr constant = Expr.nullary(name, expr); + variant.tests(() -> Stream.of( + neg.f(constant), + add.f(constant, r()), + sub.f(r(), constant), + mul.f(r(), constant), + div.f(constant, r()) + )); + } + + @Override + public void unary(final String name, final String alias, final DoubleUnaryOperator op) { + variant.unary(name, op); + variant.alias(name, alias); + unaryTests(name); + } + + private void unaryTests(final String name) { + final F op = f(name, 1); + variant.tests(() -> Stream.of( + op.c(), + op.v(), + op.f(sub.r()), + op.f(add.r()), + op.f(div.f(op.r(), add.r())), + add.f(op.f(op.f(add.r())), mul.f(r(), mul.f(r(), op.r()))) + )); + } + + @Override + public void binary(final String name, final String alias, final DoubleBinaryOperator op) { + variant.binary(name, op); + variant.alias(name, alias); + binaryTests(name); + } + + private void binaryTests(final String name) { + final F op = f(name, 2); + variant.tests(() -> Stream.of( + op.c(), + op.v(), + op.r(), + op.f(neg.r(), add.r()), + op.f(sub.r(), neg.r()), + op.f(neg.r(), op.r()), + op.f(op.r(), neg.r()) + )); + } + + private record F(String name, int arity, BaseVariant variant) { + public Expr f(final Expr... args) { + assert arity < 0 || arity == args.length; + return variant.f(name, args); + } + + public Expr v() { + return g(variant::v); + } + + public Expr c() { + return g(variant::c); + } + + public Expr r() { + return g(variant::r); + } + + private Expr g(final Supplier g) { + return f(Stream.generate(g).limit(arity).toArray(Expr[]::new)); + } + } + + private F f(final String name, final int arity) { + return new F(name, arity, variant); + } + + private Expr r() { + return variant.r(); + } + + private Expr f(final String name, final Expr... args) { + return variant.f(name, args); + } + + @Override + public void infix(final String name, final String alias, final int priority, final DoubleBinaryOperator op) { + variant.infix(name, priority, op); + variant.alias(name, alias); + binaryTests(name); + } + + + @Override + public void fixed( + final String name, + final String alias, + final int arity, + final ExprTester.Func f + ) { + variant.fixed(name, arity, f); + variant.alias(name, alias); + + if (arity == 1) { + unaryTests(name); + } else if (arity == 2) { + binaryTests(name); + } else if (arity == 3) { + final F op = f(name, 3); + variant.tests(() -> { + final Expr e1 = op.c(); + final Expr e2 = op.v(); + final Expr e3 = op.f(add.r(), sub.r(), mul.r()); + return Stream.of( + op.f(variant.c(), r(), r()), + op.f(r(), variant.c(), r()), + op.f(r(), r(), variant.c()), + op.f(variant.v(), mul.v(), mul.v()), + op.f(mul.v(), variant.v(), mul.v()), + op.f(mul.v(), r(), mul.v()), + op.r(), + e1, + e2, + e3, + op.f(e1, e2, e3) + ); + }); + } else if (arity == 4) { + final F op = f(name, 4); + variant.tests(() -> { + final Expr e1 = op.c(); + final Expr e2 = op.v(); + final Expr e3 = op.r(); + final Expr e4 = op.f(add.r(), sub.r(), mul.r(), div.r()); + return Stream.of( + op.r(), + op.r(), + op.r(), + e1, + e2, + e3, + e4, + op.f(e1, e2, e3, e4) + ); + }); + } else { + variant.tests(() -> Stream.concat( + Stream.of( + f(name, arity, variant::c), + f(name, arity, variant::v) + ), + IntStream.range(0, 10).mapToObj(i -> f(name, arity, variant::r)) + )); + } + } + + private Expr f(final String name, final int arity, final Supplier generator) { + return f(name, Stream.generate(generator).limit(arity).toArray(Expr[]::new)); + } + + @Override + public void any( + final String name, + final String alias, + final int minArity, + final int fixedArity, + final ExprTester.Func f + ) { + variant.alias(name, alias); + variant.any(name, minArity, fixedArity, f); + + if (variant.hasVarargs()) { + final F op = f(name, -1); + variant.tests(() -> Stream.of( + op.f(r()), + op.f(r(), r()), + op.f(r(), r(), r()), + op.f(r(), r(), r(), r()), + op.f(r(), r(), r(), r(), r()), + op.f(add.r(), r()), + op.f(r(), r(), sub.r()) + )); + } + + variant.tests(() -> IntStream.range(1, 10) + .mapToObj(i -> f(name, variant.hasVarargs() ? i : fixedArity, variant::r))); + } + + @Override + public void variable(final String name) { + variant.variable(name, variant.getVariables().size()); + } + + @Override + public void alias(final String name, final String alias) { + variant.alias(name, alias); + } + + @Override + public BaseVariant variant() { + return variant; + } +} diff --git a/common/common/expression/BaseVariant.java b/common/common/expression/BaseVariant.java new file mode 100644 index 0000000..30b370b --- /dev/null +++ b/common/common/expression/BaseVariant.java @@ -0,0 +1,201 @@ +package common.expression; + +import base.ExtendedRandom; +import common.expression.ExprTester.Func; + +import java.util.*; +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * Base expressions variant. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class BaseVariant implements Variant { + private static final int MAX_C = 1_000; + private static final Expr ZERO = c(0); + + private final ExtendedRandom random = new ExtendedRandom(getClass()); + private final boolean varargs; + + private final StringMap operators = new StringMap<>(); + private final StringMap nullary = new StringMap<>(); + private final StringMap variables = new StringMap<>(); + private final Map aliases = new HashMap<>(); + + private final Map priorities = new HashMap<>(); + + public final List>> tests = new ArrayList<>(); + + public BaseVariant(final boolean varargs) { + this.varargs = varargs; + } + + public List getTests() { + return tests.stream().flatMap(Supplier::get).toList(); + } + + public Expr randomTest(final int size) { + return generate(size / 10 + 2); + } + + private Expr generate(final int depth) { + return depth > 0 ? generateOp(depth) : r(); + } + + public Expr r() { + if (random.nextBoolean()) { + return variables.random(random); + } else if (nullary.isEmpty() || random.nextBoolean()){ + return c(); + } else { + return nullary.random(random); + } + } + + public Expr c() { + return random.nextBoolean() ? ZERO : c(random.nextInt(-MAX_C, MAX_C)); + } + + public Expr v() { + return random().randomItem(variables.values().toArray(Expr[]::new)); + } + + protected Expr generateOp(final int depth) { + if (random.nextInt(6) == 0 || operators.isEmpty()) { + return generateP(depth); + } else { + final Operator operator = operators.random(random); + final Expr[] args = Stream.generate(() -> generateP(depth)) + .limit(random.nextInt(operator.minArity, operator.maxArity)) + .toArray(Expr[]::new); + return f(operator.name, args); + } + } + + protected Expr generateP(final int depth) { + return generate(random.nextInt(depth)); + } + + public void tests(final Supplier> tests) { + this.tests.add(tests); + } + + public void fixed(final String name, final int arity, final Func f) { + op(name, arity, arity, f); + } + + public void op(final String name, final int minArity, final int maxArity, final Func f) { + operators.put(name, new Operator(name, minArity, maxArity, f)); + } + + public void any(final String name, final int minArity, final int fixedArity, final ExprTester.Func f) { + if (varargs) { + op(name, minArity, minArity + 5, f); + } else { + op(name, fixedArity, fixedArity, f); + } + } + + public void unary(final String name, final DoubleUnaryOperator answer) { + fixed(name, 1, args -> answer.applyAsDouble(args[0])); + } + + public void binary(final String name, final DoubleBinaryOperator answer) { + fixed(name, 2, args -> answer.applyAsDouble(args[0], args[1])); + } + + public void infix(final String name, final int priority, final DoubleBinaryOperator answer) { + binary(name, answer); + priorities.put(name, priority); + } + + public void nullary(final String name, final Func f) { + nullary.put(name, Expr.nullary(name, f)); + } + + public Expr f(final String name, final Expr... args) { + return Expr.f(name, operators.get(name), List.of(args)); + } + + protected Expr n(final String name) { + return nullary.get(name); + } + + public static Expr c(final int value) { + return Expr.constant(value); + } + + public Expr variable(final String name, final int index) { + final Expr variable = Expr.variable(name, index); + variables.put(name, variable); + return variable; + } + + public List getVariables() { + return List.copyOf(variables.values()); + } + + @Override + public ExtendedRandom random() { + return random; + } + + @Override + public boolean hasVarargs() { + return varargs; + } + + @Override + public Integer getPriority(final String op) { + return priorities.get(op); + } + + private record Operator(String name, int minArity, int maxArity, Func f) implements Func { + private Operator { + assert 0 <= minArity && minArity <= maxArity; + } + + @Override + public double applyAsDouble(final double[] args) { + return Arrays.stream(args).allMatch(Double::isFinite) ? f.applyAsDouble(args) : Double.NaN; + } + } + + private static class StringMap { + private final List names = new ArrayList<>(); + private final Map values = new HashMap<>(); + + public T get(final String name) { + return values.get(name); + } + + public T random(final ExtendedRandom random) { + return get(random.randomItem(names)); + } + + private boolean isEmpty() { + return values.isEmpty(); + } + + private void put(final String name, final T value) { + names.add(name); + values.put(name, value); + } + + private Collection values() { + return values.values(); + } + } + + public void alias(final String name, final String alias) { + aliases.put(name, alias); + } + + public String resolve(final String alias) { + return aliases.getOrDefault(alias, alias); + } +} diff --git a/common/common/expression/Dialect.java b/common/common/expression/Dialect.java new file mode 100644 index 0000000..d0b7603 --- /dev/null +++ b/common/common/expression/Dialect.java @@ -0,0 +1,57 @@ +package common.expression; + +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * Expression dialect. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Dialect { + private final Expr.Cata cata; + + private Dialect(final Expr.Cata cata) { + this.cata = cata; + } + + public Dialect(final String variable, final String constant, final BiFunction, String> nary) { + this(new Expr.Cata<>(variable::formatted, constant::formatted, name -> name, nary)); + } + + public Dialect(final String variable, final String constant, final String operation, final String separator) { + this(variable, constant, operation(operation, separator)); + } + + public static BiFunction, String> operation(final String template, final String separator) { + return (op, args) -> template.replace("{op}", op).replace("{args}", String.join(separator, args)); + } + + public Dialect renamed(final Function renamer) { + return updated(cata -> cata.withOperation(nary -> (name, args) -> nary.apply(renamer.apply(name), args))); + } + + public Dialect updated(final UnaryOperator> updater) { + return new Dialect(updater.apply(cata)); + } + + public String render(final Expr expr) { + return expr.cata(cata); + } + + public String meta(final String name, final String... args) { + return cata.operation(name, List.of(args)); + } + + public Dialect functional() { + return renamed(Dialect::toFunctional); + } + + private static String toFunctional(final String name) { + return name.chars().allMatch(Character::isUpperCase) + ? name.toLowerCase() + : Character.toLowerCase(name.charAt(0)) + name.substring(1); + } +} diff --git a/common/common/expression/Diff.java b/common/common/expression/Diff.java new file mode 100644 index 0000000..12e7ca0 --- /dev/null +++ b/common/common/expression/Diff.java @@ -0,0 +1,157 @@ +package common.expression; + +import base.Asserts; +import base.Named; +import common.Engine; + +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static common.expression.ExprTester.EPS; +import static common.expression.ExprTester.Test; + +/** + * Expression differentiator. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Diff { + private static final double D = 1e-6; + + private final int base; + private final Dialect dialect; + + public Diff(final int base, final Dialect dialect) { + this.dialect = dialect; + this.base = base; + } + + public void diff(final ExprTester tester, final boolean reparse) { + tester.addStage(() -> { + for (final Test expr : tester.language.getTests()) { + checkDiff(tester, expr, reparse, false); + } + }); + } + + private List> checkDiff( + final ExprTester tester, + final Test test, + final boolean reparse, + final boolean simplify + ) { + final List> results = new ArrayList<>(test.variables().size() + 1); + System.out.println(" Testing diff: " + test.parsed()); + + if (simplify) { + final Engine.Result simplified = tester.engine.prepare(dialect.meta("simplify", test.parsed())); + test.points().forEachOrdered(point -> { + final double[] vars = Arrays.stream(point).map(v -> v + base).toArray(); + tester.assertValue("simplified expression", simplified, vars, test.evaluate(vars)); + }); + results.add(tester.engine.toString(simplified)); + } + + final double[] indices = IntStream.range(0, test.variables().size()).mapToDouble(a -> a).toArray(); + for (final Expr variable : test.variables()) { + final List>> ways = new ArrayList<>(); + final String diffS = dialect.meta("diff", test.parsed(), dialect.render(variable)); + addWays("diff", tester, reparse, diffS, ways); + + if (simplify) { + final String simplifyS = dialect.meta("simplify", diffS); + results.add(tester.engine.toString(addWays("simplified", tester, reparse, simplifyS, ways))); + } + + final int index = (int) variable.evaluate(indices); + + test.points().forEachOrdered(point -> { + final double[] vars = Arrays.stream(point).map(v -> v + base).toArray(); + final double center = test.evaluate(vars); + if (ok(center)) { + final double lft = evaluate(test, vars, index, -D); + final double rt = evaluate(test, vars, index, D); + final double left = (center - lft) / D; + final double right = (rt - center) / D; + if (ok(lft) && ok(rt) && ok(left) && ok(right) && Math.abs(left - right) < EPS) { + for (final Named> way : ways) { + tester.assertValue( + "diff by %s, %s".formatted(dialect.render(variable), way.name()), + way.value(), vars, (left + right) / 2 + ); + } + } + } + }); + } + return results; + } + + private static Engine.Result addWays( + final String name, + final ExprTester tester, + final boolean reparse, + final String exprS, + final List>> ways + ) { + final Engine.Result exprR = tester.engine.prepare(exprS); + ways.add(Named.of(name, exprR)); + if (reparse) { + ways.add(Named.of("reparsed " + name, tester.parse(tester.engine.toString(exprR).value()))); + } + return exprR; + } + + private static boolean ok(final double value) { + final double abs = Math.abs(value); + return EPS < abs && abs < 1 / EPS; + } + + private static double evaluate(final Test test, final double[] vars, final int index, final double d) { + vars[index] += d; + final double result = test.evaluate(vars); + vars[index] -= d; + return result; + } + + public void simplify(final ExprTester tester) { + final List simplifications = tester.language.getSimplifications(); + if (simplifications == null) { + return; + } + + tester.addStage(() -> { + final List newSimplifications = new ArrayList<>(); + final List tests = tester.language.getTests(); + + for (int i = 0; i < simplifications.size(); i++) { + final Test expr = tests.get(i); + final int[] expected = simplifications.get(i); + final List> actual = checkDiff(tester, expr, true, true); + if (expected != null) { + for (int j = 0; j < expected.length; j++) { + final Engine.Result result = actual.get(j); + final int length = result.value().length(); + Asserts.assertTrue( + "Simplified length too long: %d instead of %d%s" + .formatted(length, expected[j], result.context()), + length <= expected[j] + ); + } + } else { + newSimplifications.add(actual.stream().mapToInt(result -> result.value().length()).toArray()); + } + } + if (!newSimplifications.isEmpty()) { + System.err.println(newSimplifications.stream() + .map(row -> Arrays.stream(row) + .mapToObj(Integer::toString) + .collect(Collectors.joining(", ", "{", "}"))) + .collect(Collectors.joining(", ", "new int[][]{", "}"))); + System.err.println(simplifications.size() + " " + newSimplifications.size()); + throw new AssertionError("Uncovered"); + } + }); + } +} diff --git a/common/common/expression/Expr.java b/common/common/expression/Expr.java new file mode 100644 index 0000000..308fe22 --- /dev/null +++ b/common/common/expression/Expr.java @@ -0,0 +1,89 @@ +package common.expression; + +import java.util.List; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntFunction; +import java.util.function.UnaryOperator; + +/** + * Expression instance. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class Expr { + private final ExprTester.Func answer; + /* There are no forall generics in Java, so using Object as placeholder. */ + private final Function, Object> coCata; + + private Expr(final ExprTester.Func answer, final Function, Object> coCata) { + this.answer = answer; + this.coCata = coCata; + } + + public T cata(final Cata cata) { + @SuppressWarnings("unchecked") + final Function, T> coCata = (Function, T>) (Function) this.coCata; + return coCata.apply(cata); + } + + public double evaluate(final double... vars) { + return answer.applyAsDouble(vars); + } + + static Expr f(final String name, final ExprTester.Func operator, final List args) { + Objects.requireNonNull(operator, "Unknown operation " + name); + return new Expr( + vars -> operator.applyAsDouble(args.stream().mapToDouble(arg -> arg.evaluate(vars)).toArray()), + cata -> cata.operation( + name, + args.stream().map(arg -> arg.cata(cata)).toList() + ) + ); + } + + static Expr constant(final int value) { + return new Expr(vars -> value, cata -> cata.constant(value)); + } + + static Expr variable(final String name, final int index) { + return new Expr(vars -> vars[index], cata -> cata.variable(name)); + } + + static Expr nullary(final String name, final ExprTester.Func answer) { + return new Expr(answer, cata -> cata.nullary(name)); + } + + /** + * Expression catamorphism. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ + public record Cata( + Function variable, + IntFunction constant, + Function nullary, + BiFunction, T> operation + ) { + public T variable(final String name) { + return variable.apply(name); + } + + public T constant(final int value) { + return constant.apply(value); + } + + public T nullary(final String name) { + return nullary.apply(name); + } + + public T operation(final String name, final List args) { + return operation.apply(name, args); + } + + public Cata withOperation(final UnaryOperator, T>> updater) { + return new Cata<>(variable, constant, nullary, updater.apply(operation)); + } + } +} diff --git a/common/common/expression/ExprTester.java b/common/common/expression/ExprTester.java new file mode 100644 index 0000000..e07f777 --- /dev/null +++ b/common/common/expression/ExprTester.java @@ -0,0 +1,221 @@ +package common.expression; + +import base.Asserts; +import base.ExtendedRandom; +import base.TestCounter; +import base.Tester; +import common.Engine; +import common.EngineException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToDoubleFunction; +import java.util.regex.Pattern; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Expressions tester. + * + * @author Niyaz Nigmatullin + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class ExprTester extends Tester { + public static final int N = 128; + public static final double EPS = 1e-3; + public static final int RANDOM_TESTS = 444; + + private final int randomTests; + /*package*/ final Engine engine; + /*package*/ final Language language; + private final List stages = new ArrayList<>(); + private final boolean testToString; + + private final Generator spoiler; + private final Generator corruptor; + + public static final Generator STANDARD_SPOILER = (input, expr, random, builder) -> builder + .add(input) + .add(addSpaces(input, random)); + + public ExprTester( + final TestCounter counter, + final int randomTests, + final Engine engine, + final Language language, + final boolean testToString, + final Generator spoiler, + final Generator corruptor + ) { + super(counter); + this.randomTests = randomTests; + this.engine = engine; + this.language = language; + this.testToString = testToString; + this.spoiler = spoiler; + this.corruptor = corruptor; + } + + private static final Predicate UNSAFE = Pattern.compile("[-\\p{Alnum}+*/.=&|^<>◀▶◁▷≤≥?⁰-⁹₀-₉:]").asPredicate(); + + private static boolean safe(final char ch) { + return !UNSAFE.test("" + ch); + } + + public static String addSpaces(final String expression, final ExtendedRandom random) { + String spaced = expression; + for (int n = StrictMath.min(10, 200 / expression.length()); n > 0;) { + final int index = random.nextInt(spaced.length() + 1); + final char c = index == 0 ? 0 : spaced.charAt(index - 1); + final char nc = index == spaced.length() ? 0 : spaced.charAt(index); + if ((safe(c) || safe(nc)) && c != '\'' && nc != '\'' && c != '"' && nc != '"') { + spaced = spaced.substring(0, index) + " " + spaced.substring(index); + n--; + } + } + return spaced; + } + + @Override + public void test() { + for (final Test test : language.getTests()) { + try { + test(test, prepared -> counter.scope( + "Testing: " + prepared, + () -> test.points().forEachOrdered(vars -> assertValue( + "original expression", + prepared, + vars, + test.evaluate(vars) + ))) + ); + } catch (final RuntimeException | AssertionError e) { + throw new AssertionError("Error while testing " + test.parsed() + ": " + e.getMessage(), e); + } + } + + counter.scope("Random tests", () -> testRandom(randomTests)); + stages.forEach(Runnable::run); + } + + public static int limit(final int variables) { + return (int) Math.floor(Math.pow(N, 1.0 / variables)); + } + + private void test(final Test test, final Consumer> check) { + final Consumer> fullCheck = parsed -> counter.test(() -> { + check.accept(parsed); + if (testToString) { + counter.test(() -> engine.toString(parsed).assertEquals(test.toStr())); + } + }); + fullCheck.accept(engine.prepare(test.parsed())); + spoiler.forEach(10, test, random(), input -> fullCheck.accept(parse(input))); + corruptor.forEach(3, test, random(), input -> input.assertError(this::parse)); + } + + public Engine.Result parse(final String expression) { + return engine.parse(expression); + } + + public void testRandom(final int n) { + for (int i = 0; i < n; i++) { + if (i % 100 == 0) { + counter.format("Completed %3d out of %d%n", i, n); + } + final double[] vars = language.randomVars(); + + final Test test = language.randomTest(i); + final double answer = test.evaluate(vars); + + test(test, prepared -> assertValue("random expression", prepared, vars, answer)); + } + } + + public void assertValue(final String context, final Engine.Result prepared, final double[] vars, final double expected) { + counter.test(() -> { + final Engine.Result result = engine.evaluate(prepared, vars); + Asserts.assertEquals("%n\tFor %s%s".formatted(context, result.context()), expected, result.value().doubleValue(), EPS); + }); + } + + public static int mode(final String[] args, final Class type, final String... modes) { + if (args.length == 0) { + System.err.println("ERROR: No arguments found"); + } else if (args.length > 1) { + System.err.println("ERROR: Only one argument expected, " + args.length + " found"); + } else if (!Arrays.asList(modes).contains(args[0])) { + System.err.println("ERROR: First argument should be one of: \"" + String.join("\", \"", modes) + "\", found: \"" + args[0] + "\""); + } else { + return Arrays.asList(modes).indexOf(args[0]); + } + System.err.println("Usage: java -ea " + type.getName() + " {" + String.join("|", modes) + "}"); + System.exit(1); + throw new AssertionError("Return from System.exit"); + } + + public void addStage(final Runnable stage) { + stages.add(stage); + } + + public interface Func extends ToDoubleFunction { + @Override + double applyAsDouble(double... args); + } + + public record Test(Expr expr, String parsed, String unparsed, String toStr, List variables) { + public double evaluate(final double... vars) { + return expr.evaluate(vars); + } + + public Stream points() { + final int n = limit(variables.size()); + return IntStream.range(0, N).mapToObj(i -> IntStream.iterate(i, j -> j / n) + .map(j -> j % n) + .limit(variables.size()) + .mapToDouble(j -> j) + .toArray()); + } + } + + public record BadInput(String prefix, String comment, String suffix) { + public String assertError(final Function> parse) { + try { + final Engine.Result parsed = parse.apply(prefix + suffix); + throw new AssertionError("Parsing error expected for '%s%s%s', got %s" + .formatted(prefix, comment, suffix, parsed.value())); + } catch (final EngineException e) { + return e.getCause().getMessage(); + } + } + } + + public interface Generator { + void generate(String input, Expr expr, ExtendedRandom random, Stream.Builder builder); + + static Generator empty() { + return (i, e, r, b) -> {}; + } + + default Generator combine(final Generator that) { + return (i, e, r, b) -> { + this.generate(i, e, r, b); + that.generate(i, e, r, b); + }; + } + + default void forEach(final int limit, final Test test, final ExtendedRandom random, final Consumer consumer) { + final Stream.Builder builder = Stream.builder(); + generate(test.unparsed(), test.expr(), random, builder); + builder.build() + .sorted(Comparator.comparingInt(Object::hashCode)) + .limit(limit) + .forEach(consumer); + } + } +} diff --git a/common/common/expression/Language.java b/common/common/expression/Language.java new file mode 100644 index 0000000..12f76a2 --- /dev/null +++ b/common/common/expression/Language.java @@ -0,0 +1,64 @@ +package common.expression; + +import common.expression.ExprTester.Test; + +import java.util.Collections; +import java.util.List; + +/** + * Expression language. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public class Language { + private final Dialect parsed; + private final Dialect unparsed; + private final Dialect toString; + private final BaseVariant variant; + private final List tests; + private final List simplifications; + + public Language( + final Dialect parsed, + final Dialect unparsed, + final Dialect toString, + final BaseVariant variant, + final List simplifications + ) { + this.parsed = parsed; + this.unparsed = unparsed; + this.toString = toString; + this.variant = variant; + + tests = variant.getTests().stream().map(this::test).toList(); + assert simplifications == null || simplifications.isEmpty() || simplifications.size() == tests.size(); + this.simplifications = simplifications != null && simplifications.isEmpty() + ? Collections.nCopies(tests.size(), null) : simplifications; + } + + private Test test(final Expr expr) { + return new Test( + expr, + parsed.render(expr), + unparsed.render(expr), + toString.render(expr), + variant.getVariables() + ); + } + + public Test randomTest(final int size) { + return test(variant.randomTest(size)); + } + + public double[] randomVars() { + return variant.random().getRandom().doubles().limit(variant.getVariables().size()).toArray(); + } + + public List getTests() { + return tests; + } + + public List getSimplifications() { + return simplifications; + } +} diff --git a/common/common/expression/LanguageBuilder.java b/common/common/expression/LanguageBuilder.java new file mode 100644 index 0000000..0756c90 --- /dev/null +++ b/common/common/expression/LanguageBuilder.java @@ -0,0 +1,79 @@ +package common.expression; + +import base.Selector; +import base.TestCounter; +import base.Tester; + +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.IntPredicate; + +/** + * Expression test builder. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class LanguageBuilder { + public final OperationsBuilder ops; + private List simplifications; + private Function, Expr.Cata> toStr = Function.identity(); + private ExprTester.Generator corruptor = ExprTester.Generator.empty(); + + public LanguageBuilder(final boolean testMulti, final List variables) { + ops = new ArithmeticBuilder(testMulti, variables); + } + + public static Selector.Composite selector( + final Class owner, + final IntPredicate testMulti, + final List variables, + final BiFunction tester, + final String... modes + ) { + return Selector.composite( + owner, + counter -> new LanguageBuilder(testMulti.test(counter.mode()), variables), + (builder, counter) -> tester.apply(builder, counter).test(), + modes + ); + } + + public static Selector.Composite selector( + final Class owner, + final IntPredicate testMulti, + final BiFunction tester, + final String... modes + ) { + return selector(owner, testMulti, List.of("x", "y", "z"), tester, modes); + } + + public Variant variant() { + return ops.variant(); + } + + public Language language(final Dialect parsed, final Dialect unparsed) { + final BaseVariant variant = ops.variant(); + return new Language(parsed.renamed(variant::resolve), unparsed.updated(toStr::apply), unparsed, variant, simplifications); + } + + public void toStr(final Function, Expr.Cata> updater) { + toStr = updater.compose(toStr); + } + + public Function, Expr.Cata> getToStr() { + return toStr; + } + + public void setSimplifications(final List simplifications) { + this.simplifications = simplifications; + } + + public void addCorruptor(final ExprTester.Generator corruptor) { + this.corruptor = this.corruptor.combine(corruptor); + } + + public ExprTester.Generator getCorruptor() { + return corruptor; + } +} diff --git a/common/common/expression/Operation.java b/common/common/expression/Operation.java new file mode 100644 index 0000000..93fd888 --- /dev/null +++ b/common/common/expression/Operation.java @@ -0,0 +1,11 @@ +package common.expression; + +import java.util.function.Consumer; + +/** + * Expression operation. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Operation extends Consumer { +} diff --git a/common/common/expression/Operations.java b/common/common/expression/Operations.java new file mode 100644 index 0000000..17378c9 --- /dev/null +++ b/common/common/expression/Operations.java @@ -0,0 +1,326 @@ +package common.expression; + +import base.Functional; +import base.Pair; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.OptionalDouble; +import java.util.function.*; +import java.util.stream.DoubleStream; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * Known expression operations. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("StaticMethodOnlyUsedInOneClass") +public enum Operations { + ; + + public static final Operation ARITH = builder -> { + builder.ops.alias("negate", "Negate"); + builder.ops.alias("+", "Add"); + builder.ops.alias("-", "Subtract"); + builder.ops.alias("*", "Multiply"); + builder.ops.alias("/", "Divide"); + }; + + public static final Operation NARY_ARITH = builder -> { + builder.ops.unary("negate", "Negate", a -> -a); + + builder.ops.any("+", "Add", 0, 2, arith(0, Double::sum)); + builder.ops.any("-", "Subtract", 1, 2, arith(0, (a, b) -> a - b)); + builder.ops.any("*", "Multiply", 0, 2, arith(1, (a, b) -> a * b)); + builder.ops.any("/", "Divide", 1, 2, arith(1, (a, b) -> a / b)); + }; + + // === Common + + public static Operation unary(final String name, final String alias, final DoubleUnaryOperator op) { + return builder -> builder.ops.unary(name, alias, op); + } + + public static Operation binary(final String name, final String alias, final DoubleBinaryOperator op) { + return builder -> builder.ops.binary(name, alias, op); + } + + public static ExprTester.Func arith(final double zero, final DoubleBinaryOperator f) { + return args -> args.length == 0 ? zero + : args.length == 1 ? f.applyAsDouble(zero, args[0]) + : Arrays.stream(args).reduce(f).orElseThrow(); + } + + + // === More common + + public record Op(String name, String alias, int minArity, ExprTester.Func f) { + public Operation fix(final int arity) { + return fix(arity, name.charAt(0) <= 0xff); + } + + public Operation fix(final int arity, final boolean unicode) { + assert arity >= minArity; + return fixed(name + (char) ((unicode ? '0' : '₀') + arity), alias + arity, arity, f); + } + + public Operation any(final int fixedArity) { + return checker -> checker.ops.any(name, alias, minArity, fixedArity, f); + } + } + + public static Op op(final String name, final String alias, final int minArity, final ExprTester.Func f) { + return new Op(name, alias, minArity, f); + } + + public static Op op1(final String alias, final int minArity, final ExprTester.Func f) { + return new Op(Character.toLowerCase(alias.charAt(0)) + alias.substring(1), alias, minArity, f); + } + + public static Op opS(final String name, final String alias, final int minArity, final ToDoubleFunction f) { + return op(name, alias, minArity, args -> f.applyAsDouble(Arrays.stream(args))); + } + + public static Op opO(final String name, final String alias, final int minArity, final Function f) { + return opS(name, alias, minArity, f.andThen(OptionalDouble::orElseThrow)::apply); + } + + public static Operation fixed(final String name, final String alias, final int arity, final ExprTester.Func f) { + return builder -> builder.ops.fixed(name, alias, arity, f); + } + + @SuppressWarnings("SameParameterValue") + public static Operation range(final int min, final int max, final Op... ops) { + final List operations = IntStream.rangeClosed(min, max) + .mapToObj(i -> Arrays.stream(ops).map(op -> op.fix(i))) + .flatMap(Function.identity()) + .toList(); + return builder -> operations.forEach(op -> op.accept(builder)); + } + + @SuppressWarnings("SameParameterValue") + public static Operation any(final int fixed, final Op... ops) { + final List operations = Arrays.stream(ops).map(op -> op.any(fixed)).toList(); + return builder -> operations.forEach(op -> op.accept(builder)); + } + + public static Operation infix( + final String name, + final String alias, + final int priority, + final DoubleBinaryOperator op + ) { + return checker -> checker.ops.infix(name, alias, priority, op); + } + + + // === Variables + public static final Operation VARIABLES = builder -> + Stream.of("y", "z", "t").forEach(builder.ops::variable); + + + // === TauPhi + + public static Operation constant(final String name, final double value) { + return builder -> builder.ops.constant(name, name, value); + } + + public static final Operation TAU = constant("tau", Math.PI * 2); + public static final Operation PHI = constant("phi", (1 + Math.sqrt(5)) / 2); + + + // === Less, Greater + + private static Op compare(final String name, final String alias, final IntPredicate p) { + return op(name, alias, 1, args -> IntStream.range(1, args.length) + .allMatch(i -> p.test(args[i - 1] == args[i] ? 0 : Double.compare(args[i - 1], args[i]))) + ? 1 : 0 + ); + } + + public static final Op LESS = compare("less", "Less", c -> c < 0); + public static final Op GREATER = compare("greater", "Greater", c -> c > 0); + + + // === LessEq, GreaterEq + public static final Op LESS_EQ = compare("lessEq", "LessEq", c -> c <= 0); + public static final Op GREATER_EQ = compare("greaterEq", "GreaterEq", c -> c >= 0); + + + // === Sinh, Cosh + public static final Operation SINH = unary("sinh", "Sinh", Math::sinh); + public static final Operation COSH = unary("cosh", "Cosh", Math::cosh); + + + // === Pow, Log + public static final Operation POW = binary("pow", "Power", Math::pow); + public static final Operation LOG = binary("log", "Log", (a, b) -> Math.log(Math.abs(b)) / Math.log(Math.abs(a))); + + + // === Normal (Multivariate normal distribution) + public static final Op NORMAL = op("normal", "Normal", 1, args -> + Math.exp(Arrays.stream(args).map(a -> a * a).sum() / -2) / Math.pow(2 * Math.PI, args.length / 2.0)); + + + // === Poly + public static final Op POLY = op("poly", "Poly", 0, args -> { + double[] pows = DoubleStream.iterate(1, p -> p * args[0]).limit(args.length - 1).toArray(); + return IntStream.range(0, args.length - 1).mapToDouble(i -> pows[i] * args[i + 1]).sum(); + }); + + + // === Clamp, wrap + public static final Operation CLAMP = fixed("clamp", "Clamp", 3, args -> + args[1] <= args[2] ? Math.min(Math.max(args[0], args[1]), args[2]) : Double.NaN); + public static final Operation SOFT_CLAMP = fixed("softClamp", "SoftClamp", 4, args -> + args[1] <= args[2] && args[3] > 0 + ? args[1] + (args[2] - args[1]) / (1 + Math.exp(args[3] * ((args[2] + args[1]) / 2 - args[0]))) + : Double.NaN); + + + // === SumCb + + private static double sumCb(final double... args) { + return Arrays.stream(args).map(a -> a * a * a).sum(); + } + + private static double meanCb(final double[] args) { + return sumCb(args) / args.length; + } + + public static final Op SUM_CB = op1("SumCb", 0, Operations::sumCb); + public static final Op MEAN_CB = op1("MeanCb", 1, Operations::meanCb); + public static final Op RMC = op1("Rmc", 1, args -> Math.cbrt(meanCb(args))); + public static final Op CB_MAX = op1("CbMax", 1, args -> args[0] * args[0] * args[0] / sumCb(args)); + + + // === SumTrig + + + private static Op sumF(final String name, final DoubleUnaryOperator f) { + return op("sum" + name, "Sum" + name, 0, args -> Arrays.stream(args).map(f).sum()); + } + + private static Op meanF(final String name, final DoubleUnaryOperator f) { + return op( + "mean" + name, "Mean" + name, 1, + args -> Arrays.stream(args).map(f).sum() / args.length + ); + } + + public static final Op SUM_SINH = sumF("Sinh", Math::sinh); + public static final Op SUM_COSH = sumF("Cosh", Math::cosh); + public static final Op MEAN_SINH = meanF("Sinh", Math::sinh); + public static final Op MEAN_COSH = meanF("Cosh", Math::cosh); + + + private static Op arcMeanF(final String name, final DoubleUnaryOperator arc, final DoubleUnaryOperator f) { + return op( + "am" + name.toLowerCase(), "AM" + name, 1, + args -> arc.applyAsDouble(Arrays.stream(args).map(f).sum() / args.length) + ); + } + + public static final Op AMSH = arcMeanF("SH", x -> Math.log(x + Math.sqrt(x * x + 1)), Math::sinh); + public static final Op AMCH = arcMeanF("CH", x -> Math.log(x + Math.sqrt(x * x - 1)), Math::cosh); + + public static final Operation ATAN = unary("atan", "ArcTan", Math::atan); + public static final Operation ATAN2 = binary("atan2", "ArcTan2", Math::atan2); + + public static final Operation ASINH = unary("asinh", "ArcSinh", x -> Math.log(x + Math.sqrt(x * x + 1))); + public static final Operation ACOSH = unary("acosh", "ArcCosh", x -> Math.log(x + Math.sqrt(x * x - 1))); + + + // === Cube + public static final Op CB_NORM = op1("CbNorm", 0, args -> Math.cbrt(sumCb(args))); + public static final Op REC_SUM_CB = op1("RecSumCb", 1, args -> 1 / sumCb(args)); + + public static final Operation CUBE = unary("cube", "Cube", a -> a * a * a); + public static final Operation CBRT = unary("cbrt", "Cbrt", Math::cbrt); + + // === Infix PowLog + public static final Operation INFIX_POW = infix("**", "IPow", -300, Math::pow); + public static final Operation INFIX_LOG = infix("//", "ILog", -300, (a, b) -> Math.log(Math.abs(b)) / Math.log(Math.abs(a))); + + // === Exp, Ln + public static final Operation EXP = unary("exp", "Exp", Math::exp); + public static final Operation LN = unary("ln", "Ln", Math::log); + + // === Trig + public static final Operation SIN = unary("sin", "Sin", Math::sin); + public static final Operation COS = unary("cos", "Cos", Math::cos); + + // === Parentheses + public static Operation parentheses(final String... vars) { + final List> variants = Functional.toPairs(vars); + return language -> { + if (variants.size() > 1) { + language.addCorruptor((input, expr, random, builder) -> { + for (final Pair from : variants) { + final int index = input.lastIndexOf(from.first()); + if (index >= 0) { + Pair to = from; + while (Objects.equals(to.second(), from.second())) { + to = random.randomItem(variants); + } + final String head = input.substring(0, index); + final String tail = input.substring(index + from.first().length()); + builder.add(new ExprTester.BadInput(head, "", to.first() + tail)); + builder.add(new ExprTester.BadInput(head, "", tail)); + } + final int insIndex = random.nextInt(input.length()); + builder.add(new ExprTester.BadInput( + input.substring(0, insIndex), + "", + (random.nextBoolean() ? from.first() : from.second()) + input.substring(insIndex) + )); + } + }); + } + language.toStr(cata -> cata.withOperation(operation -> (op, args) -> { + final String inner = operation.apply(op, args); + final int openIndex = inner.indexOf("("); + final int closeIndex = inner.lastIndexOf(")"); + if (openIndex < 0 || closeIndex < 0) { + return inner; + } + final String prefix = inner.substring(0, openIndex); + final String middle = inner.substring(openIndex + 1, closeIndex); + final String suffix = inner.substring(closeIndex + 1); + if (variants.stream().anyMatch(v -> prefix.contains(v.first()) || suffix.contains(v.second()))) { + return inner; + } + final Pair variant = variants.get(Math.abs(inner.hashCode() % variants.size())); + return prefix + variant.first() + middle + variant.second() + suffix; + })); + }; + } + + + // === Boolean + + public static int not(final int a) { + return 1 - a; + } + + public static DoubleBinaryOperator bool(final IntBinaryOperator op) { + return (a, b) -> op.applyAsInt(bool(a), bool(b)) == 0 ? 0 : 1; + } + + public static int bool(final double a) { + return a > 0 ? 1 : 0; + } + + public static final Operation NOT = unary("!", "Not", a -> not(bool(a))); + public static final Operation INFIX_AND = infix("&&", "And", 90, bool((a, b) -> a & b)); + public static final Operation INFIX_OR = infix("||", "Or", 80, bool((a, b) -> a | b)); + public static final Operation INFIX_XOR = infix("^^", "Xor", 70, bool((a, b) -> a ^ b)); + + // === IfSwitch + public static final Operation INFIX_IF = fixed("??,::", "If", 3, args -> bool(args[0]) == 1 ? args[1] : args[2]); + public static final Operation INFIX_SWITCH = fixed(":&,:|", "Switch", 3, args -> + bool(args[0]) == 1 ? bool(args[1]) & bool(args[2]) : bool(args[1]) | bool(args[2])); +} diff --git a/common/common/expression/OperationsBuilder.java b/common/common/expression/OperationsBuilder.java new file mode 100644 index 0000000..dc80919 --- /dev/null +++ b/common/common/expression/OperationsBuilder.java @@ -0,0 +1,29 @@ +package common.expression; + +import java.util.function.DoubleBinaryOperator; +import java.util.function.DoubleUnaryOperator; + +/** + * Operations builder. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface OperationsBuilder { + void constant(String name, String alias, double value); + + void variable(String name); + + void unary(String name, String alias, DoubleUnaryOperator op); + + void binary(String name, String alias, DoubleBinaryOperator op); + + void infix(String name, String alias, int priority, DoubleBinaryOperator op); + + void fixed(String name, String alias, int arity, ExprTester.Func f); + + void any(String name, String alias, int minArity, int fixedArity, ExprTester.Func f); + + void alias(String name, String alias); + + BaseVariant variant(); +} diff --git a/common/common/expression/Variant.java b/common/common/expression/Variant.java new file mode 100644 index 0000000..2aa8ba2 --- /dev/null +++ b/common/common/expression/Variant.java @@ -0,0 +1,16 @@ +package common.expression; + +import base.ExtendedRandom; + +/** + * Expression variant meta-info. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public interface Variant { + ExtendedRandom random(); + + boolean hasVarargs(); + + Integer getPriority(String op); +} diff --git a/java/search/BinarySearch.java b/java/search/BinarySearch.java new file mode 100644 index 0000000..4cb9d52 --- /dev/null +++ b/java/search/BinarySearch.java @@ -0,0 +1,65 @@ +package search; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class BinarySearch { + + public static void main(String[] args) { + IntList a = new IntList(); + int x = Integer.parseInt(args[0]); + + int n = args.length; + + for (int i = 1; i < n; i++) { + a.put(Integer.parseInt(args[i])); + } + + System.out.println(searchRecursive(x, a)); + // System.out.println(searchIterative(x, a)); + } + + static int searchIterative(int x, IntList a) { + if (a.getLength() == 0) { + return 0; + } + + int low = 0, + high = a.getLength() - 1; + + while (low <= high) { + int mid = low + (high - low) / 2; + + if (a.get(mid) <= x) { + high = mid - 1; + } else { + low = mid + 1; + } + } + + return low; + } + + static int searchRecursive(int x, IntList a) { + return searchRecursiveHelper(x, a, 0, a.getLength() - 1); + } + + private static int searchRecursiveHelper( + int x, + IntList a, + int low, + int high + ) { + if (low > high) { + return low; + } + + int mid = low + (high - low) / 2; + + if (a.get(mid) <= x) { + return searchRecursiveHelper(x, a, low, mid - 1); + } else { + return searchRecursiveHelper(x, a, mid + 1, high); + } + } +} diff --git a/java/search/BinarySearchTest.java b/java/search/BinarySearchTest.java new file mode 100644 index 0000000..3410b9b --- /dev/null +++ b/java/search/BinarySearchTest.java @@ -0,0 +1,137 @@ +package search; + +import base.MainChecker; +import base.Runner; +import base.Selector; +import base.TestCounter; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +public final class BinarySearchTest { + public static final int[] SIZES = {5, 4, 2, 1, 10, 50, 100, 200, 300}; + public static final int[] VALUES = new int[]{5, 4, 2, 1, 0, 10, 100, Integer.MAX_VALUE / 2}; + + private BinarySearchTest() { + } + + // === Base + /* package-private */ static int base(final int c, final int x, final int[] a) { + return IntStream.range(0, a.length).filter(i -> Integer.compare(a[i], x) != c).findFirst().orElse(a.length); + } + + + // === Common code + + public static final Selector SELECTOR = new Selector(BinarySearchTest.class) + .variant("Base", Solver.variant0("", Kind.DESC, BinarySearchTest::base)) + ; + + public static void main(final String... args) { + SELECTOR.main(args); + } + + /* package-private */ static Consumer variant(final String name, final Consumer variant) { + final String className = "BinarySearch" + name; + return counter -> variant.accept(new Variant(counter, new MainChecker(Runner.packages("search").args(className)))); + } + + /* package-private */ interface Solver { + static Consumer variant0(final String name, final Kind kind, final Solver solver) { + return variant(name, kind, true, solver); + } + + static Consumer variant1(final String name, final Kind kind, final Solver solver) { + return variant(name, kind, false, solver); + } + + private static Consumer variant(final String name, final Kind kind, final boolean empty, final Solver solver) { + final Sampler sampler = new Sampler(kind, true, true); + return BinarySearchTest.variant(name, vrt -> { + if (empty) { + solver.test(kind, vrt); + } + solver.test(kind, vrt, 0); + for (final int s1 : SIZES) { + final int size = s1 > 3 * TestCounter.DENOMINATOR ? s1 / TestCounter.DENOMINATOR : s1; + for (final int max : VALUES) { + solver.test(kind, vrt, sampler.sample(vrt, size, max)); + } + } + }); + } + + private static int[] probes(final int[] a, final int limit) { + return Stream.of( + Arrays.stream(a), + IntStream.range(1, a.length).map(i -> (a[i - 1] + a[i]) / 2), + IntStream.of( + 0, Integer.MIN_VALUE, Integer.MAX_VALUE, + a.length > 0 ? a[0] - 1 : -1, + a.length > 0 ? a[a.length - 1] + 1 : 1 + ) + ) + .flatMapToInt(Function.identity()) + .distinct() + .sorted() + .limit(limit) + .toArray(); + } + + Object solve(final int c, final int x, final int... a); + + default void test(final Kind kind, final Variant variant, final int... a) { + test(kind, variant, a, Integer.MAX_VALUE); + } + + default void test(final Kind kind, final Variant variant, final int[] a, final int limit) { + for (final int x : probes(a, limit)) { + variant.test(solve(kind.d, x, a), IntStream.concat(IntStream.of(x), Arrays.stream(a))); + } + } + } + + public enum Kind { + ASC(-1), DESC(1); + + public final int d; + + Kind(final int d) { + this.d = d; + } + } + + public record Variant(TestCounter counter, MainChecker checker) { + void test(final Object expected, final IntStream ints) { + final List input = ints.mapToObj(Integer::toString).toList(); + checker.testEquals(counter, input, List.of(expected.toString())); + } + + public void test(final Object expected, final int[] a) { + test(expected, Arrays.stream(a)); + } + } + + public record Sampler(Kind kind, boolean dups, boolean zero) { + public int[] sample(final Variant variant, final int size, final int max) { + final IntStream sorted = variant.counter.random().getRandom().ints(zero ? size : Math.max(size, 1), -max, max + 1).sorted(); + final int[] ints = (dups ? sorted : sorted.distinct()).toArray(); + if (kind == Kind.DESC) { + final int sz = ints.length; + for (int i = 0; i < sz / 2; i++) { + final int t = ints[i]; + ints[i] = ints[sz - i - 1]; + ints[sz - i - 1] = t; + } + } + return ints; + } + } +} diff --git a/java/search/IntList.java b/java/search/IntList.java new file mode 100644 index 0000000..c86ee4e --- /dev/null +++ b/java/search/IntList.java @@ -0,0 +1,44 @@ +package search; + +import java.util.Arrays; + +/** + * @author Nikita Doschennikov (me@fymio.us) + */ +public class IntList { + + protected int[] list = new int[8]; + protected int idx = 0; + + public IntList() {} + + public void put(int val) { + if (idx >= list.length) { + list = Arrays.copyOf(list, list.length * 2); + } + + list[idx++] = val; + } + + public int getLength() { + return idx; + } + + public int get(int index) { + return list[index]; + } + + public String toString() { + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < idx; i++) { + if (i == idx - 1) { + sb.append(String.valueOf(list[i]) + "\n"); + } else { + sb.append(String.valueOf(list[i]) + " "); + } + } + + return sb.toString(); + } +} diff --git a/java/search/package-info.java b/java/search/package-info.java new file mode 100644 index 0000000..286d139 --- /dev/null +++ b/java/search/package-info.java @@ -0,0 +1,7 @@ +/** + * Tests for Binary Search homework + * of Paradigms of Programming course. + * + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +package search; \ No newline at end of file diff --git a/lectures/README.md b/lectures/README.md new file mode 100644 index 0000000..e2df236 --- /dev/null +++ b/lectures/README.md @@ -0,0 +1,292 @@ +# Лекция 1. Программирование по контракту. + +Мы видим следующий код: + +```java +public class Magic { + int magic(int a, int n) { + int r = 1; + while (n != 0) { + if (n % 2 == 1) { + r *= a; + } + n /= 2; + a *= a; + } + return r; + } +} +``` + +Что это за код? + +Как понять: + +1. Пристально посмотреть. +2. Написать тесты. + * Это не поможет понять, работает ли код. Тесты могут только показать, что код **НЕ работает**. +3. Написать формальное доказательство. + +Для последнего пункта нам понадобится *теорема о структурной декомпозиции*. + +**Формулировка**: Любой код, который мы можем написать на каком-то языке, можно представить в виде замыкания следующих операций: + +1. Ничего +2. Последовательное исполнение операций (последовательность действий) +3. Присваивание +4. Ветвление +5. Цикл `while` + +С точки зрения теоремы, этих действий достаточно, чтобы написать любую содержательную программу. + +Можно ли ввести такую конструкцию при помощи примитивов выше: + +```java +try { + // some code here +} catch (...) { + // some code here +} finally { + // some code here +} +``` + +Давайте введем переменную `exitCode` и для каждого действия, в зависимости от того, успешно оно выполнилось или нет, будем обновлять `exitCode`. + +```java +if (exitCode == 0) { // без ошибок + // делаем дальше +} else { + // ничего не делаем +} +``` + +Попытаемся этим воспользоваться. + +Мы хотим наложить какие-то условия на нашу функцию, так что если эти условия на вход выполняются, то мы получим гарантированно правильное значение. Чтобы это доказать, нам помогут *тройки Хоара*. + +Хоар придумал quick sort. + +Тройка состоит из `P`, `C` и `Q`, где + +* `P` - пред-условие +* `C` - код, +* `Q` - пост-условие + +Если у нас есть код `C`, и мы выполняем какое-то пред-условие, то после исполнения кода, у нас будет выполнено какое-то пост-условие. + +Для операции *ничего*: + +```java +/* +Pred: Condition +Code: ; +Post: Condition +*/ +``` + +То условие, которое было до того, как мы сделали *ничего*, останется. + +Для *последовательности действий*: + +```java +/* +Pred: P1 +Code: ... +Post: Q1 +Pred: P2 +Code: ... +Post: Q2 +*/ +``` + +Таким образом из `Q1` должно следовать `P2`. + +```java +/* +Pred: P1 +Q1 -> P2 +Post: Q2 +*/ +``` + +Для *присваивания*: + +```java +/* +Pred: Q[x = expr] +Code: x = expr +Post: Q +*/ +``` + +Например + +```java +// Pred: (x = a + 2)[x = 6] -> a = 4 +x = a + 2 +// Post: x = 6 +``` + +То есть только при `a == 4`, выполнится пост-условие. + +Для операции `ветвление`: + +```java +/* + +// Pred: cond && P1 || ~cond && P2 +if (cond) { + // Pred: P1 + ... + // Post: Q1 +} else { + // Pred: P2 + ... + // Post: Q2 +} +// Q1 -> Q && Q2 -> Q +// Post: Q +*/ +``` + +Для цикла `while`: + +```java +/* +// Pred: P = I +while (cond) { + // Pred: I + // Post: I (инвариант цикла) +} +// Post: Q = I +*/ +``` + +Посмотрим на примере: + +```java +// Pred: true +// Post: r = A' ^ N' +int magic(int a, int n) { + // A' = a, N' = n -- начальные значения + + + // Pred: A' == a && N' == n + int r = 1; + // Post: r == 1 && a ** n * r == A' ** N' + + + // Pred: a ** n * r == A' ** N' + // Inv: (a ** n) * r = A' ** N' + while (n != 0) { + // Pred: a ** n * r == A' ** N' + if (n % 2 == 1) { + // Pred: a ** n * r == A' ** N' + r = r * a; + // Post: a ** (n - 1) * r = A' ** N' + + // Pred: a ** (n - 1) * r = A' ** N' + n = n - 1; + // Post: a ** n * r = A' ** N' + } + // Post: a ** n * r = A' ** N' + + // Pred: a ** n * r = A' ** N' + n /= 2; + // Post: a ** (2 * n) * r = A' ** N' + + + // Pred: a ** (2 * n) * r = A' ** N' + a = a * a; + // Post: a ** n * r = A' ** N' + } + // Post: a ** n * r = A' ** N' && n == 0 + + + // Pred: r = A' ** N' + return r; + // Post: r = A' ** N' +} +``` + +Мы формально доказали, что метод `magic()` возводит число `a` в степень `n`. Такая функция называется чистой, так как она не зависит от внешних переменных. + +То, что мы написали, называется контракт. Участниками контракта являются *пользователь* и *разработчик*. + +Мы, как разработчик, требуем пред-условие, и тогда можем гарантировать, что пост-условие будет выполняться. + +`interface` в java -- частный случай контракта. + +Это были случаи определения контракта для *чистых* функций. А как действовать в других случаях. Приведем пример + +```java +int x; + +// Pred: +// Post: +int add(int y) { + x = x + y; + return x; +} +``` + +Определим *модель* как некоторое состояние нашего класса. + +```java +/* +Model: x (целое число) +*/ +``` + +```java +int x; + +// Pred: true +// Post: x = x' + y (x' -- старый x) +int add(int y) { + x = x + y; + return x; +} +``` + +Здесь контракт соблюдается. + +А здесь: + +```java +int x; + +// Pred: true +// Post: x = x' + y (x' -- старый x) +int add(int y) { + x = x + y * 2; + return x / 2; +} +``` + +Контракт также соблюдается. То есть нам не важны детали реализации. +Можно даже сделать вот так: + +```java +private int x = 10; + +// Post: x = 0 +void init() { + x = 1; +} + +// Pred: true +// Post: x = x' + y +int add(int y) { + x = x + y * 2; + return (x - 1) / 2; +} + +// Pred: true +// Post: R := x +int get() { + return (x - 1) / 2; +} +``` + diff --git a/lectures/lec1/Magic.java b/lectures/lec1/Magic.java new file mode 100644 index 0000000..7d801d1 --- /dev/null +++ b/lectures/lec1/Magic.java @@ -0,0 +1,19 @@ +public class Magic { + + public static void main(String[] args) { + System.out.println(magic()); + } + + int magic(int a, int n) { + int r = 1; + while (n != 0) { + if (n % 2 == 1) { + r *= a; + } + n /= 2; + a *= a; + } + return r; + } +} +