Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/example/v1/exception/ExceptionMessages.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
86 changes: 86 additions & 0 deletions src/main/java/com/example/v1/processor/OperationProcessor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
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<Double, Double, Double> expression;

private static final Map<String, OperationProcessor> OPERATOR_MAP = Arrays.stream(values())
.collect(Collectors.toMap(OperationProcessor::getOperator, Function.identity()));

OperationProcessor(String operator, int priority, BinaryOperator<Double> 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 -> isEquals(operator, v))
.findFirst()
.orElseThrow(() -> new NotSupportedOperationException(operator));
}

// forEach를 통해 구현한 방식 O(n)으로 접근

public static OperationProcessor forEachOf(String operator) {
for (OperationProcessor op : values()) {
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 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 static boolean isEquals(String operator, OperationProcessor v) {
return v.operator.equals(operator);
}

private String getOperator() {
return operator;
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/example/v1/specification/Calculator.java
Original file line number Diff line number Diff line change
@@ -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<Double> stack = new ConcurrentLinkedDeque<>();
for (String token : tokens) {
transaction(stack, token);
}
return stack.pop();
}

private void transaction(Deque<Double> 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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> 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(" ");
}
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example.v1.specification;

public interface CalculatorInterface {
double calculate(String expression);
}
Original file line number Diff line number Diff line change
@@ -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<Arguments> 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);
}
}
94 changes: 94 additions & 0 deletions src/test/java/com/example/v1/processor/OperationProcessorTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 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<Arguments> operatorDummy() {
return Stream.of(
Arguments.of("+", OperationProcessor.PLUS),
Arguments.of("-", OperationProcessor.SUBTRACT),
Arguments.of("*", OperationProcessor.MULTIPLY),
Arguments.of("/", OperationProcessor.DIVIDE)
);
}

private static Stream<Arguments> 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));
}

private static Stream<Arguments> 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);
}

}
Loading