From d92980f9b775a509628a1906222647da5fea2aa5 Mon Sep 17 00:00:00 2001 From: seokrae Date: Mon, 17 Oct 2022 22:29:31 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=201.=20OperationProcessor=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=20=EA=B8=B0=EB=8A=A5=202.=20Thread=20Map,=20arrays=20?= =?UTF-8?q?Stream,=20forEach=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 +- .../v1/exception/ExceptionMessages.java | 14 +++ .../NotSupportedOperationException.java | 10 ++ .../v1/processor/OperationProcessor.java | 87 +++++++++++++++++ .../v1/processor/OperationProcessorTest.java | 68 +++++++++++++ .../OperationProcessorThreadTest.java | 96 +++++++++++++++++++ 6 files changed, 277 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/v1/exception/ExceptionMessages.java create mode 100644 src/main/java/com/example/v1/exception/NotSupportedOperationException.java create mode 100644 src/main/java/com/example/v1/processor/OperationProcessor.java create mode 100644 src/test/java/com/example/v1/processor/OperationProcessorTest.java create mode 100644 src/test/java/com/example/v1/processor/OperationProcessorThreadTest.java diff --git a/build.gradle b/build.gradle index fec5db5..60c489f 100644 --- a/build.gradle +++ b/build.gradle @@ -13,8 +13,8 @@ repositories { } dependencies { - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.9.0' + testImplementation 'org.assertj:assertj-core:3.23.1' } test { diff --git a/src/main/java/com/example/v1/exception/ExceptionMessages.java b/src/main/java/com/example/v1/exception/ExceptionMessages.java new file mode 100644 index 0000000..9753bda --- /dev/null +++ b/src/main/java/com/example/v1/exception/ExceptionMessages.java @@ -0,0 +1,14 @@ +package com.example.v1.exception; + +public enum ExceptionMessages { + NOT_SUPPORTED_OPERATION_EXCEPTION("지원하지 않는 연산입니다. {}"); + + private final String message; + ExceptionMessages(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/example/v1/exception/NotSupportedOperationException.java b/src/main/java/com/example/v1/exception/NotSupportedOperationException.java new file mode 100644 index 0000000..d168ac3 --- /dev/null +++ b/src/main/java/com/example/v1/exception/NotSupportedOperationException.java @@ -0,0 +1,10 @@ +package com.example.v1.exception; + +import static com.example.v1.exception.ExceptionMessages.NOT_SUPPORTED_OPERATION_EXCEPTION; + +public class NotSupportedOperationException extends RuntimeException { + + public NotSupportedOperationException(String express) { + super(String.format(NOT_SUPPORTED_OPERATION_EXCEPTION.getMessage(), express)); + } +} diff --git a/src/main/java/com/example/v1/processor/OperationProcessor.java b/src/main/java/com/example/v1/processor/OperationProcessor.java new file mode 100644 index 0000000..00a6daa --- /dev/null +++ b/src/main/java/com/example/v1/processor/OperationProcessor.java @@ -0,0 +1,87 @@ +package com.example.v1.processor; + +import com.example.v1.exception.NotSupportedOperationException; + +import java.util.Arrays; +import java.util.Map; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.stream.Collectors; + +public enum OperationProcessor { + PLUS("+", 0, Double::sum), + SUBTRACT("-", 0, (left, right) -> (left - right)), + MULTIPLY("*", 1, (left, right) -> (left * right)), + DIVIDE("/", 1, (left, right) -> { + double result = left / right; + if (Double.isInfinite(result)) { + throw new ArithmeticException("0으로는 나눌 수 없습니다."); + } + return result; + }); + + private static final String OPERATOR_VALUE = "^[+\\-*/]$"; + private final String operator; + private final int priority; + private final BiFunction expression; + + private static final Map OPERATOR_MAP = Arrays.stream(values()) + .collect(Collectors.toMap(OperationProcessor::getOperator, Function.identity())); + + OperationProcessor(String operator, int priority, BinaryOperator expression) { + this.operator = operator; + this.priority = priority; + this.expression = expression; + } + + /* == 정적 팩토리 메서드 패턴으로 제공하는 API == */ + // Map을 통해 구현한 방식 O(1)으로 접근 + public static OperationProcessor of(String operator) { + return Optional.ofNullable(OPERATOR_MAP.get(operator)) + .orElseThrow(() -> new NotSupportedOperationException(operator)); + } + + // array stream을 통해 구현한 방식 O(n)으로 접근 + public static OperationProcessor arrayOf(String operator) { + return Arrays.stream(values()) + .filter(v -> v.operator.equals(operator)) + .findFirst() + .orElseThrow(() -> new NotSupportedOperationException(operator)); + } + + // forEach를 통해 구현한 방식 O(n)으로 접근 + public static OperationProcessor forEachOf(String operator) { + for (OperationProcessor op : values()) { + if (op.operator.equals(operator)) { + return op; + } + } + throw new NotSupportedOperationException(operator); + } + + /* == 정적 팩토리 메서드 패턴으로 제공하는 API == */ + + public static boolean isOperator(String userInput) { + return Optional.ofNullable(userInput) + .filter(s -> s.matches(OPERATOR_VALUE)) + .isPresent(); + } + + public static boolean compareTo(String peek, String s) { + return OperationProcessor.of(peek).priority >= OperationProcessor.of(s).priority; + } + + public double calculate(double left, double right) { + return expression.apply(left, right); + } + + private String getOperator() { + return operator; + } + + private static boolean isMatchesOperator(String operator, OperationProcessor op) { + return op.operator.equals(operator); + } +} \ No newline at end of file diff --git a/src/test/java/com/example/v1/processor/OperationProcessorTest.java b/src/test/java/com/example/v1/processor/OperationProcessorTest.java new file mode 100644 index 0000000..7baf54f --- /dev/null +++ b/src/test/java/com/example/v1/processor/OperationProcessorTest.java @@ -0,0 +1,68 @@ +package com.example.v1.processor; + +import com.example.v1.exception.NotSupportedOperationException; +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 org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +class OperationProcessorTest { + + private static Stream operatorDummy() { + return Stream.of( + Arguments.of("+", OperationProcessor.PLUS), + Arguments.of("-", OperationProcessor.SUBTRACT), + Arguments.of("*", OperationProcessor.MULTIPLY), + Arguments.of("/", OperationProcessor.DIVIDE) + ); + } + + private static Stream notOperatorDummy() { + return Stream.of( + Arguments.of("1"), + Arguments.of("a"), + Arguments.of("A") + ); + } + + @DisplayName("연산자 프로세서 연산자 확인 테스트") + @MethodSource("operatorDummy") + @ParameterizedTest(name = "{index} => operator={0}, expected={1}") + void testCase1(String operator, OperationProcessor expected) { + // given + + // when + OperationProcessor actual = OperationProcessor.of(operator); + + // then + assertThat(actual).isEqualTo(expected); + } + + @DisplayName("연산자 프로세서 연산자 확인 실패 테스트") + @MethodSource("notOperatorDummy") + @ParameterizedTest(name = "{index} => operator={0}") + void testCase2(String notOperator) { + assertThatExceptionOfType(NotSupportedOperationException.class) + .isThrownBy(() -> OperationProcessor.of(notOperator)); + } + + @DisplayName("연산자 프로세서 연산자 확인 실패 테스트") + @NullAndEmptySource + @ParameterizedTest(name = "{index} => operator={0}") + void testCase3(String notOperator) { + assertThatExceptionOfType(NotSupportedOperationException.class) + .isThrownBy(() -> OperationProcessor.of(notOperator)); + } + +} \ No newline at end of file diff --git a/src/test/java/com/example/v1/processor/OperationProcessorThreadTest.java b/src/test/java/com/example/v1/processor/OperationProcessorThreadTest.java new file mode 100644 index 0000000..69dfd88 --- /dev/null +++ b/src/test/java/com/example/v1/processor/OperationProcessorThreadTest.java @@ -0,0 +1,96 @@ +package com.example.v1.processor; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +class OperationProcessorThreadTest { + + @DisplayName("연산자 프로세서(Map) 멀티스레드 시간 측정 테스트") + @ValueSource(ints = {1000}) + @ParameterizedTest(name = "{index} => threadCount={0}") + void testCase1(int nThreads) { + // given + ExecutorService executorService = Executors.newFixedThreadPool(nThreads); + CountDownLatch countDownLatch = new CountDownLatch(nThreads); + + // when + long start = System.currentTimeMillis(); + for (int i = 0; i < nThreads; i++) { + executorService.execute(() -> { + OperationProcessor.of("+"); + }); + countDownLatch.countDown(); + } + + try { + countDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + long end = System.currentTimeMillis(); + + // then + System.out.println("Map time : " + (end - start)); + } + + @DisplayName("연산자 프로세서(arrays) 멀티스레드 시간 측정 테스트") + @ValueSource(ints = {1000}) + @ParameterizedTest(name = "{index} => threadCount={0}") + void testCase2(int nThreads) { + // given + ExecutorService executorService = Executors.newFixedThreadPool(nThreads); + CountDownLatch countDownLatch = new CountDownLatch(nThreads); + + // when + long start = System.currentTimeMillis(); + for (int i = 0; i < nThreads; i++) { + executorService.execute(() -> { + OperationProcessor.arrayOf("+"); + }); + countDownLatch.countDown(); + } + + try { + countDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + long end = System.currentTimeMillis(); + + // then + System.out.println("arrays time : " + (end - start)); + } + + @DisplayName("연산자 프로세서(forEach) 멀티스레드 시간 측정 테스트") + @ValueSource(ints = {1000}) + @ParameterizedTest(name = "{index} => threadCount={0}") + void testCase3(int nThreads) { + // given + ExecutorService executorService = Executors.newFixedThreadPool(nThreads); + CountDownLatch countDownLatch = new CountDownLatch(nThreads); + + // when + long start = System.currentTimeMillis(); + for (int i = 0; i < nThreads; i++) { + executorService.execute(() -> { + OperationProcessor.forEachOf("+"); + }); + countDownLatch.countDown(); + } + + try { + countDownLatch.await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + long end = System.currentTimeMillis(); + + // then + System.out.println("forEach time : " + (end - start)); + } +} \ No newline at end of file From 919cea1034858a92629e00a39655e87635bdf7d4 Mon Sep 17 00:00:00 2001 From: seokrae Date: Mon, 17 Oct 2022 22:45:11 +0900 Subject: [PATCH 2/5] =?UTF-8?q?feat:=20priorityTo=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../v1/processor/OperationProcessor.java | 19 ++++--- .../v1/processor/OperationProcessorTest.java | 52 ++++++++++++++----- 2 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/example/v1/processor/OperationProcessor.java b/src/main/java/com/example/v1/processor/OperationProcessor.java index 00a6daa..58dbd6b 100644 --- a/src/main/java/com/example/v1/processor/OperationProcessor.java +++ b/src/main/java/com/example/v1/processor/OperationProcessor.java @@ -46,42 +46,41 @@ public static OperationProcessor of(String operator) { // array stream을 통해 구현한 방식 O(n)으로 접근 public static OperationProcessor arrayOf(String operator) { return Arrays.stream(values()) - .filter(v -> v.operator.equals(operator)) + .filter(v -> isEquals(operator, v)) .findFirst() .orElseThrow(() -> new NotSupportedOperationException(operator)); } // forEach를 통해 구현한 방식 O(n)으로 접근 + public static OperationProcessor forEachOf(String operator) { for (OperationProcessor op : values()) { - if (op.operator.equals(operator)) { + if (isEquals(operator, op)) { return op; } } throw new NotSupportedOperationException(operator); } - /* == 정적 팩토리 메서드 패턴으로 제공하는 API == */ - public static boolean isOperator(String userInput) { return Optional.ofNullable(userInput) .filter(s -> s.matches(OPERATOR_VALUE)) .isPresent(); } - public static boolean compareTo(String peek, String s) { - return OperationProcessor.of(peek).priority >= OperationProcessor.of(s).priority; + public static boolean priorityTo(String origin, String other) { + return OperationProcessor.of(origin).priority >= OperationProcessor.of(other).priority; } public double calculate(double left, double right) { return expression.apply(left, right); } - private String getOperator() { - return operator; + private static boolean isEquals(String operator, OperationProcessor v) { + return v.operator.equals(operator); } - private static boolean isMatchesOperator(String operator, OperationProcessor op) { - return op.operator.equals(operator); + private String getOperator() { + return operator; } } \ No newline at end of file diff --git a/src/test/java/com/example/v1/processor/OperationProcessorTest.java b/src/test/java/com/example/v1/processor/OperationProcessorTest.java index 7baf54f..c823429 100644 --- a/src/test/java/com/example/v1/processor/OperationProcessorTest.java +++ b/src/test/java/com/example/v1/processor/OperationProcessorTest.java @@ -7,11 +7,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; @@ -21,18 +17,18 @@ class OperationProcessorTest { private static Stream operatorDummy() { return Stream.of( - Arguments.of("+", OperationProcessor.PLUS), - Arguments.of("-", OperationProcessor.SUBTRACT), - Arguments.of("*", OperationProcessor.MULTIPLY), - Arguments.of("/", OperationProcessor.DIVIDE) + Arguments.of("+", OperationProcessor.PLUS), + Arguments.of("-", OperationProcessor.SUBTRACT), + Arguments.of("*", OperationProcessor.MULTIPLY), + Arguments.of("/", OperationProcessor.DIVIDE) ); } private static Stream notOperatorDummy() { return Stream.of( - Arguments.of("1"), - Arguments.of("a"), - Arguments.of("A") + Arguments.of("1"), + Arguments.of("a"), + Arguments.of("A") ); } @@ -54,7 +50,7 @@ void testCase1(String operator, OperationProcessor expected) { @ParameterizedTest(name = "{index} => operator={0}") void testCase2(String notOperator) { assertThatExceptionOfType(NotSupportedOperationException.class) - .isThrownBy(() -> OperationProcessor.of(notOperator)); + .isThrownBy(() -> OperationProcessor.of(notOperator)); } @DisplayName("연산자 프로세서 연산자 확인 실패 테스트") @@ -62,7 +58,37 @@ void testCase2(String notOperator) { @ParameterizedTest(name = "{index} => operator={0}") void testCase3(String notOperator) { assertThatExceptionOfType(NotSupportedOperationException.class) - .isThrownBy(() -> OperationProcessor.of(notOperator)); + .isThrownBy(() -> OperationProcessor.of(notOperator)); + } + + private static Stream operatorMatches() { + return Stream.of( + Arguments.of("+", "+", true), + Arguments.of("-", "-", true), + Arguments.of("*", "*", true), + Arguments.of("/", "/", true), + Arguments.of("+", "-", true), + Arguments.of("-", "+", true), + Arguments.of("*", "/", true), + Arguments.of("/", "*", true), + Arguments.of("+", "*", false), + Arguments.of("-", "/", false), + Arguments.of("*", "+", true), + Arguments.of("/", "-", true) + ); + } + + @DisplayName("연산자 프로세서 priorityTo 테스트") + @MethodSource("operatorMatches") + @ParameterizedTest(name = "{index} => operator={0}, other={1}, expected={2}") + void testCase4(String origin, String other, boolean expected) { + // given + + // when + boolean actual = OperationProcessor.priorityTo(origin, other); + + // then + assertThat(actual).isEqualTo(expected); } } \ No newline at end of file From 6ec6c69a5753e06d44e1fb5987a8af738d2477b8 Mon Sep 17 00:00:00 2001 From: seokrae Date: Mon, 17 Oct 2022 23:18:27 +0900 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=EC=97=B0=EC=82=B0=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20calculate=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OperationProcessorCalculatorTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/test/java/com/example/v1/processor/OperationProcessorCalculatorTest.java diff --git a/src/test/java/com/example/v1/processor/OperationProcessorCalculatorTest.java b/src/test/java/com/example/v1/processor/OperationProcessorCalculatorTest.java new file mode 100644 index 0000000..f378f26 --- /dev/null +++ b/src/test/java/com/example/v1/processor/OperationProcessorCalculatorTest.java @@ -0,0 +1,35 @@ +package com.example.v1.processor; + +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.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class OperationProcessorCalculatorTest { + + private static Stream operatorAndExpressionDummy() { + return Stream.of( + Arguments.of("+", 1, 2, 3), + Arguments.of("-", 1, 2, -1), + Arguments.of("*", 1, 2, 2), + Arguments.of("/", 1, 2, 0.5) + ); + } + + @DisplayName("Calculator 연산 테스트") + @MethodSource("operatorAndExpressionDummy") + @ParameterizedTest(name = "{index} => {1} {0} {2} = {3}") + void testCase1(String operator, int left, int right, double expected) { + // given + + // when + double actual = OperationProcessor.of(operator).calculate(left, right); + + // then + assertThat(actual).isEqualTo(expected); + } +} \ No newline at end of file From 2ee3eae6d7f6f43f10621943392a086cf998295a Mon Sep 17 00:00:00 2001 From: seokrae Date: Mon, 17 Oct 2022 23:20:39 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20calculator=20stack=EC=9D=84=20?= =?UTF-8?q?=EC=9D=B4=EC=9A=A9=ED=95=9C=20=ED=9B=84=EC=9C=84=ED=91=9C?= =?UTF-8?q?=EA=B8=B0=EC=8B=9D=20=EC=97=B0=EC=82=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/v1/specification/Calculator.java | 33 +++++++++++++++++++ .../v1/specification/CalculatorInterface.java | 5 +++ .../v1/specification/CalculatorTest.java | 22 +++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 src/main/java/com/example/v1/specification/Calculator.java create mode 100644 src/main/java/com/example/v1/specification/CalculatorInterface.java create mode 100644 src/test/java/com/example/v1/specification/CalculatorTest.java diff --git a/src/main/java/com/example/v1/specification/Calculator.java b/src/main/java/com/example/v1/specification/Calculator.java new file mode 100644 index 0000000..9276c1d --- /dev/null +++ b/src/main/java/com/example/v1/specification/Calculator.java @@ -0,0 +1,33 @@ +package com.example.v1.specification; + +import com.example.v1.processor.OperationProcessor; + +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * 후위 표기법으로 정의된 수식을 계산하는 클래스 + */ +public final class Calculator implements CalculatorInterface { + + @Override + public double calculate(String expression) { + String[] tokens = expression.split(" "); + Deque stack = new ConcurrentLinkedDeque<>(); + for (String token : tokens) { + transaction(stack, token); + } + return stack.pop(); + } + + private void transaction(Deque stack, String token) { + if (OperationProcessor.isOperator(token)) { + double rightOperand = stack.pop(); + double leftOperand = stack.pop(); + double calculate = OperationProcessor.of(token).calculate(leftOperand, rightOperand); + stack.push(calculate); + } else { + stack.push(Double.parseDouble(token)); + } + } +} diff --git a/src/main/java/com/example/v1/specification/CalculatorInterface.java b/src/main/java/com/example/v1/specification/CalculatorInterface.java new file mode 100644 index 0000000..99da8f3 --- /dev/null +++ b/src/main/java/com/example/v1/specification/CalculatorInterface.java @@ -0,0 +1,5 @@ +package com.example.v1.specification; + +public interface CalculatorInterface { + double calculate(String expression); +} diff --git a/src/test/java/com/example/v1/specification/CalculatorTest.java b/src/test/java/com/example/v1/specification/CalculatorTest.java new file mode 100644 index 0000000..ee856f3 --- /dev/null +++ b/src/test/java/com/example/v1/specification/CalculatorTest.java @@ -0,0 +1,22 @@ +package com.example.v1.specification; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class CalculatorTest { + + @DisplayName("계산 테스트") + @CsvSource(value = {"1 2 +, 3", "1 2 -, -1", "1 2 *, 2"}, delimiter = ',') + @ParameterizedTest(name = "{index} => expression={0}, expected={1}") + void testCase1(String expression, double expected) { + // given + + // when + double calculate = new Calculator().calculate(expression); + // then + assertThat(calculate).isEqualTo(expected); + } +} \ No newline at end of file From 7b94aa315703c98f4b55d56b0c51d1df33d39010 Mon Sep 17 00:00:00 2001 From: seokrae Date: Mon, 17 Oct 2022 23:44:45 +0900 Subject: [PATCH 5/5] feat: infix to postfix convert --- .../v1/specification/CalculatorConverter.java | 51 +++++++++++ .../v1/specification/CalculatorDecorator.java | 15 ++++ .../CalculatorConverterTest.java | 89 +++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/main/java/com/example/v1/specification/CalculatorConverter.java create mode 100644 src/main/java/com/example/v1/specification/CalculatorDecorator.java create mode 100644 src/test/java/com/example/v1/specification/CalculatorConverterTest.java diff --git a/src/main/java/com/example/v1/specification/CalculatorConverter.java b/src/main/java/com/example/v1/specification/CalculatorConverter.java new file mode 100644 index 0000000..ba9e393 --- /dev/null +++ b/src/main/java/com/example/v1/specification/CalculatorConverter.java @@ -0,0 +1,51 @@ +package com.example.v1.specification; + + +import com.example.v1.processor.OperationProcessor; + +import java.util.Deque; +import java.util.concurrent.ConcurrentLinkedDeque; + +public final class CalculatorConverter extends CalculatorDecorator { + + private Calculator calculator; + + public CalculatorConverter(CalculatorInterface calculator) { + super(calculator); + } + + public double calculate(String infixExpression) { + if (calculator == null) { + this.calculator = new Calculator(); + } + String postFix = parseExpressionInfixToPostFix(infixExpression); + return calculator.calculate(postFix); + } + + String parseExpressionInfixToPostFix(String inFixExpression) { + StringBuilder postFixExpression = new StringBuilder(); + Deque stack = new ConcurrentLinkedDeque<>(); + String[] tokens = inFixExpression.split(" "); + for (String token : tokens) { + makePostfixExpression(postFixExpression, stack, token); + } + while (!stack.isEmpty()) { + postFixExpression.append(stack.pop()); + postFixExpression.append(" "); + } + return postFixExpression.toString().trim(); + } + + private void makePostfixExpression(StringBuilder stringBuilder, Deque stack, String token) { + if (OperationProcessor.isOperator(token)) { + if (!stack.isEmpty() && OperationProcessor.priorityTo(stack.peek(), token)) { + stringBuilder.append(stack.pop()); + stringBuilder.append(" "); + } + stack.push(token); + } else { + stringBuilder.append(token); + stringBuilder.append(" "); + } + } +} diff --git a/src/main/java/com/example/v1/specification/CalculatorDecorator.java b/src/main/java/com/example/v1/specification/CalculatorDecorator.java new file mode 100644 index 0000000..ca0e4da --- /dev/null +++ b/src/main/java/com/example/v1/specification/CalculatorDecorator.java @@ -0,0 +1,15 @@ +package com.example.v1.specification; + +public abstract class CalculatorDecorator implements CalculatorInterface { + + private final CalculatorInterface core; + + protected CalculatorDecorator(CalculatorInterface core) { + this.core = core; + } + + @Override + public double calculate(String expression) { + return core.calculate(expression); + } +} diff --git a/src/test/java/com/example/v1/specification/CalculatorConverterTest.java b/src/test/java/com/example/v1/specification/CalculatorConverterTest.java new file mode 100644 index 0000000..77949b2 --- /dev/null +++ b/src/test/java/com/example/v1/specification/CalculatorConverterTest.java @@ -0,0 +1,89 @@ +package com.example.v1.specification; + +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.Assertions.assertThat; + +class CalculatorConverterTest { + + private static Stream infixExpressionDummy() { + return Stream.of( + Arguments.of("1 + 2", "1 2 +"), + Arguments.of("1 - 2", "1 2 -"), + Arguments.of("1 * 2", "1 2 *"), + Arguments.of("1 / 2", "1 2 /"), + Arguments.of("1 + 2 + 3", "1 2 + 3 +"), + Arguments.of("1 + 2 - 3", "1 2 + 3 -"), + Arguments.of("1 + 2 * 3", "1 2 3 * +"), + Arguments.of("1 + 2 / 3", "1 2 3 / +"), + Arguments.of("1 - 2 + 3", "1 2 - 3 +"), + Arguments.of("1 - 2 - 3", "1 2 - 3 -"), + Arguments.of("1 - 2 * 3", "1 2 3 * -") + ); + } + + @DisplayName("중위 표기법 변환 테스트") + @MethodSource("infixExpressionDummy") + @ParameterizedTest(name = "{index} => {0} ==> {1}") + void testCase1(String infixExpression, String expected) { + // given + + // when + CalculatorConverter calculatorConverter = new CalculatorConverter(new Calculator()); + String actual = calculatorConverter.parseExpressionInfixToPostFix(infixExpression); + + // then + assertThat(actual).isEqualTo(expected); + } + + private static Stream infixExpressionCalculatorDummy() { + return Stream.of( + Arguments.of("1 + 2", 3), + Arguments.of("1 - 2", -1), + Arguments.of("1 * 2", 2), + Arguments.of("1 / 2", 0.5), + Arguments.of("1 + 2 + 3", 6), + Arguments.of("1 + 2 - 3", 0), + Arguments.of("1 + 2 * 3", 7), + Arguments.of("1 - 2 + 3", 2), + Arguments.of("1 - 2 - 3", -4), + Arguments.of("1 - 2 * 3", -5), + Arguments.of("1 * 2 + 3", 5), + Arguments.of("1 * 2 - 3", -1), + Arguments.of("1 * 2 * 3", 6), + Arguments.of("1 * 2 / 3", 0.6666666666666666), + Arguments.of("1 / 2 + 3", 3.5), + Arguments.of("1 / 2 - 3", -2.5), + Arguments.of("1 / 2 * 3", 1.5), + Arguments.of("1 / 2 / 3", 0.16666666666666666), + Arguments.of("1 + 2 + 3 + 4", 10), + Arguments.of("1 + 2 + 3 - 4", 2), + Arguments.of("1 + 2 + 3 * 4", 15), + Arguments.of("1 + 2 + 3 / 4", 3.75), + Arguments.of("1 + 2 - 3 + 4", 4), + Arguments.of("1 + 2 - 3 - 4", -4), + Arguments.of("1 + 2 - 3 * 4", -9), + Arguments.of("1 + 2 - 3 / 4", 2.25) + ); + } + + @DisplayName("중위 표기법 변환 연산 테스트") + @MethodSource("infixExpressionCalculatorDummy") + @ParameterizedTest(name = "{index} => {0} ==> {1}") + void testCase2(String infixExpression, double expected) { + // given + + // when + CalculatorConverter calculatorConverter = new CalculatorConverter(new Calculator()); + double actual = calculatorConverter.calculate(infixExpression); + + // then + assertThat(actual).isEqualTo(expected); + } +} \ No newline at end of file