Sources/OrgCore/Compute/TableFormulas.swift
1241 lines · 66563 bytes
75 symbols in this file
RemoteTablesTableAnalysistableAnalyzenamesrestsplitFieldsstoredFormulasfirstTblfmhandleFirstLastrowTypedescriptorLinehastableFieldisDataLinegotoTableLinetableRangegrouprowcolumnTableRangeValueReferenceModeLispnumberTextmakeReferenceblanklispcalcintegertimeStringToSecondssecondsDurationFormatsecondsToStringrefsToRCtableConstantsubstituteNamesremoteRangefirstheadingtableEvalFormulareferenceevalLispdefineLookupsmatchesnormalizeTimestampsstringToNumbertableSetFieldreplaceRangetableRecalculaterangeCornersgrouprowcolumnlineToDlineTableRecalculateappliestblfmLineruninEmacsformulaSortKeysortFormulastableStoreFormulastableGetFormulatableEvalFormulatableMaybeEvalFormulaTableEvalFormulaappliesrunformulasparseFormulasinstallFormulasfieldinstallFieldTableEditFieldappliesrun
1import Foundation
2
3// Table formulas (org-table.el, Org 9.8.7): `org-table-recalculate` and
4// `org-table-eval-formula` with Calc and Lisp evaluated natively. Symbolic results, Calc
5// features beyond `Calc` and Lisp beyond `Elisp` throw `Calc.Unsupported`; the caller then
6// recalculates the table in Emacs. `remote()` finds tables by name in the file, and by ID
7// here or, through `RemoteTables`, in other files.
8
9/// Where `remote()` finds tables by ID in other files.
10public enum RemoteTables {
11 /// The text of the file holding the entry with an ID; the app sets it, from its index.
12 nonisolated(unsafe) public static var fileWithID: ((String) -> String?)?
13}
14
15extension EmacsBuffer {
16 struct TableAnalysis {
17 /// Table-relative line index of each data line, from 1 (index 0 unused).
18 var dlines: [Int]
19 /// Hline flag per line, with an extra hline after the last.
20 var types: [Bool]
21 var ncol: Int
22 var begin: Int
23 /// `org-table-column-names`: from the first `!` row.
24 var columnNames: [(name: String, column: Int)] = []
25 /// `org-table-local-parameters`, in the order org pushes them; the last match wins.
26 var parameters: [(name: String, value: String)] = []
27 /// `org-table-named-field-locations`: the table-relative line and the column.
28 var namedFields: [(name: String, line: Int, column: Int)] = []
29 /// A row marked for calculation (`org-table-calculate-mark-regexp`).
30 var marked = false
31 }
32
33 static let tblfmPattern = "^[ \\t]*#\\+TBLFM: *(.*)"
34 static let rangePattern = "@([-+]?I*[-+]?[0-9]*)(\\$[-+]?[0-9]+)?(\\.\\.@?([-+]?I*[-+]?[0-9]*)(\\$[-+]?[0-9]+)?)?"
35
36 /// `org-table-analyze`.
37 func tableAnalyze() throws -> TableAnalysis {
38 let begin = tableBegin()
39 let end = tableEnd()
40 return try saveExcursion {
41 point = begin
42 var dlines = [0]
43 var types: [Bool] = []
44 var lines: [String] = []
45 var line = 0
46 while point < end || (point == end && false) {
47 let content = substring(lineStart(point), lineEnd(point))
48 guard content.range(of: "^[ \\t]*\\|", options: .regularExpression) != nil else { break }
49 lines.append(content)
50 let isHline = content.range(of: "^[ \\t]*\\|-", options: .regularExpression) != nil
51 types.append(isHline)
52 if !isHline { dlines.append(line) }
53 line += 1
54 if forwardLine(1) != 0 || point >= end { break }
55 }
56 types.append(true)
57 guard dlines.count > 1 else { throw UserError(message: "No data lines in table") }
58 point = begin
59 forwardLine(dlines[1])
60 let first = substring(lineStart(point), lineEnd(point))
61 let ncol = Self.splitFields(first, separator: "[ \\t]*\\|[ \\t]*").count
62 var analysis = TableAnalysis(dlines: dlines, types: types, ncol: ncol, begin: begin)
63 Self.names(lines, into: &analysis)
64 return analysis
65 }
66 }
67
68 /// The names part of `org-table-analyze`.
69 static func names(_ lines: [String], into analysis: inout TableAnalysis) {
70 let identifier = "^[a-zA-Z][_a-zA-Z0-9]*$"
71 func rest(_ line: String, _ pattern: String) -> String? {
72 guard let m = try! NSRegularExpression(pattern: pattern).firstMatch(in: line, range: NSRange(location: 0, length: (line as NSString).length)) else { return nil }
73 return (line as NSString).substring(with: m.range(at: m.numberOfRanges - 1))
74 }
75 if let names = lines.lazy.compactMap({ rest($0, "^[ \\t]*\\| *! *(\\|.*)") }).first {
76 for (i, name) in splitFields(names, separator: " *\\| *").enumerated() where name.range(of: identifier, options: .regularExpression) != nil {
77 analysis.columnNames.append((name, i + 2))
78 }
79 }
80 for line in lines {
81 guard let fields = rest(line, "^[ \\t]*\\| *\\$ *(\\|.*)") else { continue }
82 for field in splitFields(fields, separator: " *\\| *") {
83 if let m = field.firstMatch(of: /^([a-zA-Z][_a-zA-Z0-9]*|%) *= *(.*)/) { analysis.parameters.append((String(m.1), String(m.2))) }
84 }
85 }
86 for (k, line) in lines.enumerated() {
87 guard let m = line.firstMatch(of: /^[ \t]*\| *([_^]) *(\|.*)/) else { continue }
88 let target = m.1 == "_" ? k + 1 : k - 1
89 guard lines.indices.contains(target), let values = rest(lines[target], "^[ \\t]*\\|[^|]*(\\|.*)") else { continue }
90 for (i, (name, value)) in zip(splitFields(String(m.2), separator: " *\\| *"), splitFields(values, separator: " *\\| *")).enumerated()
91 where name.range(of: identifier, options: .regularExpression) != nil {
92 analysis.parameters.append((name, value))
93 analysis.namedFields.append((name, target, i + 2))
94 }
95 }
96 analysis.marked = lines.contains { $0.range(of: "^[ \\t]*\\| *[!$^_#*] *(\\||$)", options: .regularExpression) != nil }
97 }
98
99 /// `org-split-string`: leading and trailing separators produce no empty strings.
100 static func splitFields(_ s: String, separator: String) -> [String] {
101 let ns = s as NSString
102 let regex = try! NSRegularExpression(pattern: separator)
103 let matches = regex.matches(in: s, range: NSRange(location: 0, length: ns.length))
104 guard !matches.isEmpty else { return [s] }
105 var results: [String] = []
106 var i = 0
107 for (k, m) in matches.enumerated() {
108 if k == 0, m.range.location == 0 {
109 i = NSMaxRange(m.range)
110 continue
111 }
112 results.append(ns.substring(with: NSRange(location: i, length: m.range.location - i)))
113 i = NSMaxRange(m.range)
114 }
115 if i != ns.length { results.append(ns.substring(from: i)) }
116 return results
117 }
118
119 /// `org-table-get-stored-formulas` from the `#+TBLFM` text `line`.
120 static func storedFormulas(_ line: String) throws -> [(lhs: String, rhs: String)] {
121 var result: [(String, String)] = []
122 var seen: Set<String> = []
123 let regex = try! NSRegularExpression(pattern: "^(@[-+I<>0-9.$@]+|\\$([_a-zA-Z0-9]+|[<>]+)) *= *(.*[^ \\t])")
124 for part in splitFields(line, separator: " *:: *") {
125 let ns = part as NSString
126 guard let m = regex.firstMatch(in: part, range: NSRange(location: 0, length: ns.length)) else { continue }
127 let whole = ns.substring(with: m.range(at: 1))
128 var lhs = whole
129 if m.range(at: 2).location != NSNotFound,
130 whole.range(of: "^\\$([0-9]+|[<>]+)$", options: .regularExpression) == nil {
131 lhs = ns.substring(with: m.range(at: 2))
132 }
133 guard seen.insert(lhs).inserted else { throw UserError(message: "Double definition `\(lhs)=' in TBLFM line, please fix by hand") }
134 result.append((lhs, ns.substring(with: m.range(at: 3))))
135 }
136 return result
137 }
138
139 /// The first `#+TBLFM` line after the table, blank lines allowed between.
140 func firstTblfm(after end: Int) -> String? {
141 saveExcursion {
142 point = end
143 while !eobp {
144 let line = substring(lineStart(point), lineEnd(point))
145 if line.trimmingCharacters(in: .whitespaces).isEmpty {
146 if forwardLine(1) != 0 { return nil }
147 continue
148 }
149 guard let r = line.range(of: Self.tblfmPattern, options: [.regularExpression, .caseInsensitive]) else { return nil }
150 let text = String(line[r])
151 return String(text[text.index(after: text.firstIndex(of: ":")!)...]).trimmingCharacters(in: .init(charactersIn: " "))
152 }
153 return nil
154 }
155 }
156
157 /// `org-table-formula-handle-first/last-rc`.
158 static func handleFirstLast(_ s: String, analysis: TableAnalysis) throws -> String {
159 var result = s
160 var start = 0
161 let regex = try! NSRegularExpression(pattern: "([@$])(<+|>+)|(remote\\([^)]+\\))")
162 while let m = regex.firstMatch(in: result, range: NSRange(location: start, length: (result as NSString).length - start)) {
163 let ns = result as NSString
164 if m.range(at: 3).location != NSNotFound {
165 start = NSMaxRange(m.range(at: 3))
166 continue
167 }
168 let kind = ns.substring(with: m.range(at: 1))
169 let nmax = kind == "@" ? analysis.dlines.count - 1 : analysis.ncol
170 let marks = ns.substring(with: m.range(at: 2))
171 let n = marks.first == "<" ? marks.count : nmax - marks.count + 1
172 if n < 1 || n > nmax { throw UserError(message: "Reference \"\(ns.substring(with: m.range))\" in expression \"\(result)\" points outside table") }
173 start = m.range.location
174 result = ns.replacingCharacters(in: m.range, with: kind + String(n))
175 }
176 return result
177 }
178
179 /// `org-table--row-type`.
180 static func rowType(hline: Bool, _ n: Int, _ start: Int, backwards: Bool, relative: Bool, analysis: TableAnalysis, desc: String) throws -> Int {
181 var i = start
182 let l = analysis.types.count
183 for _ in 0..<n {
184 while true {
185 i += backwards ? -1 : 1
186 guard i >= 0, i < l, analysis.types[i] != hline else { break }
187 }
188 }
189 if i < 0 || i >= l { throw UserError(message: "Row descriptor \(desc) leads outside table") }
190 return i == l - 1 ? i - 1 : i
191 }
192
193 /// `org-table--descriptor-line`, with `org-table-get-range`'s fallback to the current
194 /// line for `@0`, whose `org-table-dlines` entry is nil.
195 static func descriptorLine(_ desc: String, _ cline: Int, analysis: TableAnalysis) throws -> Int {
196 if let n = Int(desc), desc.allSatisfy(\.isNumber) {
197 guard n < analysis.dlines.count else { throw Calc.Unsupported("row out of range") }
198 return n == 0 ? cline : analysis.dlines[n]
199 }
200 let ns = desc as NSString
201 let regex = try! NSRegularExpression(pattern: "^(([-+])?(I+))?(([-+])?([0-9]+))?")
202 let m = regex.firstMatch(in: desc, range: NSRange(location: 0, length: ns.length))!
203 func has(_ i: Int) -> Bool { m.range(at: i).location != NSNotFound }
204 if (!has(3) && !has(6)) || (has(3) && has(6) && !has(5)) { throw UserError(message: "Invalid row descriptor `\(desc)'") }
205 var hn = has(3) ? m.range(at: 3).length : nil
206 var hdir = has(2) ? ns.substring(with: m.range(at: 2)) : nil
207 let odir = has(5) ? ns.substring(with: m.range(at: 5)) : nil
208 let on = has(6) ? Int(ns.substring(with: m.range(at: 6))) : nil
209 let rel = has(6) && ((has(1) && !has(3)) || has(5))
210 var cline = cline
211 if hn != nil, hdir == nil {
212 cline = 0
213 hdir = "+"
214 if analysis.types[0] { hn! -= 1 }
215 }
216 if hn == nil, on != nil, odir == nil { throw UserError(message: "Should never happen") }
217 if let hn { cline = try rowType(hline: true, hn, cline, backwards: hdir == "-", relative: false, analysis: analysis, desc: desc) }
218 if let on { cline = try rowType(hline: false, on, cline, backwards: odir == "-", relative: rel, analysis: analysis, desc: desc) }
219 return cline
220 }
221
222 /// `org-table-get-field` on the line at point.
223 func tableField(_ column: Int) -> String {
224 saveExcursion {
225 tableGotoColumn(column)
226 skipBackwardNot([124, 10])
227 if bolp || looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) != nil { return "" }
228 let start = point
229 while let c = character(at: point), c != 124, c != 10, c != 13 { point += 1 }
230 return substring(start, point)
231 }
232 }
233
234 func isDataLine() -> Bool {
235 looking("^[ \\t]*\\|[^-]", caseInsensitive: false, setsMatch: false) != nil
236 || substring(lineStart(point), lineEnd(point)).range(of: "^[ \\t]*\\|[^-]", options: .regularExpression) != nil
237 }
238
239 func gotoTableLine(_ line: Int, _ analysis: TableAnalysis) {
240 point = analysis.begin
241 forwardLine(line)
242 }
243
244 /// `org-table-get-range`: one field's text, or the fields of a rectangle.
245 func tableRange(_ desc: String, thisline: Int, col: Int, analysis: TableAnalysis) throws -> TableRangeValue {
246 var desc = desc
247 if desc.range(of: "^\\$[0-9]+\\.\\.\\$[0-9]+$", options: .regularExpression) != nil {
248 desc = desc.replacingOccurrences(of: "$", with: "@0$")
249 }
250 let ns = desc as NSString
251 guard let m = try! NSRegularExpression(pattern: Self.rangePattern).firstMatch(in: desc, range: NSRange(location: 0, length: ns.length)) else {
252 throw UserError(message: "Invalid table range specifier `\(desc)'")
253 }
254 func group(_ i: Int) -> String? { m.range(at: i).location == NSNotFound ? nil : ns.substring(with: m.range(at: i)) }
255 let isRange = group(3) != nil
256 func row(_ r: String?) throws -> Int {
257 guard let r, r.contains(where: { !$0.isWhitespace }) else { return thisline }
258 return try Self.descriptorLine(r, thisline, analysis: analysis)
259 }
260 func column(_ c: String?) -> Int {
261 guard let c else { return col }
262 let body = String(c.dropFirst())
263 let n = Int(body) ?? 0
264 if n == 0 { return col }
265 return n + (body.first == "-" || body.first == "+" ? col : 0)
266 }
267 let r1 = try row(group(1))
268 let r2 = try row(group(4))
269 let c1 = column(group(2))
270 let c2 = column(group(5))
271 return try saveExcursion {
272 if !isRange || (r1 == r2 && c1 == c2) {
273 gotoTableLine(r1, analysis)
274 while !isDataLine() {
275 if forwardLine(1) != 0 { throw Calc.Unsupported("reference outside table") }
276 }
277 return .field(tableField(c1).trimmingCharacters(in: .whitespaces))
278 }
279 var firstRow = min(r1, r2)
280 let lastRow = max(r1, r2)
281 let firstColumn = min(c1, c2)
282 let lastColumn = max(c1, c2)
283 gotoTableLine(firstRow, analysis)
284 while !isDataLine() {
285 if forwardLine(1) != 0 { throw Calc.Unsupported("reference outside table") }
286 firstRow += 1
287 }
288 let startLine = lineStart(point)
289 forwardLine(lastRow - firstRow)
290 while !isDataLine() { forwardLine(-1) }
291 let endLine = lineStart(point)
292 var fields: [String] = []
293 point = startLine
294 while point <= endLine {
295 if !atTableHline {
296 for c in firstColumn...lastColumn { fields.append(tableField(c).trimmingCharacters(in: .whitespaces)) }
297 }
298 if forwardLine(1) != 0 { break }
299 }
300 return .list(fields)
301 }
302 }
303
304 enum TableRangeValue {
305 case field(String)
306 case list([String])
307 }
308
309 static let calcNumber = "^[-+]?([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE][-+]?[0-9]+)?$"
310 /// `org-ts-regexp-both`.
311 static let timestampBoth = "[\\[<]([0-9]{4}-[0-9]{2}-[0-9]{2}(?: [^\\]>\\n]*)?)[\\]>]"
312
313 /// How `org-table-make-reference` turns fields into formula text.
314 struct ReferenceMode {
315 enum Lisp { case none, lisp, literal }
316 var keepEmpty = false
317 var numbers = false
318 var lisp = Lisp.none
319 }
320
321 /// `number-to-string` of `string-to-number`.
322 static func numberText(_ s: String) -> String { Elisp.printed(Elisp.stringToNumber(s)) }
323
324 /// `org-table-make-reference`. For Calc, a field that isn't a number, a date or `nan`
325 /// would be a symbol, which isn't supported.
326 static func makeReference(_ value: TableRangeValue, _ mode: ReferenceMode) throws -> String {
327 func blank(_ s: String) -> Bool { !s.contains { !$0.isWhitespace } }
328 func lisp(_ s: String) -> String {
329 switch mode.lisp {
330 case .literal: s
331 default: mode.numbers ? numberText(s) : Elisp.printed(.string(s), escape: true)
332 }
333 }
334 func calc(_ s: String) throws -> String {
335 let text = mode.numbers ? numberText(s) : s
336 guard text.range(of: calcNumber, options: .regularExpression) != nil || text == "nan"
337 || text.range(of: "^" + timestampBoth + "$", options: .regularExpression) != nil else {
338 throw Calc.Unsupported("non-numeric field \(text)")
339 }
340 return text
341 }
342 let empty = !mode.keepEmpty || mode.numbers ? "0" : "nan"
343 switch value {
344 case .field(let s):
345 if mode.lisp != .none { return lisp(s) }
346 return blank(s) ? (empty == "0" ? "(0)" : "nan") : "(" + (try calc(s)) + ")"
347 case .list(var items):
348 if !mode.keepEmpty { items = items.filter { !blank($0) } }
349 if mode.lisp != .none { return items.map(lisp).joined(separator: " ") }
350 return "[" + (try items.map { blank($0) ? empty : try calc($0) }).joined(separator: ",") + "]"
351 }
352 }
353
354 /// A number written in a formula, small enough to work with.
355 static func integer(_ s: String) throws -> Int {
356 guard let n = Int(s.hasPrefix("+") ? String(s.dropFirst()) : s), (-999_999_999...999_999_999).contains(n) else { throw Calc.Unsupported("number size \(s)") }
357 return n
358 }
359
360 /// `org-table-time-string-to-seconds`.
361 static func timeStringToSeconds(_ s: String) throws -> String {
362 guard !s.isEmpty else { return s }
363 func seconds(_ sign: Substring, _ parts: [Substring]) throws -> String {
364 var total = 0
365 for (i, part) in parts.enumerated() {
366 let (scaled, o1) = (Int(part) ?? Int.max).multipliedReportingOverflow(by: [3600, 60, 1][i])
367 let (sum, o2) = total.addingReportingOverflow(scaled)
368 if o1 || o2 || Int(part) == nil { throw Calc.Unsupported("duration size") }
369 total = sum
370 }
371 return String(sign.isEmpty ? total : -total)
372 }
373 if let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+):([0-9]+)/) { return try seconds(m.1, [m.2, m.3, m.4]) }
374 if s.range(of: timestampBoth, options: .regularExpression) == nil, let m = s.firstMatch(of: /(-?)([0-9]+):([0-9]+)/) {
375 return try seconds(m.1, [m.2, m.3])
376 }
377 return numberText(s)
378 }
379
380 enum DurationFormat { case hms, hhmm, hours }
381
382 /// `org-table-time-seconds-to-string`, with `org-table-duration-custom-format` `hours`
383 /// and zero-padded hours.
384 static func secondsToString(_ seconds: Sexp, _ format: DurationFormat) throws -> String {
385 let value = (try? Elisp.number(seconds))?.double ?? 0
386 let magnitude = abs(value)
387 guard magnitude.isFinite, magnitude < 1e15 else { throw Calc.Unsupported("duration size") }
388 let result: String
389 switch format {
390 case .hours:
391 result = String(format: "%.2f", magnitude / 3600)
392 case .hms, .hhmm:
393 let total = Int(magnitude.rounded(.down))
394 let hms = String(format: "%02d:%02d:%02d", total / 3600, total / 60 % 60, total % 60)
395 result = format == .hhmm ? String(hms.dropLast(3)) : hms
396 }
397 return value < 0 ? "-" + result : result
398 }
399
400 /// `org-table-convert-refs-to-rc` for a remote reference: `B3` is `@3$2`.
401 static func refsToRC(_ s: String) -> String {
402 let regex = try! NSRegularExpression(pattern: "(?<![A-Za-z0-9_$@])([a-zA-Z]{1,2})([0-9]+)(?![A-Za-z0-9_])")
403 var out = s
404 while let m = regex.firstMatch(in: out, range: NSRange(location: 0, length: (out as NSString).length)) {
405 let ns = out as NSString
406 let column = ns.substring(with: m.range(at: 1)).uppercased().unicodeScalars.reduce(0) { $0 * 26 + Int($1.value) - 64 }
407 out = ns.replacingCharacters(in: m.range, with: "@\(ns.substring(with: m.range(at: 2)))$\(column)")
408 }
409 return out
410 }
411
412 /// `org-table-get-constant`: a parameter or named field, a `#+CONSTANTS` value, or the
413 /// entry's property for `PROP_name`.
414 func tableConstant(_ name: String, analysis: TableAnalysis) -> String {
415 if let parameter = analysis.parameters.last(where: { $0.name == name }) { return parameter.value }
416 for m in (settings.setupKeywords + string).matches(of: #/(?mi)^[ \t]*#\+CONSTANTS:(.*)$/#) {
417 for pair in m.1.split(whereSeparator: \.isWhitespace) {
418 let parts = pair.split(separator: "=", maxSplits: 1)
419 if parts.count == 2, parts[0] == name { return String(parts[1]) }
420 }
421 }
422 if name.hasPrefix("PROP_"), let value = inheritedProperty(String(name.dropFirst(5)))?.value { return value }
423 return "#UNDEFINED_NAME"
424 }
425
426 /// `org-table-formula-substitute-names`: column names become `$N`; parameters, named
427 /// fields and constants their values, in parentheses outside Lisp formulas.
428 func substituteNames(_ formula: String, analysis: TableAnalysis) throws -> String {
429 let parenthesize = !formula.hasPrefix("'")
430 let duration = formula.range(of: ";.*[Tt].*$", options: .regularExpression) != nil
431 var new = formula
432 if !analysis.columnNames.isEmpty {
433 let names = analysis.columnNames.map { NSRegularExpression.escapedPattern(for: $0.name) }.joined(separator: "|")
434 let regex = try! NSRegularExpression(pattern: "\\$(\(names))(?![[:alnum:]])")
435 var start = 0
436 while let m = regex.firstMatch(in: new, range: NSRange(location: start, length: (new as NSString).length - start)) {
437 let ns = new as NSString
438 let column = analysis.columnNames.first { $0.name == ns.substring(with: m.range(at: 1)) }!.column
439 new = ns.replacingCharacters(in: m.range, with: "$\(column)")
440 start = m.range.location + 1
441 }
442 }
443 let regex = try! NSRegularExpression(pattern: "\\$([a-zA-Z][_a-zA-Z0-9]*)|(\\bremote\\([^)]*\\))")
444 var start = 0
445 var substitutions = 0
446 while let m = regex.firstMatch(in: new, range: NSRange(location: start, length: (new as NSString).length - start)) {
447 let ns = new as NSString
448 // A constant whose value names itself would go on forever, as it does in Org.
449 substitutions += 1
450 if substitutions > 1000 { throw Calc.Unsupported("constants that refer to themselves") }
451 if m.range(at: 2).location != NSNotFound {
452 start = NSMaxRange(m.range(at: 2))
453 continue
454 }
455 start = m.range.location + 1
456 var value = tableConstant(ns.substring(with: m.range(at: 1)), analysis: analysis)
457 if duration, value.contains(where: { !$0.isWhitespace }) { value = try Self.timeStringToSeconds(value) }
458 new = ns.replacingCharacters(in: m.range, with: parenthesize ? "(" + value + ")" : value)
459 }
460 return new
461 }
462
463 /// `org-table-get-remote-range`: the table after `#+NAME: name` in this file, or the first
464 /// table in the entry with that ID, in this file or another.
465 func remoteRange(_ name: String, _ form: String) throws -> TableRangeValue {
466 let escaped = NSRegularExpression.escapedPattern(for: name)
467 let keyword = try! NSRegularExpression(pattern: "^[ \\t]*#\\+(tbl)?name:[ \\t]*" + escaped + "[ \\t]*$", options: [.anchorsMatchLines, .caseInsensitive])
468 let idLine = try! NSRegularExpression(pattern: "^[ \\t]*:ID:[ \\t]+" + escaped + "[ \\t]*$", options: [.anchorsMatchLines, .caseInsensitive])
469 func first(_ regex: NSRegularExpression, in text: String) -> Int? {
470 regex.firstMatch(in: text, range: NSRange(location: 0, length: (text as NSString).length))?.range.location
471 }
472 // `org-id-find`: the entry's heading, here or in the file holding the ID.
473 func heading(_ text: String, _ offset: Int) -> Int {
474 let before = try! NSRegularExpression(pattern: "^\\*+ ", options: .anchorsMatchLines)
475 return before.matches(in: text, range: NSRange(location: 0, length: offset)).last?.range.location ?? 0
476 }
477 var text = string
478 let location: Int
479 if let found = first(keyword, in: string) {
480 location = found
481 } else if let found = first(idLine, in: string) {
482 location = heading(string, found)
483 } else if let other = RemoteTables.fileWithID?(name), let found = first(idLine, in: other) {
484 text = other
485 location = heading(other, found)
486 } else {
487 throw UserError(message: "Can't find remote table \"\(name)\"")
488 }
489 let ns = text as NSString
490 let after = min(ns.length, location + 1)
491 let next = try! NSRegularExpression(pattern: "^(\\*+ )|^[ \\t]*\\|", options: .anchorsMatchLines)
492 guard let m = next.firstMatch(in: text, range: NSRange(location: after, length: ns.length - after)),
493 m.range(at: 1).location == NSNotFound else {
494 throw UserError(message: "Cannot find a table at NAME or ID \(name)")
495 }
496 let remote = EmacsBuffer(text, point: m.range.location, settings: settings, options: options)
497 let analysis = try remote.tableAnalyze()
498 let resolved = try remote.substituteNames(try Self.handleFirstLast(Self.refsToRC(form), analysis: analysis), analysis: analysis)
499 let range = try! NSRegularExpression(pattern: Self.rangePattern)
500 let rns = resolved as NSString
501 if let r = range.firstMatch(in: resolved, range: NSRange(location: 0, length: rns.length)), r.range.length > 1 {
502 return try remote.tableRange(rns.substring(with: r.range), thisline: 0, col: 1, analysis: analysis)
503 }
504 return .field(resolved)
505 }
506
507 /// `org-table-eval-formula` with the equation given, at point. `substituted` when names
508 /// and constants were already replaced, as `org-table-recalculate` does.
509 func tableEvalFormula(_ formula: String, analysis: TableAnalysis, substituted: Bool = true) throws {
510 var formula = formula
511 var modes = Calc.Modes()
512 var mode = ReferenceMode()
513 var duration: DurationFormat?
514 var literal = false
515 var fmt: String?
516 if let semicolon = formula.lastIndex(of: ";") {
517 var flags = (analysis.parameters.last { $0.name == "%" }?.value ?? "") + formula[formula.index(after: semicolon)...]
518 formula = String(formula[..<semicolon])
519 let numeric = try! NSRegularExpression(pattern: "([pnfse])(-?[0-9]+)")
520 while let m = numeric.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) {
521 let ns = flags as NSString
522 let n = try Self.integer(ns.substring(with: m.range(at: 2)))
523 switch ns.substring(with: m.range(at: 1)) {
524 case "p": modes.precision = n
525 case "n": modes.format = .float(n)
526 case "f": modes.format = .fix(n)
527 case "s": modes.format = .sci(n)
528 default: modes.format = .eng(n)
529 }
530 flags = ns.replacingCharacters(in: m.range, with: "")
531 }
532 let letters = try! NSRegularExpression(pattern: "[tTUNLEDRFSu]")
533 while let m = letters.firstMatch(in: flags, range: NSRange(location: 0, length: (flags as NSString).length)) {
534 let ns = flags as NSString
535 switch ns.substring(with: m.range) {
536 case "t": (duration, mode.numbers) = (.hours, true)
537 case "T": (duration, mode.numbers) = (.hms, true)
538 case "U": (duration, mode.numbers) = (.hhmm, true)
539 case "N": mode.numbers = true
540 case "L": literal = true
541 case "E": mode.keepEmpty = true
542 case "F": modes.preferFractions = true
543 case "D": modes.degrees = true
544 case "R": modes.degrees = false
545 default: throw Calc.Unsupported("format flag \(ns.substring(with: m.range))")
546 }
547 flags = ns.replacingCharacters(in: m.range, with: "")
548 }
549 if flags.contains(where: { !$0.isWhitespace }) { fmt = flags }
550 }
551 if !substituted { formula = try Self.handleFirstLast(try substituteNames(formula, analysis: analysis), analysis: analysis) }
552 var fields = Self.splitFields(substring(lineStart(point), lineEnd(point)).trimmingCharacters(in: .whitespaces), separator: " *\\| *")
553 if duration != nil { fields = try fields.map(Self.timeStringToSeconds) }
554 if mode.numbers { fields = fields.map { $0.contains(where: { !$0.isWhitespace }) ? Self.numberText($0) : $0 } }
555 if formula.count > 2, formula.hasPrefix("'(") { mode.lisp = literal ? .literal : .lisp }
556 func reference(_ value: TableRangeValue) throws -> String {
557 guard duration != nil else { return try Self.makeReference(value, mode) }
558 switch value {
559 case .field(let s): return try Self.makeReference(.field(try Self.timeStringToSeconds(s)), mode)
560 case .list(let items): return try Self.makeReference(.list(try items.map(Self.timeStringToSeconds)), mode)
561 }
562 }
563 let n0 = tableCurrentColumn()
564 let thisline = try saveExcursion { () -> Int in
565 let here = lineStart(point)
566 point = analysis.begin
567 var count = 0
568 while point < here {
569 if forwardLine(1) != 0 { throw Calc.Unsupported("table position") }
570 count += 1
571 }
572 return count
573 }
574 var form = formula
575 form = form.replacingOccurrences(of: "@#", with: String(tableCurrentDataLine()))
576 form = form.replacingOccurrences(of: "$#", with: String(n0))
577 // `org-table-remote-reference-indirection`: `remote($1, …)` names the table in a field.
578 let indirect = try! NSRegularExpression(pattern: "\\bremote\\([ \\t]*([@$][^ \\t,]+)[ \\t]*,[ \\t]*([^\\n)]+)\\)")
579 var start = 0
580 while let m = indirect.firstMatch(in: form, range: NSRange(location: start, length: (form as NSString).length - start)) {
581 let ns = form as NSString
582 var eq = try Self.handleFirstLast(ns.substring(with: m.range(at: 1)), analysis: analysis)
583 if eq.range(of: "^\\$[0-9]+$", options: .regularExpression) != nil { eq = "@0" + eq }
584 guard case .field(let name) = try tableRange(eq, thisline: thisline, col: n0, analysis: analysis) else {
585 throw Calc.Unsupported("remote table name from a range")
586 }
587 form = ns.replacingCharacters(in: m.range(at: 1), with: name)
588 start = m.range(at: 1).location + (name as NSString).length
589 }
590 let remote = try! NSRegularExpression(pattern: "\\bremote\\([ \\t]*([^,)]+)[ \\t]*,[ \\t]*([^\\n)]+)\\)")
591 while let m = remote.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) {
592 let ns = form as NSString
593 let name = ns.substring(with: m.range(at: 1)).trimmingCharacters(in: .whitespaces)
594 let value = try remoteRange(name, ns.substring(with: m.range(at: 2)))
595 form = ns.replacingCharacters(in: m.range, with: try reference(value))
596 }
597 let range = try! NSRegularExpression(pattern: Self.rangePattern)
598 while let m = range.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)), m.range.length > 1 {
599 let ns = form as NSString
600 let replacement = try reference(try tableRange(ns.substring(with: m.range), thisline: thisline, col: n0, analysis: analysis))
601 if replacement.contains(form) { throw UserError(message: "Spreadsheet error: invalid reference \"\(form)\"") }
602 form = ns.replacingCharacters(in: m.range, with: replacement)
603 }
604 let rowRange = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)\\.\\.\\$(([-+])?[0-9]+)")
605 while let m = rowRange.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) {
606 let ns = form as NSString
607 let a = try Self.integer(ns.substring(with: m.range(at: 1))) + (m.range(at: 2).location != NSNotFound ? n0 : 0)
608 let b = try Self.integer(ns.substring(with: m.range(at: 3))) + (m.range(at: 4).location != NSNotFound ? n0 : 0)
609 guard a >= 1, b <= fields.count, a - 1 <= b else { throw Calc.Unsupported("range outside row") }
610 form = ns.replacingCharacters(in: m.range, with: try Self.makeReference(.list(Array(fields[(a - 1)..<b])), mode))
611 }
612 let single = try! NSRegularExpression(pattern: "\\$(([-+])?[0-9]+)")
613 while let m = single.firstMatch(in: form, range: NSRange(location: 0, length: (form as NSString).length)) {
614 let ns = form as NSString
615 let n = try Self.integer(ns.substring(with: m.range(at: 1))) + (m.range(at: 2).location != NSNotFound ? n0 : 0)
616 let index = (n == 0 ? n0 : max(n, 1)) - 1
617 guard index < fields.count else { throw UserError(message: "Invalid field specifier \"\(ns.substring(with: m.range))\"") }
618 let replacement = try Self.makeReference(.field(fields[index]), mode)
619 if replacement.contains(formula) { throw UserError(message: "Invalid field specifier \"\(ns.substring(with: m.range))\"") }
620 form = ns.replacingCharacters(in: m.range, with: replacement)
621 }
622
623 var result: String
624 if mode.lisp != .none {
625 result = try evalLisp(form, duration: duration)
626 } else {
627 form = form.replacingOccurrences(of: "\\[([0-9]{4}-[0-9]{2}-[0-9]{2}(?: [^\\]>\\n]*)?)\\]", with: "<$1>", options: .regularExpression)
628 form = Self.normalizeTimestamps(form)
629 if duration != nil, form.range(of: "^[0-9]+:[0-9]+(:[0-9]+)?$", options: .regularExpression) != nil {
630 result = form
631 } else {
632 do {
633 result = try Calc.evaluate(form, modes: modes, numeric: !mode.keepEmpty && mode.numbers)
634 } catch is Calc.Failure {
635 (result, fmt) = ("#ERROR", nil)
636 }
637 }
638 if let duration, !result.isEmpty, result != "#ERROR" {
639 let seconds = result.range(of: "^[0-9]+:[0-9]+(:[0-9]+)?$", options: .regularExpression) != nil ? try Self.timeStringToSeconds(result) : result
640 result = try Self.secondsToString(Elisp.stringToNumber(seconds), duration)
641 }
642 }
643 if let fmt {
644 do {
645 result = try Elisp.format([.string(fmt), Elisp.stringToNumber(result)])
646 } catch let error as Elisp.Signal {
647 throw UserError(message: error.message)
648 }
649 } else {
650 result = result.replacingOccurrences(of: "<([0-9]{4}-[0-9]{2}-[0-9]{2}(?: [^\\]>\\n]*)?)>", with: "[$1]", options: .regularExpression)
651 }
652 tableSetField(result)
653 }
654
655 /// `(eval (eval (read form)))` for a Lisp formula; an error is `#ERROR`.
656 func evalLisp(_ form: String, duration: DurationFormat?) throws -> String {
657 let lisp = Elisp()
658 Self.defineLookups(lisp)
659 let value: Sexp
660 do {
661 value = try lisp.eval(try lisp.eval(try LispReader.readFirst(form).sexp))
662 } catch let unsupported as Elisp.Unsupported {
663 throw Calc.Unsupported("Lisp \(unsupported.what)")
664 } catch {
665 return "#ERROR"
666 }
667 var result = Elisp.printed(value)
668 if let duration { result = try Self.secondsToString(Elisp.stringToNumber(result), duration) }
669 return result
670 }
671
672 /// `org-lookup-first`, `org-lookup-last` and `org-lookup-all`.
673 static func defineLookups(_ lisp: Elisp) {
674 func matches(_ lisp: Elisp, _ args: [Sexp]) throws -> [Sexp] {
675 try Elisp.arity(args, 3...4, "org-lookup")
676 let keys = try Elisp.elements(args[1])
677 let values = Elisp.isNil(args[2]) ? keys : try Elisp.elements(args[2])
678 let predicate = args.count > 3 && !Elisp.isNil(args[3]) ? args[3] : .symbol("equal")
679 var found: [Sexp] = []
680 for (i, key) in keys.enumerated() where !Elisp.isNil(try lisp.call(predicate, [args[0], key])) {
681 found.append(i < values.count ? values[i] : .nil)
682 }
683 return found
684 }
685 lisp.define("org-lookup-first") { lisp, args in try matches(lisp, args).first ?? .nil }
686 lisp.define("org-lookup-last") { lisp, args in try matches(lisp, args).last ?? .nil }
687 lisp.define("org-lookup-all") { lisp, args in
688 let found = try matches(lisp, args)
689 return found.isEmpty ? .nil : .list(found)
690 }
691 }
692
693 /// Timestamps in a formula as Calc reads them: `<YYYY-MM-DD Www>`, with ` HH:MM` when
694 /// the stamp has a time, as org formats them for Calc.
695 static func normalizeTimestamps(_ form: String) -> String {
696 let regex = try! NSRegularExpression(pattern: "<([0-9]{4}-[0-9]{2}-[0-9]{2}(?: [^\\]>\\n]*)?)>")
697 var out = form
698 var start = 0
699 while let m = regex.firstMatch(in: out, range: NSRange(location: start, length: (out as NSString).length - start)) {
700 let ns = out as NSString
701 let stamp = ns.substring(with: m.range(at: 1))
702 let parts = stamp.firstMatch(of: /^([0-9]{4})-([0-9]{2})-([0-9]{2})/)!
703 let day = Days.absolute(year: Int(parts.1)!, month: Int(parts.2)!, day: Int(parts.3)!)
704 let date = Days.date(day)
705 var text = String(format: "<%04d-%02d-%02d %@", date.year, date.month, date.day, Calc.weekdays[Days.weekday(day)])
706 if let time = stamp.firstMatch(of: /([0-9]{1,2}):([0-9]{2})/) {
707 text += String(format: " %02d:%02d", Int(time.1)!, Int(time.2)!)
708 }
709 text += ">"
710 out = ns.replacingCharacters(in: m.range, with: text)
711 start = m.range.location + (text as NSString).length
712 }
713 return out
714 }
715
716 /// `string-to-number`.
717 static func stringToNumber(_ s: String) -> Double {
718 guard let r = s.range(of: "^[-+]?([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE][-+]?[0-9]+)?", options: .regularExpression) else { return 0 }
719 return Double(s[r]) ?? 0
720 }
721
722 /// `org-table-justify-field-maybe` while the table needs realigning: the field becomes
723 /// ` NEW |`.
724 func tableSetField(_ value: String) {
725 let pos = point
726 guard tableCurrentColumn() > 0 else { return }
727 skipBackwardNot([124])
728 let start = point
729 var end = point
730 while let c = character(at: end), c != 124, c != 10 { end += 1 }
731 let closed = character(at: end) == 124
732 replaceRange(start, closed ? end + 1 : end, with: " \(value) |")
733 point = min(pos, length)
734 }
735
736 func replaceRange(_ start: Int, _ end: Int, with text: String) {
737 let saved = point
738 deleteRegion(start, end)
739 point = start
740 insert(text)
741 point = saved
742 }
743
744 /// `org-table-recalculate`: `all` for every row, otherwise the line at point; `tblfm`
745 /// replaces the stored formulas, as C-c C-c on a `#+TBLFM` line does.
746 func tableRecalculate(all: Bool, tblfm: String? = nil) throws {
747 guard atTable else { throw UserError(message: "Not at a table") }
748 let analysis = try tableAnalyze()
749 let end = tableEnd()
750 guard let line = tblfm ?? firstTblfm(after: end) else { return }
751 let stored = try Self.storedFormulas(line).sorted { $0.lhs < $1.lhs }
752 guard !stored.isEmpty else { return }
753 var columnEquations: [(Int, String)] = []
754 var fieldEquations: [(String, String)] = []
755 for (oldLHS, rawRHS) in stored {
756 let rhs = try substituteNames(try Self.handleFirstLast(rawRHS, analysis: analysis), analysis: analysis)
757 if oldLHS.range(of: "^@-?I+", options: .regularExpression) != nil { throw UserError(message: "Can't assign to hline relative reference") }
758 let lhs = try Self.handleFirstLast(oldLHS, analysis: analysis)
759 if lhs.range(of: "^\\$[0-9]+$", options: .regularExpression) != nil {
760 columnEquations.append((try Self.integer(String(lhs.dropFirst())), rhs))
761 } else {
762 fieldEquations.append((lhs, rhs))
763 }
764 }
765 // `org-table-expand-lhs-ranges`.
766 var fields: [(row: Int, column: Int, rhs: String)] = []
767 for (lhs, rhs) in fieldEquations {
768 if let m = lhs.firstMatch(of: /^@([0-9]+)\$([0-9]+)$/) {
769 fields.append((try Self.integer(String(m.1)), try Self.integer(String(m.2)), rhs))
770 } else if lhs.range(of: "^[a-zA-Z][_a-zA-Z0-9]*$", options: .regularExpression) != nil {
771 guard let location = analysis.namedFields.last(where: { $0.name == lhs }), let row = Self.lineToDline(location.line, analysis) else {
772 throw UserError(message: "Unknown field: \(lhs)")
773 }
774 fields.append((row, location.column, rhs))
775 } else if lhs.range(of: "^@[-+0-9]+\\$-?[0-9]+$", options: .regularExpression) != nil {
776 // `org-table-goto-field` knows only absolute fields and names.
777 throw UserError(message: "Unknown field: \(lhs)")
778 } else if let m = lhs.firstMatch(of: /^@([0-9]+)$/) {
779 let row = try Self.integer(String(m.1))
780 for c in 1...analysis.ncol { fields.append((row, c, rhs)) }
781 } else {
782 let corners = try rangeCorners(lhs, analysis: analysis)
783 guard let r1 = Self.lineToDline(corners.r1, analysis), let r2 = Self.lineToDline(corners.r2, analysis, above: true), r1 <= r2 else {
784 throw Calc.Unsupported("formula range")
785 }
786 for r in r1...r2 { for c in corners.c1...corners.c2 { fields.append((r, c, rhs)) } }
787 }
788 }
789 let savedLine = lineStart(point)
790 let savedColumn = tableCurrentColumn()
791 let lineMarker = marker(savedLine)
792 defer { release(lineMarker) }
793
794 let endMarker = marker(end)
795 defer { release(endMarker) }
796 var beg: Int
797 var lineRegexp = "^[ \\t]*\\|[^-]"
798 if all, analysis.marked {
799 beg = analysis.begin
800 lineRegexp = "^[ \\t]*\\| *[#*] *(\\||$)"
801 } else if all {
802 beg = analysis.begin
803 point = beg
804 if searchForward("^[ \\t]*\\|[^-]", bound: end, caseInsensitive: false) != nil,
805 searchForward("^[ \\t]*\\|-", bound: end, caseInsensitive: false) != nil,
806 let body = searchForward("^[ \\t]*\\|[^-]", bound: end, caseInsensitive: false) {
807 beg = body[0]!.lowerBound
808 }
809 } else {
810 beg = lineStart(point)
811 endMarker.position = min(length, lineEnd(point) + 1)
812 }
813
814 let currentLine = savedLine - 0
815 var seen: Set<String> = []
816 var untouchable: Set<String> = []
817 for field in fields {
818 let reference = "@\(field.row)$\(field.column)"
819 guard seen.insert(reference).inserted else { throw UserError(message: "Several field/range formulas try to set \(reference)") }
820 guard field.row < analysis.dlines.count else { throw UserError(message: "Invalid row number in \(reference)") }
821 let lineStartOffset = saveExcursion { () -> Int in
822 gotoTableLine(analysis.dlines[field.row], analysis)
823 return lineStart(point)
824 }
825 if all || lineStartOffset == currentLine { untouchable.insert(reference) }
826 }
827
828 point = beg
829 let rowMarker = marker(beg)
830 defer { release(rowMarker) }
831 while let match = searchForward(lineRegexp, bound: endMarker.position, caseInsensitive: false) {
832 _ = match
833 let rowStart = lineStart(point)
834 rowMarker.position = rowStart
835 let first = saveExcursion { () -> String in
836 point = rowStart
837 return tableField(1)
838 }
839 if first.range(of: "^ *[_^!$/] *$", options: .regularExpression) == nil {
840 let dline = saveExcursion { () -> Int in
841 point = rowStart
842 return tableCurrentDataLine()
843 }
844 for (column, rhs) in columnEquations {
845 point = rowMarker.position
846 tableGotoColumn(column, force: true)
847 if untouchable.contains("@\(dline)$\(column)") { continue }
848 try tableEvalFormula(rhs, analysis: analysis)
849 }
850 }
851 point = rowMarker.position
852 if forwardLine(1) != 0 || point >= endMarker.position { break }
853 }
854 for field in fields {
855 gotoTableLine(analysis.dlines[field.row], analysis)
856 let count = substring(lineStart(point), lineEnd(point)).filter { $0 == "|" }.count - 1
857 if field.column > count { throw UserError(message: "Missing columns in the table. Aborting") }
858 tableGotoColumn(field.column)
859 try tableEvalFormula(field.rhs, analysis: analysis)
860 }
861 tableAlign()
862 point = lineMarker.position
863 tableGotoColumn(savedColumn)
864 }
865
866 /// `org-table-get-range` with `corners-only`.
867 func rangeCorners(_ desc: String, analysis: TableAnalysis) throws -> (r1: Int, c1: Int, r2: Int, c2: Int) {
868 let ns = desc as NSString
869 guard let m = try! NSRegularExpression(pattern: Self.rangePattern).firstMatch(in: desc, range: NSRange(location: 0, length: ns.length)) else {
870 throw UserError(message: "Invalid table range specifier `\(desc)'")
871 }
872 func group(_ i: Int) -> String? { m.range(at: i).location == NSNotFound ? nil : ns.substring(with: m.range(at: i)) }
873 let thisline = 0
874 func row(_ r: String?) throws -> Int {
875 guard let r, !r.isEmpty else { return thisline }
876 return try Self.descriptorLine(r, thisline, analysis: analysis)
877 }
878 func column(_ c: String?) -> Int {
879 guard let c, let n = Int(c.dropFirst()), n != 0 else { return 1 }
880 return n + (c.dropFirst().first == "-" || c.dropFirst().first == "+" ? 1 : 0)
881 }
882 let r1 = try row(group(1))
883 let r2 = try row(group(4))
884 let c1 = column(group(2))
885 let c2 = column(group(5))
886 return (min(r1, r2), min(c1, c2), max(r1, r2), max(c1, c2))
887 }
888
889 /// `org-table-line-to-dline`.
890 static func lineToDline(_ line: Int, _ analysis: TableAnalysis, above: Bool = false) -> Int? {
891 let dlines = analysis.dlines
892 var low = 1
893 var high = dlines.count - 1
894 if dlines[low] > line || dlines[high] < line { return nil }
895 if line == dlines[high] { return high }
896 while high - low > 1 {
897 let mean = (high + low) / 2
898 if dlines[mean] == line { return mean }
899 if dlines[mean] > line { high = mean } else { low = mean }
900 }
901 if line == dlines[high] { return high }
902 if line == dlines[low] { return low }
903 return above ? low : high
904 }
905}
906
907/// C-c * and C-u C-c *: `org-table-recalculate`, and C-c C-c on a `#+TBLFM` line. Tables the
908/// native evaluator can't do fail with `TableRecalculate.emacsMessage`, and the app
909/// recalculates them in Emacs.
910public struct TableRecalculate: OrgCommand {
911 public let all: Bool
912 public init(all: Bool) { self.all = all }
913 public var id: String { all ? "org.table.recalc-all" : "org.table.recalc" }
914 public var title: String { all ? "Recalculate Table" : "Recalculate Table Row" }
915
916 public static let emacsMessage = "needs Emacs: "
917
918 public func applies(in context: EditContext) -> Bool {
919 KeyContext.holds("table", in: context) || Self.tblfmLine(in: context) != nil
920 }
921
922 static func tblfmLine(in context: EditContext) -> String? {
923 let buffer = EmacsBuffer(context.text, point: context.caret)
924 let line = buffer.substring(buffer.lineStart(buffer.point), buffer.lineEnd(buffer.point))
925 guard line.range(of: EmacsBuffer.tblfmPattern, options: [.regularExpression, .caseInsensitive]) != nil else { return nil }
926 return line
927 }
928
929 public func run(in context: EditContext) -> CommandStep {
930 let step = commitBuffer(context) { buffer in
931 do {
932 if let line = Self.tblfmLine(in: context) {
933 // `org-table-calc-current-TBLFM`: that line's formulas, every row, with
934 // point at the end of the table.
935 let formulas = String(line[line.index(after: line.firstIndex(of: ":")!)...]).trimmingCharacters(in: .init(charactersIn: " "))
936 let saved = buffer.marker(buffer.point)
937 defer { buffer.release(saved) }
938 guard let tableEnd = buffer.saveExcursion({ () -> Int? in
939 while buffer.forwardLine(-1) == 0 {
940 if buffer.lookingAtLine("^[ \\t]*\\|") { return buffer.lineEnd(buffer.point) }
941 }
942 return nil
943 }) else { throw EmacsBuffer.UserError(message: "Not at a table") }
944 buffer.point = tableEnd
945 try buffer.tableRecalculate(all: true, tblfm: formulas)
946 buffer.point = saved.position
947 } else {
948 try buffer.tableRecalculate(all: all)
949 }
950 } catch let error as Calc.Unsupported {
951 throw EmacsBuffer.UserError(message: Self.emacsMessage + error.reason)
952 }
953 }
954 let form = Self.tblfmLine(in: context) != nil ? "(org-table-calc-current-TBLFM)" : all ? "(org-table-recalculate t)" : "(org-table-recalculate nil)"
955 return Self.inEmacs(step, form: form, in: context)
956 }
957
958 /// A step that failed with `emacsMessage` as a request to run `form` on the table in
959 /// Emacs, whose text then replaces the table here; other steps unchanged.
960 static func inEmacs(_ step: CommandStep, form: String, in context: EditContext) -> CommandStep {
961 guard case .failed(let message) = step, message.hasPrefix(emacsMessage) else { return step }
962 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options)
963 let onTblfm = tblfmLine(in: context) != nil
964 if onTblfm {
965 while buffer.forwardLine(-1) == 0, !buffer.lookingAtLine("^[ \\t]*\\|") {}
966 }
967 let table = buffer.tableBegin()..<buffer.tableEnd()
968 let tblfm = onTblfm ? tblfmLine(in: context) ?? "" : buffer.firstTblfm(after: table.upperBound) ?? ""
969 return .external(.emacsTable(
970 table: table, point: context.caret, form: form,
971 reason: String(message.dropFirst(emacsMessage.count)), lisp: tblfm.contains("'(")
972 ))
973 }
974}
975
976// Formulas typed in a field (`org-table-maybe-eval-formula`) and C-c = (`org-table-eval-formula`).
977extension EmacsBuffer {
978 /// `org-table-formula-make-cmp-string`.
979 static func formulaSortKey(_ lhs: String, analysis: TableAnalysis) -> String? {
980 var a = lhs
981 if let arrow = a.firstMatch(of: /^\$([<>])/) {
982 a = (try? handleFirstLast(a, analysis: analysis)) ?? a
983 a = "$\(10000 + (arrow.1 == "<" ? -1000 : 0) + Int(stringToNumber(String(a.dropFirst()))))"
984 }
985 guard let m = a.firstMatch(of: /^(@([0-9]+))?(\$?([0-9]+))?(\$?[a-zA-Z0-9]+)?/) else { return nil }
986 var key = ""
987 if let row = m.2 { key += String(format: "@%05d", Int(row) ?? 0) }
988 if let column = m.4 { key += String(format: "$%05d", Int(column) ?? 0) }
989 if let name = m.5 { key += "@@" + name }
990 return key
991 }
992
993 /// `sort` with `org-table-formula-less-p`.
994 static func sortFormulas(_ formulas: [(lhs: String, rhs: String)], analysis: TableAnalysis) -> [(lhs: String, rhs: String)] {
995 var sorted: [(formula: (lhs: String, rhs: String), key: String?)] = []
996 // Stable insertion, as `sort` with a partial predicate.
997 for formula in formulas {
998 let key = formulaSortKey(formula.lhs, analysis: analysis)
999 var index = sorted.count
1000 while index > 0, let a = key, let b = sorted[index - 1].key, a < b { index -= 1 }
1001 sorted.insert((formula, key), at: index)
1002 }
1003 return sorted.map(\.formula)
1004 }
1005
1006 /// `org-table-store-formulas`, sorted by `org-table-formula-less-p`.
1007 func tableStoreFormulas(_ formulas: [(lhs: String, rhs: String)], analysis: TableAnalysis) {
1008 let sorted = Self.sortFormulas(formulas, analysis: analysis)
1009 saveExcursion {
1010 point = tableEnd()
1011 if let match = looking("([ \\t]*\\n)*[ \\t]*(#\\+TBLFM:)(.*\\n?)", caseInsensitive: true, setsMatch: false) {
1012 deleteRegion(match[3]!.lowerBound, match[0]!.upperBound)
1013 point = match[3]!.lowerBound
1014 } else {
1015 insert("#+TBLFM:")
1016 }
1017 insert(" " + sorted.map { $0.lhs + "=" + $0.rhs }.joined(separator: "::") + "\n")
1018 }
1019 }
1020
1021 /// `org-table-get-formula`: stores EQUATION for the current column, or with `named` the
1022 /// current field, and returns it; nil when a blank EQUATION removed the formula. `read`
1023 /// when EQUATION is the prompt's answer, which the stored formula never replaces.
1024 func tableGetFormula(_ equation: String, named: Bool, read: Bool = false, analysis: TableAnalysis) throws -> String? {
1025 var stored = try Self.storedFormulas(firstTblfm(after: tableEnd()) ?? "")
1026 let ref = "@\(tableCurrentDataLine())$\(tableCurrentColumn())"
1027 let scol = named ? ref : "$\(tableCurrentColumn())"
1028 let old = stored.first { $0.lhs == scol }?.rhs
1029 var eq = !read && old != nil && equation.range(of: "^ *=? *$", options: .regularExpression) != nil ? old! : equation
1030 guard eq.contains(where: { !$0.isWhitespace }) else {
1031 stored.removeAll { $0.lhs == scol }
1032 tableStoreFormulas(stored, analysis: analysis)
1033 // Org's user error, which keeps the stored formulas and stops the command.
1034 message = "Formula removed"
1035 return nil
1036 }
1037 eq = eq.replacingOccurrences(of: "^ *=?", with: "", options: .regularExpression)
1038 eq = eq.replacingOccurrences(of: " *$", with: "", options: .regularExpression)
1039 var mustSave = false
1040 if !named, let index = stored.firstIndex(where: { $0.lhs == ref }) {
1041 stored.remove(at: index)
1042 mustSave = true
1043 }
1044 if let index = stored.firstIndex(where: { $0.lhs == scol }) {
1045 stored[index].rhs = eq
1046 } else {
1047 stored.insert((scol, eq), at: 0)
1048 }
1049 if mustSave || old != eq { tableStoreFormulas(stored, analysis: analysis) }
1050 return eq
1051 }
1052
1053 /// `org-table-eval-formula` for the current field with EQUATION, which is stored first.
1054 /// A formula only Emacs can evaluate stays stored, the field unchanged.
1055 func tableEvalFormula(equation: String, named: Bool, read: Bool = false) throws {
1056 try checkInsideDataField()
1057 let analysis = try tableAnalyze()
1058 guard let formula = try tableGetFormula(equation, named: named, read: read, analysis: analysis) else { return }
1059 do {
1060 try tableEvalFormula(formula, analysis: analysis, substituted: false)
1061 } catch let error as Calc.Unsupported {
1062 message = "The formula was stored; recalculating it needs Emacs: " + error.reason
1063 }
1064 tableAlign()
1065 }
1066
1067 /// `org-table-maybe-eval-formula`.
1068 func tableMaybeEvalFormula() throws {
1069 skipBackwardNot([124, 10])
1070 guard !bolp, looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) == nil else { return }
1071 let start = point
1072 var end = point
1073 while let c = character(at: end), c != 124, c != 10, c != 13 { end += 1 }
1074 point = min(lineEnd(point), start + 1)
1075 let field = substring(start, end).trimmingCharacters(in: CharacterSet(charactersIn: " \t\n\r"))
1076 guard let m = field.firstMatch(of: /^:?=(.*[^=])$/) else { return }
1077 try tableEvalFormula(equation: String(m.1), named: field.hasPrefix(":"))
1078 }
1079}
1080
1081/// C-c = and C-u C-c =: `org-table-eval-formula`, asking for the column's or the field's
1082/// formula, the stored one offered.
1083public struct TableEvalFormula: OrgCommand {
1084 let named: Bool
1085 public init(named: Bool) { self.named = named }
1086 public var id: String { named ? "org.table.field-formula" : "org.table.column-formula" }
1087 public var title: String { named ? "Set Field Formula" : "Set Column Formula" }
1088 public func applies(in context: EditContext) -> Bool { KeyContext.holds("table", in: context) }
1089 public func run(in context: EditContext) -> CommandStep {
1090 guard let answer = context.answers["formula"] else {
1091 let buffer = EmacsBuffer(context.text, point: context.caret, settings: context.tree.settings, options: context.options)
1092 guard buffer.atTable else { return .failed("Not at a table") }
1093 let column = buffer.tableCurrentColumn()
1094 let scol = named ? "@\(buffer.tableCurrentDataLine())$\(column)" : "$\(column)"
1095 let stored = (try? EmacsBuffer.storedFormulas(buffer.firstTblfm(after: buffer.tableEnd()) ?? ""))?.first { $0.lhs == scol }?.rhs
1096 return .prompt(Prompt(key: "formula", message: "\(named ? "Field" : "Column") formula \(scol)=", initial: stored ?? ""))
1097 }
1098 return commitBuffer(context) { try $0.tableEvalFormula(equation: answer, named: named, read: true) }
1099 }
1100}
1101
1102// The formula editor (`org-table-edit-formulas`, C-c ' in a table), in the block editor.
1103extension BlockEdit {
1104 static let formulasKind = "formulas"
1105
1106 /// The formulas of the table at `offset`, or of the `#+TBLFM` line there, one per line as
1107 /// org's editor lists them. The range runs from the table (or that line) to the formulas.
1108 public static func formulas(at offset: Int, text: String) -> BlockEdit? {
1109 let buffer = EmacsBuffer(text, point: offset)
1110 let line = buffer.substring(buffer.lineStart(offset), buffer.lineEnd(offset))
1111 let atTblfm = line.range(of: EmacsBuffer.tblfmPattern, options: [.regularExpression, .caseInsensitive]) != nil
1112 guard atTblfm || buffer.atTable else { return nil }
1113 let start: Int
1114 let end: Int
1115 let formulas: [(lhs: String, rhs: String)]
1116 if atTblfm {
1117 start = buffer.lineStart(offset)
1118 end = min(buffer.length, buffer.lineEnd(offset) + 1)
1119 let tblfm = String(line[line.range(of: ":")!.upperBound...]).trimmingCharacters(in: .init(charactersIn: " "))
1120 formulas = (try? EmacsBuffer.storedFormulas(tblfm)) ?? []
1121 guard buffer.searchBackward("^[ \\t]*\\|", caseInsensitive: false) != nil else { return nil }
1122 } else {
1123 start = buffer.tableBegin()
1124 let tableEnd = buffer.tableEnd()
1125 end = buffer.saveExcursion {
1126 buffer.point = tableEnd
1127 return buffer.looking("([ \\t]*\\n)*[ \\t]*#\\+TBLFM:.*\\n?", caseInsensitive: true, setsMatch: false)?[0]?.upperBound ?? tableEnd
1128 }
1129 formulas = (try? EmacsBuffer.storedFormulas(buffer.firstTblfm(after: tableEnd) ?? "")) ?? []
1130 }
1131 guard let analysis = try? buffer.tableAnalyze() else { return nil }
1132 var content = ""
1133 var titles: [(type: String, title: String)] = [("column", "# Column Formulas\n"), ("field", "# Field and Range Formulas\n"), ("named", "# Named Field Formulas\n")]
1134 for (lhs, rhs) in EmacsBuffer.sortFormulas(formulas, analysis: analysis) {
1135 let type = lhs.range(of: "^\\$([0-9]+|[<>]+)$", options: .regularExpression) != nil ? "column" : lhs.hasPrefix("@") ? "field" : "named"
1136 if let index = titles.firstIndex(where: { $0.type == type }) {
1137 if !content.isEmpty { content += "\n" }
1138 content += titles[index].title
1139 titles.remove(at: index)
1140 }
1141 content += (lhs.hasPrefix("@") || lhs.hasPrefix("$") ? "" : "$") + lhs + " = " + rhs + "\n"
1142 }
1143 let ns = text as NSString
1144 return BlockEdit(range: start..<end, original: ns.substring(with: NSRange(location: start, length: end - start)), content: content, language: nil, kind: formulasKind)
1145 }
1146
1147 /// `org-table-fedit-finish`'s reading of the editor's text.
1148 static func parseFormulas(_ content: String) throws -> [(lhs: String, rhs: String)] {
1149 let regex = try! NSRegularExpression(pattern: "^(@[-+I<>0-9.$@]+|@?[0-9]+|\\$([a-zA-Z0-9]+|[<>]+)) *= *(.*(\\n[ \\t]+.*$)*)", options: .anchorsMatchLines)
1150 let ns = content as NSString
1151 var result: [(lhs: String, rhs: String)] = []
1152 for m in regex.matches(in: content, range: NSRange(location: 0, length: ns.length)) {
1153 let lhs = ns.substring(with: m.range(at: 1))
1154 var rhs = ns.substring(with: m.range(at: 3)).trimmingCharacters(in: CharacterSet(charactersIn: " \t\n\r"))
1155 guard !rhs.isEmpty else { continue }
1156 rhs = rhs.replacingOccurrences(of: "[ \\t]*\\n[ \\t]*", with: " ", options: .regularExpression)
1157 if result.contains(where: { $0.lhs == lhs }) { throw EmacsBuffer.UserError(message: "Double formulas for \(lhs)") }
1158 result.insert((lhs, rhs), at: 0)
1159 }
1160 return result
1161 }
1162
1163 /// Installs edited formulas, as `org-table-fedit-finish` without a prefix argument.
1164 func installFormulas(_ content: String, in context: EditContext) -> CommandStep {
1165 let step = commitBuffer(context) { buffer in
1166 let formulas = try Self.parseFormulas(content)
1167 buffer.point = range.lowerBound
1168 let analysis = try buffer.saveExcursion { () throws -> EmacsBuffer.TableAnalysis in
1169 if !buffer.atTable { _ = buffer.searchBackward("^[ \\t]*\\|", caseInsensitive: false) }
1170 return try buffer.tableAnalyze()
1171 }
1172 buffer.tableStoreFormulas(formulas, analysis: analysis)
1173 }
1174 guard case .commit(let result) = step else { return step }
1175 return .commit(EditResult(baseRevision: result.baseRevision, edits: result.edits, selection: result.selection,
1176 effects: result.effects + [.message("New formulas installed - Recalculate Table (C-c C-c on the #+TBLFM line) applies them.")]))
1177 }
1178}
1179
1180// `org-table-edit-field` (C-c `): a field's text edited apart, then put back.
1181extension BlockEdit {
1182 static let fieldKind = "field"
1183
1184 /// The data field at `offset`, trimmed. The range is its line, to check it's unchanged.
1185 public static func field(at offset: Int, text: String) -> BlockEdit? {
1186 let buffer = EmacsBuffer(text, point: offset)
1187 guard buffer.insideDataField else { return nil }
1188 let coordinate = "@\(buffer.tableCurrentDataLine())$\(buffer.tableCurrentColumn())"
1189 let field = buffer.saveExcursion { () -> String in
1190 buffer.skipBackwardNot([124, 10])
1191 return buffer.substring(buffer.point, buffer.looking("[^|\\r\\n]*", caseInsensitive: false, setsMatch: false)![0]!.upperBound)
1192 }
1193 let start = buffer.lineStart(offset)
1194 let end = buffer.lineEnd(offset)
1195 return BlockEdit(range: start..<end, original: buffer.substring(start, end), content: field.trimmingCharacters(in: .whitespacesAndNewlines),
1196 language: nil, kind: fieldKind, point: offset, coordinate: coordinate)
1197 }
1198
1199 /// `org-table-finish-edit-field`: comment lines go, line breaks become spaces, and the
1200 /// table is aligned.
1201 func installField(_ content: String, in context: EditContext) -> CommandStep {
1202 var text = content.replacingOccurrences(of: "(?m)^#.*\n?", with: "", options: .regularExpression)
1203 text = text.replacingOccurrences(of: "[ \t]*\n[ \t\n]*", with: " ", options: .regularExpression)
1204 text = text.trimmingCharacters(in: .whitespacesAndNewlines)
1205 return commitBuffer(context) { buffer in
1206 buffer.point = point ?? range.lowerBound
1207 guard buffer.insideDataField else { throw EmacsBuffer.UserError(message: "Not in table data field") }
1208 buffer.skipBackwardNot([124, 10])
1209 let end = buffer.looking("[^|\\r\\n]*", caseInsensitive: false, setsMatch: false)![0]!.upperBound
1210 buffer.replaceRange(buffer.point, end, with: text.isEmpty ? " " : text)
1211 buffer.point = min(buffer.lineEnd(buffer.point), buffer.point + 1)
1212 buffer.tableAlign()
1213 }
1214 }
1215}
1216
1217extension EmacsBuffer {
1218 /// `org-table-check-inside-data-field` with NOERROR.
1219 var insideDataField: Bool {
1220 atTable && !atTableHline
1221 && !saveExcursion { () -> Bool in
1222 while let c = character(at: point - 1), c == 32 || c == 9 { point -= 1 }
1223 return bolp
1224 }
1225 && looking("[ \\t]*$", caseInsensitive: false, setsMatch: false) == nil
1226 }
1227}
1228
1229/// C-c `: `org-table-edit-field`, in the block editor.
1230public struct TableEditField: OrgCommand {
1231 public init() {}
1232 public var id: String { "org.table.edit-field" }
1233 public var title: String { "Edit Table Field" }
1234 public func applies(in context: EditContext) -> Bool { KeyContext.holds("table", in: context) }
1235 public func run(in context: EditContext) -> CommandStep {
1236 guard let edit = BlockEdit.field(at: context.caret, text: context.text) else {
1237 return .failed(EmacsBuffer(context.text, point: context.caret).atTable ? "Not in table data field" : "Not at a table")
1238 }
1239 return .external(.editBlock(edit))
1240 }
1241}