C++ 문자열 입력 사칙연산 계산기

0

알고리즘

목록 보기
13/13

들어가기에 앞서,,

아는 후배가 과제 때문에 끙끙 앓고 있어서 도움을 주게 되었어요.
컴공으로 막 전과한 학생인데 저희 학교도 이런 과제는 잘 안내주는데 꽤나 어렵더라고요.
문자열을 입력받아서 사칙 연산을 구현해라~
근데, 조건이 몇 가지 까다로운게 있었어요.
그래서 저도 쉬울거라고 생각하고 선뜻 도와준다고 했는데,,,

카페에서 커피 호로롭 호로롭 하면서 한 시간 정도는 쓴 것 같네요.
덕분에 카공했다~ 친구얌

과제 조건

아무래도 대학생 분들 중에 이런 과제를 받은 분들이 꽤 있을 것 같아요. 그래서 그 과제가 이 과제랑 내용이 똑같나~ 하고 비교를 해야되니까 제가 조건을 명시해놓도록 하겠습니다.
이 과제가 까다로웠던 이유는 진짜 계산기처럼 숫자를 하나씩 입력받는게 아니라, 하나의 수식을 입력받는 형태에요.

  1. 수식은 문자열 형태로 입력받는다.
  2. 잘못된 수식이 입력될 경우, 판별할 수 있어야한다.
  3. 사칙연산 순서를 지켜야한다.
  4. substr(), find(), stoi() 함수를 적극 활용해야한다.

조건 4 때문에 저는 굳이 vector와 같은 걸 쓰지 않고 구현을 해야 했어요. 그게 저한테는 좀 어려웠달까요...

과제 풀이

먼저 문자열 형태의 수식을 입력받으면, 이 수식에서 사칙연산의 순서를 지켜야겠죠.
그래서 제가 잊지말고 있어야 하는 사실들을 나열해봤어요.

사실 아이디어라기 보다는 잊지말아야 할 사실? 조건? 이라고 할 수 있어요.
이는 소프트웨어 공학에서 요구 공학과 비슷한데요? 사용자 요구사항이라기보다는 개발자의 입장에서 쓰여진 시스템 요구사항이라고 볼 수 있겠네요.

하하,, 제가 요즘 소프트웨어 공학 개론 수업을 듣고 있어서요.
근데 확실히 저도 모르게 요즘은 이렇게 요구사항을 정리하는 편이에요.

코딩을 하기 전에 무턱대고 하기 보다는 이렇게 간단하게 명세하는 습관을 들이는 건 아주 중요하답니다.

연산 기호 찾기

이 아이디어들을 구현하기 위해서 실제로 해야할 사항은 무엇일까요?

바로 연산 기호를 찾는 것입니다. 그런데 연산 기호들에는 순서가 있기 때문에 곱셈 기호 (*)와 나눗셈 기호 (/)를 먼저 찾아야해요.
그리고 이들이 존재하지 않는다면 덧셈 기호 (+)와 뺄셈 기호 (-)를 찾아야해요.

그러고 나서 1-1과 1-2처럼 작동하면 돼요.
곱셈 기호와 나눗셈 기호가 둘 다 존재한다면, 수식에서 먼저 오는 녀석부터 계산하면 됩니다.
이는 find(*)와 find(/)을 통해 나온 인덱스가 더 작은 녀석을 먼저 계산하면 되겠죠.

둘 중에 하나만 존재한다면, find(*) 또는 find(/)가 하나는 정상적인 인덱스가 반환되지만 하나는 -1이 반환될거에요.
따라서 그럴 경우에는 정상적인 인덱스가 반환된 녀석을 먼저 계산해주면 됩니다.

이것은 덧셈 기호와 뺄셈 기호 연산에서도 동일하게 작용해요.

연산 기호를 기준으로 앞 뒤 숫자 찾기

자 이제 기호를 찾았으면 무엇을 해야할까요?

