diff --git a/java-calculator/.gitignore b/java-calculator/.gitignore new file mode 100644 index 000000000..f1d1fba6e --- /dev/null +++ b/java-calculator/.gitignore @@ -0,0 +1,46 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +gradlew.bat +gradlew +gradle + +### IntelliJ IDEA ### +.idea +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/java-calculator/README.md b/java-calculator/README.md new file mode 100644 index 000000000..37cb21731 --- /dev/null +++ b/java-calculator/README.md @@ -0,0 +1,100 @@ + +## 자바 계산기 구현 미션 기능목록 작성 + +동작 예시 +``` +1. 조회 +2. 계산 + +선택 : 2 + +1 + 2 +3 + +1. 조회 +2. 계산 + +선택 : 2 + +1 + 2 * 3 +7 + +1. 조회 +2. 계산 + +선택 : 1 + +1 + 2 = 3 +1 + 2 * 3 = 7 + +선택 : 2 + +3 - 2 * 2 +-1 +``` + +- [x] 입력 + - [x] Scanner를 통한 콘솔 입력 + - [x] 선택 값 입력 받기 + - [x] 계산 기능 선택 시 -> 식 입력 받기 + - [x] BufferedReader로 입력 받는 것 교체 / 확장 가능 하게 추상화 시키기 (OCP) +- [x] 출력 + - [x] 조회 기능 선택 시 -> 조회 시 넘겨 받은 결과값 출력하기 + - [x] 계산 기능 선택 시 -> 계산 후 계산한 결과 출력하기 + - [x] OutputView 추상화 시키기 + +- [x] 컨트롤러 및 application 생성 + - 컨트롤러 역할 : view와 domain을 이어 준다 / repository에서 값을 가져온다. + - 컨트롤러에서 담당 : repository를 결정한다 + - view는 주입 받는다(사용자가 결정) + - 게임을 실행하는 `run()`메서드를 가진다. + - Application 클래스 역할 : 사용자 + - 컨트롤러를 실행한다. + - view 객체를 생성해 컨트롤러에 주입시킨다. + +- [x] 조회 기능 구현 + - [x] 조회시 계산한 값이 없으면 `계산 값이 존재하지 않습니다` 메세지 출력 -> OutputView에서 담당 + - [x] 계산 후 이력을 List로 저장 -> Repository에서 생성 + - [x] repository 패키지 안 List 가진 일급 컬렉션으로 구현 + - [x] 인터페이스로 repository 추상화 + - [x] 조회 기능 선택 시 계산 이력을 List에서 가져 오기 (repository에서 controller로 직접적으로 가져오기) + + +- [x] 계산 기능 구현 + - [x] 덧셈 + - [x] 뺄셈 + - [x] 나눗셈 + - [x] 곱하기 + - [x] 우선순위 (사칙연산) + - [x] 후위 표기식 찾아보기 + - [x] 나눗셈과 곱셈이 덧셈과 뺄셈보다 우선순위가 높다 +``` +중위 표현식 -> 후위 표현식 전환 알고리즘 : +1. 피연산자는 스택에 넣지 않고 그냥 출력한다. +2. 연산자는 스택이 비었으면 스택에 push한다. +3. 연산자는 스택이 비어있지 않으면 스택에 있는 연산자와의 우선순위를 비교해 스택에 있는 연산자의 우선순위가 같거나 크다면 스택에 있는 연산자를 pop을 한 후 출력하고 현재 연산자는 스택에 push한다. +4. 만약 3번에서 우선순위가 현재 연산자가 더 크면 현재 연산자를 push한다.(스택에서 pop하지 않음) +5. 수식이 끝나면 스택이 빌 때 까지 pop을 한 후 출력한다. + +후위 표현식 -> 계산 알고리즘 +1. 피연산자면 스택에 push한다. +2. 연산자를 만나면 pop을 두번하고 각각 값을 저장한 후, 연산자에 맞는 계산을 한다. +3. 계산을 한 뒤, 결과 값은 다시 스택에 넣는다. 이 과정을 수식이 끝날 때 까지 반복한다. +4. 수식이 끝났다면 스택에 마지막 남은 값이 결과 값이 된다. +``` +- [x] 식을 입력 받을 때 계산기에서 수행가능한 연산 범위를 제한 할 수 있다. + - [x] Regex를 통해서 검증하기. +- [x] 중위표현식, 후위 표현식에 따른 계산 확장 가능성 기능 구현 + +- [x] 정규식 문제 해결 => \\ 추가해서 해설 +- [x] 0으로 나눌 경우 문제 해결 +- [x] var 사용 x -> 정확히 타입 표기를 통한 명시성 증가ㅈ + +- [x] 예외 처리 + - [x] 조회(1), 계산(2) 이외의 값을 입력 했을 경우 `IllegalArgumentException` 발생 -> controller에서 처리 + - [x] 형식 (숫자 + 공백 + 기호 + 공백 + 숫자 + 공백 + 기호 + ...)이 잘못된 경우 `IllegalArgumentException` 발생 + - 공백은 무조건 하나로 처리 -> split시 space 하나로 처리해야 자를 수 있음. split 특성상 delimiter 포함 하지를 못한다. + - 정규식으로 처리 + - [x] 식에 int 형 범위 벗어 나 있는 경우 `IllegalArgumentException` 발생 + - [x] 계산 과정에 있어 int 자료형 overflow / underflow 발생 시 `IllegalArgumentException` 발생 + - Math 클래스의 `~Exact()` 메서드 사용 \ No newline at end of file diff --git a/java-calculator/build.gradle b/java-calculator/build.gradle new file mode 100644 index 000000000..fce1a7376 --- /dev/null +++ b/java-calculator/build.gradle @@ -0,0 +1,20 @@ +plugins { + id 'java' +} + +group = 'com.programmers' +version = '1.0-SNAPSHOT' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation 'org.assertj:assertj-core:3.22.0' + testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation 'org.junit.jupiter:junit-jupiter' +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/java-calculator/settings.gradle b/java-calculator/settings.gradle new file mode 100644 index 000000000..e0721b86f --- /dev/null +++ b/java-calculator/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'java-calculator' \ No newline at end of file diff --git a/java-calculator/src/main/java/com/programmers/blackdog/Application.java b/java-calculator/src/main/java/com/programmers/blackdog/Application.java new file mode 100644 index 000000000..739bc8a0e --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/Application.java @@ -0,0 +1,15 @@ +package com.programmers.blackdog; + +import com.programmers.blackdog.controller.CalculatorController; +import com.programmers.blackdog.view.*; + +public class Application { + public static void main(String[] args) { + InputView inputView = new ScannerInputView(); + OutputView outputView = new PrintStreamOutputView(); + Console console = new Console(inputView, outputView); + + CalculatorController calculator = new CalculatorController(console); + calculator.run(); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/controller/CalculatorController.java b/java-calculator/src/main/java/com/programmers/blackdog/controller/CalculatorController.java new file mode 100644 index 000000000..f56e60e9f --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/controller/CalculatorController.java @@ -0,0 +1,65 @@ +package com.programmers.blackdog.controller; + +import com.programmers.blackdog.controller.constant.Selection; +import com.programmers.blackdog.service.CalculatorService; +import com.programmers.blackdog.service.Service; +import com.programmers.blackdog.view.Console; + +import java.util.List; + +import static com.programmers.blackdog.controller.constant.Selection.findByCode; + +public class CalculatorController { + + private final Service service; + private final Console console; + + public CalculatorController(Console console) { + this.console = console; + this.service = new CalculatorService(); + } + + public void run() { + while (true) { + try { + switch (getCode()) { + case CHECK_DATA: + printAllPreviousData(); + break; + case CALCULATE: + printCalculatedResultAndSave(); + break; + case EXIT: + exitProgram(); + return; + } + } catch (Exception e) { + console.printErrorMessage(e.getMessage()); + } + } + } + + private Selection getCode() { + int selectionCode = console.getSelectionCode(); + return findByCode(selectionCode); + } + + + private void printAllPreviousData() { + List calculatedData = service.findAll(); + console.printExpressions(calculatedData); + } + + private void printCalculatedResultAndSave() { + String expression = console.getExpression(); + int result = service.calculate(expression); + + service.save(expression, result); + + console.printCalculatedResult(result); + } + + private void exitProgram() { + console.printEndMessage(); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/controller/constant/Selection.java b/java-calculator/src/main/java/com/programmers/blackdog/controller/constant/Selection.java new file mode 100644 index 000000000..35bd773aa --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/controller/constant/Selection.java @@ -0,0 +1,20 @@ +package com.programmers.blackdog.controller.constant; + +import java.util.Arrays; + +public enum Selection { + CHECK_DATA(1), CALCULATE(2), EXIT(3); + + private final int code; + + Selection(int code) { + this.code = code; + } + + public static Selection findByCode(int code) { + return Arrays.stream(Selection.values()) + .filter(selection -> selection.code == code) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("해당 값이 없습니다")); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/ArithmeticOperators.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/ArithmeticOperators.java new file mode 100644 index 000000000..e46561777 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/ArithmeticOperators.java @@ -0,0 +1,63 @@ +package com.programmers.blackdog.domain; + +import java.util.Arrays; + +public enum ArithmeticOperators implements Operable { + ADDITION("+", 0) { + @Override + public int apply(int a, int b) { + return Math.addExact(a, b); + } + }, SUBTRACTION("-", 0) { + @Override + public int apply(int a, int b) { + return Math.subtractExact(a, b); + } + }, MULTIPLICATION("*", 1) { + @Override + public int apply(int a, int b) { + return Math.multiplyExact(a, b); + } + }, DIVISION("/", 1) { + @Override + public int apply(int a, int b) { + validateDivideNumIsZero(b); + return a / b; + } + }; + + private static void validateDivideNumIsZero(int num) { + if (num == 0) { + throw new ArithmeticException("0으로 나눌 수 없습니다."); + } + } + + private final String operator; + private final int priority; + + ArithmeticOperators(String operator, int priority) { + this.operator = operator; + this.priority = priority; + + } + + public static ArithmeticOperators convertTokenToOperator(String token) { + return Arrays.stream(values()) + .filter(operator -> operator.getOperator().equals(token)) + .findAny() + .orElseThrow(); + } + + public static boolean isNotOperator(String token) { + return Arrays.stream(values()) + .noneMatch(operator -> operator.getOperator().equals(token)); + } + + public String getOperator() { + return operator; + } + + public int getPriority() { + return priority; + } +} \ No newline at end of file diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/Operable.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/Operable.java new file mode 100644 index 000000000..58102b1ee --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/Operable.java @@ -0,0 +1,6 @@ +package com.programmers.blackdog.domain; + +@FunctionalInterface +public interface Operable { + int apply(int a, int b); +} \ No newline at end of file diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/RegexGenerator.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/RegexGenerator.java new file mode 100644 index 000000000..23ee93387 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/RegexGenerator.java @@ -0,0 +1,29 @@ +package com.programmers.blackdog.domain; + +import java.util.Arrays; +import java.util.stream.Collectors; + +import static com.programmers.blackdog.domain.ArithmeticOperators.values; + +public class RegexGenerator { + + private static final String REGEX_PREFIX = "^\\d+\\s(["; + private static final String REGEX_SUFFIX = "]\\s\\d+\\s)+$"; + public static final String PREFIX = "\\"; + public static final String SUFFIX = ""; + + public String generateWithOperator(ArithmeticOperators... arithmeticOperators) { + StringBuilder operators = new StringBuilder(); + for (ArithmeticOperators arithmeticOperator : arithmeticOperators) { + operators.append(PREFIX).append(arithmeticOperator.getOperator()); + } + return REGEX_PREFIX + operators.append(REGEX_SUFFIX); + } + + public String generateWithAllOperator() { + String allOperators = Arrays.stream(values()) + .map(ArithmeticOperators::getOperator) + .collect(Collectors.joining(PREFIX, PREFIX, SUFFIX)); + return REGEX_PREFIX + allOperators + REGEX_SUFFIX; + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/calculator/AbstractCalculator.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/calculator/AbstractCalculator.java new file mode 100644 index 000000000..fb14afb89 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/calculator/AbstractCalculator.java @@ -0,0 +1,7 @@ +package com.programmers.blackdog.domain.calculator; + +import com.programmers.blackdog.domain.expression.Expression; + +public interface AbstractCalculator { + int calculate(Expression expression); +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/calculator/Calculator.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/calculator/Calculator.java new file mode 100644 index 000000000..47aabc4c2 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/calculator/Calculator.java @@ -0,0 +1,39 @@ +package com.programmers.blackdog.domain.calculator; + +import com.programmers.blackdog.domain.expression.Expression; + +import java.util.Stack; + +import static com.programmers.blackdog.domain.ArithmeticOperators.convertTokenToOperator; +import static com.programmers.blackdog.domain.ArithmeticOperators.isNotOperator; +import static com.programmers.blackdog.domain.constant.Regex.DELIMITER; +import static com.programmers.blackdog.domain.utils.StringUtil.convertStringToInt; + +public class Calculator implements AbstractCalculator { + + @Override + public int calculate(Expression expression) { + String postfixExpression = expression.getPostfixExpression(); + return calculateWithPostfixExpression(postfixExpression); + } + + private Integer calculateWithPostfixExpression(String postfixExpression) { + String[] tokens = postfixExpression.split(DELIMITER); + Stack stack = new Stack<>(); + + for (String token : tokens) { + evaluatePostfixNotation(stack, token); + } + return stack.pop(); + } + + private void evaluatePostfixNotation(Stack stack, String token) { + if (isNotOperator(token)) { + stack.push(convertStringToInt(token)); + return; + } + Integer secondValue = stack.pop(); + Integer firstValue = stack.pop(); + stack.push(convertTokenToOperator(token).apply(firstValue, secondValue)); + } +} \ No newline at end of file diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/constant/Regex.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/constant/Regex.java new file mode 100644 index 000000000..441303783 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/constant/Regex.java @@ -0,0 +1,19 @@ +package com.programmers.blackdog.domain.constant; + +import com.programmers.blackdog.domain.RegexGenerator; + +public final class Regex { + + private Regex(){ + throw new AssertionError("인스턴스화 할 수 없습니다!"); + } + + public static final String BLANK = " "; + public static final String DELIMITER; + public static final String REGEX; + + static { + DELIMITER = " "; + REGEX = new RegexGenerator().generateWithAllOperator(); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/expression/Expression.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/expression/Expression.java new file mode 100644 index 000000000..d02ea6ce4 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/expression/Expression.java @@ -0,0 +1,5 @@ +package com.programmers.blackdog.domain.expression; + +public interface Expression { + String getPostfixExpression(); +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/expression/InfixExpression.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/expression/InfixExpression.java new file mode 100644 index 000000000..be1e4f2d4 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/expression/InfixExpression.java @@ -0,0 +1,76 @@ +package com.programmers.blackdog.domain.expression; + +import com.programmers.blackdog.domain.ArithmeticOperators; + +import java.util.Objects; +import java.util.Stack; +import java.util.regex.Pattern; + +import static com.programmers.blackdog.domain.ArithmeticOperators.convertTokenToOperator; +import static com.programmers.blackdog.domain.ArithmeticOperators.isNotOperator; +import static com.programmers.blackdog.domain.constant.Regex.*; + +public class InfixExpression implements Expression { + private final String expression; + + public InfixExpression(String expression) { + validateInfixExpression(expression); + this.expression = expression; + } + + private void validateInfixExpression(String expression) { + if (!Pattern.matches(REGEX, appendBlankForRegex(expression))) { + throw new IllegalArgumentException("잘못된 형식의 식을 입력하셨습니다."); + } + } + + private String appendBlankForRegex(String expression) { + return expression + BLANK; + } + + public String getPostfixExpression() { + StringBuilder postfixExpression = new StringBuilder(); + Stack stack = new Stack<>(); + + String[] tokens = this.expression.split(DELIMITER); + for (String token : tokens) { + convertToPostfix(postfixExpression, stack, token); + } + appendAllExistingElement(postfixExpression, stack); + return postfixExpression.toString().trim(); + } + + private void convertToPostfix(StringBuilder postfixExpression, Stack stack, String token) { + if (isNotOperator(token)) { + postfixExpression.append(token).append(BLANK); + return; + } + ArithmeticOperators operator = convertTokenToOperator(token); + while (isStackNotEmptyAndOperatorPriorityLower(stack, operator)) { + postfixExpression.append(stack.pop().getOperator()).append(BLANK); + } + stack.push(operator); + } + + private void appendAllExistingElement(StringBuilder postfixExpression, Stack stack) { + while (!stack.isEmpty()) { + postfixExpression.append(stack.pop().getOperator()).append(BLANK); + } + } + private boolean isStackNotEmptyAndOperatorPriorityLower(Stack stack, ArithmeticOperators operator) { + return !stack.isEmpty() && stack.peek().getPriority() >= operator.getPriority(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InfixExpression that = (InfixExpression) o; + return Objects.equals(expression, that.expression); + } + + @Override + public int hashCode() { + return Objects.hash(expression); + } +} \ No newline at end of file diff --git a/java-calculator/src/main/java/com/programmers/blackdog/domain/utils/StringUtil.java b/java-calculator/src/main/java/com/programmers/blackdog/domain/utils/StringUtil.java new file mode 100644 index 000000000..476414da8 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/domain/utils/StringUtil.java @@ -0,0 +1,16 @@ +package com.programmers.blackdog.domain.utils; + +public final class StringUtil { + + private StringUtil() { + throw new AssertionError("Util 클래스 생성 불가합니다."); + } + + public static int convertStringToInt(String str) { + try { + return Integer.parseInt(str); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("정수형 범위를 초과했습니다"); + } + } +} \ No newline at end of file diff --git a/java-calculator/src/main/java/com/programmers/blackdog/repository/CalculatorRepository.java b/java-calculator/src/main/java/com/programmers/blackdog/repository/CalculatorRepository.java new file mode 100644 index 000000000..56060338f --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/repository/CalculatorRepository.java @@ -0,0 +1,8 @@ +package com.programmers.blackdog.repository; + +import java.util.List; + +public interface CalculatorRepository { + void save(String calculatedResult); + List findAll(); +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/repository/MemoryCalculatorRepository.java b/java-calculator/src/main/java/com/programmers/blackdog/repository/MemoryCalculatorRepository.java new file mode 100644 index 000000000..1b8dca115 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/repository/MemoryCalculatorRepository.java @@ -0,0 +1,24 @@ +package com.programmers.blackdog.repository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class MemoryCalculatorRepository implements CalculatorRepository { + + private final List calculatedData; + + public MemoryCalculatorRepository() { + this.calculatedData = new ArrayList<>(); + } + + @Override + public void save(String calculatedResult) { + calculatedData.add(calculatedResult); + } + + @Override + public List findAll() { + return Collections.unmodifiableList(calculatedData); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/service/CalculatorService.java b/java-calculator/src/main/java/com/programmers/blackdog/service/CalculatorService.java new file mode 100644 index 000000000..2722487cb --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/service/CalculatorService.java @@ -0,0 +1,42 @@ +package com.programmers.blackdog.service; + +import com.programmers.blackdog.domain.calculator.Calculator; +import com.programmers.blackdog.domain.expression.Expression; +import com.programmers.blackdog.domain.expression.InfixExpression; +import com.programmers.blackdog.domain.calculator.AbstractCalculator; +import com.programmers.blackdog.repository.CalculatorRepository; +import com.programmers.blackdog.repository.MemoryCalculatorRepository; + +import java.util.List; + +// 계산 및 저장 로직 담당 +public class CalculatorService implements Service { + private static final String EQUAL = " = "; + private final CalculatorRepository calculatorRepository; + private final AbstractCalculator calculator; + + public CalculatorService() { + this.calculatorRepository = new MemoryCalculatorRepository(); + this.calculator = new Calculator(); + } + + @Override + public int calculate(String value) { + Expression expression = new InfixExpression(value); + return calculator.calculate(expression); + } + + @Override + public void save(String expression, int result) { + this.calculatorRepository.save(generateTotalResult(expression, result)); + } + + private String generateTotalResult(String expression, int result) { + return expression + EQUAL + result; + } + + @Override + public List findAll() { + return this.calculatorRepository.findAll(); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/service/Service.java b/java-calculator/src/main/java/com/programmers/blackdog/service/Service.java new file mode 100644 index 000000000..01d461152 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/service/Service.java @@ -0,0 +1,12 @@ +package com.programmers.blackdog.service; + +import java.util.List; + +public interface Service { + int calculate(String expression); + + void save(String expression, int result); + + List findAll(); + +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/view/BufferedReaderInputView.java b/java-calculator/src/main/java/com/programmers/blackdog/view/BufferedReaderInputView.java new file mode 100644 index 000000000..7dcfd84ef --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/view/BufferedReaderInputView.java @@ -0,0 +1,19 @@ +package com.programmers.blackdog.view; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public class BufferedReaderInputView implements InputView { + + private final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); + + @Override + public String read() { + try { + return bufferedReader.readLine(); + } catch (IOException e) { + throw new RuntimeException("입출력 관련 문제가 생겼습니다."); + } + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/view/Console.java b/java-calculator/src/main/java/com/programmers/blackdog/view/Console.java new file mode 100644 index 000000000..37296ad78 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/view/Console.java @@ -0,0 +1,55 @@ +package com.programmers.blackdog.view; + +import java.util.List; + +public class Console { + + private static final String CHOICE_MESSAGE = "1. 조회\n" + "2. 계산\n" + "3. 종료\n"; + + private final InputView inputView; + private final OutputView outputView; + + public Console(InputView inputView, OutputView outputView) { + this.inputView = inputView; + this.outputView = outputView; + } + + public int getSelectionCode() { + outputView.print(CHOICE_MESSAGE); + outputView.print("선택 : "); + try { + return Integer.parseInt(inputView.read()); + } catch (NumberFormatException e) { + throw new NumberFormatException("잘못된 값을 입력하셨습니다."); + } + } + + + public void printExpressions(List calculatedData) { + validateIfIsEmpty(calculatedData); + outputView.print(calculatedData); + } + + private void validateIfIsEmpty(List expressions) { + if (expressions.isEmpty()) { + outputView.print("계산 값이 존재하지 않습니다."); + } + } + + public String getExpression() { + outputView.print("식 : "); + return inputView.read(); + } + + public void printCalculatedResult(int result) { + outputView.print(result); + } + + public void printEndMessage() { + outputView.print("프로그램을 종료합니다."); + } + + public void printErrorMessage(String message) { + outputView.print(message); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/view/InputView.java b/java-calculator/src/main/java/com/programmers/blackdog/view/InputView.java new file mode 100644 index 000000000..32ed4488e --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/view/InputView.java @@ -0,0 +1,5 @@ +package com.programmers.blackdog.view; + +public interface InputView { + String read(); +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/view/OutputView.java b/java-calculator/src/main/java/com/programmers/blackdog/view/OutputView.java new file mode 100644 index 000000000..653ad2d24 --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/view/OutputView.java @@ -0,0 +1,11 @@ +package com.programmers.blackdog.view; + +import java.util.List; + +public interface OutputView { + void print(List list); + + void print(String string); + + void print(int integer); +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/view/PrintStreamOutputView.java b/java-calculator/src/main/java/com/programmers/blackdog/view/PrintStreamOutputView.java new file mode 100644 index 000000000..d787c6e0d --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/view/PrintStreamOutputView.java @@ -0,0 +1,21 @@ +package com.programmers.blackdog.view; + +import java.util.List; + +public class PrintStreamOutputView implements OutputView { + + @Override + public void print(int result) { + System.out.println(result); + } + + @Override + public void print(List list) { + list.forEach(System.out::println); + } + + @Override + public void print(String string) { + System.out.println(string); + } +} diff --git a/java-calculator/src/main/java/com/programmers/blackdog/view/ScannerInputView.java b/java-calculator/src/main/java/com/programmers/blackdog/view/ScannerInputView.java new file mode 100644 index 000000000..0ea677ffe --- /dev/null +++ b/java-calculator/src/main/java/com/programmers/blackdog/view/ScannerInputView.java @@ -0,0 +1,12 @@ +package com.programmers.blackdog.view; + +import java.util.Scanner; + +public class ScannerInputView implements InputView { + private final Scanner scanner = new Scanner(System.in); + + @Override + public String read() { + return scanner.nextLine(); + } +} diff --git a/java-calculator/src/test/java/com/programmers/blackdog/domain/ArithmeticOperatorsTest.java b/java-calculator/src/test/java/com/programmers/blackdog/domain/ArithmeticOperatorsTest.java new file mode 100644 index 000000000..eaf2aefd4 --- /dev/null +++ b/java-calculator/src/test/java/com/programmers/blackdog/domain/ArithmeticOperatorsTest.java @@ -0,0 +1,46 @@ +package com.programmers.blackdog.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static com.programmers.blackdog.domain.ArithmeticOperators.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ArithmeticOperatorsTest { + + @DisplayName("두 수를 더하면 올바른 값이 반환된다.") + @Test + void when_AddTwoNum_Expects_correctResult() { + int result = ADDITION.apply(1, 2); + assertThat(result).isEqualTo(3); + } + + @DisplayName("두 수를 빼면 올바른 값이 반환된다.") + @Test + void when_SubtractTwoNum_Expects_correctResult() { + int result = SUBTRACTION.apply(1, 2); + assertThat(result).isEqualTo(-1); + } + + @DisplayName("두 수를 곱하면 올바른 값이 반환된다.") + @Test + void when_MultiplyTwoNum_Expects_correctResult() { + int result = MULTIPLICATION.apply(4, 2); + assertThat(result).isEqualTo(8); + } + + @DisplayName("두 수를 나누면 올바른 값이 반환된다.") + @Test + void when_DivideTwoNum_Expects_correctResult() { + int result = DIVISION.apply(4, 2); + assertThat(result).isEqualTo(2); + } + + @DisplayName("0으로 나누면 예외를 발생한다.") + @Test + void when_DivideWithZero_Expects_ThrowsException() { + assertThatThrownBy(() -> DIVISION.apply(10, 0)) + .isInstanceOf(ArithmeticException.class); + } +} diff --git a/java-calculator/src/test/java/com/programmers/blackdog/domain/CalculatorTest.java b/java-calculator/src/test/java/com/programmers/blackdog/domain/CalculatorTest.java new file mode 100644 index 000000000..22ed67383 --- /dev/null +++ b/java-calculator/src/test/java/com/programmers/blackdog/domain/CalculatorTest.java @@ -0,0 +1,53 @@ +package com.programmers.blackdog.domain; + +import com.programmers.blackdog.domain.calculator.Calculator; +import com.programmers.blackdog.domain.expression.Expression; +import com.programmers.blackdog.domain.expression.InfixExpression; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class CalculatorTest { + + private Calculator calculator; + + @BeforeEach + void setUp() { + calculator = new Calculator(); + } + + private static Expression getExpression(String expression) { + return new InfixExpression(expression); + } + + @DisplayName("계산 식을 입력하면 올바른 계산 결과를 반환한다.") + @ParameterizedTest(name = "식: {0}, 결과: {1}") + @CsvSource(value = {"1 + 2=3", "1 + 2 * 3=7", "3 - 2 * 2=-1", "44 / 2 * 8=176", "22 * 8=176", "12 + 44 / 2 * 8 - 10 + 18 / 2 / 3=181"}, delimiter = '=') + void when_ExpressionIsGiven_CalculateCorrectResult(String expression, int calculatedResult) { + int actual = calculator.calculate(getExpression(expression)); + + assertThat(actual).isEqualTo(calculatedResult); + } + + @DisplayName("계산 값이 정수 형 값을 벗어났을 경우 예외가 발생한다.") + @ParameterizedTest(name = "식: {0}") + @ValueSource(strings = {"999999 * 999999 * 99999 * 9999999", "1 - 999999999 - 999999999 - 999999999 - 999999999"}) + void when_CalculatedResultIsOutOfIntegerRange_Expects_ThrowException(String expression) { + assertThatThrownBy(() -> calculator.calculate(getExpression(expression))) + .isInstanceOf(ArithmeticException.class); + } + + @DisplayName("입력값인 식에 정수 범위를 초과하는 값이 있으면 예외를 발생한다.") + @Test + void when_InputIsOutOfIntegerRange_Expects_ThrowException() { + String expression = "999999999999999 + 1"; + assertThatThrownBy(() -> calculator.calculate(getExpression(expression))) + .isInstanceOf(IllegalArgumentException.class); + } +} \ No newline at end of file diff --git a/java-calculator/src/test/java/com/programmers/blackdog/domain/InfixExpressionTest.java b/java-calculator/src/test/java/com/programmers/blackdog/domain/InfixExpressionTest.java new file mode 100644 index 000000000..9ca4e6e16 --- /dev/null +++ b/java-calculator/src/test/java/com/programmers/blackdog/domain/InfixExpressionTest.java @@ -0,0 +1,67 @@ +package com.programmers.blackdog.domain; + + +import com.programmers.blackdog.domain.expression.InfixExpression; +import org.junit.jupiter.api.DisplayName; +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 static org.assertj.core.api.Assertions.*; + +class InfixExpressionTest { + @DisplayName("올바른 형식의 식이면 예외가 발생하지 않는다.") + @ParameterizedTest + @ValueSource(strings = {"1 + 2", "1 - 3", "1 * 2", "4 / 2", "12 + 23 * 45"}) + void when_CorrectFormat_Expects_DoesNotThrowException(String expression) { + assertThatNoException().isThrownBy(() -> new InfixExpression(expression)); + } + + @DisplayName("값과 연산자 사이에 공백이 존재하지 않으면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"1+2", "1-3", "12+23* 45"}) + void when_SpaceDoesNotExists_Expects_ThrowException(String expression) { + assertThatThrownBy(() -> new InfixExpression(expression)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("값과 연산자 사이에 공백 개수가 하나 이상이면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"1 + 2, 1 - 2", "3 * 4 / 6"}) + void when_MoreThanOneSpaceExists_Expects_ThrowException(String expression) { + assertThatThrownBy(() -> new InfixExpression(expression)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("잘못된 형식이나 순서의 식이면 예외가 발생한다.") + @ParameterizedTest + @ValueSource(strings = {"1 & 2", "/ 1 + ", "1 / + 2"}) + void when_WrongFormat_Expects_ThrowException(String expression) { + assertThatThrownBy(() -> new InfixExpression(expression)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("중위 표현식을 후위 표현식으로 변환한다.") + @ParameterizedTest(name = "중위 : {0}, 후위 : {1}") + @CsvSource(value = {"3 + 4 * 2=3 4 2 * +", "1 + 2 * 3 + 4 / 2 + 2=1 2 3 * + 4 2 / + 2 +", "4 + 5 * 6 / 2 - 3=4 5 6 * 2 / + 3 -", "44 / 2=44 2 /"}, delimiter = '=') + void when_InfixNotationIsGiven_Expects_ReturnPostfixNotation(String infixExpression, String postfixExpression) { + InfixExpression expression = new InfixExpression(infixExpression); + + String actual = expression.getPostfixExpression(); + + assertThat(actual).isEqualTo(postfixExpression); + } + + @DisplayName("동일한 식을 가진 Expression 객체의 동등성을 보장한다.") + @Test + public void when_givenEqualExpression_Expects_Equal() { + String expression = "3 + 4 * 2"; + + InfixExpression infixExpression1 = new InfixExpression(expression); + InfixExpression infixExpression2 = new InfixExpression(expression); + + assertThat(infixExpression1).hasSameHashCodeAs(infixExpression2); + assertThat(infixExpression1).isEqualTo(infixExpression2); + } +} \ No newline at end of file diff --git a/java-calculator/src/test/java/com/programmers/blackdog/domain/RegexGeneratorTest.java b/java-calculator/src/test/java/com/programmers/blackdog/domain/RegexGeneratorTest.java new file mode 100644 index 000000000..ca0e1a24e --- /dev/null +++ b/java-calculator/src/test/java/com/programmers/blackdog/domain/RegexGeneratorTest.java @@ -0,0 +1,43 @@ +package com.programmers.blackdog.domain; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + + +class RegexGeneratorTest { + + private static final String REGEX_PREFIX = "^\\d+\\s(["; + private static final String REGEX_SUFFIX = "]\\s\\d+\\s)+$"; + public static final String OPERATOR_PREFIX = "\\"; + + private RegexGenerator regexGenerator; + + @BeforeEach + void setUp() { + regexGenerator = new RegexGenerator(); + } + + @DisplayName("연산자 설정에 따른 정규식을 만들어 반환한다.") + @ParameterizedTest + @CsvSource(value = {"ADDITION/SUBTRACTION", "SUBTRACTION/ADDITION", "SUBTRACTION/DIVISION"}, delimiter = '/') + void when_ConfineSpecificOperator_Expects_GenerateRegexWithInputOperator(ArithmeticOperators firstOperator, ArithmeticOperators secondOperator) { + String regex = regexGenerator.generateWithOperator(firstOperator, secondOperator); + + assertThat(regex).isEqualTo(REGEX_PREFIX + OPERATOR_PREFIX + firstOperator.getOperator()+ OPERATOR_PREFIX + secondOperator.getOperator() + REGEX_SUFFIX); + } + + @DisplayName("모든 연산자를 포함한 정규식을 반환한다.") + @Test + void when_GenerateWithAllOperator_Expects_GenerateRegexWithAllOperator() { + String regex = regexGenerator.generateWithAllOperator(); + + String expected = "^\\d+\\s([\\+\\-\\*\\/]\\s\\d+\\s)+$"; + + assertThat(regex).isEqualTo(expected); + } +} \ No newline at end of file diff --git a/java-calculator/src/test/java/com/programmers/blackdog/domain/utils/StringUtilTest.java b/java-calculator/src/test/java/com/programmers/blackdog/domain/utils/StringUtilTest.java new file mode 100644 index 000000000..0db7c9e1c --- /dev/null +++ b/java-calculator/src/test/java/com/programmers/blackdog/domain/utils/StringUtilTest.java @@ -0,0 +1,27 @@ +package com.programmers.blackdog.domain.utils; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class StringUtilTest { + @DisplayName("입력값이 숫자가 아니면 예외를 발생한다.") + @Test + void when_InputIsNotNumber_Expects_ThrowsException() { + String input = "a"; + + assertThatThrownBy(() -> StringUtil.convertStringToInt(input)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("입력값이 숫자이면 예외를 발생하지 않는다.") + @Test + void when_InputIsNumber_Expects_DoesNotThrowsException() { + String input = "111"; + + assertThatNoException() + .isThrownBy(() -> StringUtil.convertStringToInt(input)); + } +} \ No newline at end of file