Sources/OrgCore/Compute/TableFormulas.swift
607 lines · 30317 bytes
1import Foundation
2
3// Table formulas (org-table.el, Org 9.8.7): `org-table-recalculate` and
4// `org-table-eval-formula` with Calc evaluated natively. Column names, constants, named
5// fields, marked rows, `remote()`, Lisp formulas, durations and Calc features beyond `Calc`
6// throw `Calc.Unsupported`; the caller then recalculates the table in Emacs.
7
8extension EmacsBuffer {
9 struct TableAnalysis {
10 /// Table-relative line index of each data line, from 1 (index 0 unused).
11 var dlines: [Int]
12 /// Hline flag per line, with an extra hline after the last.
13 var types: [Bool]
14 var ncol: Int
15 var begin: Int
16 }
17
18 static let tblfmPattern = "^[ \\t]*#\\+TBLFM: *(.*)"
19 static let rangePattern = "@([-+]?I*[-+]?[0-9]*)(\\$[-+]?[0-9]+)?(\\.\\.@?([-+]?I*[-+]?[0-9]*)(\\$[-+]?[0-9]+)?)?"
20
21 /// `org-table-analyze`.
22 func tableAnalyze() throws -> TableAnalysis {
23 let begin = tableBegin()
24 let end = tableEnd()
25 return try saveExcursion {
26 point = begin
27 var dlines = [0]
28 var types: [Bool] = []
29 var line = 0
30 while point < end || (point == end && false) {
31 let content = substring(lineStart(point), lineEnd(point))
32 guard content.range(of: "^[ \\t]*\\|", options: .regularExpression) != nil else { break }
33 if content.range(of: "^[ \\t]*\\| *[!$_^] *(\\||$)", options: .regularExpression) != nil {
34 throw Calc.Unsupported("named columns, parameters or fields")
35 }
36 let isHline = content.range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil
37 types.append(isHline)
38 if !isHline { dlines.append(line) }
39 line += 1
40 if forwardLine(1) != 0 || point >= end { break }
41 }
42 types.append(true)
43 guard dlines.count > 1 else { throw UserError(message: "No data lines in table") }
44 point = begin
45 forwardLine(dlines[1])
46 let first = substring(lineStart(point), lineEnd(point))
47 let ncol = Self.splitFields(first, separator: "[ \\t]*\\|[ \\t]*").count
48 return TableAnalysis(dlines: dlines, types: types, ncol: ncol, begin: begin)
49 }
50 }
51
52 /// `org-split-string`: leading and trailing separators produce no empty strings.
53 static func splitFields(_ s: String, separator: String) -> [String] {
54 let ns = s as NSString
55 let regex = try! NSRegularExpression(pattern: separator)
56 let matches = regex.matches(in: s, range: NSRange(location: 0, length: ns.length))
57 guard !matches.isEmpty else { return [s] }
58 var results: [String] = []
59 var i = 0
60 for (k, m) in matches.enumerated() {
61 if k == 0, m.range.location == 0 {
62 i = NSMaxRange(m.range)
63 continue
64 }
65 results.append(ns.substring(with: NSRange(location: i, length: m.range.location - i)))
66 i = NSMaxRange(m.range)
67 }
68 if i != ns.length { results.append(ns.substring(from: i)) }
69 return results
70 }
71
72 /// `org-table-get-stored-formulas` from the `#+TBLFM` text `line`.
73 static func storedFormulas(_ line: String) throws -> [(lhs: String, rhs: String)] {
74 var result: [(String, String)] = []
75 var seen: Set<String> = []
76 let regex = try! NSRegularExpression(pattern: "^(@[-+I<>0-9.$@]+|\\$([_a-zA-Z0-9]+|[<>]+)) *= *(.*[^ \\t])")
77 for part in splitFields(line, separator: " *:: *") {
78 let ns = part as NSString
79 guard let m = regex.firstMatch(in: part, range: NSRange(location: 0, length: ns.length)) else { continue }
80 let whole = ns.substring(with: m.range(at: 1))
81 var lhs = whole
82 if m.range(at: 2).location != NSNotFound,
83 whole.range(of: "^\\$([0-9]+|[<>]+)$", options: .regularExpression) == nil {
84 lhs = ns.substring(with: m.range(at: 2))
85 }
86 guard seen.insert(lhs).inserted else { throw UserError(message: "Double definition `\(lhs)=' in TBLFM line, please fix by hand") }
87 result.append((lhs, ns.substring(with: m.range(at: 3))))
88 }
89 return result
90 }
91
92 /// The first `#+TBLFM` line after the table, blank lines allowed between.
93 func firstTblfm(after end: Int) -> String? {
94 saveExcursion {
95 point = end
96 while !eobp {
97 let line = substring(lineStart(point), lineEnd(point))
98 if line.trimmingCharacters(in: .whitespaces).isEmpty {
99 if forwardLine(1) != 0 { return nil }
100 continue
101 }
102 guard let r = line.range(of: Self.tblfmPattern, options: [.regularExpression, .caseInsensitive]) else { return nil }
103 let text = String(line[r])
104 return String(text[text.index(after: text.firstIndex(of: ":")!)...]).trimmingCharacters(in: .init(charactersIn: " "))
105 }
106 return nil
107 }
108 }
109
110 /// `org-table-formula-handle-first/last-rc`.
111 static func handleFirstLast(_ s: String, analysis: TableAnalysis) throws -> String {
112 var result = s
113 var start = 0
114 let regex = try! NSRegularExpression(pattern: "([@$])(<+|>+)|(remote\\([^)]+\\))")
115 while let m = regex.firstMatch(in: result, range: NSRange(location: start, length: (result as NSString).length - start)) {
116 let ns = result as NSString
117 if m.range(at: 3).location != NSNotFound {
118 start = NSMaxRange(m.range(at: 3))
119 continue
120 }
121 let kind = ns.substring(with: m.range(at: 1))
122 let nmax = kind == "@" ? analysis.dlines.count - 1 : analysis.ncol
123 let marks = ns.substring(with: m.range(at: 2))
124 let n = marks.first == "<" ? marks.count : nmax - marks.count + 1
125 if n < 1 || n > nmax { throw UserError(message: "Reference \"\(ns.substring(with: m.range))\" in expression \"\(result)\" points outside table") }
126 start = m.range.location
127 result = ns.replacingCharacters(in: m.range, with: kind + String(n))
128 }
129 return result
130 }
131
132 /// `org-table--row-type`.
133 static func rowType(hline: Bool, _ n: Int, _ start: Int, backwards: Bool, relative: Bool, analysis: TableAnalysis, desc: String) throws -> Int {
134 var i = start
135 let l = analysis.types.count
136 for _ in 0..<n {
137 while true {
138 i += backwards ? -1 : 1
139 guard i >= 0, i < l, analysis.types[i] != hline else { break }
140 }
141 }
142 if i < 0 || i >= l { throw UserError(message: "Row descriptor \(desc) leads outside table") }
143 return i == l - 1 ? i - 1 : i
144 }
145
146 /// `org-table--descriptor-line`.
147 static func descriptorLine(_ desc: String, _ cline: Int, analysis: TableAnalysis) throws -> Int {
148 if let n = Int(desc), desc.allSatisfy(\.isNumber) {
149 guard n < analysis.dlines.count else { throw Calc.Unsupported("row out of range") }
150 return analysis.dlines[n]
151 }
152 let ns = desc as NSString
153 let regex = try! NSRegularExpression(pattern: "^(([-+])?(I+))?(([-+])?([0-9]+))?")
154 let m = regex.firstMatch(in: desc, range: NSRange(location: 0, length: ns.length))!
155 func has(_ i: Int) -> Bool { m.range(at: i).location != NSNotFound }
156 if (!has(3) && !has(6)) || (has(3) && has(6) && !has(5)) { throw UserError(message: "Invalid row descriptor `\(desc)'") }
157 var hn = has(3) ? m.range(at: 3).length : nil
158 var hdir = has(2) ? ns.substring(with: m.range(at: 2)) : nil
159 let odir = has(5) ? ns.substring(with: m.range(at: 5)) : nil
160 let on = has(6) ? Int(ns.substring(with: m.range(at: 6))) : nil
161 let rel = has(6) && ((has(1) && !has(3)) || has(5))
162 var cline = cline
163 if hn != nil, hdir == nil {
164 cline = 0
165 hdir = "+"
166 if analysis.types[0] { hn! -= 1 }
167 }
168 if hn == nil, on != nil, odir == nil { throw UserError(message: "Should never happen") }
169 if let hn { cline = try rowType(hline: true, hn, cline, backwards: hdir == "-", relative: false, analysis: analysis, desc: desc) }
170 if let on { cline = try rowType(hline: false, on, cline, backwards: odir == "-", relative: rel, analysis: analysis, desc: desc) }
171 return cline
172 }
173
174 /// `org-table-get-field` on the line at point.
175 func tableField(_ column: Int) -> String {
176 saveExcursion {
177 tableGotoColumn(column)
178 skipBackwardNot([124, 10])
179 if bolp || looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil { return "" }
180 let start = point
181 while let c = character(at: point), c != 124, c != 10, c != 13 { point += 1 }
182 return substring(start, point)
183 }
184 }
185
186 func isDataLine() -> Bool {
187 looking("^[ \\t]*\\|[^-]", caseInsensitive: false, setsMatch: false) != nil
188 || substring(lineStart(point), lineEnd(point)).range(of: "^[ \\t]*\\|[^-]", options: .regularExpression) != nil
189 }
190
191 func gotoTableLine(_ line: Int, _ analysis: TableAnalysis) {
192 point = analysis.begin
193 forwardLine(line)
194 }
195
196 /// `org-table-get-range`: one field's text, or the fields of a rectangle.
197 func tableRange(_ desc: String, thisline: Int, col: Int, analysis: TableAnalysis) throws -> TableRangeValue {
198 var desc = desc
199 if desc.range(of: "^\\$[0-9]+\\.\\.\\$[0-9]+$", options: .regularExpression) != nil {
200 desc = desc.replacingOccurrences(of: "$", with: "@0$")
201 }
202 let ns = desc as NSString
203 guard let m = try! NSRegularExpression(pattern: Self.rangePattern).firstMatch(in: desc, range: NSRange(location: 0, length: ns.length)) else {
204 throw UserError(message: "Invalid table range specifier `\(desc)'")
205 }
206 func group(_ i: Int) -> String? { m.range(at: i).location == NSNotFound ? nil : ns.substring(with: m.range(at: i)) }
207 let isRange = group(3) != nil
208 func row(_ r: String?) throws -> Int {
209 guard let r, r.contains(where: { !$0.isWhitespace }) else { return thisline }
210 return try Self.descriptorLine(r, thisline, analysis: analysis)
211 }
212 func column(_ c: String?) -> Int {
213 guard let c else { return col }
214 let body = String(c.dropFirst())
215 let n = Int(body) ?? 0
216 if n == 0 { return col }
217 return n + (body.first == "-" || body.first == "+" ? col : 0)
218 }
219 let r1 = try row(group(1))
220 let r2 = try row(group(4))
221 let c1 = column(group(2))
222 let c2 = column(group(5))
223 return try saveExcursion {
224 if !isRange || (r1 == r2 && c1 == c2) {
225 gotoTableLine(r1, analysis)
226 while !isDataLine() {
227 if forwardLine(1) != 0 { throw Calc.Unsupported("reference outside table") }
228 }
229 return .field(tableField(c1).trimmingCharacters(in: .whitespaces))
230 }
231 var firstRow = min(r1, r2)
232 let lastRow = max(r1, r2)
233 let firstColumn = min(c1, c2)
234 let lastColumn = max(c1, c2)
235 gotoTableLine(firstRow, analysis)
236 while !isDataLine() {
237 if forwardLine(1) != 0 { throw Calc.Unsupported("reference outside table") }
238 firstRow += 1
239 }
240 let startLine = lineStart(point)
241 forwardLine(lastRow - firstRow)
242 while !isDataLine() { forwardLine(-1) }
243 let endLine = lineStart(point)
244 var fields: [String] = []
245 point = startLine
246 while point <= endLine {
247 if !atTableHline {
248 for c in firstColumn...lastColumn { fields.append(tableField(c).trimmingCharacters(in: .whitespaces)) }
249 }
250 if forwardLine(1) != 0 { break }
251 }
252 return .list(fields)
253 }
254 }
255
256 enum TableRangeValue {
257 case field(String)
258 case list([String])
259 }
260
261 static let calcNumber = "^[-+]?([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE][-+]?[0-9]+)?$"
262
263 /// `org-table-make-reference` for Calc, refusing what isn't a number.
264 static func reference(_ value: TableRangeValue) throws -> String {
265 func number(_ s: String) throws -> String {
266 guard s.range(of: calcNumber, options: .regularExpression) != nil else { throw Calc.Unsupported("non-numeric field \(s)") }
267 return s
268 }
269 switch value {
270 case .field(let s):
271 return s.contains(where: { !$0.isWhitespace }) ? "(" + (try number(s)) + ")" : "(0)"
272 case .list(let items):
273 return "[" + (try items.filter { $0.contains(where: { !$0.isWhitespace }) }.map(number)).joined(separator: ",") + "]"
274 }
275 }
276
277 /// `org-table-eval-formula` with the equation given, at point.
278 func tableEvalFormula(_ formula: String, analysis: TableAnalysis) throws {
279 var formula = formula
280 var modes = Calc.Modes()
281 var fmt: String?
282 if let semicolon = formula.lastIndex(of: ";") {
283 var flags = String(formula[formula.index(after: semicolon)...])
284 formula = String(formula[..<semicolon])
285 let numeric = try! NSRegularExpression(pattern: "([pnfse])(-?[0-9]+)")
286 while let m = numeric.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) {
287 let ns = flags as NSString
288 let n = Int(ns.substring(with: m.range(at: 2)))!
289 switch ns.substring(with: m.range(at: 1)) {
290 case "p": modes.precision = n
291 case "n": modes.format = .float(n)
292 case "f": modes.format = .fix(n)
293 case "s": modes.format = .sci(n)
294 default: modes.format = .eng(n)
295 }
296 flags = ns.replacingCharacters(in: m.range, with: "")
297 }
298 let letters = try! NSRegularExpression(pattern: "[tTUNLEDRFSu]")
299 while let m = letters.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) {
300 let ns = flags as NSString
301 switch ns.substring(with: m.range) {
302 case "D": modes.degrees = true
303 case "R": modes.degrees = false
304 default: throw Calc.Unsupported("format flag \(ns.substring(with: m.range))")
305 }
306 flags = ns.replacingCharacters(in: m.range, with: "")
307 }
308 if flags.contains(where: { !$0.isWhitespace }) {
309 guard flags.range(of: "^%[-+ #0]*[0-9]*(\\.[0-9]+)?[fgeE]$", options: .regularExpression) != nil else {
310 throw Calc.Unsupported("format \(flags)")
311 }
312 fmt = flags
313 }
314 }
315 if formula.hasPrefix("'(") { throw Calc.Unsupported("Lisp formula") }
316 if formula.range(of: "\\$[a-zA-Z_]", options: .regularExpression) != nil || formula.contains("remote(")
317 || formula.range(of: "[<\\[][0-9]{4}-", options: .regularExpression) != nil || formula.contains("&") {
318 throw Calc.Unsupported("names, constants, remote references or dates")
319 }
320 let line = substring(lineStart(point), lineEnd(point)).trimmingCharacters(in: .whitespaces)
321 let fields = Self.splitFields(line, separator: " *\\| *")
322 let n0 = tableCurrentColumn()
323 let thisline = try saveExcursion { () -> Int in
324 let here = lineStart(point)
325 point = analysis.begin
326 var count = 0
327 while point < here {
328 if forwardLine(1) != 0 { throw Calc.Unsupported("table position") }
329 count += 1
330 }
331 return count
332 }
333 var form = formula
334 form = form.replacingOccurrences(of: "@#", with: String(tableCurrentDataLine()))
335 form = form.replacingOccurrences(of: "$#", with: String(n0))
336 let range = try! NSRegularExpression(pattern: Self.rangePattern)
337 while let m = range.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)), m.range.length > 1 {
338 let ns = form as NSString
339 let replacement = try Self.reference(try tableRange(ns.substring(with: m.range), thisline: thisline, col: n0, analysis: analysis))
340 form = ns.replacingCharacters(in: m.range, with: replacement)
341 }
342 let rowRange = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)\\.\\.\\$(([-+])?[0-9]+)")
343 while let m = rowRange.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) {
344 let ns = form as NSString
345 let a = Int(ns.substring(with: m.range(at: 1)))! + (m.range(at: 2).location != NSNotFound ? n0 : 0)
346 let b = Int(ns.substring(with: m.range(at: 3)))! + (m.range(at: 4).location != NSNotFound ? n0 : 0)
347 guard a >= 1, b <= fields.count, a - 1 <= b else { throw Calc.Unsupported("range outside row") }
348 form = ns.replacingCharacters(in: m.range, with: try Self.reference(.list(Array(fields[(a - 1)..<b]))))
349 }
350 let single = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)")
351 while let m = single.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) {
352 let ns = form as NSString
353 let n = Int(ns.substring(with: m.range(at: 1)))! + (m.range(at: 2).location != NSNotFound ? n0 : 0)
354 let index = (n == 0 ? n0 : max(n, 1)) - 1
355 guard index < fields.count else { throw UserError(message: "Invalid field specifier \"\(ns.substring(with: m.range))\"") }
356 form = ns.replacingCharacters(in: m.range, with: try Self.reference(.field(fields[index])))
357 }
358 var result = try Calc.evaluate(form, modes: modes)
359 if let fmt {
360 result = String(format: fmt, Self.stringToNumber(result))
361 }
362 tableSetField(result)
363 }
364
365 /// `string-to-number`.
366 static func stringToNumber(_ s: String) -> Double {
367 guard let r = s.range(of: "^[-+]?([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE][-+]?[0-9]+)?", options: .regularExpression) else { return 0 }
368 return Double(s[r]) ?? 0
369 }
370
371 /// `org-table-justify-field-maybe` while the table needs realigning: the field becomes
372 /// ` NEW |`.
373 func tableSetField(_ value: String) {
374 let pos = point
375 guard tableCurrentColumn() > 0 else { return }
376 skipBackwardNot([124])
377 let start = point
378 var end = point
379 while let c = character(at: end), c != 124, c != 10 { end += 1 }
380 let closed = character(at: end) == 124
381 replaceRange(start, closed ? end + 1 : end, with: " \(value) |")
382 point = min(pos, length)
383 }
384
385 func replaceRange(_ start: Int, _ end: Int, with text: String) {
386 let saved = point
387 deleteRegion(start, end)
388 point = start
389 insert(text)
390 point = saved
391 }
392
393 /// `org-table-recalculate`: `all` for every row, otherwise the line at point; `tblfm`
394 /// replaces the stored formulas, as C-c C-c on a `#+TBLFM` line does.
395 func tableRecalculate(all: Bool, tblfm: String? = nil) throws {
396 guard atTable else { throw UserError(message: "Not at a table") }
397 let analysis = try tableAnalyze()
398 let end = tableEnd()
399 guard let line = tblfm ?? firstTblfm(after: end) else { return }
400 let stored = try Self.storedFormulas(line).sorted { $0.lhs < $1.lhs }
401 guard !stored.isEmpty else { return }
402 var columnEquations: [(Int, String)] = []
403 var fieldEquations: [(String, String)] = []
404 for (oldLHS, rawRHS) in stored {
405 let rhs = try Self.handleFirstLast(rawRHS, analysis: analysis)
406 if oldLHS.range(of: "^@-?I+", options: .regularExpression) != nil { throw UserError(message: "Can't assign to hline relative reference") }
407 let lhs = try Self.handleFirstLast(oldLHS, analysis: analysis)
408 if lhs.range(of: "^\\$[0-9]+$", options: .regularExpression) != nil {
409 columnEquations.append((Int(lhs.dropFirst())!, rhs))
410 } else {
411 fieldEquations.append((lhs, rhs))
412 }
413 }
414 // `org-table-expand-lhs-ranges`.
415 var fields: [(row: Int, column: Int, rhs: String)] = []
416 for (lhs, rhs) in fieldEquations {
417 if let m = lhs.firstMatch(of: /^@([0-9]+)\$([0-9]+)$/) {
418 fields.append((Int(m.1)!, Int(m.2)!, rhs))
419 } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil || lhs.range(of: "^[a-zA-Z]", options: .regularExpression) != nil {
420 throw Calc.Unsupported("relative or named field formula")
421 } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) {
422 for c in 1...analysis.ncol { fields.append((Int(m.1)!, c, rhs)) }
423 } else {
424 let corners = try rangeCorners(lhs, analysis: analysis)
425 guard let r1 = Self.lineToDline(corners.r1, analysis), let r2 = Self.lineToDline(corners.r2, analysis, above: true), r1 <= r2 else {
426 throw Calc.Unsupported("formula range")
427 }
428 for r in r1...r2 { for c in corners.c1...corners.c2 { fields.append((r, c, rhs)) } }
429 }
430 }
431 let savedLine = lineStart(point)
432 let savedColumn = tableCurrentColumn()
433 let lineMarker = marker(savedLine)
434 defer { release(lineMarker) }
435
436 let endMarker = marker(end)
437 defer { release(endMarker) }
438 var beg: Int
439 if all {
440 beg = analysis.begin
441 point = beg
442 if searchForward("^[ \\t]*\\|[^-]", bound: end, caseInsensitive: false) != nil,
443 searchForward("^[ \\t]*\\|-", bound: end, caseInsensitive: false) != nil,
444 let body = searchForward("^[ \\t]*\\|[^-]", bound: end, caseInsensitive: false) {
445 beg = body[0]!.lowerBound
446 }
447 } else {
448 beg = lineStart(point)
449 endMarker.position = min(length, lineEnd(point) + 1)
450 }
451
452 let currentLine = savedLine - 0
453 var seen: Set<String> = []
454 var untouchable: Set<String> = []
455 for field in fields {
456 let reference = "@\(field.row)$\(field.column)"
457 guard seen.insert(reference).inserted else { throw UserError(message: "Several field/range formulas try to set \(reference)") }
458 guard field.row < analysis.dlines.count else { throw UserError(message: "Invalid row number in \(reference)") }
459 let lineStartOffset = saveExcursion { () -> Int in
460 gotoTableLine(analysis.dlines[field.row], analysis)
461 return lineStart(point)
462 }
463 if all || lineStartOffset == currentLine { untouchable.insert(reference) }
464 }
465
466 point = beg
467 let rowMarker = marker(beg)
468 defer { release(rowMarker) }
469 while let match = searchForward("^[ \\t]*\\|[^-]", bound: endMarker.position, caseInsensitive: false) {
470 _ = match
471 let rowStart = lineStart(point)
472 rowMarker.position = rowStart
473 let first = saveExcursion { () -> String in
474 point = rowStart
475 return tableField(1)
476 }
477 if first.range(of: "^ *[_^!$/] *$", options: .regularExpression) == nil {
478 let dline = saveExcursion { () -> Int in
479 point = rowStart
480 return tableCurrentDataLine()
481 }
482 for (column, rhs) in columnEquations {
483 point = rowMarker.position
484 tableGotoColumn(column, force: true)
485 if untouchable.contains("@\(dline)$\(column)") { continue }
486 try tableEvalFormula(rhs, analysis: analysis)
487 }
488 }
489 point = rowMarker.position
490 if forwardLine(1) != 0 || point >= endMarker.position { break }
491 }
492 for field in fields {
493 gotoTableLine(analysis.dlines[field.row], analysis)
494 let count = substring(lineStart(point), lineEnd(point)).filter { $0 == "|" }.count - 1
495 if field.column > count { throw UserError(message: "Missing columns in the table. Aborting") }
496 tableGotoColumn(field.column)
497 try tableEvalFormula(field.rhs, analysis: analysis)
498 }
499 tableAlign()
500 point = lineMarker.position
501 tableGotoColumn(savedColumn)
502 }
503
504 /// `org-table-get-range` with `corners-only`.
505 func rangeCorners(_ desc: String, analysis: TableAnalysis) throws -> (r1: Int, c1: Int, r2: Int, c2: Int) {
506 let ns = desc as NSString
507 guard let m = try! NSRegularExpression(pattern: Self.rangePattern).firstMatch(in: desc, range: NSRange(location: 0, length: ns.length)) else {
508 throw UserError(message: "Invalid table range specifier `\(desc)'")
509 }
510 func group(_ i: Int) -> String? { m.range(at: i).location == NSNotFound ? nil : ns.substring(with: m.range(at: i)) }
511 let thisline = 0
512 func row(_ r: String?) throws -> Int {
513 guard let r, !r.isEmpty else { return thisline }
514 return try Self.descriptorLine(r, thisline, analysis: analysis)
515 }
516 func column(_ c: String?) -> Int {
517 guard let c, let n = Int(c.dropFirst()), n != 0 else { return 1 }
518 return n + (c.dropFirst().first == "-" || c.dropFirst().first == "+" ? 1 : 0)
519 }
520 let r1 = try row(group(1))
521 let r2 = try row(group(4))
522 let c1 = column(group(2))
523 let c2 = column(group(5))
524 return (min(r1, r2), min(c1, c2), max(r1, r2), max(c1, c2))
525 }
526
527 /// `org-table-line-to-dline`.
528 static func lineToDline(_ line: Int, _ analysis: TableAnalysis, above: Bool = false) -> Int? {
529 let dlines = analysis.dlines
530 var low = 1
531 var high = dlines.count - 1
532 if dlines[low] > line || dlines[high] < line { return nil }
533 if line == dlines[high] { return high }
534 while high - low > 1 {
535 let mean = (high + low) / 2
536 if dlines[mean] == line { return mean }
537 if dlines[mean] > line { high = mean } else { low = mean }
538 }
539 if line == dlines[high] { return high }
540 if line == dlines[low] { return low }
541 return above ? low : high
542 }
543}
544
545/// C-c * and C-u C-c *: `org-table-recalculate`, and C-c C-c on a `#+TBLFM` line. Tables the
546/// native evaluator can't do fail with `TableRecalculate.emacsMessage`, and the app
547/// recalculates them in Emacs.
548public struct TableRecalculate: OrgCommand {
549 public let all: Bool
550 public init(all: Bool) { self.all = all }
551 public var id: String { all ? "org.table.recalc-all" : "org.table.recalc" }
552 public var title: String { all ? "Recalculate Table" : "Recalculate Table Row" }
553
554 public static let emacsMessage = "needs Emacs: "
555
556 public func applies(in context: EditContext) -> Bool {
557 KeyContext.holds("table", in: context) || Self.tblfmLine(in: context) != nil
558 }
559
560 static func tblfmLine(in context: EditContext) -> String? {
561 let buffer = EmacsBuffer(context.text, point: context.caret)
562 let line = buffer.substring(buffer.lineStart(buffer.point), buffer.lineEnd(buffer.point))
563 guard line.range(of: EmacsBuffer.tblfmPattern, options: [.regularExpression, .caseInsensitive]) != nil else { return nil }
564 return line
565 }
566
567 public func run(in context: EditContext) -> CommandStep {
568 let step = commitBuffer(context) { buffer in
569 do {
570 if let line = Self.tblfmLine(in: context) {
571 // `org-table-calc-current-TBLFM`: that line's formulas, every row, with
572 // point at the end of the table.
573 let formulas = String(line[line.index(after: line.firstIndex(of: ":")!)...]).trimmingCharacters(in: .init(charactersIn: " "))
574 let saved = buffer.marker(buffer.point)
575 defer { buffer.release(saved) }
576 guard let tableEnd = buffer.saveExcursion({ () -> Int? in
577 while buffer.forwardLine(-1) == 0 {
578 if buffer.lookingAtLine("^[ \\t]*\\|") { return buffer.lineEnd(buffer.point) }
579 }
580 return nil
581 }) else { throw EmacsBuffer.UserError(message: "Not at a table") }
582 buffer.point = tableEnd
583 try buffer.tableRecalculate(all: true, tblfm: formulas)
584 buffer.point = saved.position
585 } else {
586 try buffer.tableRecalculate(all: all)
587 }
588 } catch let error as Calc.Unsupported {
589 throw EmacsBuffer.UserError(message: Self.emacsMessage + error.reason)
590 }
591 }
592 guard case .failed(let message) = step, message.hasPrefix(Self.emacsMessage) else { return step }
593 // Emacs recalculates the table; its text replaces the table here.
594 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options)
595 let onTblfm = Self.tblfmLine(in: context) != nil
596 if onTblfm {
597 while buffer.forwardLine(-1) == 0, !buffer.lookingAtLine("^[ \\t]*\\|") {}
598 }
599 let table = buffer.tableBegin()..<buffer.tableEnd()
600 let tblfm = onTblfm ? Self.tblfmLine(in: context) ?? "" : buffer.firstTblfm(after: table.upperBound) ?? ""
601 let form = onTblfm ? "(org-table-calc-current-TBLFM)" : all ? "(org-table-recalculate t)" : "(org-table-recalculate nil)"
602 return .external(.emacsTable(
603 table: table, point: context.caret, form: form,
604 reason: String(message.dropFirst(Self.emacsMessage.count)), lisp: tblfm.contains("'(")
605 ))
606 }
607}