반응형

개인 프로젝트를 진행 중 계산기 기능을 추가하려고 로직을 구현 중이었는데 생각보다 생각할게 많았다.

나는 중위 표기법으로 표현된 수식을 후위 표기법으로 변환시켜 계산하였다. 

중위표기법이란 흔히 아는 연산자가 피연산자들의 가운데 위치하는 형태이다.

예 ) 3 + 5 * 4

진행 중인 웹프로젝트의 계산기 기능이다.

 

간략하게 기능을 설명하자면 C버튼은 Clear를 뜻하고 누르면 입력했던 숫자들이 초기화된다.

계산할 식을 입력 후 "=" 버튼을 누르면 계산된 값을 반환받는 방식으로 제작하였다.

계산기 하단에는 계산했던 기록이 남아있도록 했다.

 

 

코드 설명

   public double calculator(CalculatorRequestDto calculatorRequestDto) {
        String calContents = calculatorRequestDto.getCalContents();
        return evaluateExpression(calContents);
    }

CalculatorRequestDto를 받아서 계산하도록 했다. dto에서는 String 값으로 calContents라는 문자열을 입력받아서 가져온다. 그 뒤 evaluateExpression 메서드에 calContents값을 넘겨주고 받은 반환값을 최종값으로 리턴해준다.

 

 

 

    private double evaluateExpression(String expression) {
        Stack<Double> numbers = new Stack<>();
        Stack<Character> operators = new Stack<>();

        for (int i = 0; i < expression.length(); i++) {
            char ch = expression.charAt(i);

            if (Character.isDigit(ch)) {
                StringBuilder num = new StringBuilder();
                while (i < expression.length() && (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
                    num.append(expression.charAt(i));
                    i++;
                }
                i--;

                numbers.push(Double.parseDouble(num.toString()));
            } else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
                while (!operators.isEmpty() && hasPrecedence(ch, operators.peek())) {
                    numbers.push(applyOperator(operators.pop(), numbers.pop(), numbers.pop()));
                }
                operators.push(ch);
            }
        }

        while (!operators.isEmpty()) {
            numbers.push(applyOperator(operators.pop(), numbers.pop(), numbers.pop()));
        }

        return numbers.pop();
    }

계산을 담당하는 핵심 메서드이다.

Stack형 변수를 두 개 만들었다. Double형과 Character형으로 만들어줬고 numbers는 각 숫자들을 담당, operators는 각 기호(+, -, *, /)를 담당하도록 했다.

 

그 뒤 for문을 사용해서 넘겨받은 문자열의 길이만큼 반복문을 돌렸다.

char형 변수 ch에는 expression의 i번째 문자를 넣어줬고, 첫 번째 만나는 if에서는 ch가 0-9에 해당하는 정수형인지 체크했다. 만약 숫자라면 변수 num을 하나 만들어준 뒤 while문을 사용해서 num에 문자를 추가한다. 그 뒤 i를 1 증가시킨다. while문의 실행조건은 i가 expression의 길이보다 작고, 현재 i번째 문자가 숫자 거나.(소수점)이 들어간다면 실행하도록 했다. 마지막에 i--를 해주는 이유는 while문에서 i를 증가시키고 있는데 마지막에 중복의 가능성이 있기 때문에 i--를 추가해 줬다. 그 뒤 numbers에 push를 사용해서 num을 Double형으로 변환한 값을 넣어줬다. 

else if에서는 ch가 사칙연산중 하나라면 실행한다. while문을 사용했고 while문의 실행조건은 operators가 비어있지 않고, hasPrecedence 메서드를 활용해서 기호의 우선순위를 비교해서 true일 때만 실행하도록 했다. 내부에서는 numbers 스택에 applyOperator 메서드를 사용해 결괏값을 구해서 넣어준다. 마지막으로 numbers.pop()을 사용해서 마지막값을 반환해 준다.

 

 

 

 

    private boolean hasPrecedence(char op1, char op2) {
        return (op2 != '+' && op2 != '-') || (op1 != '*' && op1 != '/');
    }

 

연산자의 우선순위를 확인하는 메서드이다.

매개변수로 두 개의 기호를 받아서 비교한다. 두 번째 변수가 +, - 가 아니면 true를 반환하고 첫 번째 변수가 *, / 가 아닐 때도 true를 반환한다. 다시 말해 op1이 스택 제일 위에 있는 op2보다 우선순위가 높으면 true를 반환, 아니라면 false를 반환한다. 

 

 

 

 

    private double applyOperator(char operator, double b, double a) {
        switch (operator) {
            case '+':
                return a + b;
            case '-':
                return a - b;
            case '*':
                return a * b;
            case '/':
                if (b == 0) {
                    throw new ArithmeticException("0으로는 나눗셈이 불가능합니다.");
                }
                return a / b;
            default:
                throw new IllegalArgumentException("기호가 잘못되었습니다.");
        }
    }

연산을 담당하는 메서드이다.

각 기호에 맞는 계산을 해서 반환한다. 예외 상황으로 나눗셈에서 나누는 값이 0이라면 예외를 던지도록 하였고, 기호가 +, -, *, / 중 하나가 아니라면 마찬가지로 예외를 던졌다.

 

 

 

 

 

전체코드

package com.gunwook.jpeople.calculator.service;

import com.gunwook.jpeople.calculator.dto.CalculatorRequestDto;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Stack;

@Service
@RequiredArgsConstructor
public class CalculatorService {

    public double calculator(CalculatorRequestDto calculatorRequestDto) {
        String calContents = calculatorRequestDto.getCalContents();
        return evaluateExpression(calContents);
    }

    private double evaluateExpression(String expression) {
        Stack<Double> numbers = new Stack<>();
        Stack<Character> operators = new Stack<>();

        for (int i = 0; i < expression.length(); i++) {
            char ch = expression.charAt(i);

            if (Character.isDigit(ch)) {
                StringBuilder num = new StringBuilder();
                while (i < expression.length() && (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
                    num.append(expression.charAt(i));
                    i++;
                }
                i--;

                numbers.push(Double.parseDouble(num.toString()));
            } else if (ch == '+' || ch == '-' || ch == '*' || ch == '/') {
                while (!operators.isEmpty() && hasPrecedence(ch, operators.peek())) {
                    numbers.push(applyOperator(operators.pop(), numbers.pop(), numbers.pop()));
                }
                operators.push(ch);
            }
        }

        while (!operators.isEmpty()) {
            numbers.push(applyOperator(operators.pop(), numbers.pop(), numbers.pop()));
        }

        return numbers.pop();
    }

    private boolean hasPrecedence(char op1, char op2) {
        return (op2 != '+' && op2 != '-') || (op1 != '*' && op1 != '/');
    }

    private double applyOperator(char operator, double b, double a) {
        switch (operator) {
            case '+':
                return a + b;
            case '-':
                return a - b;
            case '*':
                return a * b;
            case '/':
                if (b == 0) {
                    throw new ArithmeticException("0으로는 나눗셈이 불가능합니다.");
                }
                return a / b;
            default:
                throw new IllegalArgumentException("기호가 잘못되었습니다.");
        }
    }
}
반응형

+ Recent posts