바로 기호 앞과 뒤의 숫자를 찾아야겠죠.

이 부분이 조금 어려울 수 있어요.

찾은 기호를 기준으로 기호 앞의 서브 문자열 (sub1)과 기호 뒤의 서브 문자열 (sub2)로 나눕니다.

그리고 sub1과 sub2에서 저희가 찾고자 하는 수들을 찾을 수 있겠죠.
sub1에서는 뒤에서부터 하나씩 숫자를 읽다가 연산 기호가 등장하기 전까지가 저희가 찾는 수에요.

반대로 sub2에서는 앞에서부터 하나씩 숫자를 읽다가 연산 기호가 등장하기 전까지가 저희가 찾는 수에요.

하지만 찾은 숫자는 문자열 형태입니다. 예를 들어, 123을 찾았지만 실제로 자료형은 "123"과 같아요.
이것을 숫자로 바꿔주기 위해서 stoi() 함수에 숫자 문자열을 넣어서 정수형 형태로 바꿔줄 수 있어요.

그러면 저희는 앞서 찾은 연산 기호의 앞 뒤 숫자를 찾았네요?

하지만!!!
아주 중요한 사실은 수식이 잘못 입력되었을 경우 연산 기호 앞에 숫자가 아니라 또 기호가 있을 수 있어요.
따라서 위 그림에서와 같이 l_numberr_number를 찾았는데 둘 중에 하나라도 숫자가 아니면 잘못된 수식을 입력한 것으로 판단하고
계산기를 종료하면 됩니다.

우헤헤 이제 거의 다왔어요.

찾은 숫자와 기호로 계산한 결과를 다시 문자열에 집어넣기


자자, 마지막 단계에요.
아까 찾은 l_numberr_number 그리고 맨 첫 단계에서 찾은 기호를 실제로 계산하고 다시 원래의 문자열에 집어넣어주면 돼요.

해당 과정은 위 그림과 동일합니다.
그리고 새롭게 정의된 str도 옆에서 알려주고 있죠.

이것은 substr() 함수를 통해서 sub1, sub2에서 반환된 결과를 잘 생각해보면 해당 그림과 같이 적절하게 변경된다는 것을 알 수 있어요.
이 정도는 쉽게 계산할 수 있을 겁니다.

소감 및 한계

가장 큰 한계는 실수 형태로 계산을 하지 못해요.
왜냐하면 문자열에서 숫자를 가져올 때 기본적으로 stoi() 함수, 즉 문자열을 정수형으로 바꿔주는 함수를 사용하기 때문에 실수형태를 가져올 수가 없습니다.
그래서 입력된 문자열 수식은 항상 정확하게 나누어 떨어지는 수들로만 구성할 수 있습니다.
만약 그렇지 않는다면 알아서 형변환이 되면서 소수점 자리들이 떨어지게 될거에요

그리고 제가 아래에서 코드를 보여드릴텐데 vector와 같은 것을 사용하지 않고 오로지 문자열 처리로만 구현된 계산기여서 조금 복잡합니다.

개선

그래서 더욱 더 개선하자면 이런 아이디어가 있어요.

  1. 입력된 문자열 수식이 정상인지 비정상인지 판단하는 함수를 둬요. 예를들어, 연산 기호가 맨 앞, 또는 맨 뒤에 등장했거나 연산 기호 여러 개가 연달아 등장하는 경우가 있는 경우에 계산기를 종료할 수 있어요.

  2. 수식이 정상이라면 앞에서부터 읽으면서 숫자와 기호들을 분리해요. 각각 숫자 벡터와 기호 벡터에 담아서 유지할 수 있습니다.

  3. 기호 벡터에서 가장 먼저 계산되어야 할 기호를 찾은 뒤에 해당 기호 앞 뒤 숫자를 숫자 벡터에서 찾고 벡터에서 뺍니다. 그러고나서 계산하고 계산 결과를 해당 위치에 다시 넣어줍니다. 물론 찾은 기호도 기호 벡터에서 제거된 상태에요.
    이 과정은 실제로 벡터에서 값을 빼고 넣고가 아니고 해당 숫자들 전까지는 새로운 벡터에 담고, 찾은 숫자들의 계산 결과를 담고, 해당 숫자들 이후에 존재하는 숫자들을 다시 담는 식으로 구현될거에요.

