Skip to content

Commit 3add6f3

Browse files
authored
Merge pull request #71 from valery1707/freelance.habr.com/tasks/472595
Habr: Разработать калькулятор на Java
2 parents 55a81fe + 29f4bf9 commit 3add6f3

File tree

2 files changed

+459
-0
lines changed

2 files changed

+459
-0
lines changed
Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,383 @@
1+
package name.valery1707.problem.habr.freelance;
2+
3+
import name.valery1707.problem.leet.code.IntegerToRomanK;
4+
import name.valery1707.problem.leet.code.RomanToIntegerK;
5+
6+
import java.math.BigDecimal;
7+
import java.math.MathContext;
8+
import java.math.RoundingMode;
9+
import java.util.ArrayList;
10+
import java.util.Arrays;
11+
import java.util.EnumSet;
12+
import java.util.List;
13+
import java.util.Objects;
14+
import java.util.Set;
15+
import java.util.Stack;
16+
import java.util.function.Function;
17+
import java.util.stream.Stream;
18+
19+
import static java.util.Arrays.binarySearch;
20+
import static java.util.Objects.checkIndex;
21+
import static java.util.Objects.requireNonNull;
22+
import static java.util.stream.Collectors.toUnmodifiableSet;
23+
24+
/**
25+
* <h1>Разработать калькулятор на Java</h1>
26+
* <p>
27+
* Требования:
28+
* <ol>
29+
* <li>Калькулятор умеет выполнять операции сложения, вычитания, умножения и деления с двумя числами: a + b, a - b, a * b, a / b.
30+
* Данные передаются в одну строку (смотри пример)!
31+
* Решения, в которых каждое число и арифметическая операция передаются с новой строки считаются неверными.</li>
32+
* <li>Калькулятор умеет работать как с арабскими (1,2,3,4,5…), так и с римскими (I,II,III,IV,V…) числами.</li>
33+
* <li>Калькулятор должен принимать на вход числа от 1 до 10 включительно, не более.
34+
* На выходе числа не ограничиваются по величине и могут быть любыми.</li>
35+
* <li>Калькулятор умеет работать только с целыми числами.</li>
36+
* <li>Калькулятор умеет работать только с арабскими или римскими цифрами одновременно, при вводе пользователем строки вроде 3 + II калькулятор должен выбросить исключение и прекратить свою работу.</li>
37+
* <li>При вводе римских чисел, ответ должен быть выведен римскими цифрами, соответственно, при вводе арабских - ответ ожидается арабскими.</li>
38+
* <li>При вводе пользователем неподходящих чисел приложение выбрасывает исключение и завершает свою работу.</li>
39+
* <li>При вводе пользователем строки, не соответствующей одной из вышеописанных арифметических операций, приложение выбрасывает исключение и завершает свою работу.</li>
40+
* <li>Результатом операции деления является целое число, остаток отбрасывается.</li>
41+
* <li>Результатом работы калькулятора с арабскими числами могут быть отрицательные числа и ноль.
42+
* Результатом работы калькулятора с римскими числами могут быть только положительные числа, если результат работы меньше единицы, выбрасывается исключение</li>
43+
* </ol>
44+
* <p>
45+
* Пример работы программы:
46+
* <ul>
47+
* <li></li>
48+
* Input: 1 + 2 Output: 3
49+
* Input: VI / III Output: II
50+
* Input: I - II Output: throws Exception //т.к. в римской системе нет отрицательных чисел
51+
* Input: I + 1 Output: throws Exception //т.к. используются одновременно разные системы счисления
52+
* Input: 1 Output: throws Exception //т.к. строка не является математической операцией
53+
* Input: 1 + 2 + 3 Output: throws Exception //т.к. формат математической операции не удовлетворяет заданию - два операнда и один оператор (+, -, /, *)
54+
* </ul>
55+
*
56+
* @see <a href="https://freelance.habr.com/tasks/472595">Заказ</a>
57+
*/
58+
public class Task_472595_Calculator {
59+
60+
private final Set<Parser<?>> parsers = Stream.concat(
61+
Stream.of(Operator.values()).<Parser<?>>map(OperatorParser::new),
62+
Stream.of(OperandParser.values())
63+
).collect(toUnmodifiableSet());
64+
65+
private final Set<? extends Validator> validators;
66+
67+
public Task_472595_Calculator(Set<? extends Validator> validators) {
68+
this.validators = validators;
69+
}
70+
71+
public Task_472595_Calculator() {
72+
this(EnumSet.allOf(Validators.class));
73+
}
74+
75+
public String calculate(String expression) {
76+
var items = parse(expression);
77+
validate(items);
78+
return calculate(items);
79+
}
80+
81+
private String calculate(List<Item> expression) {
82+
MathContext context = new MathContext(4, RoundingMode.HALF_UP);
83+
var operands = new Stack<Operand>();
84+
var operators = new Stack<Operator>();
85+
for (Item item : expression) {
86+
if (item instanceof Operand operand) {
87+
operands.push(operand);
88+
} else if (item instanceof Operator operator) {
89+
while (!operators.isEmpty()) {
90+
calculate(operators.pop(), operands, context);
91+
}
92+
operators.push(operator);
93+
}
94+
}
95+
while (!operators.isEmpty()) {
96+
calculate(operators.pop(), operands, context);
97+
}
98+
return operands.pop().format();
99+
}
100+
101+
private void calculate(Operator operator, Stack<Operand> operands, MathContext context) {
102+
operands.push(calculate(operator, operands.pop(), operands.pop(), context));
103+
}
104+
105+
private Operand calculate(Operator operator, Operand operand2, Operand operand1, MathContext context) {
106+
BigDecimal value = operator.func.apply(operand1.value(), operand2.value(), context);
107+
return operand1.build(value);
108+
}
109+
110+
private void validate(List<Item> items) {
111+
validators.stream()
112+
.map(it -> it.apply(items))
113+
.filter(Objects::nonNull).findFirst()
114+
.ifPresent(it -> {throw it;});
115+
}
116+
117+
private List<Item> parse(String expression) {
118+
requireNonNull(expression, "Expression is null");
119+
List<Item> items = new ArrayList<>();
120+
121+
var len = expression.length();
122+
var from = 0;
123+
var to = 0;
124+
Parser<?> curr = null;
125+
126+
while (to < len) {
127+
char c = expression.charAt(to);
128+
var next = parsers.stream().filter(it -> it.supports(c)).findFirst();
129+
if (next.isPresent()) {//Switch to something
130+
if (!next.get().equals(curr)) {//to something different
131+
if (curr != null) {//Finish previous parsing
132+
items.add(curr.parse(expression.substring(from, to)));
133+
from = to;
134+
}
135+
curr = next.get();
136+
}
137+
} else {//to skipping items
138+
if (curr != null) {//Finish previous parsing
139+
items.add(curr.parse(expression.substring(from, to)));
140+
from = to;
141+
}
142+
curr = null;
143+
from++;
144+
}
145+
to++;
146+
}
147+
if (curr != null) {//Finish previous parsing
148+
items.add(curr.parse(expression.substring(from, to)));
149+
}
150+
151+
return items;
152+
}
153+
154+
private sealed interface Parser<T extends Item> {
155+
156+
boolean supports(char c);
157+
158+
T parse(String value);
159+
160+
}
161+
162+
private sealed interface Item {
163+
}
164+
165+
private static final class OperatorParser implements Parser<Operator> {
166+
167+
private final Operator operator;
168+
169+
public OperatorParser(Operator operator) {
170+
this.operator = operator;
171+
}
172+
173+
@Override
174+
public boolean supports(char c) {
175+
return operator.code == c;
176+
}
177+
178+
@Override
179+
public Operator parse(String value) {
180+
checkIndex(value.length() - 1, 1);
181+
return operator;
182+
}
183+
184+
}
185+
186+
private enum Operator implements Item {
187+
ADD('+', (op1, op2, context) -> op1.add(op2, context)),
188+
SUBTRACT('-', (op1, op2, context) -> op1.subtract(op2, context)),
189+
DIVIDE('/', (op1, op2, context) -> op1.divide(op2, context)),
190+
MULTIPLY('*', (op1, op2, context) -> op1.multiply(op2, context));
191+
192+
private final char code;
193+
private final OperatorFunc func;
194+
195+
Operator(char code, OperatorFunc func) {
196+
this.code = code;
197+
this.func = func;
198+
}
199+
}
200+
201+
@FunctionalInterface
202+
private interface OperatorFunc {
203+
204+
BigDecimal apply(BigDecimal op1, BigDecimal op2, MathContext context);
205+
206+
}
207+
208+
private enum OperandParser implements Parser<Operand> {
209+
ARAB("0123456789", ArabOperand::parse),
210+
ROMAN("IVXLCDM", RomanOperand::parse);
211+
212+
private final char[] alphabet;
213+
private final Function<String, Operand> parser;
214+
215+
OperandParser(String alphabet, Function<String, Operand> parser) {
216+
this.alphabet = alphabet.toCharArray();
217+
Arrays.sort(this.alphabet);
218+
this.parser = parser;
219+
}
220+
221+
222+
@Override
223+
public boolean supports(char c) {
224+
return binarySearch(alphabet, c) >= 0;
225+
}
226+
227+
@Override
228+
public Operand parse(String value) {
229+
return parser.apply(value);
230+
}
231+
}
232+
233+
private static sealed abstract class Operand implements Item {
234+
235+
private final BigDecimal value;
236+
237+
protected Operand(BigDecimal value) {
238+
this.value = value;
239+
}
240+
241+
public BigDecimal value() {
242+
return value;
243+
}
244+
245+
public abstract String format();
246+
247+
public abstract Operand build(BigDecimal value);
248+
249+
}
250+
251+
private static final class ArabOperand extends Operand {
252+
253+
public ArabOperand(BigDecimal value) {
254+
super(value);
255+
}
256+
257+
@Override
258+
public String format() {
259+
return value().toBigInteger().toString();
260+
}
261+
262+
@Override
263+
public Operand build(BigDecimal value) {
264+
return new ArabOperand(value);
265+
}
266+
267+
public static ArabOperand parse(String value) {
268+
//todo MathContext
269+
return new ArabOperand(new BigDecimal(value));
270+
}
271+
272+
}
273+
274+
private static final class RomanOperand extends Operand {
275+
276+
public RomanOperand(BigDecimal value) {
277+
super(value);
278+
}
279+
280+
@Override
281+
public String format() {
282+
int num = value().toBigInteger().intValueExact();
283+
if (num < 1) {
284+
throw new CalculationException("Roman numeric system not support negative values");
285+
}
286+
//todo Zero
287+
return IntegerToRomanK.Implementation.simple.intToRoman(num);
288+
}
289+
290+
@Override
291+
public Operand build(BigDecimal value) {
292+
return new RomanOperand(value);
293+
}
294+
295+
public static RomanOperand parse(String value) {
296+
//todo MathContext?
297+
return new RomanOperand(new BigDecimal(RomanToIntegerK.Implementation.maps.romanToInt(value)));
298+
}
299+
300+
}
301+
302+
@FunctionalInterface
303+
private interface Validator extends Function<List<Item>, CalculationException> {
304+
}
305+
306+
enum Validators implements Validator {
307+
CHECK_EMPTY {
308+
@Override
309+
public CalculationException apply(List<Item> items) {
310+
return items.isEmpty() ? new CalculationException("Expression is empty") : null;
311+
}
312+
},
313+
CHECK_OPERATOR_COUNT_MIN {
314+
@Override
315+
public CalculationException apply(List<Item> items) {
316+
return items.stream().anyMatch(Operator.class::isInstance) ? null : new CalculationException("Expression without operator");
317+
}
318+
},
319+
CHECK_OPERATOR_COUNT_MAX {
320+
@Override
321+
public CalculationException apply(List<Item> items) {
322+
return items.stream().filter(Operator.class::isInstance).count() > 1 ? new CalculationException("Expression has too many operators") : null;
323+
}
324+
},
325+
CHECK_OPERATOR_COUNT {
326+
@Override
327+
public CalculationException apply(List<Item> items) {
328+
long operators = items.stream().filter(Operator.class::isInstance).count();
329+
long operands = items.stream().filter(Operand.class::isInstance).count();
330+
return (operators != (operands - 1)) ? new CalculationException("Expression has invalid operators count") : null;
331+
}
332+
},
333+
/**
334+
* Калькулятор должен принимать на вход числа от 1 до 10 включительно, не более.
335+
*/
336+
CHECK_VALUE_BOUNDS {
337+
private final BigDecimal MIN = BigDecimal.ONE;
338+
private final BigDecimal MAX = BigDecimal.TEN;
339+
340+
@Override
341+
public CalculationException apply(List<Item> items) {
342+
return items.stream()
343+
.filter(Operand.class::isInstance)
344+
.map(Operand.class::cast)
345+
.map(Operand::value)
346+
.filter(it -> !between(MIN, it, MAX))
347+
.findFirst()
348+
.map(it -> new CalculationException("Value out of range: " + it))
349+
.orElse(null);
350+
}
351+
},
352+
/**
353+
* Калькулятор умеет работать только с арабскими или римскими цифрами одновременно.
354+
*/
355+
CHECK_OPERAND_TYPE {
356+
@Override
357+
public CalculationException apply(List<Item> items) {
358+
var types = items.stream()
359+
.filter(Operand.class::isInstance)
360+
.map(Item::getClass)
361+
.collect(toUnmodifiableSet());
362+
if (types.size() != 1) {
363+
return new CalculationException("Expression with several numeric systems");
364+
} else {
365+
return null;
366+
}
367+
}
368+
},
369+
}
370+
371+
public static class CalculationException extends RuntimeException {
372+
373+
public CalculationException(String message) {
374+
super(message);
375+
}
376+
377+
}
378+
379+
private static <T extends Comparable<T>> boolean between(T min, T val, T max) {
380+
return val.compareTo(min) >= 0 && val.compareTo(max) <= 0;
381+
}
382+
383+
}

0 commit comments

Comments
 (0)