commit c4eaad130f23808ddc817731ac2c64e9d96f9bf2 Author: me Date: Tue Feb 17 09:35:39 2026 +0300 update 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 0000000..47c113f Binary files /dev/null and b/artifacts/search/BinarySearchTest.jar differ diff --git a/common/base/Asserts.java b/common/base/Asserts.java new file mode 100644 index 0000000..17ea3e6 --- /dev/null +++ b/common/base/Asserts.java @@ -0,0 +1,84 @@ +package base; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +/** + * @author Georgiy Korneev (kgeorgiy@kgeorgiy.info) + */ +@SuppressWarnings("StaticMethodOnlyUsedInOneClass") +public final class Asserts { + static { + Locale.setDefault(Locale.US); + } + + private Asserts() { + } + + public static void assertEquals(final String message, final Object expected, final Object actual) { + final String reason = String.format("%s:%n expected `%s`,%n actual `%s`", + message, toString(expected), toString(actual)); + assertTrue(reason, Objects.deepEquals(expected, actual)); + } + + public static String toString(final Object value) { + if (value != null && value.getClass().isArray()) { + final String result = Arrays.deepToString(new Object[]{value}); + return result.substring(1, result.length() - 1); + } else { + return Objects.toString(value); + } + } + + public static void assertEquals(final String message, final List expected, final List actual) { + for (int i = 0; i < Math.min(expected.size(), actual.size()); i++) { + assertEquals(message + ":" + (i + 1), expected.get(i), actual.get(i)); + } + assertEquals(message + ": Number of items", expected.size(), actual.size()); + } + + public static void assertTrue(final String message, final boolean value) { + if (!value) { + throw error("%s", message); + } + } + + public static void assertEquals(final String message, final double expected, final double actual, final double precision) { + assertTrue( + String.format("%s: Expected %.12f, found %.12f", message, expected, actual), + isEqual(expected, actual, precision) + ); + } + + public static boolean isEqual(final double expected, final double actual, final double precision) { + final double error = Math.abs(actual - expected); + return error <= precision + || error <= precision * Math.abs(expected) + || !Double.isFinite(expected) + || Math.abs(expected) > 1e100 + || Math.abs(expected) < precision && !Double.isFinite(actual); + } + + public static void assertSame(final String message, final Object expected, final Object actual) { + assertTrue(String.format("%s: expected same objects: %s and %s", message, expected, actual), expected == actual); + } + + public static void checkAssert(final Class c) { + if (!c.desiredAssertionStatus()) { + throw error("You should enable assertions by running 'java -ea %s'", c.getName()); + } + } + + public static AssertionError error(final String format, final Object... args) { + final String message = String.format(format, args); + return args.length > 0 && args[args.length - 1] instanceof Throwable + ? new AssertionError(message, (Throwable) args[args.length - 1]) + : new AssertionError(message); + } + + public static void printStackTrace(final String message) { + new Exception(message).printStackTrace(System.out); + } +} diff --git a/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; + } +} +