이런 식으로 계산하면 실수 형태도 계산할 수 있는 계산기가 될거에요.
하지만 제가 생각했을 때, 해당 과제의 교수님은 문자열 처리를 기본으로 원하시는 것 같고 실수에 대한 어떤 조건도 달려있지 않았기 때문에 제 식대로 해석하여 코딩을 해봤습니다.

이게 여러 사람에게 도움이 되었으면 좋겠네요. 아래는 코드입니다.

#include <string.h>
#include <iostream>
using namespace std;

string str;

bool check_op(char c) {
    if (c == '*' || c == '/' || c == '+' || c == '-')
        return true;
    return false;
}

string get_left_number(string str) {
    int cnt = 0;
    for (int i = str.length() - 1; i > -1; i--) {
        if (!check_op(str[i])) {
            cnt++;
        }
        else
            break;
    }
    if (cnt == 0)
        return "";
    else
        return str.substr(str.length()-cnt);
}

string get_right_number(string str) {
    int cnt = 0;
    for (int i = 0; i < str.length(); i++) {
        if (!check_op(str[i])) {
            cnt++;
        }
        else
            break;
    }
    if (cnt == 0)
        return "";
    else
        return str.substr(0, cnt);
}

string calculate(int index) {
    // 연산 기호를 중심으로 앞 뒤 문자열로 나눔
    string sub1 = str.substr(0, index);
    string sub2 = str.substr(index + 1);

    // 연산 기호 앞 숫자를 가져옴
    string l_number = get_left_number(sub1);
    // 연산 기호 뒤 숫자를 가져옴
    string r_number = get_right_number(sub2);

    if (l_number.length() == 0 || r_number.length() == 0) {
        // 수식이 성립하지 못하는 경우 빈 문자열 리턴
        return "";
    }
    int result = 0;
    switch(str[index]) {
        case '*':
            result = stoi(l_number) * stoi(r_number);
            break;
        case '/':
            result = stoi(l_number) / stoi(r_number);
            break;
        case '+':
            result = stoi(l_number) + stoi(r_number);
            break;
        case '-':
            result = stoi(l_number) - stoi(r_number);
            break;
    }
    return sub1.substr(0, sub1.length() - l_number.length()) + to_string(result) + sub2.substr(r_number.length());
}

int find_first_op(int find1, int find2) {
    int index = -1;
    if (find1 != -1 && find2 != -1) {
        if (find1 < find2) {
            index = find1;
        } else {
            index = find2;
        }
    } else if (find1 != -1) {
        index = find1;
    } else if (find2 != -1) {
        index = find2;
    }
    return index;
}

int main() {
    cin >> str;

    while (true) {
        // 곱셈 또는 나눗셈 문자열 찾기
        int find1 = str.find('*');
        int find2 = str.find('/');

        int index = -1;
        index = find_first_op(find1, find2);

        if (index > -1) {
            str = calculate(index);
                if (str == "") {
                    cout << "잘못된 수식 입력" << '\n';
                    break;
                }
            continue;
        }

        // 더하기 또는 빼기 문자열 찾기
        find1 = str.find('+');
        find2 = str.find('-');

        index = find_first_op(find1, find2);

        if (index > -1) {
            str = calculate(index);
                if (str == "") {
                    cout << "잘못된 수식 입력" << '\n';
                    break;
                }
            continue;
        }

        if (index == -1) {
            cout << "결과: " << str << '\n';
            cout << "연산을 종료합니다." << '\n';
            break;
        }
    }
}
profile
최악의 환경에서 최선을 다하기

0개의 댓글