实战:构建一个Kotlin版本的四则运算计算器

实战:构建一个Kotlin版本的四则运算计算器

计算器 1.0

/*
* 大致列举一下计算器的功能需求:
* 交互式界面,输入算式,按下回车,程序就会帮我们计算出结果;
* 数字与字符之间要求有空格,“1 + 1”是可以的,“1+1”则不行;
* 输入 exit,按下回车,程序就会退出;支持“加减乘除”,四种运算,仅支持两个数的运算。
*/

val help = """
--------------------------------------
使用说明:
1. 输入 1 + 1,按回车,即可使用计算器;
2. 注意:数字与符号之间要有空格;
3. 想要退出程序,请输入:exit
--------------------------------------""".trimIndent()

fun main() {
    while (true) {
        println(help)

        val input = readLine() ?: continue
        if (input == "exit") exitProcess(0)

        val inputList = input.split(" ")
        val result = calculate(inputList)

        if (result == null) {
            println("输入格式不对")
            continue
        } else {
            println("$input = $result")
        }
    }
}

private fun calculate(inputList: List<String>): Int? {
    if (inputList.size != 3) return null

    val left = inputList[0].toInt()
    val operation = Operation.valueOf(inputList[1])
    val right = inputList[2].toInt()

    return when (operation) {
        Operation.ADD -> left + right
        Operation.MINUS -> left - right
        Operation.MULTI -> left * right
        Operation.DIVI -> left / right
    }
}

计算器 2.0

/*
在 2.0 版本中,我们会分成两个阶段:
* 第一个阶段,融入面向对象的思想。1.0 版本中,我们只写了两个函数,一个是 main() 函数,
  另一个是 calculate() 函数。虽然这样的设计非常直观且便于理解,但却不太符合我们工程界的思维习惯。
  我们应该将程序封装到一个类当中,并且尽量让每个函数的功能划分清楚,保持每个函数尽量简单。
* 第二个阶段,兼容输入格式。1.0 版本中,我们对输入有严格的要求,数字和符号之间必须有空格,否则我们的算式解析会出错。
  在 2.0 版本中,我们尝试兼容不同的输入格式,不管数字和符号之间有没有空格,我们都要能成功执行。
*/

class CalculatorV2 {

    val exit = "exit"
    val help = """
--------------------------------------
使用说明:
1. 输入 1 + 1,按回车,即可使用计算器;
2. 注意:数字与符号之间要有空格;
3. 想要退出程序,请输入:exit
--------------------------------------""".trimIndent()

    fun start() {
        while (true) {
            println(help)

            val input = readLine() ?: continue
            val result = calculate(input)

            if (result == null) {
                println("输入格式不对")
                continue
            } else {
                println("$input = $result")
            }
        }
    }

    /*
    拆分 calculate() 方法主要做了三件事:
    * 第一,将“是否退出”的逻辑封装到了 shouldExit() 方法当中,如果将来这部分逻辑变得更复杂,我们只改动这一个方法即可。
    * 第二,将算式的解析,封装到了 parseExpression() 方法当中,而解析算式的时候也需要解析操作符,这时候我们也需要 parseOperator()。
    * 第三,将具体的计算逻辑交给了对应的方法。这么做的原因,是可以让我们的程序变得更加灵活。
      比如,我们在下个版本当中会更改“加法”的计算逻辑,那么我们就只需要改动这一个方法就行了。
    */
    private fun calculate(input: String): String? {
        if (shouldExit(input)) exitProcess(0)

        val exp = parseExpression(input) ?: return null

        val left = exp.left
        val operator = exp.operator
        val right = exp.right

        return when (operator) {
            Operation.ADD -> addString(left, right)
            Operation.MINUS -> minusString(left, right)
            Operation.MULTI -> multiString(left, right)
            Operation.DIVI -> diviString(left, right)
        }
    }

    private fun addString(left: String, right: String): String {
        val result = left.toInt() + right.toInt()
        return result.toString()
    }

    private fun minusString(left: String, right: String): String {
        val result = left.toInt() - right.toInt()
        return result.toString()
    }

    private fun multiString(left: String, right: String): String {
        val result = left.toInt() * right.toInt()
        return result.toString()
    }

    private fun diviString(left: String, right: String): String {
        val result = left.toInt() / right.toInt()
        return result.toString()
    }

    private fun shouldExit(input: String): Boolean {
        return input == exit
    }

