수식을 받을 땐 문자열이기 때문에 공백을 신경써야 한다.
@Override
public String process(String expression) {
// 공백 처리
expression = replaceExpression(expression);
String[] splitWhiteSpace = expression.split(" ");
for (String str : splitWhiteSpace) {
if (str.isBlank()) {
continue;
}
// 숫자일 경우 바로 리스트에 추가
if (!isOperator(str)) {
convertList.add(str + " ");
} else {
// 괄호일 경우 방향에 따라 다르게 처리
if (fromSymbol(str.charAt(0)).equals(LEFT_BRACE)) {
operatorStack.push(str.charAt(0));
} else if (fromSymbol(str.charAt(0)).equals(RIGHT_BRACE)) {
while (operatorStack.peek() != LEFT_BRACE.getSymbol() && !operatorStack.empty()) {
convertList.add(operatorStack.pop().toString() + " ");
}
operatorStack.pop();
} else {
// 괄호를 제외한 연산 기호는 스택에 저장된 값과 우선 순위를 비교
if (operatorStack.empty() || priority(operatorStack.peek()) < priority(str.charAt(0))) {
operatorStack.push(str.charAt(0));
} else if (priority(operatorStack.peek()) >= priority(str.charAt(0))) {
convertList.add(operatorStack.pop().toString() + " ");
operatorStack.push(str.charAt(0));
}
}
}
}
// 스택에 남은 연산자를 맨 뒤에 추가
while (!operatorStack.empty()) {
convertList.add(operatorStack.pop().toString() + " ");
}
for (String str : convertList) {
postfixResult.append(str);
}
return postfixResult.toString();
}
// 문자열 공백 처리
private static String replaceExpression(String expression) {
expression = expression.trim()
.replace("(", " ( ")
.replace(")", " ) ")
.replace("+", " + ")
.replace("-", " - ")
.replace("*", " * ")
.replace("/", " / ")
.replace(" ", " ");
return expression;
}
공백을 처리할 때 문자열 시작과 끝에 있는 공백은 제거하고, 나머지 공백은 이후 변환할 때 한 자리 수 이상의 숫자도 처리할 수 있도록 split()을 이용해서 String[]로 처리하기 위해 최소 1칸은 남겨두었다.
추가로
split()을 사용한 이유는 두 자리 이상의 숫자를 처리하기 위함도 있다
반복문을 통해 변환할 땐 공백의 경우 continue;로 스킵하고, 그 외의 경우는 로직에 따라서 움직이도록 했다. 이때 결과를 도출하기 위한 값은 List<String>에 저장하고, 연산자는 Stack<Charactor>에 보관했다.
코드를 보면 List에 저장할 때 공백을 주고 있는 것을 볼 수 있다. 이는 중위 표기법을 변환 했을 때와 마찬가지로 두 자리 이상의 숫자를 처리하기 위함이다. 그리고 연산자의 경우 무조건 길이가 1이기 때문에 charAt(0)을 사용해서 처리했다.
수식을 모두 처리한 뒤에는 Stack에 남은 연산자를 붙여주기 위해 while문으로 List에 넣고 연산을 위해 String으로 바꿔서 넘겨주었다.
AbstractExpressionProcessor추상 클래스를표기법 변환 클래스와연산 클래스가 상속하고, 추상 클래스 내부의String process(String mathExpression)추상 메서드를 사용했기 때문에String으로 변환했다.
표기법 연산을 할 때도 미리 한 칸씩 띄워두었기 때문에 split()을 통해 String[]로 바꿔서 처리했다.
@Override
public String process(String expression) {
Operator<Number> operator;
String[] splitWhiteSpace = expression.split(" ");
for (String item : splitWhiteSpace) {
operandStack.push(item);
if (isOperator(item)) {
// 연산자 제거 용도
operandStack.pop();
Number firstInputNum;
Number secondInputNum;
try {
secondInputNum = NumberFormat.getInstance().parse(operandStack.pop());
firstInputNum = NumberFormat.getInstance().parse(operandStack.pop());
} catch (ParseException e) {
throw new BaseException("문자열을 숫자로 파싱하는 과정에서 문제가 발생했습니다.");
}
StringBuilder result = new StringBuilder();
operator = getOperator(fromSymbol(item.charAt(0)));
result.append(operator.operation(firstInputNum, secondInputNum));
operandStack.push(result.toString());
}
}
return operandStack.pop();
}
반복문을 통해 String[]에서 문자열을 하나씩 꺼내서 처리하도록 했다. 이때 다양한 타입으로의 변환을 위해 Operator<T extends Number>로 제네릭 선언을 했고, 연산 클래스에선 Number로 지정했다.
연산자와 피연산자 모두 일단 Stack<String>에 저장하고, 연산자의 경우 바로 pop()으로 제거한다. 그리고 Stack에서 String으로 저장된 숫자를 꺼내 NumberFormat.getInstance().parse()를 통해 Number 타입으로 파싱한다. 이때 ParseException을 핸들링하기 위해서 try-catch로 처리해줬다.
파싱 후 변수 선언을 할 땐 순서에 주의한다
Operator에 연산자에 맞는 클래스를 적용하기 위해 심플 팩토리 패턴을 사용했다.
public class OperatorFactory {
public static <T extends Number> Operator<T> getOperator(OperatorType type) {
return switch (type) {
case PLUS -> new AddOperator<>();
case MINUS -> new SubtractOperator<>();
case MULTIPLY -> new MultiplyOperator<>();
case DIVIDE -> new DivideOperator<>();
default -> throw new BaseException("지원하지 않는 연산 기호입니다.");
};
}
}
클래스 적용 이후엔 operation()으로 실제 계산을 하고, 결과를 다시 Stack<String>에 넣기 위해 StringBuilder()에 append()하고 toString()으로 바꿔서 넣어줬다.
일련의 방법을 반복해서 최종 결과가 나오면 Stack에 있는 결과를 pop()으로 빼서 리턴하도록 했다.
실제 코드는 여기에서 확인