ScriptCalculator - 不只是程序员的计算器

ScriptCalculator: https://calc.iwill.im/

GitHub Repo: https://github.com/iwill/ScriptCalculator

ScriptCalculator 是我为了满足个人需求开发的一款计算器。

先看效果,假设某同学月薪 10w,用 ScriptCalculator 计算一下他的税后收入(为了便于演示,这里不考虑五险一金、绩效、ESPP、加班费、餐补等):

查看计算脚本 or 算算我的 👀

script-calculator.png

最初的需求是想保留计算过程、能展示中间结果,能随时修改数据和计算逻辑,计算结果实时更新,必要的地方可以加备注。好像很正常的需求,但找了好久就没发现哪个计算器能满足,所以只能写代码计算。

开始我用 Chrome 控制台写 JavaScript 比较多,随手就能拿过来写,但问题是输入不方便、输出结果也不直观,要在文本编辑器和控制台之间来回拷贝代码和计算结果。Swift Playgrounds 的结果展示很友好,但代码里多少还是夹杂了数据之外的东西,还要要操心数据类型,而且 Xcode 也不是很轻量。气氛烘托到位,就是时候出手了。

ScriptCalculator 基于网页,核心思想是解析输入的计算脚本,通过预处理变成 JavaScript 代码,然后用 eval() 运行代码完成计算,最后模仿 Swift Playgrounds 输出结果。后来边用边迭代,逐步支持了自定义变量和函数、百分数和各种进制、位运算和布尔值、折行和注释,以及通过 URL 分享计算脚本等丰富的特性。

最终,脚本语法简洁、功能强大,充分地利用了 JavaScript 语言的多种特性。(至少我自己)用起来非常顺手,舒坦!

这里有 丰富的功能演示,不喜欢看文档的可以直接操练起来。

特性

类型扩展

  • 百分数:支持输入百分数,支持结果以百分数形式展示

    1
    2
    3
    4
    5
    6
    10%       // 0.1
    10 % // 0.1
    10% + 1 // 0.1 + 1 == 1.1
    10% % 1 // 0.1 % 1 == 0.1
    10% - 7 // 0.1 - 7 == -6.9
    10 % (-7) // 10 % 7 == 3

    | 需要注意百分号与取余运算符的使用差异

  • 进制:支持输入二、八、十、十六进制数字,支持结果以二至三十六进制形式展示

    结果默认以十进制形式展示:

    1
    2
    3
    4
    0b10   // 2
    0o10 // 8
    10 // 10
    0x10 // 16

    末尾加 #<进制数字> 可以切换为展示进制,但并不改变实际结果,支持二至三十六进制:

    1
    2
    3
    4
    5
    6
    7
    8
    // 这里每一行都是获取上一行的结果、转换进制展示
    32 #2 // 0b100000
    #8 // 0o40
    #10 // 32
    #16 // 0x20
    #32 // 10
    #36 // W
    $ // 32

运算

  • 基础运算:支持 +-*/%(取余)、() 等运算符
  • 比较运算:支持 <<=>>===!= 等运算符
  • 逻辑运算:支持布尔类型,支持 &&||! 和三目等运算符 ?:
  • 位运算:支持 &|~^<<>>>>> 等运算符

常量、变量

  • 变量:

    • 行首为运算符时在前面自动填入上一行结果
    • 使用 $n 获取第 n 个结果
    • 使用 $ 获取上一个结果(不是上一行)
  • JavaScript 常量、方法:

    允许访问 JavaScript 中 MathNumber 的属性和方法。

    Math 的属性可以作为常量使用:

    1
    2
     E // 2.718281828459045
    PI // 3.141592653589793

    Math 的方法可以直接作为函数使用:

    1
    2
    3
    min(max(1, 2), 3) // 2
    pow(2, 10) // 1024
    sqrt(1024) // 32

    Number 的属性可以通过 Number. 形式访问:

    1
    2
    3
    4
    Number.MAX_SAFE_INTEGER // 9007199254740991
    == pow(2, 53) - 1 // true
    Number.MIN_SAFE_INTEGER // -9007199254740991
    == - (pow(2, 53) - 1) // true
  • 扩展的函数:

    Math 的小数截取方法只能截取整数,想要截取小数会比较麻烦,toFixed() 方法又不够灵活、而且输出的是字符串格式:

    1
    2
    Math.round(2000 / 3 * 100) / 100 //  666.67  | Math 的 round() 方法
    (2000 / 3).toFixed(2) // "666.66" | Number 实例的 toFixed() 方法

    扩展的函数支持可选的第二个参数就比较方便了,并且还支持整数部分的取舍:

    1
    2
    3
    4
    5
    6
    7
    8
    _666: 2000 / 3  // 666.6666666666666
    round(_666, 3) // 666.667
    floor(_666, 2) // 666.66
    ceil(_666 / 2, 1) // 333.4
    round(_666, 0) // 667
    round(_666) // 667
    round(_666, -1) // 670
    round(_666, -2) // 700
  • 自定义变量

    定义及使用:

    1
    2
    3
    a: 1  // 1
    b: 2 // 2
    a + b // 3
  • 自定义函数

    定义及调用:

    1
    2
    add(a, b): a + b
    add(3, 4) // 7

    支持更具语义化的 RORO 形式参数:

    1
    2
    3
    add({ a, b }): \
    a + b
    add({ a: 4, b: 5 }) // 9

空白、注释

  • 支持任意的空格,便于纵向对齐
  • 支持任意的空行,空行不影响结果行号
  • 行尾加 \ 可以折行,特别是自定义复杂的函数时可以避免一行过长
  • 支持 #// 两种形式的注释

设置、按钮

  • decimal:全局结果精确位数
  • rounding:全局小数截取方法
  • underline:给强迫症删除空格用
  • reset:重置设置
  • copy:拷贝 注释 + 算式 + 结果

辅助功能

  • 行号:有计算结果的行显示行号,可用于 $ + 行号 获取指定行结果
  • 撤销、重做:支持默认的撤销、重做功能
  • 快捷键:
    • control+u 单词或选中文本大写
    • control+shift+u 单词或选中文本小写
    • control+c 首字母大写
    • control+shift+c 首字母小写
  • URL Hash:URL Hash 中包含完整计算内容和设置,可前进/后退、刷新不丢数据,拷贝 URL 可以分享或备份,重新打开后 100% 还原

已知问题

  • 暂不支持 BigInt

  • “行首为运算符时自动在前面填入上一行结果”导致正负号存在歧义

    例如:

    1
    2
    3
    4
    5
          // exp   got
    a: 1 // 1 1
    b: -1 // -1 0
    c: 1 // 1 1
    d: +1 // 1 2

    解决办法是在带正负号的数字外加括号、或在带正负号的数字前换行:

    1
    2
    3
    4
    5
    a:   1  //  1
    b: (-1) // -1
    c: 1 // 1

    d: +1 // 1