    private fun parseExpression(input: String): Expression? {
        val operation = parseOperator(input) ?: return null
        val list = input.split(operation.value)
        if (list.size != 2) return null
        return Expression(
            left = list[0].trim(),
            operator = operation,
            right = list[1].trim()
        )
    }

    private fun parseOperator(input: String): Operation? {
        Operation.values().forEach {
            if (input.contains(it.value)) {
                return it
            }
        }
        return null
    }

    private fun parseOperator1(input: String): Operation? {
        return when {
            input.contains(Operation.ADD.value) -> Operation.ADD
            input.contains(Operation.MINUS.value) -> Operation.MINUS
            input.contains(Operation.MULTI.value) -> Operation.MULTI
            input.contains(Operation.DIVI.value) -> Operation.DIVI
            else -> null
        }
    }
}

enum class Operation(val value: String) {
    ADD("+"),
    MINUS("-"),
    MULTI("*"),
    DIVI("/")
}

data class Expression(
    val left: String,
    val operator: Operation,
    val right: String
)

fun main() {
    val calculator = CalculatorV2()
    calculator.start()
}

/*
在这个过程中,创建了三个类:
* “Calculator”类,代表整个计算器;
* “Operation”枚举类,代表加减乘除四种运算操作符;
* “Expression”数据类,代表我们算式当中的数字和操作符。
  之后,我们又对计算器的核心功能进行了更细颗粒度的拆分,
  提高了程序的灵活性,为我们的功能扩展打下了基础。
*/

计算器 3.0

/*
针对 3.0 这个版本,我们也分为了两个阶段:
* 第一阶段,增加单元测试。单元测试是软件工程当中的一个概念,它指的是对软件当中的最小可执行单元进行测试,
  以提高软件的稳定性。在 Java 当中,最小单元一般会认为是类,因此,我们一般会以类为单元,对类当中的方法进行一一测试。
* 第二阶段,支持大数的加法。我们知道 Java、Kotlin 当中的整型都是有范围限制的,
  如果我们输入两个特别大的数字进行计算,那么程序是无法正常工作的。因此,我们需要对特别大的数进行兼容。
*/

//在 Kotlin 当中,如果要使用单元测试,我们需要在 gradle 文件当中,添加 Kotlin 官方提供的依赖:
//testImplementation 'org.jetbrains.kotlin:kotlin-test'

单元测试的代码,我们一般会放在工程的 test 目录下:
在这里插入图片描述
可以从这个图中看出很多信息:
·第一,test 目录、main 目录,它们是平级的目录,内部拥有着相同的结构。main 目录下放的是功能代码,test 目录下放的则是测试代码。
·第二,由于我们要开发 3.0 版本,所以我们在 main 目录下创建了 CalculatorV3 这个类;另外,由于我们需要在 3.0 版本加入单元测试,所以对应的,我们在 test 目录下相同的地方,创建了 TestCalculatorV3。这两个类的关系是一一对应的,CalculatorV3 是为了实现 3.0 版本的功能,TestCalculatorV3 是为了测试 3.0 版本的功能,确保功能正常。

/*
针对 3.0 这个版本,我们也分为了两个阶段:
* 第一阶段,增加单元测试。单元测试是软件工程当中的一个概念,它指的是对软件当中的最小可执行单元进行测试,
  以提高软件的稳定性。在 Java 当中,最小单元一般会认为是类,因此,我们一般会以类为单元,对类当中的方法进行一一测试。
* 第二阶段,支持大数的加法。我们知道 Java、Kotlin 当中的整型都是有范围限制的,
  如果我们输入两个特别大的数字进行计算,那么程序是无法正常工作的。因此,我们需要对特别大的数进行兼容。
*/

//在 Kotlin 当中,如果要使用单元测试,我们需要在 gradle 文件当中,添加 Kotlin 官方提供的依赖:
//testImplementation 'org.jetbrains.kotlin:kotlin-test'

//单元测试的代码,我们一般会放在工程的 test 目录下:

class CalculatorV3 {

    private val exit = "exit"
    private val help = """
--------------------------------------
使用说明:
1. 输入 1 + 1,按回车,即可使用计算器;
2. 注意:数字与符号之间要有空格;
3. 想要退出程序,请输入:exit
--------------------------------------""".trimIndent()

    fun start() {
        while (true) {
            println(help)

            val input = readLine() ?: continue
            val result = calculate(input)

            if (result == null) {
                println("输入格式不对")
                continue
            } else {
                println("$input = $result")
            }
        }
    }

