
Калькулятор
Мы с вами будем делать КАЛЬКУЛЯТОР! В том числе парсер выражения.
Обратная польская запись
На вики описано хорошо.
Ваша программа должна уметь вычислять выражения в обратной польской записи и поддерживать:
- бинарные операции + - * /
- целые числа, в том числе отрицательные.
Пример использования:
public static void main(String[] args) {
int expected = (4 + 1) * 5;
int actual = calc("4 1 + 5 *");
if (expected == actual) {
System.out.println("Вычисления правильны, ответ: " + actual);
} else {
System.out.println("Ожидалось: " + expected + "; но получили " + actual);
}
}
Программируемый польский калькулятор
Усложним программу: добавим поддержку создания своих операторов произвольной арности, добавим поддержку float типов.
Проектируйте калькулятор, как библиотеку, чтобы пользователь мог дописать свой оператор и встроить в наш калькулятор, не меняя код самой библиотеки.
Главные нововведения:
- Парсинг float чисел
- Добавление арности для операторов. Если по-простому, арность - это количество параметров у оператора. Например, у оператора
+арность 2, у*тоже 2. Важный момент в том, что отрицательное число из прошлого пункта - это не число вида-31, а число31к которому применили операцию унарный минус-. Различать по контексту унарный минус с арностью 1 от минуса с арностью 2 может быть проблематично, поэтому в качестве оператора унарного минуса можно использоватьm - Создание операций произвольной арности, например,
sumдолжна просуммировать вообще всё что в стеке лежит. Но будьте внимательны, помимо оператораsumмогут быть и другие операторы, например1 2 3 sum 4 *
Нельзя использовать Double.parseDouble и все подобные методы. Сами встройте парсер числа.
Пример того, как должно всё это использоваться.
public static void main(String[] args) {
Calculator calc = new Calculator();
calc.register("+", new AdditionOperation());
calc.register("%", new ModOperation());
calc.register("sum", new SumOperation());
Tokenizer tokenizer = new PostfixTokenizer();
String equasion1 = "6 8 + 3 %";
List<String> tokens1 = tokenizer.tokenize(equasion1);
double result1 = calc.evaluate(tokens1);
System.out.println("Тест 1 результат: " + result1);
String equasion2 = "1 2 3 4 5 6 sum";
List<String> tokens2 = tokenizer.tokenize(equasion2);
double result2 = calc.evaluate(tokens2);
System.out.println("Тест 2 результат: " + result2);
}
Примерный вариант интерфейсов калькулятора
Общий интерфейс для каждой операции, к которой будет привязан оператор. Все +, -, /, * должны реализовывать его Operator.java
public interface Operator {
/**
* Возвращает количество аргументов, необходимых оператору.
* Если возвращает -1, значит оператор поглощает весь стек (вариативная арность).
*
* @return значение арности
*/
int getArity();
/**
* Выполнить операцию над стеком калькулятора и записать полученное значение в него.
*
* @param stack текущее состояние стека
*/
void execute(Stack<Double> stack);
}
Это интерфейс токенизатора, который будет разбивать входную строку на список отдельных токенов. Tokenizer.java
public interface Tokenizer {
/**
* Преобразование входной строки в список токенов.
*
* @param expression строка математического выражения
* @return список строковых токенов
*/
List<String> tokenize(String expression);
}
Дальше у него можно сделать класс PostfixTokenizer, который разобъёт входную строку на эти токены.
У класса Calculator методы представлены в примере, на данный момент их хватит
Нормальный калькулятор
Сделайте так, чтобы ваша библиотека поддерживала парсинг записей в инфиксной нотации: примение алгоритм сортировочной станции.
Важный момент касательно оператора sum из прошлого пункта. Написание 1 2 3 4 sum - это его обратная польская запись. В инфиксной записи он должен выглядеть так sum(1, 2, 3, 4) или хотя бы так sum(1 2 3 4)
public static void main(String[] args) {
CalculatorConfig infixConfig = new CalculatorConfig(
new InfixTokenizer(),
new ShuntingYardParser()
);
Calculator calc = new Calculator(infixConfig);
calc.register("+", new AdditionOperation());
calc.register("/", new DivisionOperation());
calc.register("%", new ModOperation());
calc.register("sum", new SumOperation());
String equasion = "sum(1, 2, 3, 4) / 5 + 9 % 2";
List<String> tokens = calc.tokenize(equasion);
List<String> polishTokens = calc.toPolishNotation(tokens);
double result = calc.evaluate(polishTokens);
System.out.println("Результат: " + result);
}
На что можно опереться здесь
В интерфейс Operator.java добавим два новых метода.
public interface Operator {
/**
* Возвращает количество аргументов, необходимых оператору.
* Если возвращает -1, значит оператор поглощает весь стек (вариативная арность).
*
* @return значение арности
*/
int getArity();
/**
* Получить значение приоритета, заданное для определённого оператора.
*
* @return значение приоритета
*/
int getPrecedence();
/**
* @return `true` если оператор левоассоциативен
*/
boolean isLeftAssociative();
/**
* Выполнить операцию над стеком калькулятора и записать полученное значение в него.
*
* @param stack текущее состояние стека
*/
void execute(Stack<Double> stack);
}
Добавьте новую реализацию интерфейса Tokenizer.java для инфиксной записи выражения - InfixTokenizer.
Класс ShuntingYardParser будет содержать внутри себя алгоритм сортировочной станции.
Под него можно выделить интерфейс типа такого
public interface ExpressionParser {
/**
* Переводим один формат токенов в другой.
*
* @param tokens изначальные токены
* @param registry мапа с зарегистрированными в калькуляторе токенами
* @return список преобразованныех токенов
*/
List<String> parse(List<String> tokens, Map<String, Operator> registry);
}
Класс CalculatorConfig можно добавить для того, чтобы инкапсулировать в себе возможные настройки для нашего Calculator.
Если он будет в себе содержать поля с типами интерфейсов, то такой калькулятор можно будет просто перенастроить снова
на работу с постфиксной записью. Достаточно будет сделать новый объект класса-конфига
CalculatorConfig postfixConfig = new CalculatorConfig(
new PostfixTokenizer(),
new IdentityParser()
);
Дополнение
Все предыдущие изыскания оперались на том, что основной метод интерфейса-токенизатора такой: List<String> tokenize(String expression);. Важный момент в том, что он возвращает список строк, которые являются токенами. Но...почему строк?
В идеальном мире интерфейс-токенизатора должен выглядеть так
public interface Tokenizer {
/**
* Преобразование входной строки в список токенов.
*
* @param expression строка математического выражения
* @return список токенов
*/
List<Token> tokenize(String expression);
}
Да, лучше будет сделать отдельный класс Token, который будет инкапсулировать в себе всю информацию о конкретном токене.
Чем это поможет? Если вы читаете это после реализации класса ShuntingYardParser, то вы скорее всего столкнулись с большим количеством проверок вида token.equals(")") и постоянных проверок строки на то, является ли она числом.
Так при токенизации сразу получится список готовых токенов Token, каждый из которых будет хранить и оригинальное строковое значние (типа "(", "+" или "67"), и готовое числовое значение (если это число), и тип токена.
Под тип токена лучше сделать отдельный энам TokenType типа такого
public enum TokenType {
NUMBER,
OPERATOR,
OPEN_PAREN,
CLOSE_PAREN,
UNKNOWN
}
Это очень сильно сократит вам код и ShuntingYardParser, и самого калькулятора.
И если вы будете выбрасывать какие-то исключения во время работы калькулятора (а вы должны это делать в определённых кейсах), можно кидать не стандартные исключения, а свои кастомные типа такого
class CalculatorException extends RuntimeException {
CalculatorException(String message) {
super(message);
}
}
Подсказки
Применяйте ООП.
Можете посмотреть мой старый стрим на твитче про Польский калькулятор.