Support the F formula flag: exact fractions !145

merged merged by cmc on 2026-10-07 23:22 UTC · krz/orgstar:calc-fractions into main

3 files changed, +83 −14

Layout: unified · split

Sources/OrgCore/Compute/Calc.swift +80 −14
@@ -26,6 +26,8 @@ public enum Calc {
26 /// A date form: the day number, with the time of day as a fraction. 26 /// A date form: the day number, with the time of day as a fraction.
27 case date(Value) 27 case date(Value)
28 case nan 28 case nan
29 /// An exact fraction, reduced, the denominator above 1 (`calc-prefer-frac`).
30 case frac(Int, Int)
29 } 31 }
30 32
31 public enum FloatFormat: Equatable, Sendable { 33 public enum FloatFormat: Equatable, Sendable {
@@ -36,6 +38,8 @@ public enum Calc {
36 public var precision = 12 38 public var precision = 12
37 public var format = FloatFormat.float(8) 39 public var format = FloatFormat.float(8)
38 public var degrees = true 40 public var degrees = true
41 /// `calc-prefer-frac`: dividing integers gives a fraction.
42 public var preferFractions = false
39 public init() {} 43 public init() {}
40 } 44 }
41 45
@@ -111,15 +115,48 @@ public enum Calc {
111 switch v { 115 switch v {
112 case .int(let i): return (Int128(i), 0) 116 case .int(let i): return (Int128(i), 0)
113 case .float(let m, let e): return (Int128(m), e) 117 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)))
114 case .vector: throw Unsupported("vector arithmetic") 119 case .vector: throw Unsupported("vector arithmetic")
115 case .date, .nan: throw Unsupported("date arithmetic") 120 case .date, .nan: throw Unsupported("date arithmetic")
116 } 121 }
117 } 122 }
118 123
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
119 static func double(_ v: Value) throws -> Double { 155 static func double(_ v: Value) throws -> Double {
120 switch v { 156 switch v {
121 case .int(let i): return Double(i) 157 case .int(let i): return Double(i)
122 case .float(let m, let e): return Double(m) * pow(10, Double(e)) 158 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)
123 case .vector: throw Unsupported("vector arithmetic") 160 case .vector: throw Unsupported("vector arithmetic")
124 case .date, .nan: throw Unsupported("date arithmetic") 161 case .date, .nan: throw Unsupported("date arithmetic")
125 } 162 }
@@ -150,6 +187,13 @@ public enum Calc {
150 if overflow { throw Unsupported("integer size") } 187 if overflow { throw Unsupported("integer size") }
151 return .int(sum) 188 return .int(sum)
152 } 189 }
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 }
153 let (m1, e1) = try parts(a) 197 let (m1, e1) = try parts(a)
154 let (m2, e2) = try parts(b) 198 let (m2, e2) = try parts(b)
155 if m1 == 0 { return try makeFloat(m2, e2) } 199 if m1 == 0 { return try makeFloat(m2, e2) }
@@ -169,6 +213,7 @@ public enum Calc {
169 guard i != .min else { throw Unsupported("integer size") } 213 guard i != .min else { throw Unsupported("integer size") }
170 return .int(-i) 214 return .int(-i)
171 case .float(let m, let e): return .float(-m, e) 215 case .float(let m, let e): return .float(-m, e)
216 case .frac(let n, let d): return .frac(-n, d)
172 case .vector: throw Unsupported("vector arithmetic") 217 case .vector: throw Unsupported("vector arithmetic")
173 case .date: throw Unsupported("date arithmetic") 218 case .date: throw Unsupported("date arithmetic")
174 case .nan: return .nan 219 case .nan: return .nan
@@ -195,6 +240,11 @@ public enum Calc {
195 if overflow { throw Unsupported("integer size") } 240 if overflow { throw Unsupported("integer size") }
196 return .int(product) 241 return .int(product)
197 } 242 }
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 }
198 let (m1, e1) = try parts(a) 248 let (m1, e1) = try parts(a)
199 let (m2, e2) = try parts(b) 249 let (m2, e2) = try parts(b)
200 let (product, overflow) = m1.multipliedReportingOverflow(by: m2) 250 let (product, overflow) = m1.multipliedReportingOverflow(by: m2)
@@ -202,8 +252,16 @@ public enum Calc {
202 return try makeFloat(product, e1 + e2) 252 return try makeFloat(product, e1 + e2)
203 } 253 }
204 254
205 static func divide(_ a: Value, _ b: Value) throws -> Value { 255 static func divide(_ a: Value, _ b: Value, fractions: Bool = false) throws -> Value {
206 if isNaN(a, b) { return .nan } 256 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 }
207 if case .int(let x) = a, case .int(let y) = b { 265 if case .int(let x) = a, case .int(let y) = b {
208 guard y != 0 else { throw Unsupported("division by zero") } 266 guard y != 0 else { throw Unsupported("division by zero") }
209 if x % y == 0 { return .int(x / y) } 267 if x % y == 0 { return .int(x / y) }
@@ -223,8 +281,8 @@ public enum Calc {
223 } 281 }
224 282
225 /// `math-ipow`. 283 /// `math-ipow`.
226 static func integerPower(_ a: Value, _ n: Int) throws -> Value { 284 static func integerPower(_ a: Value, _ n: Int, fractions: Bool = false) throws -> Value {
227 if n < 0 { return try integerPower(try divide(.int(1), a), -n) } 285 if n < 0 { return try integerPower(try divide(.int(1), a, fractions: fractions), -n) }
228 if n == 0 { return .int(1) } 286 if n == 0 { return .int(1) }
229 if n == 1 { return a } 287 if n == 1 { return a }
230 let square = try multiply(a, a) 288 let square = try multiply(a, a)
@@ -232,9 +290,9 @@ public enum Calc {
232 return try multiply(a, try integerPower(square, n / 2)) 290 return try multiply(a, try integerPower(square, n / 2))
233 } 291 }
234 292
235 static func power(_ a: Value, _ b: Value) throws -> Value { 293 static func power(_ a: Value, _ b: Value, fractions: Bool = false) throws -> Value {
236 if isNaN(a, b) { return .nan } 294 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) }
238 let base = try double(a) 296 let base = try double(a)
239 guard base >= 0 else { throw Unsupported("complex result") } 297 guard base >= 0 else { throw Unsupported("complex result") }
240 return try fromDouble(pow(base, try double(b))) 298 return try fromDouble(pow(base, try double(b)))
@@ -254,6 +312,7 @@ public enum Calc {
254 static func format(_ value: Value, _ floatFormat: FloatFormat) throws -> String { 312 static func format(_ value: Value, _ floatFormat: FloatFormat) throws -> String {
255 switch value { 313 switch value {
256 case .int(let i): return String(i) 314 case .int(let i): return String(i)
315 case .frac(let n, let d): return "\(n):\(d)"
257 case .vector: throw Unsupported("vector result") 316 case .vector: throw Unsupported("vector result")
258 case .nan: return "nan" 317 case .nan: return "nan"
259 case .date(let d): return "<" + (try formatDate(d)) + ">" 318 case .date(let d): return "<" + (try formatDate(d)) + ">"
@@ -577,10 +636,10 @@ public enum Calc {
577 case "+": return try add(a, b) 636 case "+": return try add(a, b)
578 case "-": return try subtract(a, b) 637 case "-": return try subtract(a, b)
579 case "*": return try multiply(a, b) 638 case "*": return try multiply(a, b)
580 case "/": return try divide(a, b) 639 case "/": return try divide(a, b, fractions: modes.preferFractions)
581 case "%": return try mod(a, b) 640 case "%": return try mod(a, b)
582 case "\\": return try idiv(a, b) 641 case "\\": return try idiv(a, b)
583 case "^": return try power(a, b) 642 case "^": return try power(a, b, fractions: modes.preferFractions)
584 case "|": return .int(try truth(a) || truth(b) ? 1 : 0) 643 case "|": return .int(try truth(a) || truth(b) ? 1 : 0)
585 case "&": return .int(try truth(a) && truth(b) ? 1 : 0) 644 case "&": return .int(try truth(a) && truth(b) ? 1 : 0)
586 default: 645 default:
@@ -640,7 +699,7 @@ public enum Calc {
640 func deviations(_ args: [Value]) throws -> (Value, Int) { 699 func deviations(_ args: [Value]) throws -> (Value, Int) {
641 let items = try elements(args) 700 let items = try elements(args)
642 guard !items.isEmpty else { throw Unsupported("empty vector") } 701 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)
644 let squares = try items.reduce(Value.int(0)) { sum, x in 703 let squares = try items.reduce(Value.int(0)) { sum, x in
645 let d = try subtract(x, mean) 704 let d = try subtract(x, mean)
646 return try add(sum, try multiply(d, d)) 705 return try add(sum, try multiply(d, d))
@@ -678,7 +737,7 @@ public enum Calc {
678 case "vmean": 737 case "vmean":
679 let items = try elements(args) 738 let items = try elements(args)
680 guard !items.isEmpty else { throw Unsupported("empty vector") } 739 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)
682 case "vmax", "vmin": 741 case "vmax", "vmin":
683 let items = try elements(args) 742 let items = try elements(args)
684 guard var best = items.first else { throw Unsupported("empty vector") } 743 guard var best = items.first else { throw Unsupported("empty vector") }
@@ -700,7 +759,7 @@ public enum Calc {
700 let (squares, n) = try deviations(args) 759 let (squares, n) = try deviations(args)
701 let population = name == "vpvar" || name == "vpsdev" 760 let population = name == "vpvar" || name == "vpsdev"
702 if !population, n < 2 { throw Unsupported("variance of one value") } 761 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)
704 return name.hasSuffix("sdev") ? try root(variance) : variance 763 return name.hasSuffix("sdev") ? try root(variance) : variance
705 case "mod": 764 case "mod":
706 guard args.count == 2 else { throw Unsupported("arguments") } 765 guard args.count == 2 else { throw Unsupported("arguments") }
@@ -725,7 +784,8 @@ public enum Calc {
725 let x = try one(args) 784 let x = try one(args)
726 return try compare(x, .int(0)) < 0 ? try negate(x) : x 785 return try compare(x, .int(0)) < 0 ? try negate(x) : x
727 case "floor", "ceil", "trunc": 786 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)) }
729 if case .int = x { return x } 789 if case .int = x { return x }
730 let d = try double(x) 790 let d = try double(x)
731 let r = name == "floor" ? d.rounded(.down) : name == "ceil" ? d.rounded(.up) : d.rounded(.towardZero) 791 let r = name == "floor" ? d.rounded(.down) : name == "ceil" ? d.rounded(.up) : d.rounded(.towardZero)
@@ -733,7 +793,8 @@ public enum Calc {
733 return .int(i) 793 return .int(i)
734 case "round": 794 case "round":
735 guard args.count == 1 || args.count == 2 else { throw Unsupported("arguments") } 795 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)) }
737 if args.count == 2 { 798 if args.count == 2 {
738 guard case .int(let places) = args[1] else { throw Unsupported("arguments") } 799 guard case .int(let places) = args[1] else { throw Unsupported("arguments") }
739 guard case .float(let m, let e) = x else { return x } 800 guard case .float(let m, let e) = x else { return x }
@@ -744,11 +805,16 @@ public enum Calc {
744 return .int(i) 805 return .int(i)
745 case "sqrt": 806 case "sqrt":
746 let x = try one(args) 807 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 }
747 if case .int(let i) = x { 813 if case .int(let i) = x {
748 guard i >= 0 else { throw Unsupported("complex result") } 814 guard i >= 0 else { throw Unsupported("complex result") }
749 let root = Int(Double(i).squareRoot().rounded()) 815 if let root = exactRoot(i) { return .int(root) }
750 if root * root == i { return .int(root) }
751 } 816 }
817 if case .frac(let n, let d) = x, let rn = exactRoot(n), let rd = exactRoot(d) { return try fraction(rn, rd) }
752 let d = try double(x) 818 let d = try double(x)
753 guard d >= 0 else { throw Unsupported("complex result") } 819 guard d >= 0 else { throw Unsupported("complex result") }
754 return try fromDouble(d.squareRoot()) 820 return try fromDouble(d.squareRoot())
Sources/OrgCore/Compute/TableFormulas.swift +1
@@ -537,6 +537,7 @@ extension EmacsBuffer {
537 case "N": mode.numbers = true 537 case "N": mode.numbers = true
538 case "L": literal = true 538 case "L": literal = true
539 case "E": mode.keepEmpty = true 539 case "E": mode.keepEmpty = true
540 case "F": modes.preferFractions = true
540 case "D": modes.degrees = true 541 case "D": modes.degrees = true
541 case "R": modes.degrees = false 542 case "R": modes.degrees = false
542 default: throw Calc.Unsupported("format flag \(ns.substring(with: m.range))") 543 default: throw Calc.Unsupported("format flag \(ns.substring(with: m.range))")
Tests/OrgCoreTests/TableFormulaTests.swift +2
@@ -54,6 +54,8 @@ struct TableFormulaTests {
54 "| | a | b |\n|---+---+---|\n| | 2 | |\n| | 3 | |\n| $ | %=%.2f | |\n#+TBLFM: $3=$2/3;\n", 54 "| | a | b |\n|---+---+---|\n| | 2 | |\n| | 3 | |\n| $ | %=%.2f | |\n#+TBLFM: $3=$2/3;\n",
55 "| a | b |\n|---+---|\n| 4 | |\n| 9 | |\n#+TBLFM: $2=vsdev(@I$1..@II$1)+fact(3)\n", 55 "| a | b |\n|---+---|\n| 4 | |\n| 9 | |\n#+TBLFM: $2=vsdev(@I$1..@II$1)+fact(3)\n",
56 "| a | b |\n|---+---|\n| 1 | |\n| 2 | |\n#+TBLFM: @-1$2=5\n", 56 "| 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",
57 ] 59 ]
58 60
59 @Test func remoteFindsTablesByIDInOtherFiles() { 61 @Test func remoteFindsTablesByIDInOtherFiles() {