Skip to content

Instantly share code, notes, and snippets.

@dotNetTree
Last active June 7, 2022 01:33
Show Gist options
  • Save dotNetTree/9035dda235cb171025bdbbcc7c9e114f to your computer and use it in GitHub Desktop.
Save dotNetTree/9035dda235cb171025bdbbcc7c9e114f to your computer and use it in GitHub Desktop.
Code Spitz 90 - 코틀린 언어편 (1) 과제
val trim = """[^.\d-+*/()]""".toRegex()
fun trim(v: String): String = v.replace(trim, "")
fun repMMtoP(v: String) = v.replace("--", "+")
fun repMtoPM(v: String) = v.replace("-", "+-")
val groupMD = """((?:\+|\+-)?[.\d]+)([*/])((?:\+|\+-)?[.\d]+)""".toRegex()
tailrec fun removeMultiDiv(v: String): String = groupMD.find(v).let {
if (it != null) {
val (target, left, op, right) = it.groupValues
val leftValue = left.replace("+", "").toDouble()
val rightValue = right.replace("+", "").toDouble()
val result = when (op) {
"*" -> leftValue * rightValue
"/" -> leftValue / rightValue
else -> throw Throwable("invalid operator $op")
}
removeMultiDiv(v.replace(target, "+$result"))
} else v
}
fun calcWithBracket(v: String): Double {
return v
.let { v.drop(1).dropLast(1) }
.let { trim(it) }
.let { repMMtoP(it) }
.let { repMtoPM(it) }
.let { removeMultiDiv(it) }
.split("""(\++)""".toRegex())
.fold(0.0) { accu, curr ->
if (curr.isBlank()) { accu } else accu + curr.toDouble()
}
}
tailrec fun removeBracket(v: String): String {
return """\([.\d+\-*/]+\)""".toRegex().findAll(v).let {
if (it.count() == 0) v
else removeBracket(
it.fold(v) { acc, cur ->
val target = cur.value
val ret = calcWithBracket(target)
acc.replace(target, "$ret")
}
)
}
}
fun calc(v: String): Double = removeBracket("(${trim(v)})").toDouble()
fun main(args: Array<String>) {
val exps: Array<String> = arrayOf(
"16 / 2 / 2 / 2",
"-2 -3 + 0.4",
"-2 * (-3 + 0.4) / -0.2",
"-2 * (-3 + 0.4) / -0.2 + .55",
"-2 - (-3 + -8 * (7 + 3)) + (0.4 / -0.2)",
)
for (exp in exps) {
println("$exp = ${calc(exp)}")
}
}
@hannut91
Copy link

hannut91 commented Jun 3, 2022

fun calcWithoutBracket(v: String): Double {
    return v
        .let { trim(it) }
        .let { repMMtoP(it) }
        .let { repMtoPM(it) }
        .let { removeMultiDiv(it) }
        .split("""(\++)""".toRegex())
        .fold(0.0) { accu, curr ->
            if (curr.isBlank()) { accu } else accu + curr.toDouble()
        }
}

tailrec fun removeBracket(v: String): String {
    var hasBracket = false
    var r: String = v
    val regex = """\([.\d+\-*/]+\)""".toRegex()
    for (it in regex.findAll(v)) {
        val target = it.value
        val ret = calcWithoutBracket(target.substring(range = IntRange(start = 1, endInclusive = target.length-2)))
        r = r.replace(target, "$ret")
        hasBracket = true
    }
    return if (hasBracket) { removeBracket(r) } else r
}

calcWithoutBracket을 호출할 때 문자열에서 가장 앞 1개와 가장 뒤 1개를 잘라내어서 calcWithoutBracket을 호출하고 있는 것 같아요.
calcWithoutBracket 함수 입장에서 보면은 사실 괄호에 대해서 아예 모르기 때문에, 사실상 calculate라고 함수를 붙여도 될 것 같아요. 이미 괄호를 제거하고 함수를 호출하니까요.

만약 호출하기 전에 괄호를 제거한다는 것을 드러내고 싶으시다면

fun removeBracket(target) = target.drop(1).dropLast(1)

// ... 생략

val ret = calculate(removeBracket(target))

이렇게 써주면 좋을 것 같아요.

만약에 괄호를 삭제하고 뭔가 계산한다는 것을 드러내고 싶으시다면, 오히려 이름을 calWithBracket으로 하고, 책임을 떠넘겨도 될 것 같아요.

fun calcWithBracket(v: String): Double {
    return v
        .let { v.drop(1).dropLast(1) }
        .let { trim(it) }
        .let { repMMtoP(it) }
        .let { repMtoPM(it) }
        .let { removeMultiDiv(it) }
        .split("""(\++)""".toRegex())
        .fold(0.0) { accu, curr ->
            if (curr.isBlank()) { accu } else accu + curr.toDouble()
        }
}

// ... 생략

val ret = calWithBracket(target)

@hannut91
Copy link

hannut91 commented Jun 3, 2022

tailrec fun removeBracket(v: String): String {
    var hasBracket = false
    var r: String = v
    val regex = """\([.\d+\-*/]+\)""".toRegex()
    for (it in regex.findAll(v)) {
        val target = it.value
        val ret = calcWithoutBracket(target.substring(range = IntRange(start = 1, endInclusive = target.length-2)))
        r = r.replace(target, "$ret")
        hasBracket = true
    }
    return if (hasBracket) { removeBracket(r) } else r
}

