diff --git a/README.md b/README.md index e79684b..1248de2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,27 @@ # java-calculator 문자열 계산기 미션 저장소 +## 짝 프로그래밍 진행 절차 +1. 네비게이터, 드라이버 결정 + - 1st: 네비게이터 이동민, 드라이버 이찬규 + - 2nd: 네비게이터 이찬규, 드라이버 이동민 + +2. 기능 분석 + - 입력기: 사용자 입력을 받는다. + - 출력기: 프로그램 메시지를 출력한다. + - 계산기: 식을 계산한다. + - 파서: 문자열 식을 의미 단위로 나눈다. + +3. 테스트 케이스 작성 + +4. 구현 + +5. 다른 테스트 케이스 작성 + +6. 구현 + +7. 3~6 반복 + ## 단위 테스트 실습 - 문자열 계산기 - 다음 요구사항을 junit을 활용해 단위 테스트 코드를 추가해 구현한다. ## 요구사항 @@ -14,8 +35,8 @@ - 문자열을 입력 받은 후(scanner의 nextLine() 메소드 활용) 빈 공백 문자열을 기준으로 문자들을 분리해야 한다. ```java String value = scanner.nextLine(); -String[] values = value.split(" "); + String[] values = value.split(" "); ``` - 문자열을 숫자로 변경하는 방법 -`int number = Integer.parseInt("문자열");` +`int number = Integer.parseInt("문자열");` \ No newline at end of file diff --git a/build.gradle b/build.gradle index db8d2fd..b7e31be 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'java' apply plugin: 'eclipse' version = '1.0.0' -sourceCompatibility = 1.8 +sourceCompatibility = 11 repositories { mavenCentral() diff --git a/src/main/java/CalculatorApplication.java b/src/main/java/CalculatorApplication.java new file mode 100644 index 0000000..b52c67f --- /dev/null +++ b/src/main/java/CalculatorApplication.java @@ -0,0 +1,64 @@ +import domain.calculator.Calculator; +import domain.calculator.exception.InvalidInputException; +import domain.expression.operator.exception.DivideByZeroException; +import ui.printer.ConsolePrinter; +import ui.printer.Printer; +import ui.receiver.ConsoleReceiver; +import ui.receiver.Receiver; + +import java.util.Optional; + +public class CalculatorApplication { + private static final boolean CONTINUE_PROGRAM = true; + private static final String EXIT_REQUEST = "exit"; + + private Printer printer; + private Receiver receiver; + private Calculator calculator; + + public CalculatorApplication() { + printer = new ConsolePrinter(); + receiver = new ConsoleReceiver(); + calculator = new Calculator(); + } + + public void run() { + printer.greet(); + + while (CONTINUE_PROGRAM) { + printer.printWaitingForInputText(); + String expression = receiver.receiveExpression(); + if (expression.equalsIgnoreCase(EXIT_REQUEST)) { + break ; + } + + Optional optionalResult = calculateExpression(expression); + if (optionalResult.isEmpty()) { + continue ; + } + + int result = optionalResult.get(); + printer.printResult(result); + } + } + + private Optional calculateExpression(String expression) { + Integer result; + try { + result = calculator.calculate(expression); + } catch (DivideByZeroException e) { + System.out.println(e.getMessage()); + return Optional.empty(); + } catch (InvalidInputException e) { + System.out.println(e.getMessage()); + return Optional.empty(); + } + + return Optional.of(result); + } + + public static void main(String[] args) { + CalculatorApplication app = new CalculatorApplication(); + app.run(); + } +} \ No newline at end of file diff --git a/src/main/java/domain/calculator/Calculator.java b/src/main/java/domain/calculator/Calculator.java new file mode 100644 index 0000000..22d1ec6 --- /dev/null +++ b/src/main/java/domain/calculator/Calculator.java @@ -0,0 +1,21 @@ +package domain.calculator; + +import domain.expression.Expression; + +public class Calculator { + + public int calculate(String strExpression) { + Parser parser = new Parser(strExpression); + + int left = Integer.parseInt(parser.nextToken()); + Expression expression = Expression.from(left); + + while (parser.hasNext()) { + String operator = parser.nextToken(); + int right = Integer.parseInt(parser.nextToken()); + expression = Expression.of(expression, right, operator); + } + + return expression.evaluate(); + } +} \ No newline at end of file diff --git a/src/main/java/domain/calculator/ExpressionValidator.java b/src/main/java/domain/calculator/ExpressionValidator.java new file mode 100644 index 0000000..92f8703 --- /dev/null +++ b/src/main/java/domain/calculator/ExpressionValidator.java @@ -0,0 +1,55 @@ +package domain.calculator; + +import domain.calculator.exception.InvalidInputException; + +import java.util.List; +import java.util.regex.Pattern; + +public class ExpressionValidator { + + public void validateTokenList(List tokenList) { + long invalidTokenCount = tokenList.stream() + .filter(token -> !isNumber(token) && !isOperator(token)) + .count(); + + if (invalidTokenCount > 0) { + throw new InvalidInputException("올바르지 않은 입력입니다."); + } + } + + public void validateTokenSequence(List tokenList) { + throwIfConditionIsTrue(hasNotAnyToken(tokenList)); + + String firstToken = tokenList.get(0); + throwIfConditionIsTrue(!isNumber(firstToken)); + + for (int i = 1; i < tokenList.size(); i += 2) { + String operatorToken = tokenList.get(i); + throwIfConditionIsTrue(!isOperator(operatorToken)); + + if (isNotLastOperation(i, tokenList.size())) { + String rightOperandToken = tokenList.get(i + 1); + throwIfConditionIsTrue(!isNumber(rightOperandToken)); + } + } + } + + private void throwIfConditionIsTrue(boolean isConditionTrue) { + if (isConditionTrue) { + throw new InvalidInputException("올바르지 않은 입력입니다."); + } + } + + private boolean hasNotAnyToken(List tokenList) { + return (tokenList.size() == 0); + } + private boolean isNumber(String token) { + return Pattern.matches("^[0-9]+$", token); + } + private boolean isOperator(String token) { + return Pattern.matches("\\+|-|\\*|/", token); + } + private boolean isNotLastOperation(int index, int tokenListSize) { + return (index + 1 < tokenListSize); + } +} \ No newline at end of file diff --git a/src/main/java/domain/calculator/Parser.java b/src/main/java/domain/calculator/Parser.java new file mode 100644 index 0000000..63c2a37 --- /dev/null +++ b/src/main/java/domain/calculator/Parser.java @@ -0,0 +1,37 @@ +package domain.calculator; + +import java.util.*; +import java.util.stream.Collectors; + +public class Parser { + + private final Queue tokenQueue; + + public Parser(String expression) { + tokenQueue = new LinkedList<>(); + ExpressionValidator validator = new ExpressionValidator(); + + List tokenList = splitExpressionToList(expression); + validator.validateTokenList(tokenList); + validator.validateTokenSequence(tokenList); + tokenQueue.addAll(tokenList); + } + + private List splitExpressionToList(String expression) { + String[] tokens = expression.split(" +"); + return filterMeaningfulToken(tokens); + } + + private List filterMeaningfulToken(String[] tokens) { + return Arrays.stream(tokens) + .filter(token -> !token.equals("")) + .collect(Collectors.toList()); + } + + public String nextToken() { + return tokenQueue.poll(); + } + public boolean hasNext() { + return !tokenQueue.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/domain/calculator/exception/InvalidInputException.java b/src/main/java/domain/calculator/exception/InvalidInputException.java new file mode 100644 index 0000000..6385bf0 --- /dev/null +++ b/src/main/java/domain/calculator/exception/InvalidInputException.java @@ -0,0 +1,15 @@ +package domain.calculator.exception; + +public class InvalidInputException extends RuntimeException { + + private String message; + + public InvalidInputException(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/domain/expression/Expression.java b/src/main/java/domain/expression/Expression.java new file mode 100644 index 0000000..4121916 --- /dev/null +++ b/src/main/java/domain/expression/Expression.java @@ -0,0 +1,32 @@ + +package domain.expression; + +import domain.expression.operator.Operator; + +public class Expression { + + private int left; + private int right; + private Operator operator; + + private Expression(int left, int right, Operator operator) { + this.left = left; + this.right = right; + this.operator = operator; + } + + public static Expression of(int left, int right, String operator) { + return new Expression(left, right, Operator.from(operator)); + } + public static Expression of(Expression left, int right, String operator) { + int leftResult = left.evaluate(); + return new Expression(leftResult, right, Operator.from(operator)); + } + public static Expression from(int left) { + return of(left, 0, "+"); + } + + public int evaluate() { + return operator.operate(left, right); + } +} \ No newline at end of file diff --git a/src/main/java/domain/expression/operator/Addition.java b/src/main/java/domain/expression/operator/Addition.java new file mode 100644 index 0000000..2f8bc7b --- /dev/null +++ b/src/main/java/domain/expression/operator/Addition.java @@ -0,0 +1,8 @@ +package domain.expression.operator; + +public class Addition extends Operator { + + public int operate(int left, int right) { + return left + right; + } +} \ No newline at end of file diff --git a/src/main/java/domain/expression/operator/Division.java b/src/main/java/domain/expression/operator/Division.java new file mode 100644 index 0000000..becd121 --- /dev/null +++ b/src/main/java/domain/expression/operator/Division.java @@ -0,0 +1,13 @@ +package domain.expression.operator; + +import domain.expression.operator.exception.DivideByZeroException; + +public class Division extends Operator { + + public int operate(int left, int right) { + if (right == 0) { + throw new DivideByZeroException("0으로 나눌 수 없습니다."); + } + return left / right; + } +} \ No newline at end of file diff --git a/src/main/java/domain/expression/operator/Multiplication.java b/src/main/java/domain/expression/operator/Multiplication.java new file mode 100644 index 0000000..997f3fe --- /dev/null +++ b/src/main/java/domain/expression/operator/Multiplication.java @@ -0,0 +1,8 @@ +package domain.expression.operator; + +public class Multiplication extends Operator { + + public int operate(int left, int right) { + return left * right; + } +} \ No newline at end of file diff --git a/src/main/java/domain/expression/operator/Operator.java b/src/main/java/domain/expression/operator/Operator.java new file mode 100644 index 0000000..edcaa63 --- /dev/null +++ b/src/main/java/domain/expression/operator/Operator.java @@ -0,0 +1,17 @@ +package domain.expression.operator; + +public abstract class Operator { + + public static Operator from(String operator) { + if (operator.equals("+")) { + return new Addition(); + } else if (operator.equals("-")) { + return new Subtraction(); + } else if (operator.equals("*")) { + return new Multiplication(); + } + return new Division(); + } + + public abstract int operate(int left, int right); +} \ No newline at end of file diff --git a/src/main/java/domain/expression/operator/Subtraction.java b/src/main/java/domain/expression/operator/Subtraction.java new file mode 100644 index 0000000..2730357 --- /dev/null +++ b/src/main/java/domain/expression/operator/Subtraction.java @@ -0,0 +1,8 @@ +package domain.expression.operator; + +public class Subtraction extends Operator { + + public int operate(int left, int right) { + return left - right; + } +} \ No newline at end of file diff --git a/src/main/java/domain/expression/operator/exception/DivideByZeroException.java b/src/main/java/domain/expression/operator/exception/DivideByZeroException.java new file mode 100644 index 0000000..a2fd3ba --- /dev/null +++ b/src/main/java/domain/expression/operator/exception/DivideByZeroException.java @@ -0,0 +1,15 @@ +package domain.expression.operator.exception; + +public class DivideByZeroException extends RuntimeException { + + private String message; + + public DivideByZeroException(String message) { + this.message = message; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file diff --git a/src/main/java/ui/printer/ConsolePrinter.java b/src/main/java/ui/printer/ConsolePrinter.java new file mode 100644 index 0000000..bf5e783 --- /dev/null +++ b/src/main/java/ui/printer/ConsolePrinter.java @@ -0,0 +1,32 @@ +package ui.printer; + +public class ConsolePrinter implements Printer { + private final String GREET_MESSAGE = "계산기 프로그램입니다.\n" + + "숫자와 사칙연산 기호를 공백으로 구분하여 입력해주세요.\n" + + "프로그램을 종료하시려면 exit을 입력해주세요."; + + private final String INPUT_REQUEST_MESSAGE = "식 입력: "; + + private final String OUTPUT_MESSAGE = "answer: %d\n"; + private final String EXIT_MESSAGE = "프로그램을 종료합니다."; + + @Override + public void greet() { + System.out.println(GREET_MESSAGE); + } + + @Override + public void printWaitingForInputText() { + System.out.print(INPUT_REQUEST_MESSAGE); + } + + @Override + public void printResult(int result) { + System.out.printf(OUTPUT_MESSAGE, result); + } + + @Override + public void printExitMessage() { + System.out.println(EXIT_MESSAGE); + } +} \ No newline at end of file diff --git a/src/main/java/ui/printer/Printer.java b/src/main/java/ui/printer/Printer.java new file mode 100644 index 0000000..18494e2 --- /dev/null +++ b/src/main/java/ui/printer/Printer.java @@ -0,0 +1,12 @@ +package ui.printer; + +public interface Printer { + + void greet(); + + void printWaitingForInputText(); + + void printResult(int result); + + void printExitMessage(); +} \ No newline at end of file diff --git a/src/main/java/ui/receiver/ConsoleReceiver.java b/src/main/java/ui/receiver/ConsoleReceiver.java new file mode 100644 index 0000000..e87d170 --- /dev/null +++ b/src/main/java/ui/receiver/ConsoleReceiver.java @@ -0,0 +1,17 @@ +package ui.receiver; + +import java.util.Scanner; + +public class ConsoleReceiver implements Receiver { + + private final Scanner scanner; + + public ConsoleReceiver() { + scanner = new Scanner(System.in); + } + + @Override + public String receiveExpression() { + return scanner.nextLine(); + } +} \ No newline at end of file diff --git a/src/main/java/ui/receiver/Receiver.java b/src/main/java/ui/receiver/Receiver.java new file mode 100644 index 0000000..f7106a4 --- /dev/null +++ b/src/main/java/ui/receiver/Receiver.java @@ -0,0 +1,6 @@ +package ui.receiver; + +public interface Receiver { + + String receiveExpression(); +} \ No newline at end of file diff --git a/src/test/java/CalculatorTest.java b/src/test/java/CalculatorTest.java new file mode 100644 index 0000000..df8780e --- /dev/null +++ b/src/test/java/CalculatorTest.java @@ -0,0 +1,115 @@ +import domain.calculator.Calculator; +import domain.calculator.exception.InvalidInputException; +import domain.expression.operator.exception.DivideByZeroException; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class CalculatorTest { + + private Calculator calculator = new Calculator(); + + @Test + void 더하기_테스트() { + int test1Result = calculator.calculate("1 + 1"); + assertThat(test1Result).isEqualTo(2); + + int test2Result = calculator.calculate("3 + 7"); + assertThat(test2Result).isEqualTo(10); + + int test3Result = calculator.calculate("10 + 234"); + assertThat(test3Result).isEqualTo(244); + } + + @Test + void 빼기_테스트() { + int test1Result = calculator.calculate("1 - 1"); + assertThat(test1Result).isEqualTo(0); + + int test2Result = calculator.calculate("998 - 234"); + assertThat(test2Result).isEqualTo(764); + + int test3Result = calculator.calculate("10003 - 234"); + assertThat(test3Result).isEqualTo(9769); + } + + @Test + void 곱하기_테스트() { + int test1Result = calculator.calculate("2 * 34"); + assertThat(test1Result).isEqualTo(68); + + int test2Result = calculator.calculate("60 * 61"); + assertThat(test2Result).isEqualTo(3660); + + int test3Result = calculator.calculate("430 * 289"); + assertThat(test3Result).isEqualTo(124270); + } + + @Test + void 나누기_테스트() { + int test1Result = calculator.calculate("42 / 2"); + assertThat(test1Result).isEqualTo(21); + + int test2Result = calculator.calculate("60 / 5"); + assertThat(test2Result).isEqualTo(12); + + int test3Result = calculator.calculate("22 / 6"); + assertThat(test3Result).isEqualTo(3); + } + + @Test + void 나누기는_0으로_나눌때_DivideByZeroException_발생() { + assertThatExceptionOfType(DivideByZeroException.class) + .isThrownBy(() -> calculator.calculate("2 / 0")) + .as("0으로 나눌 수 없습니다."); + } + + @Test + void 두개의_연산자가_존재하는_식을_계산할_수_있다() { + int test1Result = calculator.calculate("44 + 23 - 3"); + assertThat(test1Result).isEqualTo(64); + + int test2Result = calculator.calculate("4 / 2 - 10"); + assertThat(test2Result).isEqualTo(-8); + + int test3Result = calculator.calculate("54 * 20 / 3"); + assertThat(test3Result).isEqualTo(360); + } + + @Test + void 두개_이상의_연산자가_존재하는_식을_계산할_수_있다() { + int test1Result = calculator.calculate("44 + 23 - 3 * 5"); + assertThat(test1Result).isEqualTo(320); + + int test2Result = calculator.calculate("4 / 2 - 10 * 10 / 4"); + assertThat(test2Result).isEqualTo(-20); + + int test3Result = calculator.calculate("54 * 20 / 3 / 120 + 2 - 5"); + assertThat(test3Result).isEqualTo(0); + } + + @Test + void 정상적이지_않은_공백_입력에_대해서도_올바른_답을_낼_수_있다() { + int test1Result = calculator.calculate(" 21 + 2 "); + assertThat(test1Result).isEqualTo(23); + + int test2Result = calculator.calculate("23 - 2 / 3"); + assertThat(test2Result).isEqualTo(7); + } + + @Test + void 정상적이지_않은_입력에_대해_InvalidInputException을_던진다() { + assertThatExceptionOfType(InvalidInputException.class) + .isThrownBy(() -> calculator.calculate("2 + // 3")); + + assertThatExceptionOfType(InvalidInputException.class) + .isThrownBy(() -> calculator.calculate("abc efij 2 + 3")); + + assertThatExceptionOfType(InvalidInputException.class) + .isThrownBy(() -> calculator.calculate("3 + 3 - 3 ###")); + + assertThatExceptionOfType(InvalidInputException.class) + .isThrownBy(() -> calculator.calculate("3 + 3 3 2 - 2")); + } +} \ No newline at end of file