krz/orgstar

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

Commit 53684aa7d7

53684aa7d7e9ec4ddae59466cc2c00a5b23c56e6

parent: ff45d11a82

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-07 23:22 UTC

Support the F formula flag: exact fractions

With ;F Calc keeps integer division exact, as calc-prefer-frac does:
fractions shown as n:d, exact through +, -, *, /, powers, vmean and the
variances, and exact square roots of square fractions; a float in the
expression gives a float.

Layout: unified · split

Sources/OrgCore/Compute/Calc.swift +80 −14
@@ -26,6 +26,8 @@ public enum Calc {
2626 /// A date form: the day number, with the time of day as a fraction.
2727 case date(Value)
2828 case nan
29 /// An exact fraction, reduced, the denominator above 1 (`calc-prefer-frac`).
30 case frac(Int, Int)
2931 }
3032
3133 public enum FloatFormat: Equatable, Sendable {
@@ -36,6 +38,8 @@ public enum Calc {
3638 public var precision = 12
3739 public var format = FloatFormat.float(8)
3840 public var degrees = true
41 /// `calc-prefer-frac`: dividing integers gives a fraction.
42 public var preferFractions = false
3943 public init() {}
4044 }
4145
@@ -111,15 +115,48 @@ public enum Calc {
111115 switch v {
112116 case .int(let i): return (Int128(i), 0)
113117 case .float(let m, let e): return (Int128(m), e)
118 case .frac(let n, let d): return try parts(try divide(.int(n), .int(d)))
114119 case .vector: throw Unsupported("vector arithmetic")
115120 case .date, .nan: throw Unsupported("date arithmetic")
116121 }
117122 }
118123
124 /// An integer or a fraction as numerator and denominator.
125 static func rational(_ v: Value) -> (Int, Int)? {
126 switch v {
127 case .int(let i): (i, 1)
128 case .frac(let n, let d): (n, d)
129 default: nil
130 }
131 }
132
133 /// `math-make-frac`: reduced, as an integer when it is one.
134 static func fraction(_ n: Int, _ d: Int) throws -> Value {
135 guard d != 0 else { throw Unsupported("division by zero") }
136 guard n != .min, d != .min else { throw Unsupported("integer size") }
137 func gcd(_ a: Int, _ b: Int) -> Int { b == 0 ? abs(a) : gcd(b, a % b) }
138 let g = gcd(n, d)
139 var (num, den) = (n / g, d / g)
140 if den < 0 { (num, den) = (-num, -den) }
141 return den == 1 ? .int(num) : .frac(num, den)
142 }
143
144 /// Two rationals, one of them a fraction, combined exactly; nil otherwise.
145 static func exact(_ a: Value, _ b: Value, _ op: (Int, Int, Int, Int) -> (Int, Int, Bool)) throws -> Value? {
146 var fraction = false
147 if case .frac = a { fraction = true }
148 if case .frac = b { fraction = true }
149 guard fraction, let (n1, d1) = rational(a), let (n2, d2) = rational(b) else { return nil }
150 let (n, d, overflow) = op(n1, d1, n2, d2)
151 if overflow { throw Unsupported("integer size") }
152 return try self.fraction(n, d)
153 }
154
119155 static func double(_ v: Value) throws -> Double {
120156 switch v {
121157 case .int(let i): return Double(i)
122158 case .float(let m, let e): return Double(m) * pow(10, Double(e))
159 case .frac(let n, let d): return Double(n) / Double(d)
123160 case .vector: throw Unsupported("vector arithmetic")
124161 case .date, .nan: throw Unsupported("date arithmetic")
125162 }
@@ -150,6 +187,13 @@ public enum Calc {
150187 if overflow { throw Unsupported("integer size") }
151188 return .int(sum)
152189 }
190 if let sum = try exact(a, b, { n1, d1, n2, d2 in
191 let (p, o1) = n1.multipliedReportingOverflow(by: d2)
192 let (q, o2) = n2.multipliedReportingOverflow(by: d1)
193 let (n, o3) = p.addingReportingOverflow(q)
194 let (d, o4) = d1.multipliedReportingOverflow(by: d2)
195 return (n, d, o1 || o2 || o3 || o4)
196 }) { return sum }
153197 let (m1, e1) = try parts(a)
154198 let (m2, e2) = try parts(b)
155199 if m1 == 0 { return try makeFloat(m2, e2) }
@@ -169,6 +213,7 @@ public enum Calc {
169213 guard i != .min else { throw Unsupported("integer size") }
170214 return .int(-i)
171215 case .float(let m, let e): return .float(-m, e)
216 case .frac(let n, let d): return .frac(-n, d)
172217 case .vector: throw Unsupported("vector arithmetic")
173218 case .date: throw Unsupported("date arithmetic")
174219 case .nan: return .nan
@@ -195,6 +240,11 @@ public enum Calc {
195240 if overflow { throw Unsupported("integer size") }
196241 return .int(product)
197242 }
243 if let product = try exact(a, b, { n1, d1, n2, d2 in
244 let (n, o1) = n1.multipliedReportingOverflow(by: n2)
245 let (d, o2) = d1.multipliedReportingOverflow(by: d2)
246 return (n, d, o1 || o2)
247 }) { return product }
198248 let (m1, e1) = try parts(a)
199249 let (m2, e2) = try parts(b)
200250 let (product, overflow) = m1.multipliedReportingOverflow(by: m2)
@@ -202,8 +252,16 @@ public enum Calc {
202252 return try makeFloat(product, e1 + e2)
203253 }
204254
205 static func divide(_ a: Value, _ b: Value) throws -> Value {
255 static func divide(_ a: Value, _ b: Value, fractions: Bool = false) throws -> Value {
206256 if isNaN(a, b) { return .nan }
257 if fractions || { if case .frac = a { return true }; if case .frac = b { return true }; return false }(),
258 let (n1, d1) = rational(a), let (n2, d2) = rational(b) {
259 guard n2 != 0 else { throw Unsupported("division by zero") }
260 let (n, o1) = n1.multipliedReportingOverflow(by: d2)
261 let (d, o2) = d1.multipliedReportingOverflow(by: n2)
262 if o1 || o2 { throw Unsupported("integer size") }
263 return try fraction(n, d)
264 }
207265 if case .int(let x) = a, case .int(let y) = b {
208266 guard y != 0 else { throw Unsupported("division by zero") }
209267 if x % y == 0 { return .int(x / y) }
@@ -223,8 +281,8 @@ public enum Calc {
223281 }
224282
225283 /// `math-ipow`.
226 static func integerPower(_ a: Value, _ n: Int) throws -> Value {
227 if n < 0 { return try integerPower(try divide(.int(1), a), -n) }
284 static func integerPower(_ a: Value, _ n: Int, fractions: Bool = false) throws -> Value {
285 if n < 0 { return try integerPower(try divide(.int(1), a, fractions: fractions), -n) }
228286 if n == 0 { return .int(1) }
229287 if n == 1 { return a }
230288 let square = try multiply(a, a)
@@ -232,9 +290,9 @@ public enum Calc {
232290 return try multiply(a, try integerPower(square, n / 2))
233291 }
234292
235 static func power(_ a: Value, _ b: Value) throws -> Value {
293 static func power(_ a: Value, _ b: Value, fractions: Bool = false) throws -> Value {
236294 if isNaN(a, b) { return .nan }
237 if case .int(let n) = b { return try integerPower(a, n) }
295 if case .int(let n) = b { return try integerPower(a, n, fractions: fractions) }
238296 let base = try double(a)
239297 guard base >= 0 else { throw Unsupported("complex result") }
240298 return try fromDouble(pow(base, try double(b)))
@@ -254,6 +312,7 @@ public enum Calc {
254312 static func format(_ value: Value, _ floatFormat: FloatFormat) throws -> String {
255313 switch value {
256314 case .int(let i): return String(i)
315 case .frac(let n, let d): return "\(n):\(d)"
257316 case .vector: throw Unsupported("vector result")
258317 case .nan: return "nan"
259318 case .date(let d): return "<" + (try formatDate(d)) + ">"
@@ -577,10 +636,10 @@ public enum Calc {
577636 case "+": return try add(a, b)
578637 case "-": return try subtract(a, b)
579638 case "*": return try multiply(a, b)
580 case "/": return try divide(a, b)
639 case "/": return try divide(a, b, fractions: modes.preferFractions)
581640 case "%": return try mod(a, b)
582641 case "\\": return try idiv(a, b)
583 case "^": return try power(a, b)
642 case "^": return try power(a, b, fractions: modes.preferFractions)
584643 case "|": return .int(try truth(a) || truth(b) ? 1 : 0)
585644 case "&": return .int(try truth(a) && truth(b) ? 1 : 0)
586645 default:
@@ -640,7 +699,7 @@ public enum Calc {
640699 func deviations(_ args: [Value]) throws -> (Value, Int) {
641700 let items = try elements(args)
642701 guard !items.isEmpty else { throw Unsupported("empty vector") }
643 let mean = try divide(try items.reduce(Value.int(0)) { try add($0, $1) }, .int(items.count))
702 let mean = try divide(try items.reduce(Value.int(0)) { try add($0, $1) }, .int(items.count), fractions: modes.preferFractions)
644703 let squares = try items.reduce(Value.int(0)) { sum, x in
645704 let d = try subtract(x, mean)
646705 return try add(sum, try multiply(d, d))
@@ -678,7 +737,7 @@ public enum Calc {
678737 case "vmean":
679738 let items = try elements(args)
680739 guard !items.isEmpty else { throw Unsupported("empty vector") }
681 return try divide(try items.reduce(Value.int(0)) { try add($0, $1) }, .int(items.count))
740 return try divide(try items.reduce(Value.int(0)) { try add($0, $1) }, .int(items.count), fractions: modes.preferFractions)
682741 case "vmax", "vmin":
683742 let items = try elements(args)
684743 guard var best = items.first else { throw Unsupported("empty vector") }
@@ -700,7 +759,7 @@ public enum Calc {
700759 let (squares, n) = try deviations(args)
701760 let population = name == "vpvar" || name == "vpsdev"
702761 if !population, n < 2 { throw Unsupported("variance of one value") }
703 let variance = try divide(squares, .int(population ? n : n - 1))
762 let variance = try divide(squares, .int(population ? n : n - 1), fractions: modes.preferFractions)
704763 return name.hasSuffix("sdev") ? try root(variance) : variance
705764 case "mod":
706765 guard args.count == 2 else { throw Unsupported("arguments") }
@@ -725,7 +784,8 @@ public enum Calc {
725784 let x = try one(args)
726785 return try compare(x, .int(0)) < 0 ? try negate(x) : x
727786 case "floor", "ceil", "trunc":
728 let x = try one(args)
787 var x = try one(args)
788 if case .frac(let n, let d) = x { x = try divide(.int(n), .int(d)) }
729789 if case .int = x { return x }
730790 let d = try double(x)
731791 let r = name == "floor" ? d.rounded(.down) : name == "ceil" ? d.rounded(.up) : d.rounded(.towardZero)
@@ -733,7 +793,8 @@ public enum Calc {
733793 return .int(i)
734794 case "round":
735795 guard args.count == 1 || args.count == 2 else { throw Unsupported("arguments") }
736 let x = args[0]
796 var x = args[0]
797 if case .frac(let n, let d) = x { x = try divide(.int(n), .int(d)) }
737798 if args.count == 2 {
738799 guard case .int(let places) = args[1] else { throw Unsupported("arguments") }
739800 guard case .float(let m, let e) = x else { return x }
@@ -744,11 +805,16 @@ public enum Calc {
744805 return .int(i)
745806 case "sqrt":
746807 let x = try one(args)
808 func exactRoot(_ i: Int) -> Int? {
809 guard i >= 0 else { return nil }
810 let root = Int(Double(i).squareRoot().rounded())
811 return root.multipliedReportingOverflow(by: root) == (i, false) ? root : nil
812 }
747813 if case .int(let i) = x {
748814 guard i >= 0 else { throw Unsupported("complex result") }
749 let root = Int(Double(i).squareRoot().rounded())
750 if root * root == i { return .int(root) }
815 if let root = exactRoot(i) { return .int(root) }
751816 }
817 if case .frac(let n, let d) = x, let rn = exactRoot(n), let rd = exactRoot(d) { return try fraction(rn, rd) }
752818 let d = try double(x)
753819 guard d >= 0 else { throw Unsupported("complex result") }
754820 return try fromDouble(d.squareRoot())
Sources/OrgCore/Compute/TableFormulas.swift +1
@@ -537,6 +537,7 @@ extension EmacsBuffer {
537537 case "N": mode.numbers = true
538538 case "L": literal = true
539539 case "E": mode.keepEmpty = true
540 case "F": modes.preferFractions = true
540541 case "D": modes.degrees = true
541542 case "R": modes.degrees = false
542543 default: throw Calc.Unsupported("format flag \(ns.substring(with: m.range))")
Tests/OrgCoreTests/TableFormulaTests.swift +2
@@ -54,6 +54,8 @@ struct TableFormulaTests {
5454 "| | a | b |\n|---+---+---|\n| | 2 | |\n| | 3 | |\n| $ | %=%.2f | |\n#+TBLFM: $3=$2/3;\n",
5555 "| a | b |\n|---+---|\n| 4 | |\n| 9 | |\n#+TBLFM: $2=vsdev(@I$1..@II$1)+fact(3)\n",
5656 "| a | b |\n|---+---|\n| 1 | |\n| 2 | |\n#+TBLFM: @-1$2=5\n",
57 "| a | b | f |\n|---+---+---|\n| 1 | 3 | |\n| 7 | 2 | |\n| -7 | 2 | |\n| 6 | 3 | |\n| 1 | 0.5 | |\n| 4 | 6 | |\n#+TBLFM: $3=$1/$2;F\n",
58 "| a |\n|---|\n| |\n| |\n| |\n| |\n| |\n| |\n| |\n| |\n| |\n#+TBLFM: @2$1=1/3+1/6;F::@3$1=(1/3)^2;F::@4$1=vsum([1/2,1/3]);F::@5$1=1/3*3;F::@6$1=sqrt(1/4);F::@7$1=vmean([1,2]);F::@8$1=2^-2;F::@9$1=abs(-1/3)+max(1/3,1/4);F::@10$1=round(7/2)+floor(7/2);F\n",
5759 ]
5860
5961 @Test func remoteFindsTablesByIDInOtherFiles() {