This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sh text eol=lf
|
||||||
17
.gitea/workflows/search.yml
Normal file
17
.gitea/workflows/search.yml
Normal file
@@ -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
|
||||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.class
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
49
README.md
Normal file
49
README.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Тесты к курсу «Парадигмы программирования»
|
||||||
|
|
||||||
|
[Условия домашних заданий](https://www.kgeorgiy.info/courses/paradigms/homeworks.html)
|
||||||
|
|
||||||
|
|
||||||
|
## Домашнее задание 2. Бинарный поиск [](https://git.fymio.us/me/paradigms-2026/actions)
|
||||||
|
|
||||||
|
Модификации
|
||||||
|
* *Базовая* ✅
|
||||||
|
* Класс `BinarySearch` должен находиться в пакете `search`
|
||||||
|
* [Исходный код тестов](java/search/BinarySearchTest.java)
|
||||||
|
* [Откомпилированные тесты](artifacts/search/BinarySearchTest.jar)
|
||||||
|
|
||||||
|
<!--
|
||||||
|
## Домашнее задание 1. Обработка ошибок
|
||||||
|
|
||||||
|
Модификации
|
||||||
|
* *Base*
|
||||||
|
* Класс `ExpressionParser` должен реализовывать интерфейс
|
||||||
|
[ListParser](java/expression/exceptions/ListParser.java).
|
||||||
|
* Результат разбора должен реализовывать интерфейс
|
||||||
|
[ListExpression](java/expression/ListExpression.java).
|
||||||
|
* Нельзя использовать типы `long` и `double`
|
||||||
|
* Нельзя использовать методы классов `Math` и `StrictMath`
|
||||||
|
* [Исходный код тестов](java/expression/exceptions/ExceptionsTest.java)
|
||||||
|
* Первый аргумент: `easy` или `hard`
|
||||||
|
* Последующие аргументы: модификации
|
||||||
|
* *3637*
|
||||||
|
* Дополнительно реализуйте унарные операции
|
||||||
|
* `‖x‖` – модуль, `‖-5‖` равно 5;
|
||||||
|
* `³` – возведение в куб, `-5³` равно −125;
|
||||||
|
* `∛` – кубический корень, `∛-123` равно -4.
|
||||||
|
* *3839*
|
||||||
|
* Дополнительно реализуйте унарные операции:
|
||||||
|
* `‖x‖` – модуль, `‖-5‖` равно 5;
|
||||||
|
* `²` – возведение в квадрат, `-5²` равно 25;
|
||||||
|
* `√` – квадратный корень, `√24` равно 4;
|
||||||
|
* `³` – возведение в куб, `-5³` равно −125;
|
||||||
|
* `∛` – кубический корень, `∛-123` равно -4.
|
||||||
|
* *3435*
|
||||||
|
* Дополнительно реализуйте унарные операции:
|
||||||
|
* `‖x‖` – модуль, `‖-5‖` равно 5;
|
||||||
|
* `√` – квадратный корень, `√24` равно 4.
|
||||||
|
* *3233*
|
||||||
|
* Дополнительно реализуйте унарные операции:
|
||||||
|
* `‖x‖` – модуль числа, `‖-5‖` равно 5;
|
||||||
|
* `∛` – кубический корень, `∛-123` равно -4.
|
||||||
|
|
||||||
|
-->
|
||||||
BIN
artifacts/search/BinarySearchTest.jar
Normal file
BIN
artifacts/search/BinarySearchTest.jar
Normal file
Binary file not shown.
84
common/base/Asserts.java
Normal file
84
common/base/Asserts.java
Normal file
@@ -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 <T> void assertEquals(final String message, final List<T> expected, final List<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
common/base/BaseChecker.java
Normal file
20
common/base/BaseChecker.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
95
common/base/Either.java
Normal file
95
common/base/Either.java
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package base;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||||
|
*/
|
||||||
|
public interface Either<L, R> {
|
||||||
|
<NR> Either<L, NR> mapRight(final Function<? super R, NR> f);
|
||||||
|
<NR> Either<L, NR> flatMapRight(final Function<? super R, ? extends Either<L, NR>> f);
|
||||||
|
<T> T either(Function<? super L, ? extends T> lf, Function<? super R, ? extends T> rf);
|
||||||
|
|
||||||
|
boolean isRight();
|
||||||
|
|
||||||
|
L getLeft();
|
||||||
|
R getRight();
|
||||||
|
|
||||||
|
static <L, R> Either<L, R> right(final R value) {
|
||||||
|
return new Either<>() {
|
||||||
|
@Override
|
||||||
|
public <NR> Either<L, NR> mapRight(final Function<? super R, NR> f) {
|
||||||
|
return right(f.apply(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <NR> Either<L, NR> flatMapRight(final Function<? super R, ? extends Either<L, NR>> f) {
|
||||||
|
return f.apply(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T either(final Function<? super L, ? extends T> lf, final Function<? super R, ? extends T> rf) {
|
||||||
|
return rf.apply(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRight() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public L getLeft() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R getRight() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Right(%s)", value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static <L, R> Either<L, R> left(final L value) {
|
||||||
|
return new Either<>() {
|
||||||
|
@Override
|
||||||
|
public <NR> Either<L, NR> mapRight(final Function<? super R, NR> f) {
|
||||||
|
return left(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <NR> Either<L, NR> flatMapRight(final Function<? super R, ? extends Either<L, NR>> f) {
|
||||||
|
return left(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T either(final Function<? super L, ? extends T> lf, final Function<? super R, ? extends T> rf) {
|
||||||
|
return lf.apply(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isRight() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public L getLeft() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public R getRight() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("Left(%s)", value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
89
common/base/ExtendedRandom.java
Normal file
89
common/base/ExtendedRandom.java
Normal file
@@ -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> T randomItem(final T... items) {
|
||||||
|
return items[nextInt(items.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T randomItem(final List<T> items) {
|
||||||
|
return items.get(nextInt(items.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Random getRandom() {
|
||||||
|
return random;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> List<T> random(final int list, final Function<ExtendedRandom, T> generator) {
|
||||||
|
return Stream.generate(() -> generator.apply(this)).limit(list).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public double nextDouble() {
|
||||||
|
return random.nextDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <E> void shuffle(final List<E> all) {
|
||||||
|
Collections.shuffle(all, random);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
common/base/Functional.java
Normal file
92
common/base/Functional.java
Normal file
@@ -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 <T, R> List<R> map(final Collection<T> items, final Function<? super T, ? extends R> f) {
|
||||||
|
return items.stream().map(f).collect(Collectors.toUnmodifiableList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T, R> List<R> map(final List<T> items, final BiFunction<? super Integer, ? super T, ? extends R> f) {
|
||||||
|
return IntStream.range(0, items.size())
|
||||||
|
.mapToObj(i -> f.apply(i, items.get(i)))
|
||||||
|
.collect(Collectors.toUnmodifiableList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <K, T, R> Map<K, R> mapValues(final Map<K, T> map, final Function<T, R> f) {
|
||||||
|
return map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> f.apply(e.getValue())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public static <K, T> Map<K, T> mergeMaps(final Map<K, T>... 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 <T> List<T> concat(final Collection<? extends T>... items) {
|
||||||
|
final List<T> result = new ArrayList<>();
|
||||||
|
for (final Collection<? extends T> item : items) {
|
||||||
|
result.addAll(item);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> List<T> append(final Collection<T> collection, final T item) {
|
||||||
|
final List<T> list = new ArrayList<>(collection);
|
||||||
|
list.add(item);
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> List<List<T>> allValues(final List<T> 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 <K, V> V get(final Map<K, V> 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<Integer> values, final int d, final int c) {
|
||||||
|
for (int i = -d; i <= d; i++) {
|
||||||
|
values.add(c + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> void forEachPair(final T[] items, final BiConsumer<? super T, ? super T> consumer) {
|
||||||
|
assert items.length % 2 == 0;
|
||||||
|
IntStream.range(0, items.length / 2).forEach(i -> consumer.accept(items[i * 2], items[i * 2 + 1]));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static <T> List<Pair<T, T>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
56
common/base/Log.java
Normal file
56
common/base/Log.java
Normal file
@@ -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<Void> action(final Runnable action) {
|
||||||
|
return () -> {
|
||||||
|
action.run();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scope(final String name, final Runnable action) {
|
||||||
|
scope(name, action(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T scope(final String name, final Supplier<T> action) {
|
||||||
|
println(name);
|
||||||
|
indent++;
|
||||||
|
try {
|
||||||
|
return silentScope(name, action);
|
||||||
|
} finally {
|
||||||
|
indent--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T silentScope(final String ignoredName, final Supplier<T> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
28
common/base/MainChecker.java
Normal file
28
common/base/MainChecker.java
Normal file
@@ -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<String> run(final TestCounter counter, final String... input) {
|
||||||
|
return runner.run(counter, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> run(final TestCounter counter, final List<String> input) {
|
||||||
|
return runner.run(counter, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testEquals(final TestCounter counter, final List<String> input, final List<String> expected) {
|
||||||
|
runner.testEquals(counter, input, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
15
common/base/Named.java
Normal file
15
common/base/Named.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package base;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||||
|
*/
|
||||||
|
public record Named<T>(String name, T value) {
|
||||||
|
public static <T> Named<T> of(final String name, final T f) {
|
||||||
|
return new Named<>(name, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
44
common/base/Pair.java
Normal file
44
common/base/Pair.java
Normal file
@@ -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, S>(F first, S second) {
|
||||||
|
public static <F, S> Pair<F, S> of(final F first, final S second) {
|
||||||
|
return new Pair<>(first, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <F, S> Pair<F, S> of(final Map.Entry<F, S> e) {
|
||||||
|
return of(e.getKey(), e.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <F, S> UnaryOperator<Pair<F, S>> lift(final UnaryOperator<F> f, final UnaryOperator<S> s) {
|
||||||
|
return p -> of(f.apply(p.first), s.apply(p.second));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <F, S> BinaryOperator<Pair<F, S>> lift(final BinaryOperator<F> f, final BinaryOperator<S> s) {
|
||||||
|
return (p1, p2) -> of(f.apply(p1.first, p2.first), s.apply(p1.second, p2.second));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T, F, S> Function<T, Pair<F, S>> tee(
|
||||||
|
final Function<? super T, ? extends F> f,
|
||||||
|
final Function<? super T, ? extends S> s
|
||||||
|
) {
|
||||||
|
return t -> of(f.apply(t), s.apply(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "(" + first + ", " + second + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
public <R> Pair<F, R> second(final R second) {
|
||||||
|
return new Pair<>(first, second);
|
||||||
|
}
|
||||||
|
}
|
||||||
185
common/base/Runner.java
Normal file
185
common/base/Runner.java
Normal file
@@ -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<String> run(final TestCounter counter, final List<String> input);
|
||||||
|
|
||||||
|
default List<String> run(final TestCounter counter, final String... input) {
|
||||||
|
return run(counter, List.of(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
default void testEquals(final TestCounter counter, final List<String> input, final List<String> expected) {
|
||||||
|
counter.test(() -> {
|
||||||
|
final List<String> 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<String> run(String comment, TestCounter counter, List<String> input);
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Packages {
|
||||||
|
private final List<String> packages;
|
||||||
|
|
||||||
|
private Packages(final List<String> 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<String> 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<String> 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<String> 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<String> output = Files.readAllLines(ouf);
|
||||||
|
Files.delete(inf);
|
||||||
|
Files.delete(ouf);
|
||||||
|
return output;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
common/base/Selector.java
Normal file
143
common/base/Selector.java
Normal file
@@ -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<String> modes;
|
||||||
|
private final Set<String> variantNames = new LinkedHashSet<>();
|
||||||
|
private final Map<String, Consumer<TestCounter>> 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<TestCounter> 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<String> 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<String> 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<String, String> 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 <V extends Tester> Composite<V> composite(final Class<?> owner, final Function<TestCounter, V> factory, final String... modes) {
|
||||||
|
return new Composite<>(owner, factory, (tester, counter) -> tester.test(), modes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <V> Composite<V> composite(final Class<?> owner, final Function<TestCounter, V> factory, final BiConsumer<V, TestCounter> tester, final String... modes) {
|
||||||
|
return new Composite<>(owner, factory, tester, modes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getModes() {
|
||||||
|
return modes.isEmpty() ? List.of("~") : modes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getVariants() {
|
||||||
|
return List.copyOf(variants.keySet());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||||
|
*/
|
||||||
|
public static final class Composite<V> {
|
||||||
|
private final Selector selector;
|
||||||
|
private final Function<TestCounter, V> factory;
|
||||||
|
private final BiConsumer<V, TestCounter> tester;
|
||||||
|
private List<Consumer<? super V>> base;
|
||||||
|
|
||||||
|
private Composite(final Class<?> owner, final Function<TestCounter, V> factory, final BiConsumer<V, TestCounter> tester, final String... modes) {
|
||||||
|
selector = new Selector(owner, modes);
|
||||||
|
this.factory = factory;
|
||||||
|
this.tester = tester;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
public final Composite<V> variant(final String name, final Consumer<? super V>... parts) {
|
||||||
|
if ("Base".equalsIgnoreCase(name)) {
|
||||||
|
base = List.of(parts);
|
||||||
|
return v(name.toLowerCase());
|
||||||
|
} else {
|
||||||
|
return v(name, parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SafeVarargs
|
||||||
|
private Composite<V> v(final String name, final Consumer<? super V>... parts) {
|
||||||
|
selector.variant(name, counter -> {
|
||||||
|
final V variant = factory.apply(counter);
|
||||||
|
for (final Consumer<? super V> part : base) {
|
||||||
|
part.accept(variant);
|
||||||
|
}
|
||||||
|
for (final Consumer<? super V> part : parts) {
|
||||||
|
part.accept(variant);
|
||||||
|
}
|
||||||
|
tester.accept(variant, counter);
|
||||||
|
});
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Selector selector() {
|
||||||
|
return selector;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
common/base/TestCounter.java
Normal file
184
common/base/TestCounter.java
Normal file
@@ -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<String, ?> properties;
|
||||||
|
private final ExtendedRandom random;
|
||||||
|
|
||||||
|
private final long start = System.currentTimeMillis();
|
||||||
|
private int passed;
|
||||||
|
|
||||||
|
public TestCounter(final Class<?> owner, final int mode, final Map<String, ?> 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 <T> void testForEach(final Iterable<? extends T> items, final Consumer<? super T> action) {
|
||||||
|
for (final T item : items) {
|
||||||
|
test(() -> action.accept(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T testV(final Supplier<T> 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> T call(final String message, final SupplierE<T> 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> T fail(final String format, final Object... args) {
|
||||||
|
return fail(Asserts.error(format, args));
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T fail(final Throwable throwable) {
|
||||||
|
return fail(throwable, "%s: %s", throwable.getClass().getSimpleName(), throwable.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> 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 <T> Either<Exception, T> get(final SupplierE<T> 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<T> extends Supplier<Either<Exception, T>> {
|
||||||
|
T getE() throws Exception;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default Either<Exception, T> get() {
|
||||||
|
try {
|
||||||
|
return Either.right(getE());
|
||||||
|
} catch (final Exception e) {
|
||||||
|
return Either.left(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface RunnableE extends SupplierE<Void> {
|
||||||
|
void run() throws Exception;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
default Void getE() throws Exception {
|
||||||
|
run();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
common/base/Tester.java
Normal file
18
common/base/Tester.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
common/base/Unit.java
Normal file
15
common/base/Unit.java
Normal file
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
7
common/base/package-info.java
Normal file
7
common/base/package-info.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Common homeworks test classes
|
||||||
|
* of <a href="https://www.kgeorgiy.info/courses/paradigms/">Paradigms of Programming</a> course.
|
||||||
|
*
|
||||||
|
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||||
|
*/
|
||||||
|
package base;
|
||||||
37
common/common/Engine.java
Normal file
37
common/common/Engine.java
Normal file
@@ -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<X> {
|
||||||
|
Result<X> prepare(String expression);
|
||||||
|
|
||||||
|
Result<Number> evaluate(final Result<X> prepared, double[] vars);
|
||||||
|
|
||||||
|
Result<String> toString(final Result<X> prepared);
|
||||||
|
|
||||||
|
default Result<X> parse(final String expression) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
record Result<T>(String context, T value) {
|
||||||
|
public void assertEquals(final T expected) {
|
||||||
|
Asserts.assertEquals(context(), expected, value());
|
||||||
|
}
|
||||||
|
|
||||||
|
public <R> Result<R> cast(final BiFunction<T, String, R> convert) {
|
||||||
|
return new Result<>(context(), convert.apply(value(), context()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return context();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
common/common/EngineException.java
Normal file
12
common/common/EngineException.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
268
common/common/expression/ArithmeticBuilder.java
Normal file
268
common/common/expression/ArithmeticBuilder.java
Normal file
@@ -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<String> 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<F> 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<Expr> 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<Expr> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
201
common/common/expression/BaseVariant.java
Normal file
201
common/common/expression/BaseVariant.java
Normal file
@@ -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<Operator> operators = new StringMap<>();
|
||||||
|
private final StringMap<Expr> nullary = new StringMap<>();
|
||||||
|
private final StringMap<Expr> variables = new StringMap<>();
|
||||||
|
private final Map<String, String> aliases = new HashMap<>();
|
||||||
|
|
||||||
|
private final Map<String, Integer> priorities = new HashMap<>();
|
||||||
|
|
||||||
|
public final List<Supplier<Stream<Expr>>> tests = new ArrayList<>();
|
||||||
|
|
||||||
|
public BaseVariant(final boolean varargs) {
|
||||||
|
this.varargs = varargs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Expr> 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<Stream<Expr>> 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<Expr> 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<T> {
|
||||||
|
private final List<String> names = new ArrayList<>();
|
||||||
|
private final Map<String, T> 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<T> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
common/common/expression/Dialect.java
Normal file
57
common/common/expression/Dialect.java
Normal file
@@ -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<String> cata;
|
||||||
|
|
||||||
|
private Dialect(final Expr.Cata<String> cata) {
|
||||||
|
this.cata = cata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dialect(final String variable, final String constant, final BiFunction<String, List<String>, 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, List<String>, 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<String, String> renamer) {
|
||||||
|
return updated(cata -> cata.withOperation(nary -> (name, args) -> nary.apply(renamer.apply(name), args)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dialect updated(final UnaryOperator<Expr.Cata<String>> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
157
common/common/expression/Diff.java
Normal file
157
common/common/expression/Diff.java
Normal file
@@ -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 <X> void diff(final ExprTester<X> tester, final boolean reparse) {
|
||||||
|
tester.addStage(() -> {
|
||||||
|
for (final Test expr : tester.language.getTests()) {
|
||||||
|
checkDiff(tester, expr, reparse, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private <X> List<Engine.Result<String>> checkDiff(
|
||||||
|
final ExprTester<X> tester,
|
||||||
|
final Test test,
|
||||||
|
final boolean reparse,
|
||||||
|
final boolean simplify
|
||||||
|
) {
|
||||||
|
final List<Engine.Result<String>> results = new ArrayList<>(test.variables().size() + 1);
|
||||||
|
System.out.println(" Testing diff: " + test.parsed());
|
||||||
|
|
||||||
|
if (simplify) {
|
||||||
|
final Engine.Result<X> 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<Named<Engine.Result<X>>> 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<Engine.Result<X>> way : ways) {
|
||||||
|
tester.assertValue(
|
||||||
|
"diff by %s, %s".formatted(dialect.render(variable), way.name()),
|
||||||
|
way.value(), vars, (left + right) / 2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <X> Engine.Result<X> addWays(
|
||||||
|
final String name,
|
||||||
|
final ExprTester<X> tester,
|
||||||
|
final boolean reparse,
|
||||||
|
final String exprS,
|
||||||
|
final List<Named<Engine.Result<X>>> ways
|
||||||
|
) {
|
||||||
|
final Engine.Result<X> 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 <X> void simplify(final ExprTester<X> tester) {
|
||||||
|
final List<int[]> simplifications = tester.language.getSimplifications();
|
||||||
|
if (simplifications == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tester.addStage(() -> {
|
||||||
|
final List<int[]> newSimplifications = new ArrayList<>();
|
||||||
|
final List<Test> 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<Engine.Result<String>> actual = checkDiff(tester, expr, true, true);
|
||||||
|
if (expected != null) {
|
||||||
|
for (int j = 0; j < expected.length; j++) {
|
||||||
|
final Engine.Result<String> 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
89
common/common/expression/Expr.java
Normal file
89
common/common/expression/Expr.java
Normal file
@@ -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<Cata<Object>, Object> coCata;
|
||||||
|
|
||||||
|
private Expr(final ExprTester.Func answer, final Function<Cata<Object>, Object> coCata) {
|
||||||
|
this.answer = answer;
|
||||||
|
this.coCata = coCata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T cata(final Cata<T> cata) {
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
final Function<Cata<T>, T> coCata = (Function<Cata<T>, 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<Expr> 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<T>(
|
||||||
|
Function<String, T> variable,
|
||||||
|
IntFunction<T> constant,
|
||||||
|
Function<String, T> nullary,
|
||||||
|
BiFunction<String, List<T>, 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<T> args) {
|
||||||
|
return operation.apply(name, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cata<T> withOperation(final UnaryOperator<BiFunction<String, List<T>, T>> updater) {
|
||||||
|
return new Cata<>(variable, constant, nullary, updater.apply(operation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
221
common/common/expression/ExprTester.java
Normal file
221
common/common/expression/ExprTester.java
Normal file
@@ -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<E> 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<E> engine;
|
||||||
|
/*package*/ final Language language;
|
||||||
|
private final List<Runnable> stages = new ArrayList<>();
|
||||||
|
private final boolean testToString;
|
||||||
|
|
||||||
|
private final Generator<String> spoiler;
|
||||||
|
private final Generator<BadInput> corruptor;
|
||||||
|
|
||||||
|
public static final Generator<String> STANDARD_SPOILER = (input, expr, random, builder) -> builder
|
||||||
|
.add(input)
|
||||||
|
.add(addSpaces(input, random));
|
||||||
|
|
||||||
|
public ExprTester(
|
||||||
|
final TestCounter counter,
|
||||||
|
final int randomTests,
|
||||||
|
final Engine<E> engine,
|
||||||
|
final Language language,
|
||||||
|
final boolean testToString,
|
||||||
|
final Generator<String> spoiler,
|
||||||
|
final Generator<BadInput> 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<String> 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<Engine.Result<E>> check) {
|
||||||
|
final Consumer<Engine.Result<E>> 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<E> 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<E> prepared, final double[] vars, final double expected) {
|
||||||
|
counter.test(() -> {
|
||||||
|
final Engine.Result<Number> 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<double[]> {
|
||||||
|
@Override
|
||||||
|
double applyAsDouble(double... args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Test(Expr expr, String parsed, String unparsed, String toStr, List<Expr> variables) {
|
||||||
|
public double evaluate(final double... vars) {
|
||||||
|
return expr.evaluate(vars);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Stream<double[]> 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<String, Engine.Result<?>> 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<T> {
|
||||||
|
void generate(String input, Expr expr, ExtendedRandom random, Stream.Builder<? super T> builder);
|
||||||
|
|
||||||
|
static <T> Generator<T> empty() {
|
||||||
|
return (i, e, r, b) -> {};
|
||||||
|
}
|
||||||
|
|
||||||
|
default Generator<T> combine(final Generator<? extends T> 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<? super T> consumer) {
|
||||||
|
final Stream.Builder<T> builder = Stream.builder();
|
||||||
|
generate(test.unparsed(), test.expr(), random, builder);
|
||||||
|
builder.build()
|
||||||
|
.sorted(Comparator.comparingInt(Object::hashCode))
|
||||||
|
.limit(limit)
|
||||||
|
.forEach(consumer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
common/common/expression/Language.java
Normal file
64
common/common/expression/Language.java
Normal file
@@ -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<Test> tests;
|
||||||
|
private final List<int[]> simplifications;
|
||||||
|
|
||||||
|
public Language(
|
||||||
|
final Dialect parsed,
|
||||||
|
final Dialect unparsed,
|
||||||
|
final Dialect toString,
|
||||||
|
final BaseVariant variant,
|
||||||
|
final List<int[]> 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<Test> getTests() {
|
||||||
|
return tests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<int[]> getSimplifications() {
|
||||||
|
return simplifications;
|
||||||
|
}
|
||||||
|
}
|
||||||
79
common/common/expression/LanguageBuilder.java
Normal file
79
common/common/expression/LanguageBuilder.java
Normal file
@@ -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<int[]> simplifications;
|
||||||
|
private Function<Expr.Cata<String>, Expr.Cata<String>> toStr = Function.identity();
|
||||||
|
private ExprTester.Generator<ExprTester.BadInput> corruptor = ExprTester.Generator.empty();
|
||||||
|
|
||||||
|
public LanguageBuilder(final boolean testMulti, final List<String> variables) {
|
||||||
|
ops = new ArithmeticBuilder(testMulti, variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Selector.Composite<LanguageBuilder> selector(
|
||||||
|
final Class<?> owner,
|
||||||
|
final IntPredicate testMulti,
|
||||||
|
final List<String> variables,
|
||||||
|
final BiFunction<LanguageBuilder, TestCounter, Tester> 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<LanguageBuilder> selector(
|
||||||
|
final Class<?> owner,
|
||||||
|
final IntPredicate testMulti,
|
||||||
|
final BiFunction<LanguageBuilder, TestCounter, Tester> 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<String>, Expr.Cata<String>> updater) {
|
||||||
|
toStr = updater.compose(toStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Function<Expr.Cata<String>, Expr.Cata<String>> getToStr() {
|
||||||
|
return toStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSimplifications(final List<int[]> simplifications) {
|
||||||
|
this.simplifications = simplifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addCorruptor(final ExprTester.Generator<ExprTester.BadInput> corruptor) {
|
||||||
|
this.corruptor = this.corruptor.combine(corruptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExprTester.Generator<ExprTester.BadInput> getCorruptor() {
|
||||||
|
return corruptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
common/common/expression/Operation.java
Normal file
11
common/common/expression/Operation.java
Normal file
@@ -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<LanguageBuilder> {
|
||||||
|
}
|
||||||
326
common/common/expression/Operations.java
Normal file
326
common/common/expression/Operations.java
Normal file
@@ -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<DoubleStream> 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<DoubleStream, OptionalDouble> 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<Operation> 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<Operation> 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<Pair<String, String>> variants = Functional.toPairs(vars);
|
||||||
|
return language -> {
|
||||||
|
if (variants.size() > 1) {
|
||||||
|
language.addCorruptor((input, expr, random, builder) -> {
|
||||||
|
for (final Pair<String, String> from : variants) {
|
||||||
|
final int index = input.lastIndexOf(from.first());
|
||||||
|
if (index >= 0) {
|
||||||
|
Pair<String, String> 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, "<UNMATCHED PARENTHESIS->", to.first() + tail));
|
||||||
|
builder.add(new ExprTester.BadInput(head, "<REMOVED PARENTHESES>", tail));
|
||||||
|
}
|
||||||
|
final int insIndex = random.nextInt(input.length());
|
||||||
|
builder.add(new ExprTester.BadInput(
|
||||||
|
input.substring(0, insIndex),
|
||||||
|
"<INSERTED PARENTHESIS-->",
|
||||||
|
(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<String, String> 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]));
|
||||||
|
}
|
||||||
29
common/common/expression/OperationsBuilder.java
Normal file
29
common/common/expression/OperationsBuilder.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
16
common/common/expression/Variant.java
Normal file
16
common/common/expression/Variant.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
65
java/search/BinarySearch.java
Normal file
65
java/search/BinarySearch.java
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
java/search/BinarySearchTest.java
Normal file
137
java/search/BinarySearchTest.java
Normal file
@@ -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<TestCounter> variant(final String name, final Consumer<Variant> 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<TestCounter> variant0(final String name, final Kind kind, final Solver solver) {
|
||||||
|
return variant(name, kind, true, solver);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Consumer<TestCounter> variant1(final String name, final Kind kind, final Solver solver) {
|
||||||
|
return variant(name, kind, false, solver);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Consumer<TestCounter> 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<String> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
java/search/IntList.java
Normal file
44
java/search/IntList.java
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
java/search/package-info.java
Normal file
7
java/search/package-info.java
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Tests for <a href="https://www.kgeorgiy.info/courses/paradigms/homeworks.html#java-binary-search">Binary Search</a> homework
|
||||||
|
* of <a href="https://www.kgeorgiy.info/courses/paradigms/">Paradigms of Programming</a> course.
|
||||||
|
*
|
||||||
|
* @author Georgiy Korneev (kgeorgiy@kgeorgiy.info)
|
||||||
|
*/
|
||||||
|
package search;
|
||||||
292
lectures/README.md
Normal file
292
lectures/README.md
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
19
lectures/lec1/Magic.java
Normal file
19
lectures/lec1/Magic.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user