diff --git a/README.md b/README.md index 7d742d97..0a0ef45f 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,18 @@ -# java-calculator -문자열 계산기 미션 저장소 - -## 우아한테크코스 코드리뷰 -* [온라인 코드 리뷰 과정](https://github.com/woowacourse/woowacourse-docs/blob/master/maincourse/README.md) \ No newline at end of file +# 문자열 계산기 +​ +## 요구사항 +​ +- 사용자가 입력한 문자열 값에 따라 사칙연산을 수행할 수 있는 계산기를 구현해야 한다. +- 문자열 계산기는 사칙연산의 계산 우선순위가 아닌 입력 값에 따라 계산 순서가 결정된다. 즉, 수학에서는 곱셈, 나눗셈이 덧셈, 뺄셈 보다 먼저 계산해야 하지만 이를 무시한다. +- 예를 들어 "2 + 3 * 4 / 2"와 같은 문자열을 입력할 경우 2 + 3 * 4 / 2 실행 결과인 10을 출력해야 한다. +​ +## 추가할 기능 목록 +1. 사용자로부터 입력받기 +2. 홀수 인덱스와 짝수 인덱스로 나누는 함수 + 1. 짝수 인덱스일 때는 부호를 저장하는 로직 + 2. 홀수 인덱스일 경우에는 계산하는 함수 호출 +3. 부호에 따라 계산하는 함수 구현 +4. 사칙연산에 해당하는 각 함수 구현 +5. 입력값을 확인하는 핸들러 구현 + 1. 양식에 맞지 않을 경우 (부호가 연속, 숫자가 연속으로 나올 경우) + 2. [예외 처리] 0으로 나눌 경우 \ No newline at end of file diff --git a/src/main/java/Application.java b/src/main/java/Application.java new file mode 100644 index 00000000..841b4f7d --- /dev/null +++ b/src/main/java/Application.java @@ -0,0 +1,8 @@ +import calculator.domain.Calculator; + +public class Application { + public static void main(String[] args){ + Calculator calculator = new Calculator(); + calculator.run(); + } +} diff --git a/src/main/java/calculator/domain/Calculator.java b/src/main/java/calculator/domain/Calculator.java new file mode 100644 index 00000000..97207dfc --- /dev/null +++ b/src/main/java/calculator/domain/Calculator.java @@ -0,0 +1,76 @@ +package calculator.domain; + +import calculator.view.InputView; +import calculator.view.OutputView; + +public class Calculator { + private static final int INDEX_INIT = 1; + private static final int FIRST_VALUE_INDEX = 0; + private static final int EVEN_NUMBER = 0; + private static final int ODD_NUMBER = 1; + private static final String DELIMITER = " "; + static double returnValue; + static String nowSign; + + public void setNowSign(String sign) { + this.nowSign = sign; + } + + public void setReturnValue(double returnValue) { + this.returnValue = returnValue; + } + + public double getReturnValue() { + return this.returnValue; + } + + public String getNowSign() { + return this.nowSign; + } + + public static void run() { + String[] values = InputView.inputHandler().split(DELIMITER); + returnValue = Double.parseDouble(values[FIRST_VALUE_INDEX]); + selectOddNumberOrEvenNumber(values); + OutputView.printResult(returnValue); + } + + public static void selectOddNumberOrEvenNumber(String[] values) { + for (int i = INDEX_INIT; i < values.length; i++) { + calculateEvenNumber(i, values[i]); + calculateOddNumber(i, values[i]); + } + } + + public static boolean isOdd(int number) { + return number % 2 == ODD_NUMBER; + } + + public static boolean isEven(int number) { + return number % 2 == EVEN_NUMBER; + } + + public static void calculateOddNumber(int index, String value) { + if (isOdd(index)) { + nowSign = value; + } + } + + public static void calculateEvenNumber(int index, String value) { + if (isEven(index)) { + selectOperators(Double.parseDouble(value)); + } + } + + public static void selectOperators(double nowNumber) { + for (Operator operators : Operator.values()) { + calculateNumber(operators, nowNumber); + } + } + + public static void calculateNumber(Operator operators, double nowNumber) { + if (operators.getOperator().equals(nowSign)) { + returnValue = operators.calculate(nowNumber, returnValue); + } + } +} \ No newline at end of file diff --git a/src/main/java/calculator/domain/Operator.java b/src/main/java/calculator/domain/Operator.java new file mode 100644 index 00000000..f4bbf430 --- /dev/null +++ b/src/main/java/calculator/domain/Operator.java @@ -0,0 +1,26 @@ +package calculator.domain; + +import java.util.function.BiFunction; + +public enum Operator { + PLUS("+", (nowNumber, returnValue) -> returnValue + nowNumber), + MINUS("-", (nowNumber, returnValue) -> returnValue - nowNumber), + MULTIPLY("*", (nowNumber, returnValue) -> returnValue * nowNumber), + DIVIDE("/", (nowNumber, returnValue) -> returnValue / nowNumber); + + private String operator; + private BiFunction expression; + + Operator(String operator, BiFunction expression) { + this.operator = operator; + this.expression = expression; + } + + public double calculate(double nowNumber, double returnValue) { + return expression.apply(nowNumber, returnValue); + } + + public String getOperator() { + return this.operator; + } +} \ No newline at end of file diff --git a/src/main/java/calculator/util/ExceptionHandler.java b/src/main/java/calculator/util/ExceptionHandler.java new file mode 100644 index 00000000..6e27183c --- /dev/null +++ b/src/main/java/calculator/util/ExceptionHandler.java @@ -0,0 +1,65 @@ +package calculator.util; + + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static calculator.domain.Calculator.isEven; +import static calculator.domain.Calculator.isOdd; + +public class ExceptionHandler { + private static final int EVEN = 0; + private static final String DELIMITER = " "; + private static final String BLANK = " "; + private static final String EMPTY_STRING = ""; + private static final String NUMBER_FORMAT = "-?\\d+(\\.\\d+)?"; + private static final String DIVIDE_ZERO_STRING = "/0"; + private static List operatorList = Arrays.asList("+", "-", "*", "/"); + + public static String checkInputHandler(String input) { + if (checkString(input.split(DELIMITER)) == true && checkUndefinedValue(input) == true) { + return input; + } + throw new IllegalArgumentException(); + } + + public static boolean checkUndefinedValue(String str) { + if (str.replace(BLANK, EMPTY_STRING).contains(DIVIDE_ZERO_STRING)) { + return false; + } + return true; + } + + public static boolean checkString(String[] inputStrings) { + if (inputStrings.length % 2 == EVEN) { + return false; + } + + AtomicInteger index = new AtomicInteger(); + return Arrays.stream(inputStrings) + .allMatch(str -> checkIndividual(index.getAndIncrement(), str)); + } + + public static boolean checkIndividual(int i, String inputString) { + if (isEven(i)) { + return checkNumber(inputString); + } + if (isOdd(i)) { + return checkSign(inputString); + } + return false; + } + + public static boolean checkNumber(String inputString) { + return inputString.matches(NUMBER_FORMAT); + } + + public static boolean checkSign(String inputString) { + if (operatorList.contains(inputString)) { + return true; + } + return false; + } + +} \ No newline at end of file diff --git a/src/main/java/calculator/view/InputView.java b/src/main/java/calculator/view/InputView.java new file mode 100644 index 00000000..adaec3e6 --- /dev/null +++ b/src/main/java/calculator/view/InputView.java @@ -0,0 +1,31 @@ +package calculator.view; + +import java.util.InputMismatchException; +import java.util.Scanner; + +import static calculator.util.ExceptionHandler.checkInputHandler; + +public class InputView { + + private static Scanner scanner = new Scanner(System.in); + private static final String INPUT_EXPRESSION_STR = "식을 입력해주세요 : "; + private static final String CHECK_INPUT_STR = "입력값을 확인해주세요!"; + + public static String inputHandler() { + try { + return checkInputHandler(printInputExpression()); + } catch (InputMismatchException | IllegalArgumentException e) { + printInputCheck(); + return inputHandler(); + } + } + + public static String printInputExpression(){ + System.out.print(INPUT_EXPRESSION_STR); + return scanner.nextLine(); + } + + public static void printInputCheck(){ + System.out.println(CHECK_INPUT_STR); + } +} \ No newline at end of file diff --git a/src/main/java/calculator/view/OutputView.java b/src/main/java/calculator/view/OutputView.java new file mode 100644 index 00000000..c4aa4d25 --- /dev/null +++ b/src/main/java/calculator/view/OutputView.java @@ -0,0 +1,11 @@ +package calculator.view; + +public class OutputView { + public static void printResult(double result){ + String printFormat = "결과는 %f 입니다."; + if(result == Math.floor(result)){ + printFormat = "결과는 %.0f 입니다."; + } + System.out.printf(printFormat, result); + } +} diff --git a/src/main/java/empty.txt b/src/main/java/empty.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/calculator/CalculatorTest.java b/src/test/java/calculator/CalculatorTest.java new file mode 100644 index 00000000..812e180d --- /dev/null +++ b/src/test/java/calculator/CalculatorTest.java @@ -0,0 +1,66 @@ +package calculator; + +import calculator.domain.Calculator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static calculator.domain.Calculator.*; +import static org.assertj.core.api.Assertions.*; + +public class CalculatorTest { + static final double TEST_RETURN_VALUE_INIT = 10; + static final String TEST_NOW_SIGN_INIT = "+"; + Calculator calculator = new Calculator(); + + @BeforeEach + void setUp() { + calculator.setReturnValue(TEST_RETURN_VALUE_INIT); + calculator.setNowSign(TEST_NOW_SIGN_INIT); + } + + static Stream stringArrayProvider() { + return Stream.of( + Arguments.of(new String[]{"4", "*", "3", "/", "5", "-", "20"}, -17.6), + Arguments.of(new String[]{"2", "-", "1", "*", "3", "+", "2"}, 5) + ); + } + + @ParameterizedTest + @MethodSource("stringArrayProvider") + @DisplayName("문자열을 받아 계산하는 메서드") + void selectOddNumberOrEvenNumberTest(String[] values, double expected) { + calculator.setReturnValue(Double.parseDouble(values[0])); + selectOddNumberOrEvenNumber(values); + assertThat(calculator.getReturnValue()).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"0:-:+", "1:*:*", "3:-:-"}, delimiter = ':') + @DisplayName("인덱스가 홀수면 부호를 저장하는 메서드") + void calculateOddNumberTest(int index, String value, String expected) { + calculator.calculateOddNumber(index, value); + assertThat(calculator.getNowSign()).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"0:1:11", "2:2.3:12.3", "3:4.0:10"}, delimiter = ':') + @DisplayName("인덱스가 짝수면 계산하는 메서드") + void calculateEvenNumberTest(int index, String value, double expected) { + calculator.calculateEvenNumber(index, value); + assertThat(calculator.getReturnValue()).isEqualTo(expected); + } + + @ParameterizedTest + @CsvSource(value = {"1:11", "2.3:12.3", "4.0:14.0"}, delimiter = ':') + @DisplayName("연산자에 따라 계산하는 메서드") + void selectOperatorsTest(double value, double expected) { + calculator.selectOperators(value); + assertThat(calculator.getReturnValue()).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/ExceptionHandlerTest.java b/src/test/java/calculator/ExceptionHandlerTest.java new file mode 100644 index 00000000..483caf2b --- /dev/null +++ b/src/test/java/calculator/ExceptionHandlerTest.java @@ -0,0 +1,53 @@ +package calculator; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.InputMismatchException; +import java.util.Scanner; +import java.util.concurrent.atomic.AtomicInteger; + +import static calculator.util.ExceptionHandler.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ExceptionHandlerTest { + + @Test + @DisplayName("입력값을 체크해주는 테스트") + public void checkInputHandlerTest() { + String str = "321 + 3 + f"; + assertThatThrownBy(() -> { + checkInputHandler(str); + }).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("문자열을 split하고 전체적으로 체크해주는 테스트") + void checkStringTest() { + String[] inputStrings = "333 + 2434343".split(" "); + assertThat(checkString(inputStrings)).isTrue(); + } + + @Test + @DisplayName("숫자인지 체크해주는 테스트") + void checkNumberTest() { + String str = "g"; + assertThat(checkNumber(str)).isFalse(); + } + + @Test + @DisplayName("사칙연산인지 체크해주는 테스트") + void checkSignTest() { + String str = "@"; + assertThat(checkSign(str)).isFalse(); + } + + @Test + @DisplayName("0으로 나누었을 때의 예외처리를 해주는 테스트") + void checkUndefinedValueTest() { + String str = "0 / 0"; + assertThat(checkUndefinedValue(str)).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/calculator/OperatorTest.java b/src/test/java/calculator/OperatorTest.java new file mode 100644 index 00000000..ba952e45 --- /dev/null +++ b/src/test/java/calculator/OperatorTest.java @@ -0,0 +1,50 @@ +package calculator; + +import calculator.domain.Operator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OperatorTest { + static final double TEST_RETURN_VALUE_INIT = 10; + static final double TEST_NOW_NUMBER = 5; + private Operator plusOperator; + private Operator minusOperator; + private Operator multiplyOperator; + private Operator divideOperator; + + @BeforeEach + void setUp() { + plusOperator = Operator.PLUS; + minusOperator = Operator.MINUS; + multiplyOperator = Operator.MULTIPLY; + divideOperator = Operator.DIVIDE; + } + + @Test + @DisplayName("더하기 연산자 메서드 테스트") + void plusOperatorTest() { + assertThat(plusOperator.calculate(TEST_NOW_NUMBER, TEST_RETURN_VALUE_INIT)).isEqualTo(TEST_RETURN_VALUE_INIT + TEST_NOW_NUMBER); + } + + @Test + @DisplayName("빼기 연산자 메서드 테스트") + void minusOperatorTest() { + assertThat(minusOperator.calculate(TEST_NOW_NUMBER, TEST_RETURN_VALUE_INIT)).isEqualTo(TEST_RETURN_VALUE_INIT - TEST_NOW_NUMBER); + } + + @Test + @DisplayName("곱하기 연산자 메서드 테스트") + void multiplyOperatorTest() { + assertThat(multiplyOperator.calculate(TEST_NOW_NUMBER, TEST_RETURN_VALUE_INIT)).isEqualTo(TEST_RETURN_VALUE_INIT * TEST_NOW_NUMBER); + } + + @Test + @DisplayName("나누기 연산자 메서드 테스트") + void divideOperatorTest() { + assertThat(divideOperator.calculate(TEST_NOW_NUMBER, TEST_RETURN_VALUE_INIT)).isEqualTo(TEST_RETURN_VALUE_INIT / TEST_NOW_NUMBER); + } + +} diff --git a/src/test/java/empty.txt b/src/test/java/empty.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/java/study/SetTest.java b/src/test/java/study/SetTest.java new file mode 100644 index 00000000..3db2123f --- /dev/null +++ b/src/test/java/study/SetTest.java @@ -0,0 +1,42 @@ +package study; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SetTest { + private Set numbers; + + @BeforeEach + void setUp() { + numbers = new HashSet<>(); + numbers.add(1); + numbers.add(1); + numbers.add(2); + numbers.add(3); + } + + @Test + void numbersSizeTest() { + assertThat(numbers.size()).isEqualTo(3); + } + + @ParameterizedTest + @ValueSource(ints = {1, 2, 3}) + void numberExistTest(int number) { + assertThat(numbers.contains(number)).isTrue(); + } + + @ParameterizedTest + @CsvSource(value = {"1:true", "2:true", "3:true", "4:false"}, delimiter = ':') + void numberExistTest(int number, boolean result) { + assertThat(numbers.contains(number)).isEqualTo(result); + } +} \ No newline at end of file diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java new file mode 100644 index 00000000..68854319 --- /dev/null +++ b/src/test/java/study/StringTest.java @@ -0,0 +1,53 @@ +package study; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +public class StringTest { + @Test + void split() { + String value = "1,2"; + String[] result = value.split(","); + assertThat(result).contains("1"); + assertThat(result).contains("2"); + } + + @Test + void split2() { + String value2 = "1"; + String[] result2 = value2.split(","); + assertThat(result2).contains("1"); + assertThat(result2).containsExactly("1"); + } + + @Test + void substring() { + String value3 = "(1,2)"; + String result3 = value3.substring(1, 4); + assertThat(result3).contains("1,2"); + } + + @Test + @DisplayName("요구사항 3 - assertThatThrownBy, hasMessageContaining 사용") + public void testException() { + assertThatThrownBy(() -> { + String value4 = "abc"; + value4.charAt(4); + }).isInstanceOf(IndexOutOfBoundsException.class) + .hasMessageContaining("String index out of range: 4"); + } + + @Test + @DisplayName("요구사항 3 - assertThatExceptionOfType, withMessageMatching 사용") + public void testException2() { + assertThatExceptionOfType(IndexOutOfBoundsException.class) + .isThrownBy(() -> { + String value4 = "abc"; + value4.charAt(4); + }).withMessageMatching("String index out of range: \\d+"); + } +}