hasBracket의 기본값을 false로 초기화하고, 현재 식에 괄호가 있다면 true로 변경하도록 하신 것 같아요.
변수는 값이 변경될 수 있기 때문에, 코드를 읽는 사람이 계속해서 이 값이 변경되는지 안되는지 추적해야 합니다. 그래서 간단한 변수 하나라도
뇌에 인지 부하를 늘리게 됩니다. 따라서 변수를 사용하기 보다는 상수를 사용을 하는 것이 좋고, 또한 괄호가 있는 수식이 없으면 코드를 빨리 종료시켜서
코드를 더 이상 읽지 않도록 해보면 좋을 것 같아요. 그래서 아래와 같이 괄호가 없으면 빠르게 바로 결과를 리턴하도록 했습니다. 괄호가 있는 경우에만 아래와 같이 호출되어서 더 의도를 명확하게 드러낼 수 있습니다.

tailrec fun removeBracket(v: String): String {
    var r: String = v
    val regex = """\([.\d+\-*/]+\)""".toRegex()
    val matchedResults = regex.findAll(v)
    if (!matchedResults.iterator().hasNext()) {
        return r
    }

    for (it in matchedResults) {
        val target = it.value
        val ret = calcWithoutBracket(target.substring(range = IntRange(start = 1, endInclusive = target.length-2)))
        r = r.replace(target, "$ret")
    }

    return removeBracket(r);
}

그리고 하나 더, 변수를 선언한 곳과 사용하는 곳은 거리가 가까울수록 좋습니다. 특히 상수가 아닌 변수는 선언한 곳과 사용하는 곳이 멀어질수록 그 사이에 변수에 무슨 짓을 할지 모르기 때문에, 그 사이를 모두 의심의 눈초리로 코드를 봐야 합니다. 따라서 변수 r을 적절한 위치로 옮기면 아래와 같이 될 것 같습니다.

tailrec fun removeBracket(v: String): String {
    val regex = """\([.\d+\-*/]+\)""".toRegex()
    val matchedResults = regex.findAll(v)
    if (!matchedResults.iterator().hasNext()) {
        return v // 여기서는 r을 사용할 필요가 없습니다.
    }

    var r: String = v

    for (it in matchedResults) {
        val target = it.value
        val ret = calcWithoutBracket(target.substring(range = IntRange(start = 1, endInclusive = target.length-2)))
        r = r.replace(target, "$ret")
    }

    return removeBracket(r);
}

위와 같이 해보니 r을 초기화 해놓고, r에 더해가고 있네요. 1강에서 배웠던 fold를 활용해 봅시다.

tailrec fun removeBracket(v: String): String {
    val regex = """\([.\d+\-*/]+\)""".toRegex()
    val matchedResults = regex.findAll(v)
    if (!matchedResults.iterator().hasNext()) { // 저도 이렇게 하는게 좋은지는 잘 모르겠습니다. 더 좋은 방법이 있다면 알려주세요
        return v
    }

    val r = matchedResults.fold(v) { acc, cur ->
        val target = cur.value
        val ret = calcWithoutBracket(target.drop(1).dropLast(1))
        acc.replace(target, "$ret")
    }

    return removeBracket(r);
}

아예 var이 사라졌습니다! 위에 removeMultiDiv도 비슷한 방식으로 정리할 수 있을 것 같습니다! 한 번 시도해 보세요

See also

@hannut91
Copy link

hannut91 commented Jun 3, 2022

'16 / 2 / 2 / 2'의 결과가 2가 나와야 하는데 8이 나오고 있네요. 그 이유는 계산할 때 지금 아래와 같이 계산이 되고 있어요.

(16 / 2) / (2 / 2)

나누기의 경우 앞에서부터 차례대로 계산이 되어야 할 것 같습니다

@dotNetTree
Copy link
Author

dotNetTree commented Jun 6, 2022

@hannut91 코드 리뷰 감사합니다. 리뷰 해주신 내용 반영하였습니다.

그런데, 한가지 질문이 있습니다.

그리고 하나 더, 변수를 선언한 곳과 사용하는 곳은 거리가 가까울수록 좋습니다. 특히 상수가 아닌 변수는 선언한 곳과 사용하는 곳이 멀어질수록 그 사이에 변수에 무슨 짓을 할지 모르기 때문에, 그 사이를 모두 의심의 눈초리로 코드를 봐야 합니다.

class가 등장하게 되면 프로퍼티와 선언(변수)와 사용처인 메소드가 물리적으로 매우 떨어지게 되는데요. 이 경우에도 같은 룰을 적용해야 하는건가요?

그리고 guard pattern은 sequence로 부터 Interactor 객체를 얻어내는 것보단 count method를 활용하는게 더 좋을 것습니다.

@hannut91
Copy link

hannut91 commented Jun 6, 2022

class가 등장하게 되면 프로퍼티와 선언(변수)와 사용처인 메소드가 물리적으로 매우 떨어지게 되는데요. 이 경우에도 같은 룰을 적용해야 하는건가요?

가능한 가까우면 좋은데, 무조건 그런 것은 아닙니다! 클래스가 등장하면 변수를 위에 놓고 메서드를 쓰니 꼭 그렇게 되지는 않겠죠 ㅎㅎ 변수를 사용할 때도 때로는 변수를 모아놓는 것이 좋을 때가 있는 것 같아요.

그리고 guard pattern은 sequence로 부터 Interactor 객체를 얻어내는 것보단 count method를 활용하는게 더 좋을 것습니다.

count 메서드가 있었군요 감사합니다!

@dotNetTree
Copy link
Author

@hannut91 감사합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment