diff --git a/build.gradle b/build.gradle index 8172fb7..d8b34eb 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'eclipse' group = 'camp.nextstep' version = '1.0.0' -sourceCompatibility = '1.8' +sourceCompatibility = '11' repositories { mavenCentral() @@ -16,4 +16,4 @@ dependencies { test { useJUnitPlatform() -} +} \ No newline at end of file diff --git a/src/main/java/calculator/StringCalculatorApplication.java b/src/main/java/calculator/StringCalculatorApplication.java new file mode 100644 index 0000000..5b70dc1 --- /dev/null +++ b/src/main/java/calculator/StringCalculatorApplication.java @@ -0,0 +1,16 @@ +package calculator; + +import calculator.model.StringCalculator; +import calculator.model.StringParser; +import calculator.utils.StringExpression; +import calculator.view.InputView; +import calculator.view.OutputView; + +public class StringCalculatorApplication { + public static void main(String[] args) { + String[] expressionTokens = new StringParser(new InputView().getInputString()).parse(); + StringExpression stringExpression = new StringExpression(expressionTokens).validate(); + int calculationResult = new StringCalculator(stringExpression).calculate(); + new OutputView().printCalculationResult(calculationResult); + } +} diff --git a/src/main/java/calculator/model/Operator.java b/src/main/java/calculator/model/Operator.java new file mode 100644 index 0000000..999b388 --- /dev/null +++ b/src/main/java/calculator/model/Operator.java @@ -0,0 +1,50 @@ +package calculator.model; + +import calculator.utils.StringException; +import java.util.Arrays; + +public enum Operator { + PLUS("+") { + @Override + public int calculate(int operand1, int operand2) { + return operand1 + operand2; + } + }, + MINUS("-") { + @Override + public int calculate(int operand1, int operand2) { + return operand1 - operand2; + } + }, + DIVIDE("/") { + @Override + public int calculate(int operand1, int operand2) { + try { + return operand1 / operand2; + } catch (ArithmeticException e) { + throw new StringException(StringException.INVALID_DIVIDE_VALUE); + } + } + }, + MULTIPLY("*") { + @Override + public int calculate(int operand1, int operand2) { + return operand1 * operand2; + } + }; + + private final String operator; + + Operator(String operator) { + this.operator = operator; + } + + public static Operator findOperator(String operator) { + return Arrays.stream(values()) + .filter(value -> value.operator.equals(operator)) + .findAny() + .orElseThrow(() -> new StringException(StringException.INVALID_OPERATOR)); + } + + public abstract int calculate(int operand1, int operand2); +} diff --git a/src/main/java/calculator/model/StringCalculator.java b/src/main/java/calculator/model/StringCalculator.java new file mode 100644 index 0000000..3687c98 --- /dev/null +++ b/src/main/java/calculator/model/StringCalculator.java @@ -0,0 +1,22 @@ +package calculator.model; + +import calculator.utils.StringExpression; + +public class StringCalculator { + private final StringExpression stringExpression; + + public StringCalculator(StringExpression stringExpression) { + this.stringExpression = stringExpression; + } + + public int calculate() { + int expressionLength = stringExpression.getExpressionLength(); + int preNumber = stringExpression.getNumberByIndex(0); + for (int i = 1; i < expressionLength; i += 2) { + Operator operator = stringExpression.getOperatorByIndex(i); + int afterNumber = stringExpression.getNumberByIndex(i + 1); + preNumber = operator.calculate(preNumber, afterNumber); + } + return preNumber; + } +} diff --git a/src/main/java/calculator/model/StringParser.java b/src/main/java/calculator/model/StringParser.java new file mode 100644 index 0000000..107e72e --- /dev/null +++ b/src/main/java/calculator/model/StringParser.java @@ -0,0 +1,24 @@ +package calculator.model; + +import calculator.utils.StringException; + +public class StringParser { + private static final String DELIMITER = " "; + private final String expression; + + public StringParser(String inputString) { + this.expression = inputString; + } + + public String[] parse() { + if (isNullOrBlank(expression)) { + throw new StringException(StringException.NULL_STRING_EXCEPTION); + } + return expression.split(DELIMITER); + } + + private boolean isNullOrBlank(String expression) { + return expression == null || expression.isBlank(); + } +} + diff --git a/src/main/java/calculator/utils/StringException.java b/src/main/java/calculator/utils/StringException.java new file mode 100644 index 0000000..47ffdf7 --- /dev/null +++ b/src/main/java/calculator/utils/StringException.java @@ -0,0 +1,14 @@ +package calculator.utils; + +public class StringException extends RuntimeException { + public static final String NULL_STRING_EXCEPTION = "문자열이 null 또는 비어있습니다."; + public static final String INVALID_STRING_TOKEN_COUNT = "계산식의 토큰 개수가 올바르지 않습니다."; + public static final String INVALID_OPERATOR = "연산자가 올바르지 않습니다."; + public static final String INVALID_LOCATION = "문자의 위치가 올바르지 않습니다."; + public static final String INVALID_DIVIDE_VALUE = "나누는 수는 0이 될 수 없습니다."; + public static final String INDEX_OUT_OF_BOUND = "문자열 토큰의 인덱스를 벗어났습니다."; + + public StringException(String message) { + super(message); + } +} diff --git a/src/main/java/calculator/utils/StringExpression.java b/src/main/java/calculator/utils/StringExpression.java new file mode 100644 index 0000000..ed55546 --- /dev/null +++ b/src/main/java/calculator/utils/StringExpression.java @@ -0,0 +1,50 @@ +package calculator.utils; + +import calculator.model.Operator; + +public class StringExpression { + private static final int MINIMUM_TOKEN_COUNT = 3; + private final String[] expressionTokens; + + public StringExpression(String[] expressionTokens) { + this.expressionTokens = expressionTokens; + } + + public StringExpression validate() { + validateExpressionTokenCount(expressionTokens); + return new StringExpression(expressionTokens); + } + + public int getExpressionLength() { + return expressionTokens.length; + } + + public int getNumberByIndex(int index) { + try { + return Integer.parseInt(expressionTokens[index]); + } catch (NumberFormatException e) { + throw new StringException(StringException.INVALID_LOCATION); + } catch (IndexOutOfBoundsException e) { + throw new StringException(StringException.INDEX_OUT_OF_BOUND); + } + } + + public Operator getOperatorByIndex(int index) { + try { + return Operator.findOperator(expressionTokens[index]); + } catch (IndexOutOfBoundsException e) { + throw new StringException(StringException.INDEX_OUT_OF_BOUND); + } + } + + private void validateExpressionTokenCount(String[] expressionTokens) { + int tokenCount = expressionTokens.length; + if (tokenCount < MINIMUM_TOKEN_COUNT || isEven(tokenCount)) { + throw new StringException(StringException.INVALID_STRING_TOKEN_COUNT); + } + } + + private boolean isEven(int tokenCount) { + return tokenCount % 2 == 0; + } +} diff --git a/src/main/java/calculator/view/InputView.java b/src/main/java/calculator/view/InputView.java new file mode 100644 index 0000000..f62719a --- /dev/null +++ b/src/main/java/calculator/view/InputView.java @@ -0,0 +1,16 @@ +package calculator.view; + +import java.util.Scanner; + +public class InputView { + private static final Scanner scanner = new Scanner(System.in); + private static final String INPUT_STRING_MESSAGE = "계산하고 싶은 수식을 입력하세요: "; + + public InputView() { + } + + public String getInputString() { + System.out.print(INPUT_STRING_MESSAGE); + return scanner.nextLine(); + } +} diff --git a/src/main/java/calculator/view/OutputView.java b/src/main/java/calculator/view/OutputView.java new file mode 100644 index 0000000..032a6b1 --- /dev/null +++ b/src/main/java/calculator/view/OutputView.java @@ -0,0 +1,12 @@ +package calculator.view; + +public class OutputView { + private static final String OUTPUT_STRING_MESSAGE = "계산결과 입니다: "; + + public OutputView() { + } + + public void printCalculationResult(int calculationResult) { + System.out.println(OUTPUT_STRING_MESSAGE + calculationResult); + } +} diff --git a/src/test/java/study/calculator/OperatorTest.java b/src/test/java/study/calculator/OperatorTest.java new file mode 100644 index 0000000..bbca7e3 --- /dev/null +++ b/src/test/java/study/calculator/OperatorTest.java @@ -0,0 +1,77 @@ +package study.calculator; + +import calculator.model.Operator; +import calculator.utils.StringException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class OperatorTest { + @DisplayName("덧셈 연산자 계산 테스트") + @Test + void plusCalculateTest() { + //given + int operand1 = 2, operand2 = 34; + + //when + int result = Operator.PLUS.calculate(operand1, operand2); + + //then + assertThat(result).isEqualTo(36); + } + + @DisplayName("뺄셈 연산자 계산 테스트") + @Test + void minusCalculationTest() { + //given + int operand1 = 2, operand2 = 34; + + //when + int result = Operator.MINUS.calculate(operand1, operand2); + + //then + assertThat(result).isEqualTo(-32); + } + + @DisplayName("곱셈 연산자 계산 테스트") + @Test + void multiplyCalculationTest() { + //given + int operand1 = 2, operand2 = 34; + + //when + int result = Operator.MULTIPLY.calculate(operand1, operand2); + + //then + assertThat(result).isEqualTo(68); + } + + @DisplayName("나눗셈 연산자 계산 테스트") + @Test + void divideCalculationTest() { + //given + int operand1 = 100, operand2 = 5; + + //when + int result = Operator.DIVIDE.calculate(operand1, operand2); + + //then + assertThat(result).isEqualTo(20); + } + + @DisplayName("나누는 숫자가 0일때 예외처리") + @Test + void invalidDivideCalculationTest() { + //given + int operand1 = 100, operand2 = 0; + + //when + assertThatThrownBy(() -> + Operator.DIVIDE.calculate(operand1, operand2)) + //then + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INVALID_DIVIDE_VALUE); + } +} diff --git a/src/test/java/study/calculator/StringCalculatorTest.java b/src/test/java/study/calculator/StringCalculatorTest.java new file mode 100644 index 0000000..a03a714 --- /dev/null +++ b/src/test/java/study/calculator/StringCalculatorTest.java @@ -0,0 +1,70 @@ +package study.calculator; + +import calculator.model.StringCalculator; +import calculator.utils.StringException; +import calculator.utils.StringExpression; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import java.util.stream.Stream; + +public class StringCalculatorTest { + static Stream generateData() { + return Stream.of( + Arguments.of(new String[]{"2", "*", "4", "-", "45", "+", "3", "/", "7"}, -4), + Arguments.of(new String[]{"10", "*", "2", "-", "1", "+", "55", "/", "10"}, 7), + Arguments.of(new String[]{"5", "*", "7", "-", "10", "+", "346", "/", "5"}, 74) + ); + } + + @DisplayName("모든 4가지 연산자에 대해 계산 테스트") + @ParameterizedTest + @MethodSource("generateData") + void calculateAllOperatorTest(String[] tokens, int answer) { + //given + StringExpression stringExpression = new StringExpression(tokens); + StringCalculator stringCalculator = new StringCalculator(stringExpression); + + //when + int result = stringCalculator.calculate(); + + //then + assertThat(result).isEqualTo(answer); + } + + @DisplayName("문자열 토큰 숫자자리에 문자가 온 경우 예외처리") + @Test + void invalidTokenLocationTest() { + //given + String[] expressionTokens = {"2", "*", "+", "/", "8"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + StringCalculator stringCalculator = new StringCalculator(stringExpression); + + //when + assertThatThrownBy(stringCalculator::calculate) + //then + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INVALID_LOCATION); + } + + @DisplayName("연산자 자리에 잘못된 연산자가 온 경우 예외처리") + @Test + void invalidTokenOperatorTest() { + //given + String[] expressionTokens = {"2", "*", "20", "$", "8"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + StringCalculator stringCalculator = new StringCalculator(stringExpression); + + //when + assertThatThrownBy(stringCalculator::calculate) + //then + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INVALID_OPERATOR); + } +} diff --git a/src/test/java/study/calculator/StringExpressionTest.java b/src/test/java/study/calculator/StringExpressionTest.java new file mode 100644 index 0000000..e353934 --- /dev/null +++ b/src/test/java/study/calculator/StringExpressionTest.java @@ -0,0 +1,149 @@ +package study.calculator; + +import calculator.model.Operator; +import calculator.utils.StringException; +import calculator.utils.StringExpression; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +public class StringExpressionTest { + static Stream generateData() { + return Stream.of( + Arguments.of((Object) new String[]{"2", "*"}), + Arguments.of((Object) new String[]{"2", "*", "67", "%"}) + ); + } + + @DisplayName("문자열 토큰 개수가 3이상이고 홀수인 경우 테스트") + @Test + void validExpressionTokenCountTest() { + //given + String[] expressionTokens = {"2", "*", "1", "/", "8"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + StringExpression returnExpression = stringExpression.validate(); + + //then + assertThat(returnExpression).isNotEqualTo(stringExpression); + } + + @DisplayName("문자열 토큰 개수가 2이하 혹은 짝수인 경우 테스트") + @ParameterizedTest + @MethodSource("generateData") + void invalidExpressionTokenCountTest1(String[] tokens) { + //given + StringExpression stringExpression = new StringExpression(tokens); + + //when + assertThatThrownBy(stringExpression::validate) + //then + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INVALID_STRING_TOKEN_COUNT); + } + + @DisplayName("문자열 토큰 숫자로 변환 테스트") + @Test + void changeCharToNumberTest() { + //given + String[] expressionTokens = {"2", "*", "67", "%"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + int number = stringExpression.getNumberByIndex(0); + + //then + assertThat(number).isEqualTo(2); + } + + @DisplayName("문자열 토큰 숫자가 아닌 경우 예외처리") + @Test + void invalidExpressionNumberTest() { + //given + String[] expressionTokens = {"2", "*", "67", "%"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + assertThatThrownBy(() -> stringExpression.getNumberByIndex(1)) + //then + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INVALID_LOCATION); + } + + @DisplayName("문자열 숫자 토큰 반환 메서드 인덱스 범위 초과 예외처리") + @Test + void expressionNumberTokenOutOfIndex() { + //given + String[] expressionTokens = {"2", "*", "67", "%"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + assertThatThrownBy(() -> stringExpression.getNumberByIndex(4)) + //then + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INDEX_OUT_OF_BOUND); + } + + @DisplayName("문자열 토큰 올바른 연산자 확인 테스트") + @Test + void validExpressionTokenOperatorTest() { + //given + String[] expressionTokens = {"2", "*", "67", "%"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + Operator operator = stringExpression.getOperatorByIndex(1); + + //then + assertThat(operator).isEqualTo(Operator.MULTIPLY); + } + + @DisplayName("문자열 토큰이 올바른 연산자가 아닌 경우 예외처리") + @Test + void invalidExpressionTokenOperatorTest() { + //given + String[] expressionTokens = {"2", "&", "67", "%"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + assertThatThrownBy(() -> stringExpression.getOperatorByIndex(1)) + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INVALID_OPERATOR); + } + + @DisplayName("문자열 연산자 토큰 반환메서드 인덱스 범위 초과 예외처리") + @Test + void expressionOperatorTokenOutOfIndex() { + //given + String[] expressionTokens = {"2", "*", "67", "%"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + assertThatThrownBy(() -> stringExpression.getOperatorByIndex(4)) + //then + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.INDEX_OUT_OF_BOUND); + } + + @DisplayName("문자열 길이 반환 테스트") + @Test + void getExpressionLengthTest() { + //given + String[] expressionTokens = {"2", "*", "1", "/", "8", "*", "7"}; + StringExpression stringExpression = new StringExpression(expressionTokens); + + //when + int length = stringExpression.getExpressionLength(); + + //then + assertThat(length).isEqualTo(7); + } +} diff --git a/src/test/java/study/calculator/StringParserTest.java b/src/test/java/study/calculator/StringParserTest.java new file mode 100644 index 0000000..ff81970 --- /dev/null +++ b/src/test/java/study/calculator/StringParserTest.java @@ -0,0 +1,44 @@ +package study.calculator; + +import calculator.model.StringParser; +import calculator.utils.StringException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +public class StringParserTest { + @DisplayName("공백을 기준으로 문자열 분리 테스트") + @Test + void validStringParseTest() { + //given + final String string = "2 - 32 * 2 / 3"; + final StringParser stringParser = new StringParser(string); + + //when + String[] parsedString = stringParser.parse(); + + //then + assertThat(parsedString).containsExactly("2", "-", "32", "*", "2", "/", "3"); + } + + @DisplayName("null 또는 blank한 문자열의 예외처리 테스트") + @ParameterizedTest + @NullAndEmptySource + void nullOrBlankStringExceptionTest(String string) { + //given + final StringParser stringParser = new StringParser(string); + + //when + Throwable throwable = catchThrowable(stringParser::parse); + + //then + assertThat(throwable) + .isInstanceOf(StringException.class) + .hasMessageContaining(StringException.NULL_STRING_EXCEPTION); + } + +}