krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Tests/OrgCoreTests/CalcTests.swift

114 lines · 7157 bytes

5 symbols in this file
  1import Foundation
  2import Testing
  3@testable import OrgCore
  4
  5/// Runs `calc-eval` with org's table modes over a list of expressions.
  6enum CalcOracle {
  7    static func run(_ cases: [(expression: String, format: String)]) throws -> [String] {
  8        let folder = FileManager.default.temporaryDirectory.appendingPathComponent("orgstar-calc-\(UUID().uuidString)")
  9        try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
 10        defer { try? FileManager.default.removeItem(at: folder) }
 11        let input = folder.appendingPathComponent("input.json")
 12        let output = folder.appendingPathComponent("output.json")
 13        let script = folder.appendingPathComponent("calc.el")
 14        try JSONSerialization.data(withJSONObject: cases.map { [$0.expression, $0.format] }).write(to: input)
 15        try #"""
 16        ;;; -*- lexical-binding: t -*-
 17        (require 'calc)
 18        (require 'org-table)
 19        (require 'json)
 20        (let* ((cases (with-temp-buffer
 21                        (insert-file-contents (getenv "ORACLE_INPUT"))
 22                        (json-parse-buffer :array-type 'list)))
 23               (results
 24                (mapcar (lambda (c)
 25                          (let* ((modes (copy-sequence org-calc-default-modes))
 26                                 (format (car (read-from-string (nth 1 c)))))
 27                            (setq modes (plist-put modes 'calc-float-format format))
 28                            (let ((r (calc-eval (cons (car c) modes) nil)))
 29                              (if (stringp r) r (format "ERROR %S" r)))))
 30                        cases)))
 31          (with-temp-file (getenv "ORACLE_OUTPUT")
 32            (insert (json-encode (vconcat results)))))
 33        """#.write(to: script, atomically: true, encoding: .utf8)
 34        _ = try EmacsOracle.run(["-Q", "--batch", "-l", script.path], environment: ["ORACLE_INPUT": input.path, "ORACLE_OUTPUT": output.path])
 35        return try JSONDecoder().decode([String].self, from: Data(contentsOf: output))
 36    }
 37}
 38
 39struct CalcTests {
 40    static let fixed = [
 41        "1+2", "7/2", "6/3", "1/3", "2/3", "(2)^(0.5)", "sqrt(2)", "sqrt(4)", "sqrt(16.0)", "1.5*2", "3.0", "(3.0)", "2^-1",
 42        "10/4", "1e5", "1e20*1", "123456789*1000", "0.1+0.2", "1/7", "100/7", "1000000/7", "1e10/7", "1e-5/7", "0.001/7",
 43        "0.0001/3", "(1.23456789012345)", "vsum([1,2,3.5])", "vmean([1,2])", "vmean([1,2,4])", "vmax([1,5,3])", "vsum([])",
 44        "vcount([1,2])", "vmedian([1,3,2,4])", "sin(30)", "cos(60)", "tan(45)", "exp(1)", "ln(10)", "log10(1000)", "log10(2)",
 45        "-3", "(-3)*2", "--3", "(+3)", "2*(0)", "2^0.5*2^0.5", "99999999.5*1", "123456785*0.1", "0.5*1e-10", "1.5e3",
 46        "12345678.9*1", "123456789.1*1", "1.25*1", "0.000123*1", "1e100*1e100", "round(2.5)", "floor(2.7)", "abs(-3)",
 47        "max(1,2)", "1.05*3", "2.675*1", "8/2*2", "8/2/2", "2*3/4", "-2^2", "2^3^2", "2^-2", "(-2)^2", "sin(0)", "sin(90)",
 48        "cos(0)", "cos(90)", "tan(0)", "exp(0)", "ln(1)", "log10(1)", "log10(100.0)", "sqrt(2.25)", "sqrt(0)", "arcsin(1)",
 49        "arctan(1)", "arccos(0.5)", "1.5^2", "1.1^10", "0.1^3", "2.5*4", "10/2.5", "1/3*3", "1.0/3", "vmedian([1,3])",
 50        "vmedian([1,2,3])", "vmedian([1.5,2])", "vmean([1.5,2.5])", "vsum([0.1,0.2,0.3])", "vmax([1,2.5])", "vmin([3,1.0])",
 51        "round(-2.5)", "round(2.4)", "round(1.23456,2)", "floor(-2.5)", "ceil(2.1)", "trunc(-2.7)", "abs(-1.5)", "max(1,2.5)",
 52        "min(4,2)", "1e3", "1.e3", "007", "(0.5)", "(.5)", "(5.)", "123456789012*10", "1.23456789012345678", "exp(10)",
 53        "exp(100)", "ln(0.5)", "sqrt(1e-10)", "2.0^2", "0^0", "1-1.0", "0.0", "-0.0", "1.0-1.0", "(-1.5)*(-2)",
 54        "(12)+(3.5)*(2)", "vsum([12,3.5,7])/3", "(1.5)/(0.5)", "(100)*(1.07)^(5)", "(19.99)*(3)", "(0.07)*(19.99)",
 55        "vmean([10,20,25])", "(5)-(7.25)", "(-0.001)*(1000)", "1/(3)", "(2)/(3)*(3)",
 56        // Dates, nan, comparisons and logic, and the rest of the functions tables use.
 57        "<2026-10-05 Mon>+3", "<2026-10-12 Mon>-<2026-10-05 Mon>", "<2026-10-05 Mon 10:00>-<2026-10-05 Mon 08:30>",
 58        "<2026-10-05 Mon 08:30>+0.5", "<2026-10-05 Mon 10:00>+1", "<2026-10-05 Mon 08:30>-<2026-10-04 Sun>", "(<2026-12-31 Thu>)+(1)",
 59        "<2026-03-01 Sun>-30", "1+<2026-10-05 Mon>", "<2026-10-05 Mon 23:59>-<2026-10-05 Mon 00:01>", "<2026-10-05 Mon 12:00>-0.25",
 60        "vmax([<2026-10-05 Mon>,<2026-10-07 Wed>])", "<2026-10-05 Mon> < <2026-10-06 Tue>", "nan", "nan+1", "vsum([1,nan])", "2*nan",
 61        "3<4", "3>4", "3<=3", "4>=5", "2==2", "2!=2", "1 && 0", "1 || 0", "!0", "!2", "if(3>4, 1, 2)", "if($1, 7, 8)".replacingOccurrences(of: "$1", with: "0"),
 62        "if(1.5, 2.5, 3)", "7%3", "-7%3", "7%-3", "7.5%2", "7\\2", "-7\\2", "7.5\\2", "mod(10,4)", "idiv(-9,4)", "fact(5)", "fact(0)",
 63        "vsdev([1,2,3,4])", "vvar([1,2,3])", "vpvar([1,2,3,4])", "vpsdev([2,4,4,4,5,5,7,9])", "vsdev([1.5,2.5,4])",
 64        "2+3<4*2", "1<2 && 3<4", "!(1<2)", "(1<2)+(2<3)",
 65    ]
 66
 67    @Test(.enabled(if: ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil))
 68    func matchesCalc() throws {
 69        var cases = Self.fixed.map { ($0, "(float 8)") }
 70        for format in ["(float 3)", "(fix 2)", "(fix 0)", "(sci 3)", "(eng 2)"] {
 71            for e in ["2/3", "3", "1.5", "1234.5678", "0.000123", "1e20*1", "-2/3", "1/7*100"] { cases.append((e, format)) }
 72        }
 73        // Generated: random operands and operators.
 74        var generator = SystemRandomNumberGenerator()
 75        let numbers = ["1", "2", "3", "7", "10", "0.5", "1.25", "3.14159", "100", "0.001", "12.5", "99", "0.3", "1e3", "2.5e-2"]
 76        let ops = ["+", "-", "*", "/"]
 77        for _ in 0..<300 {
 78            var e = numbers.randomElement(using: &generator)!
 79            for _ in 0..<Int.random(in: 1...4, using: &generator) {
 80                e = "(\(e))\(ops.randomElement(using: &generator)!)(\(numbers.randomElement(using: &generator)!))"
 81            }
 82            cases.append((e, "(float 8)"))
 83        }
 84        let emacs = try CalcOracle.run(cases)
 85        #expect(emacs.count == cases.count)
 86        var skipped: [String] = []
 87        for (c, expected) in zip(cases, emacs) {
 88            var modes = Calc.Modes()
 89            let f = c.1.dropFirst().dropLast().split(separator: " ")
 90            let n = Int(f[1])!
 91            switch f[0] {
 92            case "fix": modes.format = .fix(n)
 93            case "sci": modes.format = .sci(n)
 94            case "eng": modes.format = .eng(n)
 95            default: modes.format = .float(n)
 96            }
 97            let ours: String
 98            do { ours = try Calc.evaluate(c.0, modes: modes) } catch { ours = "unsupported: \(error)" }
 99            if ours.hasPrefix("unsupported") {
100                skipped.append(c.0)
101                continue
102            }
103            #expect(ours == expected, "\(c.0) \(c.1)")
104        }
105        // Only what Calc itself can't show as a number.
106        #expect(skipped == [], "\(skipped)")
107    }
108
109    @Test func unsupported() {
110        for e in ["abc*2", "1/0", "vmean([])", "pi", "5%", "(-8)^(1/3)", "2 3", "vvar([5])", "1<2<3"] {
111            #expect(throws: Calc.Unsupported.self, "\(e)") { try Calc.evaluate(e) }
112        }
113    }
114}