    fun calculate(input: String): String? {
        if (shouldExit(input)) exitProcess(0)

        val exp = parseExpression(input) ?: return null

        val left = exp.left
        val operator = exp.operator
        val right = exp.right

        return when (operator) {
            Operation.ADD -> addString(left, right)
            Operation.MINUS -> minusString(left, right)
            Operation.MULTI -> multiString(left, right)
            Operation.DIVI -> diviString(left, right)
        }
    }
/*
* 注释①,我们创建了一个 StringBuilder 对象,用于存储最终结果,
  由于我们的结果是一位位计算出来的,所以每一位结果都是慢慢拼接上去的,
  在这里,为了提高程序的性能,我们选择使用 StringBuilder。
* 注释②,我们定义了两个可变的变量 index,它们分别指向了两个数字的个位,这是因为我们的计算是从个位开始的。
* 注释③,carry,我们用它来存储每一位计算结果的进位。
* 注释④,这个 while 循环当中,我们会让两个 index 从低位一直到高位,直到遍历完它们所有的数字位。
* 注释⑤,这里的逻辑是取每一位上的数字,其中有个细节就是补零操作,比如当程序运行到百位的时候,99 没有百位,这时候 rightVal = 0。
* 注释⑥,当我们的程序计算出结果后,我们要分别算出 carry,以及当前位的结果。这时候我们分别使用“除法”计算 carry,使用“取余”操作计算当前位的结果。
* 注释⑦,这里是为了兼容一个特殊的场景,在“99+1”的情况下,我们的 while 循环最多只会遍历到十位,
  如果不做特殊处理的话,结果将变成“99+1=00”。这并不是我们想要的,所以,为了兼容这种特殊情况,
  我们在 while 循环结束后增加了一个判断,如果 carry=1,那就说明在最大的那一位数计算完以后,仍然有进位,我们要手动添加。
* 注释⑧,对于一个算式“135+99”,我们的 result 拼接其实是倒叙的“432”,这时候我们需要将其翻转一下,才能得到正确的结果“135+99=234”。
*/
    private fun addString(leftNum: String, rightNum: String): String {
        // ①
        val result = StringBuilder()
        // ②
        var leftIndex = leftNum.length - 1
        var rightIndex = rightNum.length - 1
        // ③
        var carry = 0

        // ④
        while (leftIndex >= 0 || rightIndex >= 0) {
            // ⑤
            val leftVal = if (leftIndex >= 0) leftNum.get(leftIndex).digitToInt() else 0
            val rightVal = if (rightIndex >= 0) rightNum.get(rightIndex).digitToInt() else 0
            val sum = leftVal + rightVal + carry
            // ⑥
            carry = sum / 10
            result.append(sum % 10)
            leftIndex--
            rightIndex--
        }
        // ⑦
        if (carry != 0) {
            result.append(carry)
        }

        // ⑧
        return result.reverse().toString()
    }

    private fun minusString(left: String, right: String): String {
        val result = left.toInt() - right.toInt()
        return result.toString()
    }

    private fun multiString(left: String, right: String): String {
        val result = left.toInt() * right.toInt()
        return result.toString()
    }

    private fun diviString(left: String, right: String): String {
        val result = left.toInt() / right.toInt()
        return result.toString()
    }

    private fun shouldExit(input: String): Boolean {
        return input == exit
    }

    private fun parseExpression(input: String): Expression? {
        val operation = parseOperator(input) ?: return null
        val list = input.split(operation.value)
        if (list.size != 2) return null
        return Expression(
            left = list[0].trim(),
            operator = operation,
            right = list[1].trim()
        )
    }

    private fun parseOperator(input: String): Operation? {
        Operation.values().forEach {
            if (input.contains(it.value)) {
                return it
            }
        }
        return null
    }
}

fun main() {
    val calculator = CalculatorV3()
    calculator.start()
}

编写测试代码:

/*
首先,我们定义了一个方法 testCalculate(),
并且使用了一个注解 @Test 来修饰它。因为这样做以后,
IntelliJ 就会知道:哦,这是一个用来做测试的方法。
*/
class TestCalculatorV3 {
    @Test
    fun testCalculate() {
        val calculator = CalculatorV3()

        val res1 = calculator.calculate("1+2")
        assertEquals("3", res1)
    }
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值