diff --git a/README.md b/README.md index 8fe7112..34804e1 100644 --- a/README.md +++ b/README.md @@ -24,3 +24,25 @@ git checkout main // 기본 브랜치가 main인 경우 git checkout -b 브랜치이름 ex) git checkout -b apply-feedback ``` + +## 구현기능 +1. 문자열을 입력받는다. + - "숫자 연산자 숫자 연산자 숫자" 형식이어야 한다. +2. 입력받은 문자열을 숫자/연산자로 파싱한다. + - 연산자는 +, -, *, /로 구성된다. +3. 숫자와 연산자를 조합하여 결과를 계산한다. + - 연산자가 "+" 라면? -> a+b + - 연산자가 "-" 라면? -> a-b + - 연산자가 "x" 라면? -> axb + - 연산자가 "/" 라면? -> a/b +4. 계산된 값을 출력한다. + +## 객체 +- Input : 문자열을 입력 받는다. +- Output : 결과를 출력한다. +- Operator : 주어진 숫자들로 단건 연산을 진행한다. +- Operators : 연산자들을 모아놓은 일급객체 +- InputNumber : 입력받은 값의 타입을 보장한다. +- InputNumbers : 입력값들을 모아놓은 일급객체 +- Calculator : 계산기 로직의 진행을 담당한다. + diff --git a/src/main/java/Calculator.java b/src/main/java/Calculator.java new file mode 100644 index 0000000..1d42d0d --- /dev/null +++ b/src/main/java/Calculator.java @@ -0,0 +1,63 @@ +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.stream.Collectors; + +public class Calculator { + + public static final int EVEN_NUMBER = 2; + + private final String DELIMETER = " "; + + private final Input input; + + private final Output output; + + private final Operators operators; + + private final InputNumbers inputNumbers; + + public Calculator(Input input, Output output, Operators operators, InputNumbers inputNumbers) { + this.input = input; + this.output = output; + this.operators = operators; + this.inputNumbers = inputNumbers; + } + + public void calculate() { + String formula = input.inputFormula(); + + List splittedStrings = Arrays.stream(formula.split(DELIMETER)) + .collect(Collectors.toList()); + + validateInputFormula(splittedStrings); + + Queue queue = new LinkedList<>(splittedStrings); + for (int i = 0; i < splittedStrings.size(); i++) { + separateQueueData(queue, i); + } + + int result = operators.operateAll(inputNumbers.convertToList()); + + output.showResult(result); + } + + private void validateInputFormula(List splitStrings) { + if (isEven(splitStrings.size())) { + throw new IllegalArgumentException("계산식이 올바르지 않습니다."); + } + } + + private void separateQueueData(Queue queue, int position) { + if (isEven(position)) { + inputNumbers.addInputNumber(queue.poll()); + } + operators.addOperator(queue.poll()); + } + + private boolean isEven(int number) { + return (number % EVEN_NUMBER) == 0; + } + +} diff --git a/src/main/java/ConsoleInput.java b/src/main/java/ConsoleInput.java new file mode 100644 index 0000000..700f4eb --- /dev/null +++ b/src/main/java/ConsoleInput.java @@ -0,0 +1,15 @@ +import java.util.Scanner; + +public class ConsoleInput implements Input{ + + private final String REQUEST_INPUT_MESSAGE = "계산식을 입력해주세요"; + + private Scanner scanner = new Scanner(System.in); + + @Override + public String inputFormula() { + System.out.println(REQUEST_INPUT_MESSAGE); + return scanner.nextLine(); + } + +} diff --git a/src/main/java/ConsoleOutput.java b/src/main/java/ConsoleOutput.java new file mode 100644 index 0000000..fd278a1 --- /dev/null +++ b/src/main/java/ConsoleOutput.java @@ -0,0 +1,10 @@ +public class ConsoleOutput implements Output{ + + public static final String RESULT_MESSAGE = "결과 = "; + + @Override + public void showResult(int result) { + System.out.println(RESULT_MESSAGE + result); + } + +} diff --git a/src/main/java/Input.java b/src/main/java/Input.java new file mode 100644 index 0000000..636a7f5 --- /dev/null +++ b/src/main/java/Input.java @@ -0,0 +1,5 @@ +public interface Input { + + String inputFormula(); + +} diff --git a/src/main/java/InputNumber.java b/src/main/java/InputNumber.java new file mode 100644 index 0000000..3a61794 --- /dev/null +++ b/src/main/java/InputNumber.java @@ -0,0 +1,24 @@ +public class InputNumber { + + private int number; + + public InputNumber(String numberStr) { + validateNumberStr(numberStr); + number = Integer.parseInt(numberStr); + } + + public int getNumber() { + return number; + } + + private void validateNumberStr(String numberStr) { + if (numberStr.isBlank() || isNotDigit(numberStr)) { + throw new IllegalArgumentException("잘못된 형식의 숫자가 입력되었습니다."); + } + } + + private boolean isNotDigit(String numberStr) { + return !Character.isDigit(numberStr.charAt(0)); + } + +} diff --git a/src/main/java/InputNumbers.java b/src/main/java/InputNumbers.java new file mode 100644 index 0000000..9ecf7f4 --- /dev/null +++ b/src/main/java/InputNumbers.java @@ -0,0 +1,11 @@ +import java.util.List; + +public interface InputNumbers { + + void addInputNumber(String inputNumberStr); + + List convertToList(); + + int getLength(); + +} diff --git a/src/main/java/InputNumbersList.java b/src/main/java/InputNumbersList.java new file mode 100644 index 0000000..5e237e0 --- /dev/null +++ b/src/main/java/InputNumbersList.java @@ -0,0 +1,37 @@ +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class InputNumbersList implements InputNumbers { + + private List inputNumbers; + + public InputNumbersList() { + inputNumbers = new ArrayList<>(); + } + + @Override + public void addInputNumber(String inputNumberStr) { + validateInputNumberStr(inputNumberStr); + inputNumbers.add(new InputNumber(inputNumberStr)); + } + + @Override + public List convertToList() { + return inputNumbers.stream() + .map(InputNumber::getNumber) + .collect(Collectors.toList()); + } + + @Override + public int getLength() { + return inputNumbers.size(); + } + + private void validateInputNumberStr(String inputNumberStr) { + if (inputNumberStr.isEmpty()) { + throw new IllegalArgumentException("입력된 숫자가 없습니다."); + } + } + +} diff --git a/src/main/java/Operator.java b/src/main/java/Operator.java new file mode 100644 index 0000000..1415fc2 --- /dev/null +++ b/src/main/java/Operator.java @@ -0,0 +1,51 @@ +import java.util.Arrays; + +public enum Operator { + PLUS("+") { + @Override + public int operate(int preNumber, int postNumber) { + return preNumber + postNumber; + } + }, + MINUS("-") { + @Override + public int operate(int preNumber, int postNumber) { + return preNumber - postNumber; + } + }, + MULTIPLE("*") { + @Override + public int operate(int preNumber, int postNumber) { + return preNumber * postNumber; + } + }, + DIVISION("/") { + @Override + public int operate(int preNumber, int postNumber) { + if (postNumber <= 0) { + throw new ArithmeticException("0 이하로는 나눌 수 없습니다."); + } + return preNumber / postNumber; + } + }; + + private String value; + + Operator(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static Operator findOperator(String operatorStr) { + return Arrays.stream(Operator.values()) + .filter(operator -> operator.getValue().equals(operatorStr)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("입력된 연산자가 부정확합니다.")); + } + + public abstract int operate(int preNumber, int postNumber); + +} diff --git a/src/main/java/Operators.java b/src/main/java/Operators.java new file mode 100644 index 0000000..25c648b --- /dev/null +++ b/src/main/java/Operators.java @@ -0,0 +1,13 @@ +import java.util.List; + +public interface Operators { + + void addOperator(String operatorStr); + + Operator getNextOperator(); + + int operateAll(List numbers); + + int getLength(); + +} diff --git a/src/main/java/OperatorsQueue.java b/src/main/java/OperatorsQueue.java new file mode 100644 index 0000000..b396c56 --- /dev/null +++ b/src/main/java/OperatorsQueue.java @@ -0,0 +1,50 @@ +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public class OperatorsQueue implements Operators { + + private Queue operators; + + public OperatorsQueue() { + operators = new LinkedList<>(); + } + + public Queue getOperators() { + return operators; + } + + @Override + public void addOperator(String operatorStr) { + if (operatorStr.isEmpty()) { + throw new IllegalArgumentException("연산자가 없습니다."); + } + operators.add(Operator.findOperator(operatorStr)); + } + + @Override + public Operator getNextOperator() { + Operator operator = operators.poll(); + if (operator == null) { + throw new RuntimeException("더이상 연산자가 없습니다."); + } + return operator; + } + + @Override + public int operateAll(List numbers) { + if (numbers.isEmpty()) { + throw new IllegalArgumentException("계산할 값이 없습니다."); + } + return numbers.stream() + .reduce((preNum, postNum) -> getNextOperator().operate(preNum, postNum)) + .orElseThrow(() -> new RuntimeException("계산에 실패하였습니다.")); + } + + @Override + public int getLength() { + return operators.size(); + } + +} diff --git a/src/main/java/Output.java b/src/main/java/Output.java new file mode 100644 index 0000000..7bdf1a1 --- /dev/null +++ b/src/main/java/Output.java @@ -0,0 +1,5 @@ +public interface Output { + + void showResult(int result); + +} diff --git a/src/main/java/StringCalculatorApplication.java b/src/main/java/StringCalculatorApplication.java new file mode 100644 index 0000000..abdaf7b --- /dev/null +++ b/src/main/java/StringCalculatorApplication.java @@ -0,0 +1,13 @@ +public class StringCalculatorApplication { + + public static void main(String[] args) { + Input input = new ConsoleInput(); + Output output = new ConsoleOutput(); + Operators operators = new OperatorsQueue(); + InputNumbers inputNumbers = new InputNumbersList(); + + Calculator calculator = new Calculator(input, output, operators, inputNumbers); + calculator.calculate(); + } + +} diff --git a/src/test/java/InputNumberTest.java b/src/test/java/InputNumberTest.java new file mode 100644 index 0000000..4ed1194 --- /dev/null +++ b/src/test/java/InputNumberTest.java @@ -0,0 +1,26 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class InputNumberTest { + + private final String VALID_INPUT = "2"; + private final String INVALID_INPUT = "~"; + + @Test + void test_constructor_success() { + InputNumber inputNumber = new InputNumber(VALID_INPUT); + + assertThat(inputNumber.getNumber()).isNotNull(); + assertThat(inputNumber.getNumber()).isEqualTo(Integer.parseInt(VALID_INPUT)); + } + + @Test + void test_constructor_invalid_input() { + assertThatThrownBy(() -> new InputNumber(INVALID_INPUT)) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/InputNumbersListTest.java b/src/test/java/InputNumbersListTest.java new file mode 100644 index 0000000..357cec8 --- /dev/null +++ b/src/test/java/InputNumbersListTest.java @@ -0,0 +1,33 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class InputNumbersListTest { + + private InputNumbers inputNumbers; + + @BeforeEach + void setUp() { + inputNumbers = new InputNumbersList(); + } + + @Test + void test_addInputNumber_success() { + String validInputNumberStr = "1"; + inputNumbers.addInputNumber(validInputNumberStr); + + assertThat(inputNumbers.getLength()).isEqualTo(1); + } + + @Test + void test_addInputNumber_invalid_input_number_str() { + String invalidInputNumberStr = "+"; + + assertThatThrownBy(() -> inputNumbers.addInputNumber(invalidInputNumberStr)) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/OperatorTest.java b/src/test/java/OperatorTest.java new file mode 100644 index 0000000..df14152 --- /dev/null +++ b/src/test/java/OperatorTest.java @@ -0,0 +1,71 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class OperatorTest { + + private final String PLUS_STR = "+"; + private final String MINUS_STR = "-"; + private final String MULTIPLE_STR = "*"; + private final String DIVISION_STR = "/"; + private final String INVALID_OPERATOR_STR = "$"; + + private int preNumber; + private int postNumber; + + @BeforeEach + void setUp() { + preNumber = 5; + postNumber = 2; + } + + @Test + void test_findOperator_success() { + Operator foundOperator = Operator.findOperator(PLUS_STR); + + assertThat(foundOperator).isEqualTo(Operator.PLUS); + } + + @Test + void test_findOperator_invalid_operator() { + assertThatThrownBy(() -> Operator.findOperator(INVALID_OPERATOR_STR)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void test_operate_plus() { + int result = Operator.PLUS.operate(preNumber, postNumber); + + assertThat(result).isEqualTo(preNumber + postNumber); + } + + @Test + void test_operate_minus() { + int result = Operator.MINUS.operate(preNumber, postNumber); + + assertThat(result).isEqualTo(preNumber - postNumber); + } + + @Test + void test_operate_multiplication() { + int result = Operator.MULTIPLE.operate(preNumber, postNumber); + + assertThat(result).isEqualTo(preNumber * postNumber); + } + + @Test + void test_operate_division() { + int result = Operator.DIVISION.operate(preNumber, postNumber); + + assertThat(result).isEqualTo(preNumber / postNumber); + } + + @Test + void test_operate_division_denominator_is_zero() { + assertThatThrownBy(() -> Operator.DIVISION.operate(preNumber, 0)) + .isInstanceOf(ArithmeticException.class); + } + +} diff --git a/src/test/java/OperatorsQueueTest.java b/src/test/java/OperatorsQueueTest.java new file mode 100644 index 0000000..ac6a60a --- /dev/null +++ b/src/test/java/OperatorsQueueTest.java @@ -0,0 +1,74 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class OperatorsQueueTest { + + private Operators operators; + + @BeforeEach + void setUp() { + operators = new OperatorsQueue(); + } + + // TODO : Queue를 Operators러 한번 감쌌는데 이게 맞나...? + // TODO : 일단 new로 생성하고 이후에 set 메서드를 통해 값을 세팅하는데 이게 맞나..? 생성시점에는 Queue를 빈 LinkedList로 초기화하니까 불변객체는 맞는데... 뭔가 찜찜.. + @Test + void test_addOperators_success() { + String validInputOperator = "+"; + operators.addOperator(validInputOperator); + + assertThat(operators.getLength()).isEqualTo(1); + } + + // TODO : IllegalArgumentException를 던지는 부분은 Operator.enum에 있다... 이걸 여기서 체스트 하는게 맞을까? ( enum이라 의존성이 강결합되어있어서 모킹도 힘듬.. ) + @Test + void test_setOperators_invalid_operators() { + String invalidInputOperator = "1"; + assertThatThrownBy(() -> operators.addOperator(invalidInputOperator)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + void test_getNextOperator_success() { + List inputOperators = List.of("+", "-", "/", "*", "+"); + inputOperators.forEach(s -> operators.addOperator(s)); + + assertThat(operators.getNextOperator()).isEqualTo(Operator.findOperator(inputOperators.get(0))); + } + + @Test + void test_getNextOperator_empty_list() { + List inputOperators = List.of(); + inputOperators.forEach(s -> operators.addOperator(s)); + + assertThatThrownBy(() -> operators.getNextOperator()) + .isInstanceOf(RuntimeException.class); + } + + @Test + void test_operateAll_success() { + List inputOperators = List.of("+", "-"); + List inputNumbers = List.of(4, 5, 3); + inputOperators.forEach(s -> operators.addOperator(s)); + + int expect = 4 + 5 -3; + int actual = operators.operateAll(inputNumbers); + assertThat(actual).isEqualTo(expect); + } + + @Test + void test_operateAll_empty_numbers() { + List inputOperators = List.of("+", "-"); + List inputNumbers = List.of(); + inputOperators.forEach(s -> operators.addOperator(s)); + + assertThatThrownBy(() -> operators.operateAll(inputNumbers)) + .isInstanceOf(IllegalArgumentException.class); + } + +} diff --git a/src/test/java/study/StringTest.java b/src/test/java/study/StringTest.java deleted file mode 100644 index 43e47d9..0000000 --- a/src/test/java/study/StringTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package study; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -public class StringTest { - @Test - void replace() { - String actual = "abc".replace("b", "d"); - assertThat(actual).isEqualTo("adc"); - } -}