krz/orgstar

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

Sources/OrgCore/Compute/TableFormulas.swift

1241 lines · 66563 bytes

75 symbols in this file
   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}