이 계산기는 과제로 나온 계산기를 좀더 발전시켜본 버전이다.
물론 OOP를 잘 지켜서 구현하지는 못한 거 같지만 이 부분은 이후에 OOP에 대해서 더 익숙해지고나서 수정을 해보겠다.
<전체 코드>
// Calculator.kt
import kotlin.math.round
class Calculator {
// 추가연산인지 확인하기 위한 변수
private lateinit var initialValue: Pair<String, Int>
fun selectSymbol() {
// 계산식 입력
println("원하는 연산을 입력해주세요.(종료를 원하시면 q를 입력해주세요)")
// 받아야하는 형식 : (숫자)(연산기호)(숫자)
val expression = readln()
// 종료 이벤트
if (expression == "q") return
// 잘못된 입력 처리 1
if (expression.contains("[^(0-9+\\-*/.)]".toRegex())) {
println("오류 : 숫자와 연산기호 이외의 입력이 들어옴.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 2(연산기호가 여러개인 경우)
if (expression.count { it == '+' || it == '-' || it == '*' || it == '/' } > 1) {
println("오류 : 이 계산기에서는 최대 두 수에 대한 연산만 가능.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 3(.이 여러번 연속으로 찍혀서 오는 경우)
if (expression.contains("\\.{2,}".toRegex())) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.을 연속해서 입력)")
println("비정상적인 종료")
return
}
// 해당 계산식을 3부분으로 쪼갬
// Pair의 first는 값, second는 유형
// 0: 연산기호 1: 정수 2: 실수
val splitExpression = ArrayList<Pair<String, Int>>()
for (idx in expression.indices) {
// 연산기호가 나온 인덱스를 기준으로 3등분함
when (expression[idx]) {
'+', '-', '*', '/' -> {
// 연산기호 이전의 숫자
val expNum1 = expression.substring(0 until idx)
// 잘못된 입력 처리 4(.이 숫자 안에 여러번 찍혀있는 경우)
if (expNum1.count { it == '.' } > 1) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.이 숫자 안에 여러개 존재)")
println("비정상적인 종료")
return
}
// 연산기호
val operator = expression[idx].toString()
// 연산기호 이후의 숫자
val expNum2 = expression.substring(idx + 1 until expression.length)
// 잘못된 입력 처리 4(.이 숫자 안에 여러번 찍혀있는 경우)
if (expNum2.count { it == '.' } > 1) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.이 숫자 안에 여러개 존재)")
println("비정상적인 종료")
return
}
splitExpression.add(Pair(expNum1, if (expNum1.contains('.')) 2 else 1))
splitExpression.add(Pair(operator, 0))
splitExpression.add(Pair(expNum2, if (expNum2.contains('.')) 2 else 1))
// 더 이상 볼 필요 없으니 반복문 탈출
break
}
}
}
// 사용하기 편하게 변수에 따로 저장
var symbol = splitExpression[1].first
val n1 = splitExpression[0]
var n2 = splitExpression[2]
// 무한 루프
while (true) {
// 계산 결과(추가 연산일 때와 초기 연산일 때를 구분)
val result = if (this::initialValue.isInitialized) {
calculate(symbol, initialValue, n2)
} else {
calculate(symbol, n1, n2)
}
// 0으로 숫자를 나누려하지 않았다면
if (result.first != "0 ERROR") {
// 계산 결과를 이후 연산의 초기값으로 저장
initialValue = result
println("결과 : ${result.first} \n")
} else {
// 에러 출력 후 계산기 종료
println("오류 : 숫자를 0으로 나눌 수 없습니다.")
println("비정상적인 종료")
break
}
// 추가 연산 시작
println("추가로 연산을 원하신다면 연산기호와 숫자를 입력해주세요.")
println("계산을 멈추고 싶으면 q를 눌러주세요.")
// 이전의 결과값을 화면에 출력
print(result.first)
// 받아야하는 형식 : (연산기호)(숫자)
val expandedExpression = readln()
// 종료 이벤트
if (expandedExpression == "q") return
// 잘못된 입력 처리 1
if (expandedExpression.contains("[^(0-9+\\-*/.)]".toRegex())) {
println("오류 : 숫자와 연산기호 이외의 입력이 들어옴.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 2(연산기호가 여러개인 경우)
if (expression.count { it == '+' || it == '-' || it == '*' || it == '/' } > 1) {
println("오류 : 이 계산기에서는 최대 두 수에 대한 연산만 가능.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 3(추가 연산에서는 앞의 숫자가 없이 연산기호부터 시작하기 때문에 존재하는 조건)
if (expandedExpression[0] != '+' && expandedExpression[0] != '-' && expandedExpression[0] != '*' && expandedExpression[0] != '/') {
println("오류 : 올바르지 않은 형식으로 들어옴.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 4
if (expandedExpression.contains("\\.{2,}".toRegex())) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.을 연속해서 입력)")
println("비정상적인 종료")
return
}
// 추가 계산식을 2부분으로 쪼갬
// 0번째 인덱스가 연산기호인 것이 정해져있기 때문에 따로 반복문을 돌리지 않음
symbol = expandedExpression[0].toString()
// Pair의 first는 값, second는 유형
// 0: 연산기호 1: 정수 2: 실수
val expNum = expandedExpression.substring(1 until expandedExpression.length)
// 잘못된 입력 처리 5(.이 숫자 안에 여러번 찍혀있는 경우)
if (expNum.count { it == '.' } > 1) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.이 숫자 안에 여러개 존재)")
println("비정상적인 종료")
return
}
n2 = Pair(expNum, if (expNum.contains('.')) 2 else 1)
}
}
private fun calculate(symbol: String, n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// 연산 기호에 따라 해당하는 연산 함수로 이동
return when (symbol) {
"+" -> add(n1, n2)
"-" -> substract(n1, n2)
"*" -> multiply(n1, n2)
"/" -> divide(n1, n2)
else -> Pair("ERROR", -1) // 위에서 정확한 연산 기호를 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
private fun add(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() + n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() + n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1)// 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
private fun substract(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() - n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() - n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1) // 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
private fun multiply(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() * n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() * n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1) // 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
private fun divide(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// 0으로 나누려는 시도를 막기위한 예외처리
if (n2.first.toDouble() == 0.0) return Pair("0 ERROR", -1)
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() / n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() / n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1) // 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
// 시작과 끝에서 출력되는 안내 메세지
fun startMessage() {
println("두 수에 대한 연산이 가능한 계산기가 시작되었습니다.")
println("연산은 최대 소수점 둘째자리까지 출력됩니다.\n")
}
fun stopMessage() = println("\n계산기가 종료되었습니다.")
}
내가 짠 코드의 기본 틀은 두 수에 대한 식을 입력받는 것이다.
예를 들어 2+2, 3*3, 5/2 처럼 두개의 숫자 사이에 식이 들어가면 된다.
물론 실수도 들어갈 수 있다.(2.6+20.5, 405-230.1 등등)
받을 때는 그냥 String 타입으로 한번에 받는다.
val expression = readln()
그래야 계산기의 의미가 생기는 느낌이라 그렇게 했다.
한번에 받고 예외처리를 싹~했다.(종료 버튼을 눌렀는지, 올바르지않은 입력을 했는지 체크)
// 종료 이벤트
if (expression == "q") return
// 잘못된 입력 처리 1
if (expression.contains("[^(0-9+\\-*/.)]".toRegex())) {
println("오류 : 숫자와 연산기호 이외의 입력이 들어옴.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 2(연산기호가 여러개인 경우)
if (expression.count { it == '+' || it == '-' || it == '*' || it == '/' } > 1) {
println("오류 : 이 계산기에서는 최대 두 수에 대한 연산만 가능.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 3(.이 여러번 연속으로 찍혀서 오는 경우)
if (expression.contains("\\.{2,}".toRegex())) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.을 연속해서 입력)")
println("비정상적인 종료")
return
}
그 다음에 숫자와 연산기호를 나눴다.
// 해당 계산식을 3부분으로 쪼갬
// Pair의 first는 값, second는 유형
// 0: 연산기호 1: 정수 2: 실수
val splitExpression = ArrayList<Pair<String, Int>>()
for (idx in expression.indices) {
// 연산기호가 나온 인덱스를 기준으로 3등분함
when (expression[idx]) {
'+', '-', '*', '/' -> {
// 연산기호 이전의 숫자
val expNum1 = expression.substring(0 until idx)
// 잘못된 입력 처리 4(.이 숫자 안에 여러번 찍혀있는 경우)
if (expNum1.count { it == '.' } > 1) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.이 숫자 안에 여러개 존재)")
println("비정상적인 종료")
return
}
// 연산기호
val operator = expression[idx].toString()
// 연산기호 이후의 숫자
val expNum2 = expression.substring(idx + 1 until expression.length)
// 잘못된 입력 처리 4(.이 숫자 안에 여러번 찍혀있는 경우)
if (expNum2.count { it == '.' } > 1) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.이 숫자 안에 여러개 존재)")
println("비정상적인 종료")
return
}
splitExpression.add(Pair(expNum1, if (expNum1.contains('.')) 2 else 1))
splitExpression.add(Pair(operator, 0))
splitExpression.add(Pair(expNum2, if (expNum2.contains('.')) 2 else 1))
// 더 이상 볼 필요 없으니 반복문 탈출
break
}
}
}
이 때 Pair를 쓰는데 왜 이렇게 했냐면 이전에 실수와 정수를 모두 계산 가능하게 짤 때 변수 안에 조건문을 걸어 타입을 정하니까 Number 타입으로 넘어가더라
그렇게 되면 함수로 넘겼을 때 기본적인 사칙연산을 할 수 없었다.
Number 타입에는 연산메소드가 존재하지 않기 때문이었다.
그래서 생각해낸 방법이 이것이다.
Pair를 써서 값과 값의 유형이 어느 것인지 정해주는 것이다.
값의 유형을 가지고 함수에 들어가면 그에 맞춰서 계산을 하고 결과의 유형도 정해줄 수 있다.
Int와 Double의 연산에서 Double이 되도록 하는 것이 된다.
더 좋은 방법이 있다면 알려주시길...
또한 예외처리하는 구문이 있는데 이건 3.3.3.3 이런식으로 한 숫자라고 판별해야하는데 .이 여러개 들어가있는 경우를 잡기 위해서다.
그 다음에는 연산기호와 숫자를 사용하기 쉽게 변수에 저장하고 무한 루프에 들어간다.
거기서 초기 연산과 추가 연산을 구분한다.
while (true) {
// 계산 결과(추가 연산일 때와 초기 연산일 때를 구분)
val result = if (this::initialValue.isInitialized) {
calculate(symbol, initialValue, n2)
} else {
calculate(symbol, n1, n2)
}
...
}
초기 연산일 때는 값을 2개를 받아서 계산해줘야하지만 추가 연산일 때는 이전의 결과값이 있기 때문에 값을 1개만 받으면 되기 때문이다.
결과값은 다음과 같이 출력된다.
while (true) {
...
// 0으로 숫자를 나누려하지 않았다면
if (result.first != "0 ERROR") {
// 계산 결과를 이후 연산의 초기값으로 저장
initialValue = result
println("결과 : ${result.first} \n")
} else {
// 에러 출력 후 계산기 종료
println("오류 : 숫자를 0으로 나눌 수 없습니다.")
println("비정상적인 종료")
break
}
...
}
그 다음 이제 추가 연산 코드다.
이전과 거의 다 비슷하고 다른 점은 입력을 받을 때 연산자부터 써줘야한다는 것이다.
그에 따른 예외처리도 진행한다.
while (true) {
...
// 추가 연산 시작
println("추가로 연산을 원하신다면 연산기호와 숫자를 입력해주세요.")
println("계산을 멈추고 싶으면 q를 눌러주세요.")
// 이전의 결과값을 화면에 출력
print(result.first)
// 받아야하는 형식 : (연산기호)(숫자)
val expandedExpression = readln()
// 종료 이벤트
if (expandedExpression == "q") return
// 잘못된 입력 처리 1
if (expandedExpression.contains("[^(0-9+\\-*/.)]".toRegex())) {
println("오류 : 숫자와 연산기호 이외의 입력이 들어옴.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 2(연산기호가 여러개인 경우)
if (expression.count { it == '+' || it == '-' || it == '*' || it == '/' } > 1) {
println("오류 : 이 계산기에서는 최대 두 수에 대한 연산만 가능.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 3(추가 연산에서는 앞의 숫자가 없이 연산기호부터 시작하기 때문에 존재하는 조건)
if (expandedExpression[0] != '+' && expandedExpression[0] != '-' && expandedExpression[0] != '*' && expandedExpression[0] != '/') {
println("오류 : 올바르지 않은 형식으로 들어옴.")
println("비정상적인 종료")
return
}
// 잘못된 입력 처리 4
if (expandedExpression.contains("\\.{2,}".toRegex())) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.을 연속해서 입력)")
println("비정상적인 종료")
return
}
...
}
여기서는 받은 문자열을 두 부분으로 쪼갠다.
연산자와 숫자 하나만 있기 때문이다.
그리고 반복문도 돌리지 않는다.
처음에는 앞의 숫자가 어느 정도의 길이인지를 모르기 때문에 반복문을 돌았던 것이고 추가 연산 때는 제일 처음이 연산자이기 때문에 돌 필요가 없다.
while (true) {
...
// 추가 계산식을 2부분으로 쪼갬
// 0번째 인덱스가 연산기호인 것이 정해져있기 때문에 따로 반복문을 돌리지 않음
symbol = expandedExpression[0].toString()
// Pair의 first는 값, second는 유형
// 0: 연산기호 1: 정수 2: 실수
val expNum = expandedExpression.substring(1 until expandedExpression.length)
// 잘못된 입력 처리 5(.이 숫자 안에 여러번 찍혀있는 경우)
if (expNum.count { it == '.' } > 1) {
println("오류 : 올바르지 않은 숫자 입력이 들어옴.(.이 숫자 안에 여러개 존재)")
println("비정상적인 종료")
return
}
n2 = Pair(expNum, if (expNum.contains('.')) 2 else 1)
...
}
계산을 위한 함수는 연산기호에 맞춰서 해당 연산함수를 부르는 것으로 구현했다.
when에 else를 쓰고 싶지 않았는데 빨간줄이 뜨더라...
private fun calculate(symbol: String, n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// 연산 기호에 따라 해당하는 연산 함수로 이동
return when (symbol) {
"+" -> add(n1, n2)
"-" -> substract(n1, n2)
"*" -> multiply(n1, n2)
"/" -> divide(n1, n2)
else -> Pair("ERROR", -1) // 위에서 정확한 연산 기호를 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
각각의 연산함수들이다.
거의 비슷한 코드라서 하나만 이해해도 다른 것들은 이해가 될 것이다.
이걸 클래스로 나누고 추상화시킨다면 얼마나 간략해질까 궁금하기도 하다.
나누기에서 0으로 나누는 경우에 대한 조건문을 추가했다.
소수점 설정을 위한 코드도 들어가있다.
// 더하기
private fun add(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() + n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() + n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1)// 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
// 빼기
private fun substract(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() - n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() - n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1) // 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
// 곱하기
private fun multiply(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() * n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() * n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1) // 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
// 나누기
private fun divide(n1: Pair<String, Int>, n2: Pair<String, Int>): Pair<String, Int> {
// 0으로 나누려는 시도를 막기위한 예외처리
if (n2.first.toDouble() == 0.0) return Pair("0 ERROR", -1)
// Int 타입끼리의 연산이면 Int로 리턴하고 그 외의 경우에는 Double로 리턴
// Double의 경우에는 소수점 셋째 자리에서 반올림되도록 작성
return when {
n1.second == 1 && n2.second == 1 -> return Pair((n1.first.toInt() / n2.first.toInt()).toString(), 1)
n1.second == 1 && n2.second == 2 || n1.second == 2 && n2.second == 1 || n1.second == 2 && n2.second == 2-> return Pair((round((n1.first.toDouble() / n2.first.toDouble()) * 100) / 100).toString(), 2)
else -> Pair("ERROR", -1) // 위에서 정확한 타입을 입력받고 들어오기 때문에 걸리지 않는 조건
}
}
처음과 끝에 출력되는 메세지도 함수로 만들어놨다.
// 시작과 끝에서 출력되는 안내 메세지
fun startMessage() {
println("두 수에 대한 연산이 가능한 계산기가 시작되었습니다.")
println("연산은 최대 소수점 둘째자리까지 출력됩니다.\n")
}
fun stopMessage() = println("\n계산기가 종료되었습니다.")
이렇게 계산기를 완성했다.
여기서 더 발전시킨다면 구조를 잘 나누는 것과 한번에 여러 계산을 할 수있도록 하는게 있지 않을까?
또한 지금은 코드로만 작성했지만 안드로이드 스튜디오에서 이걸 UI와 함께 구현하면 앱까지 나올 수 있겠다는 생각이 든다.
아! 이건 메인에서 Calculator 객체를 생성해서 selectSymbol 메서드를 호출하면 쓸 수 있다.
'Kotlin > StoreInfo' 카테고리의 다른 글
<정리> 키오스크 프로그램 구현 (0) | 2023.12.06 |
---|---|
<정리> 호텔 예약 프로그램 구현 (2) | 2023.12.05 |
<강의> Kotlin 문법 종합반 5주차 (0) | 2023.11.30 |
<강의> Kotlin 문법 종합반 4주차 (1) | 2023.11.29 |
<강의> Kotlin 문법 종합반 3주차 (2) | 2023.11.28 |