krz/orgstar

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

Commit dc34b95792

dc34b95792c2b18730ea724b11c985ff97bea744

parent: 9879b1b41a

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-07 04:05 UTC

Show diary sexps in the agenda

%%( lines, <%%(...)> timestamps and diary sexps in SCHEDULED and
DEADLINE are evaluated for each agenda day, as org-agenda-get-sexps,
org-agenda-get-timestamps and org-time-string-to-absolute do.

A small Emacs Lisp evaluator (special forms, numbers, lists, strings,
format) runs them, with the calendar functions, diary-date,
diary-block, diary-float, diary-anniversary, diary-cyclic,
diary-remind, and org-anniversary, org-cyclic, org-block, org-date
and org-class. A sexp using anything else doesn't apply.

The parser reads <%%(...)> as a timestamp. Sexp lines before the
first heading are skipped; Org's agenda signals an error on them.

Layout: unified · split

Sources/OrgCore/Agenda/Agenda.swift +38 −7
@@ -8,6 +8,7 @@ public struct AgendaItem: Sendable, Equatable {
8 case pastScheduled = "past-scheduled" 8 case pastScheduled = "past-scheduled"
9 case timestamp 9 case timestamp
10 case block 10 case block
11 case sexp
11 case timeGrid 12 case timeGrid
12 case currentTime 13 case currentTime
13 case todo 14 case todo
@@ -108,7 +109,16 @@ public enum Agenda {
108 static func entries(_ source: AgendaSource, day: Int, today: Int, options: AgendaOptions) -> [AgendaItem] { 109 static func entries(_ source: AgendaSource, day: Int, today: Int, options: AgendaOptions) -> [AgendaItem] {
109 let deadlines = self.deadlines(source, day: day, today: today, options: options) 110 let deadlines = self.deadlines(source, day: day, today: today, options: options)
110 return (options.logMode ? progress(source, day: day, options: options) : []) + deadlines + scheduled(source, day: day, today: today, options: options) + blocks(source, day: day, options: options) 111 return (options.logMode ? progress(source, day: day, options: options) : []) + deadlines + scheduled(source, day: day, today: today, options: options) + blocks(source, day: day, options: options)
111 + timestamps(source, day: day, today: today, options: options) 112 + timestamps(source, day: day, today: today, options: options) + sexps(source, day: day, options: options)
113 }
114
115 /// `org-time-string-to-absolute` for a planning timestamp's text: a diary sexp counts on
116 /// `current` when it applies then, and otherwise the entry is skipped.
117 static func planningDay(_ s: String, current: Int) -> (day: Int, sexp: Bool)? {
118 guard s.hasPrefix("%%(") else { return Days.absolute(of: s).map { ($0, false) } }
119 guard let close = s.lastIndex(of: ")"), let (sexp, _) = try? LispReader.readFirst(String(s[s.index(s.startIndex, offsetBy: 2)...close])),
120 DiarySexp.matches(sexp, day: current) else { return nil }
121 return (current, true)
112 } 122 }
113 123
114 // MARK: - Collectors 124 // MARK: - Collectors
@@ -117,8 +127,8 @@ public enum Agenda {
117 let isToday = current == today 127 let isToday = current == today
118 var items: [AgendaItem] = [] 128 var items: [AgendaItem] = []
119 for heading in source.headings { 129 for heading in source.headings {
120 guard !heading.skipped, let (s, offset) = heading.deadline, let deadline = Days.absolute(of: s) else { continue } 130 guard !heading.skipped, let (s, offset) = heading.deadline, let (deadline, sexp) = planningDay(s, current: current) else { continue }
121 let repeat_ = current <= today ? deadline : (Days.closest(s, to: current, prefer: .future) ?? deadline) 131 let repeat_ = sexp || current <= today ? deadline : (Days.closest(s, to: current, prefer: .future) ?? deadline)
122 let diff = deadline - current 132 let diff = deadline - current
123 let warningDays = Days.warningDays(s, delay: false, defaultDays: options.deadlineWarningDays) 133 let warningDays = Days.warningDays(s, delay: false, defaultDays: options.deadlineWarningDays)
124 if current != deadline, current != repeat_ { 134 if current != deadline, current != repeat_ {
@@ -152,8 +162,8 @@ public enum Agenda {
152 let isToday = current == today 162 let isToday = current == today
153 var items: [AgendaItem] = [] 163 var items: [AgendaItem] = []
154 for heading in source.headings { 164 for heading in source.headings {
155 guard !heading.skipped, let (s, offset) = heading.scheduled, let schedule = Days.absolute(of: s) else { continue } 165 guard !heading.skipped, let (s, offset) = heading.scheduled, let (schedule, sexp) = planningDay(s, current: current) else { continue }
156 let repeat_ = current <= today ? schedule : (Days.closest(s, to: current, prefer: .future) ?? schedule) 166 let repeat_ = sexp || current <= today ? schedule : (Days.closest(s, to: current, prefer: .future) ?? schedule)
157 let diff = current - schedule 167 let diff = current - schedule
158 let past = schedule < today 168 let past = schedule < today
159 let delay = Days.warningDays(s, delay: true, defaultDays: 0) 169 let delay = Days.warningDays(s, delay: true, defaultDays: 0)
@@ -236,6 +246,7 @@ public enum Agenda {
236 return items 246 return items
237 } 247 }
238 248
249 static let diaryStamp = try! NSRegularExpression(pattern: "^<%%(\\([^>\\n]+\\))([^\\n>]*)>")
239 static let repeating = try! NSRegularExpression(pattern: "^<[0-9]+-[0-9]+-[0-9]+[^>\\n]+?\\+[0-9]+[hdwmy]>") 250 static let repeating = try! NSRegularExpression(pattern: "^<[0-9]+-[0-9]+-[0-9]+[^>\\n]+?\\+[0-9]+[hdwmy]>")
240 static let stampBoth = try! NSRegularExpression(pattern: "^[\\[<](\(AgendaSource.tsInternal))[\\]>]") 251 static let stampBoth = try! NSRegularExpression(pattern: "^[\\[<](\(AgendaSource.tsInternal))[\\]>]")
241 static let stampActive = try! NSRegularExpression(pattern: "<(\(AgendaSource.tsInternal))>") 252 static let stampActive = try! NSRegularExpression(pattern: "<(\(AgendaSource.tsInternal))>")
@@ -252,6 +263,9 @@ public enum Agenda {
252 if stamp.rest.hasPrefix(literal) { 263 if stamp.rest.hasPrefix(literal) {
253 guard let m = stampBoth.firstMatch(in: stamp.rest, range: all) else { continue } 264 guard let m = stampBoth.firstMatch(in: stamp.rest, range: all) else { continue }
254 timestamp = rest.substring(with: m.range) 265 timestamp = rest.substring(with: m.range)
266 } else if let m = diaryStamp.firstMatch(in: stamp.rest, range: all) {
267 guard let (sexp, _) = try? LispReader.readFirst(rest.substring(with: m.range(at: 1))), DiarySexp.matches(sexp, day: current) else { continue }
268 timestamp = rest.substring(with: m.range)
255 } else if let m = repeating.firstMatch(in: stamp.rest, range: all) { 269 } else if let m = repeating.firstMatch(in: stamp.rest, range: all) {
256 let repeat_ = rest.substring(with: m.range) 270 let repeat_ = rest.substring(with: m.range)
257 guard let past = Days.closest(repeat_, to: current > today ? today : current, prefer: .past) else { continue } 271 guard let past = Days.closest(repeat_, to: current > today ? today : current, prefer: .past) else { continue }
@@ -271,6 +285,23 @@ public enum Agenda {
271 return items 285 return items
272 } 286 }
273 287
288 /// `org-agenda-get-sexps`.
289 static func sexps(_ source: AgendaSource, day current: Int, options: AgendaOptions) -> [AgendaItem] {
290 var items: [AgendaItem] = []
291 for line in source.sexps {
292 guard let results = DiarySexp.entries(line.sexp, entry: line.entry, day: current) else { continue }
293 let heading = source.headings[line.heading]
294 for result in results {
295 let txt = result.contains(where: { !$0.isWhitespace }) ? result : "SEXP entry returned empty string"
296 items.append(format(
297 source, heading, kind: .sexp, marker: line.offset, extra: "", dotime: .headline, removing: nil,
298 trailing: options.gridTrailing, prefix: options.prefix("agenda"), head: txt, attached: false, urgency: { _ in 0 }
299 ))
300 }
301 }
302 return items
303 }
304
274 /// `" HH:MM"` in a planning timestamp: the time and everything after it. 305 /// `" HH:MM"` in a planning timestamp: the time and everything after it.
275 static func timeIn(_ s: String) -> String? { 306 static func timeIn(_ s: String) -> String? {
276 guard let r = s.range(of: " [012]?[0-9]:[0-9][0-9]", options: .regularExpression) else { return nil } 307 guard let r = s.range(of: " [012]?[0-9]:[0-9][0-9]", options: .regularExpression) else { return nil }
@@ -303,7 +334,7 @@ public enum Agenda {
303 static func format( 334 static func format(
304 _ source: AgendaSource, _ heading: AgendaSource.Heading, kind: AgendaItem.Kind, marker: Int, 335 _ source: AgendaSource, _ heading: AgendaSource.Heading, kind: AgendaItem.Kind, marker: Int,
305 extra: String, dotime: Dotime?, removing: NSRegularExpression?, trailing: String, prefix prefixFormat: PrefixFormat, 336 extra: String, dotime: Dotime?, removing: NSRegularExpression?, trailing: String, prefix prefixFormat: PrefixFormat,
306 head: String? = nil, habit: Habit? = nil, urgency: (Int) -> Int 337 head: String? = nil, habit: Habit? = nil, attached: Bool = true, urgency: (Int) -> Int
307 ) -> AgendaItem { 338 ) -> AgendaItem {
308 var txt = (head ?? heading.head).trimmingCharacters(in: .whitespaces) 339 var txt = (head ?? heading.head).trimmingCharacters(in: .whitespaces)
309 // `org-agenda-fix-displayed-tags`: the heading's tags are replaced by the full list. 340 // `org-agenda-fix-displayed-tags`: the heading's tags are replaced by the full list.
@@ -340,7 +371,7 @@ public enum Agenda {
340 txt = highlightTodo(txt, keywords: source.keywords) 371 txt = highlightTodo(txt, keywords: source.keywords)
341 let priority = Self.priority(prefix + txt, source.priorities) 372 let priority = Self.priority(prefix + txt, source.priorities)
342 return AgendaItem( 373 return AgendaItem(
343 kind: kind, path: source.path, headingOffset: heading.start, headingLine: heading.line, markerOffset: marker, category: category, 374 kind: kind, path: source.path, headingOffset: attached ? heading.start : nil, headingLine: attached ? heading.line : nil, markerOffset: marker, category: category,
344 timeOfDay: timed.timeOfDay, time: timed.time, extra: extra, text: txt, tags: tags.map(\.name), 375 timeOfDay: timed.timeOfDay, time: timed.time, extra: extra, text: txt, tags: tags.map(\.name),
345 todo: heading.todo, isDone: heading.isDone, urgency: urgency(priority), 376 todo: heading.todo, isDone: heading.isDone, urgency: urgency(priority),
346 warntime: heading.properties["APPT_WARNTIME"].flatMap { Int($0.trimmingCharacters(in: .whitespaces)) }, habit: habit, 377 warntime: heading.properties["APPT_WARNTIME"].flatMap { Int($0.trimmingCharacters(in: .whitespaces)) }, habit: habit,
Sources/OrgCore/Agenda/AgendaSource.swift +29 −1
@@ -74,8 +74,20 @@ public struct AgendaSource: Sendable {
74 let note: String? 74 let note: String?
75 } 75 }
76 76
77 /// A `%%(…)` line (`org-agenda-get-sexps`).
78 struct SexpLine: Sendable {
79 /// Start of the line.
80 let offset: Int
81 /// The heading the line is under.
82 let heading: Int
83 let sexp: Sexp
84 /// The rest of the line after the sexp.
85 let entry: String
86 }
87
77 let headings: [Heading] 88 let headings: [Heading]
78 let stamps: [Stamp] 89 let stamps: [Stamp]
90 let sexps: [SexpLine]
79 let blocks: [Block] 91 let blocks: [Block]
80 let progress: [Progress] 92 let progress: [Progress]
81 /// The file's TODO keywords, active and done. 93 /// The file's TODO keywords, active and done.
@@ -244,12 +256,28 @@ public struct AgendaSource: Sendable {
244 for offset in stampOffsets.sorted() { 256 for offset in stampOffsets.sorted() {
245 guard let owner = heading(at: offset), !skipped[owner] else { continue } 257 guard let owner = heading(at: offset), !skipped[owner] else { continue }
246 let rest = restOfLine(offset) 258 let rest = restOfLine(offset)
247 guard rest.range(of: "^<[0-9]{4}-[0-9]{2}-[0-9]{2}", options: .regularExpression) != nil else { continue } 259 guard rest.range(of: "^<([0-9]{4}-[0-9]{2}-[0-9]{2}|%%\\()", options: .regularExpression) != nil else { continue }
248 if Self.inDateRange(offset, ns) { continue } 260 if Self.inDateRange(offset, ns) { continue }
249 stamps.append(Stamp(offset: offset, heading: owner, rest: rest)) 261 stamps.append(Stamp(offset: offset, heading: owner, rest: rest))
250 } 262 }
251 self.stamps = stamps 263 self.stamps = stamps
252 264
265 // Sexp lines: org searches the raw text for `%%(` at the start of a line. Before the
266 // first heading org's agenda signals an error; they're left out.
267 var sexps: [SexpLine] = []
268 let sexpLine = try! NSRegularExpression(pattern: "^&?%%\\(", options: .anchorsMatchLines)
269 for m in sexpLine.matches(in: text, range: NSRange(location: 0, length: ns.length)) {
270 guard let owner = heading(at: m.range.location), !skipped[owner] else { continue }
271 let open = NSMaxRange(m.range) - 1
272 let tail = ns.substring(with: NSRange(location: open, length: min(ns.length - open, 4096)))
273 guard let (sexp, length) = try? LispReader.readFirst(tail) else { continue }
274 let afterSexp = String(tail.dropFirst(length))
275 let lineRest = afterSexp.prefix { $0 != "\n" && $0 != "\r" }
276 let entry = lineRest.trimmingCharacters(in: CharacterSet(charactersIn: " \t"))
277 sexps.append(SexpLine(offset: m.range.location, heading: owner, sexp: sexp, entry: entry))
278 }
279 self.sexps = sexps
280
253 // Ranges: org scans the raw text, skipping comment lines, skipped trees and src blocks. 281 // Ranges: org scans the raw text, skipping comment lines, skipped trees and src blocks.
254 var blocks: [Block] = [] 282 var blocks: [Block] = []
255 let range = try! NSRegularExpression(pattern: "<(\(Self.tsInternal))>--?-?<(\(Self.tsInternal))>") 283 let range = try! NSRegularExpression(pattern: "<(\(Self.tsInternal))>--?-?<(\(Self.tsInternal))>")
Sources/OrgCore/Agenda/DiarySexp.swift added +273
@@ -0,0 +1,273 @@
1import Foundation
2
3/// Diary sexps, `%%(…)` lines and `<%%(…)>` timestamps: `org-diary-sexp-entry` with the
4/// calendar, diary-lib and org functions agenda files use. A sexp that uses anything else, or
5/// signals an error, doesn't apply, as Emacs reports a bad sexp and moves on.
6public enum DiarySexp {
7 /// The entries `sexp` gives on `day` for a diary line whose text is `entry`, or nil where
8 /// it doesn't apply.
9 public static func entries(_ sexp: Sexp, entry: String, day: Int) -> [String]? {
10 let lisp = interpreter(day: day, entry: entry)
11 guard let result = try? lisp.eval(sexp), !Elisp.isNil(result) else { return nil }
12 switch result {
13 case .string(let s): return s.components(separatedBy: "; ")
14 case .dotted(let items, .string(let s)) where items.count == 1: return [s]
15 case .list(let items) where items.first?.string != nil: return items.map { Elisp.printed($0) }
16 default: return [entry]
17 }
18 }
19
20 /// Whether `sexp` applies on `day`, for diary timestamps.
21 public static func matches(_ sexp: Sexp, day: Int) -> Bool {
22 entries(sexp, entry: "", day: day) != nil
23 }
24
25 static func interpreter(day: Int, entry: String) -> Elisp {
26 let lisp = Elisp()
27 lisp.set("date", gregorian(day))
28 lisp.set("entry", .string(entry))
29 lisp.set("calendar-date-style", .symbol("american"))
30 define(lisp)
31 return lisp
32 }
33
34 /// `(MONTH DAY YEAR)`.
35 static func gregorian(_ day: Int) -> Sexp {
36 let date = Days.date(day)
37 return .list([.integer(date.month), .integer(date.day), .integer(date.year)])
38 }
39
40 static func parts(_ date: Sexp) throws -> (month: Int, day: Int, year: Int) {
41 let items = try Elisp.elements(date)
42 guard items.count == 3 else { throw Elisp.Signal(message: "Bad date \(date.description)") }
43 return (try Elisp.int(items[0]), try Elisp.int(items[1]), try Elisp.int(items[2]))
44 }
45
46 static func absolute(_ date: Sexp) throws -> Int {
47 let d = try parts(date)
48 return Days.absolute(year: d.year, month: d.month, day: d.day)
49 }
50
51 static func isLeap(_ year: Int) -> Bool { year % 4 == 0 && (year % 100 != 0 || year % 400 == 0) }
52
53 static func lastDay(month: Int, year: Int) -> Int {
54 month == 2 ? (isLeap(year) ? 29 : 28) : [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][(month - 1 + 12) % 12]
55 }
56
57 /// `calendar-nth-named-absday`.
58 static func nthNamedAbsday(_ n: Int, _ dayname: Int, _ month: Int, _ year: Int, _ day: Int?) -> Int {
59 func onOrBefore(_ date: Int) -> Int { date - (date - dayname) % 7 }
60 if n > 0 {
61 return 7 * (n - 1) + onOrBefore(6 + Days.absolute(year: year, month: month, day: day ?? 1))
62 }
63 return 7 * (n + 1) + onOrBefore(Days.absolute(year: year, month: month, day: day ?? lastDay(month: month, year: year)))
64 }
65
66 /// `diary-ordinal-suffix`.
67 static func ordinalSuffix(_ n: Int) throws -> String {
68 if [11, 12, 13].contains(n % 100) || 3 < n % 10 { return "th" }
69 guard n % 10 >= 0 else { throw Elisp.Signal(message: "Args out of range") }
70 return ["th", "st", "nd", "rd"][n % 10]
71 }
72
73 /// `diary-make-date`: arguments in `calendar-date-style` order, as `(MONTH DAY YEAR)`.
74 static func makeDate(_ lisp: Elisp, _ a: Sexp, _ b: Sexp, _ c: Sexp) throws -> [Sexp] {
75 switch try lisp.value("calendar-date-style") {
76 case .symbol("iso"): [b, c, a]
77 case .symbol("european"): [b, a, c]
78 default: [a, b, c]
79 }
80 }
81
82 /// Runs `body` with `calendar-date-style` bound to `iso`, as the `org-` wrappers do.
83 static func iso(_ lisp: Elisp, _ body: () throws -> Sexp) rethrows -> Sexp {
84 try lisp.binding(["calendar-date-style": .symbol("iso")], body)
85 }
86
87 static func optional(_ args: [Sexp], _ i: Int) -> Sexp { i < args.count ? args[i] : .nil }
88
89 static func define(_ lisp: Elisp) {
90 func date() throws -> Sexp { try lisp.value("date") }
91 func entry() throws -> Sexp { try lisp.value("entry") }
92 func arity(_ args: [Sexp], _ range: ClosedRange<Int>, _ name: String) throws {
93 try Elisp.arity(args, range, name)
94 }
95
96 lisp.define("calendar-extract-month") { _, args in try arity(args, 1...1, "calendar-extract-month"); return .integer(try parts(args[0]).month) }
97 lisp.define("calendar-extract-day") { _, args in try arity(args, 1...1, "calendar-extract-day"); return .integer(try parts(args[0]).day) }
98 lisp.define("calendar-extract-year") { _, args in try arity(args, 1...1, "calendar-extract-year"); return .integer(try parts(args[0]).year) }
99 lisp.define("calendar-absolute-from-gregorian") { _, args in
100 try arity(args, 1...1, "calendar-absolute-from-gregorian")
101 return .integer(try absolute(args[0]))
102 }
103 lisp.define("calendar-gregorian-from-absolute") { _, args in
104 try arity(args, 1...1, "calendar-gregorian-from-absolute")
105 return gregorian(try Elisp.int(args[0]))
106 }
107 lisp.define("calendar-day-of-week") { _, args in
108 try arity(args, 1...1, "calendar-day-of-week")
109 return .integer(Days.weekday(try absolute(args[0])))
110 }
111 lisp.define("calendar-leap-year-p") { _, args in
112 try arity(args, 1...1, "calendar-leap-year-p")
113 return Elisp.bool(isLeap(try Elisp.int(args[0])))
114 }
115 lisp.define("calendar-last-day-of-month") { _, args in
116 try arity(args, 2...2, "calendar-last-day-of-month")
117 return .integer(lastDay(month: try Elisp.int(args[0]), year: try Elisp.int(args[1])))
118 }
119 lisp.define("calendar-date-equal") { _, args in
120 try arity(args, 2...2, "calendar-date-equal")
121 let a = try parts(args[0])
122 let b = try parts(args[1])
123 return Elisp.bool(a == b)
124 }
125 lisp.define("calendar-day-number") { _, args in
126 try arity(args, 1...1, "calendar-day-number")
127 let d = try parts(args[0])
128 return .integer(Days.absolute(year: d.year, month: d.month, day: d.day) - Days.absolute(year: d.year, month: 1, day: 0))
129 }
130 lisp.define("calendar-nth-named-absday") { _, args in
131 try arity(args, 4...5, "calendar-nth-named-absday")
132 let day = Elisp.isNil(optional(args, 4)) ? nil : try Elisp.int(args[4])
133 return .integer(nthNamedAbsday(try Elisp.int(args[0]), try Elisp.int(args[1]), try Elisp.int(args[2]), try Elisp.int(args[3]), day))
134 }
135 lisp.define("calendar-nth-named-day") { lisp, args in
136 gregorian(try Elisp.int(try lisp.callNamed("calendar-nth-named-absday", args)))
137 }
138 lisp.define("calendar-iso-from-absolute") { _, args in
139 try arity(args, 1...1, "calendar-iso-from-absolute")
140 let day = try Elisp.int(args[0])
141 let thursday = day - (Days.weekday(day) + 6) % 7 + 3
142 return .list([.integer(Days.isoWeek(day)), .integer(Days.weekday(day)), .integer(Days.date(thursday).year)])
143 }
144
145 lisp.define("diary-ordinal-suffix") { _, args in
146 try arity(args, 1...1, "diary-ordinal-suffix")
147 return .string(try ordinalSuffix(try Elisp.int(args[0])))
148 }
149 lisp.define("diary-make-date") { lisp, args in
150 try arity(args, 3...3, "diary-make-date")
151 return .list(try makeDate(lisp, args[0], args[1], args[2]))
152 }
153 lisp.define("diary-date") { lisp, args in
154 try arity(args, 3...4, "diary-date")
155 let wanted = try makeDate(lisp, args[0], args[1], args[2])
156 let d = try parts(try date())
157 for (spec, actual) in zip(wanted, [d.month, d.day, d.year]) {
158 let holds = switch spec {
159 case .symbol("t"): true
160 case .list(let items): items.contains(.integer(actual))
161 case .integer(let i): i == actual
162 default: false
163 }
164 if !holds { return .nil }
165 }
166 return Elisp.cons(optional(args, 3), try entry())
167 }
168 lisp.define("diary-block") { lisp, args in
169 try arity(args, 6...7, "diary-block")
170 let first = try absolute(.list(try makeDate(lisp, args[0], args[1], args[2])))
171 let last = try absolute(.list(try makeDate(lisp, args[3], args[4], args[5])))
172 let d = try absolute(try date())
173 return first <= d && d <= last ? Elisp.cons(optional(args, 6), try entry()) : .nil
174 }
175 lisp.define("diary-float") { _, args in
176 try arity(args, 3...5, "diary-float")
177 let month = args[0]
178 let dayname = try Elisp.int(args[1])
179 let n = try Elisp.int(args[2])
180 let day = Elisp.isNil(optional(args, 3)) ? nil : try Elisp.int(args[3])
181 let current = try absolute(try date())
182 guard dayname == Days.weekday(current) else { return .nil }
183 let d = Days.date(current)
184 let limit = nthNamedAbsday(-n, dayname, d.month, d.year, d.day)
185 let lastAbs = n > 0 ? limit : limit + 6
186 let firstAbs = n > 0 ? limit - 6 : limit
187 let first = Days.date(firstAbs)
188 let last = Days.date(lastAbs)
189 func monthMatches(_ m: Int) -> Bool {
190 switch month {
191 case .symbol("t"): true
192 case .list(let items): items.contains(.integer(m))
193 case .integer(let i): i == m
194 default: false
195 }
196 }
197 func base(_ m: Int, _ y: Int) -> Int { day ?? (n > 0 ? 1 : lastDay(month: m, year: y)) }
198 let applies: Bool
199 if first.month == last.month {
200 let b = base(first.month, first.year)
201 applies = monthMatches(first.month) && first.day <= b && b <= last.day
202 } else {
203 applies = (first.year < last.year || (first.year == last.year && first.month < last.month))
204 && ((monthMatches(first.month) && first.day <= base(first.month, first.year))
205 || (monthMatches(last.month) && base(last.month, last.year) <= last.day))
206 }
207 return applies ? Elisp.cons(optional(args, 4), try entry()) : .nil
208 }
209 lisp.define("diary-anniversary") { lisp, args in
210 try arity(args, 2...4, "diary-anniversary")
211 let made = try makeDate(lisp, args[0], args[1], optional(args, 2))
212 var dd = try Elisp.int(made[1])
213 var mm = try Elisp.int(made[0])
214 let y = try parts(try date()).year
215 let diff = Elisp.isNil(made[2]) ? 100 : y - (try Elisp.int(made[2]))
216 if mm == 2, dd == 29, !isLeap(y) {
217 mm = 3
218 dd = 1
219 }
220 let d = try parts(try date())
221 guard diff > 0, (mm, dd, y) == (d.month, d.day, d.year) else { return .nil }
222 return Elisp.cons(optional(args, 3), .string(try Elisp.format([try entry(), .integer(diff), .string(try ordinalSuffix(diff))])))
223 }
224 lisp.define("diary-cyclic") { lisp, args in
225 try arity(args, 4...5, "diary-cyclic")
226 let n = try Elisp.int(args[0])
227 guard n > 0 else { throw Elisp.Signal(message: "Day count must be positive") }
228 let diff = try absolute(try date()) - (try absolute(.list(try makeDate(lisp, args[1], args[2], args[3]))))
229 let cycle = diff / n
230 guard diff >= 0, diff % n == 0 else { return .nil }
231 return Elisp.cons(optional(args, 4), .string(try Elisp.format([try entry(), .integer(cycle), .string(try ordinalSuffix(cycle))])))
232 }
233 lisp.define("diary-remind") { lisp, args in
234 try arity(args, 2...3, "diary-remind")
235 var days = args[1]
236 if case .integer(let n) = days, n < 0 { days = Elisp.list((1...(-n)).map { .integer($0) }) }
237 func remind(_ days: Sexp) throws -> Sexp {
238 let applies = try lisp.eval(args[0])
239 if !Elisp.isNil(applies) { return applies }
240 if case .integer(let n) = days {
241 let shifted = gregorian(try absolute(try date()) + n)
242 var found = try lisp.binding(["date": shifted]) { try lisp.eval(args[0]) }
243 guard !Elisp.isNil(found) else { return .nil }
244 if case .dotted = found { found = try Elisp.cdr(found) } else if case .list = found { found = try Elisp.cdr(found) }
245 let span = n % 7 == 0 ? String(format: "%d week%@", n / 7, n == 7 ? "" : "s") : String(format: "%d day%@", n, n == 1 ? "" : "s")
246 return .string("Reminder: Only " + span + " until " + Elisp.printed(found))
247 }
248 guard case .list(let items) = days, let first = items.first else { return .nil }
249 let head = try remind(first)
250 return Elisp.isNil(head) ? try remind(Elisp.list(Array(items.dropFirst()))) : head
251 }
252 return try remind(days)
253 }
254
255 lisp.define("org-anniversary") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-anniversary", args) } }
256 lisp.define("org-cyclic") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-cyclic", args) } }
257 lisp.define("org-block") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-block", args) } }
258 lisp.define("org-date") { lisp, args in try iso(lisp) { try lisp.callNamed("diary-date", args) } }
259 lisp.define("org-class") { _, args in
260 try arity(args, 7...Int.max, "org-class")
261 let ints = try args.prefix(7).map(Elisp.int)
262 let first = Days.absolute(year: ints[0], month: ints[1], day: ints[2])
263 let last = Days.absolute(year: ints[3], month: ints[4], day: ints[5])
264 let d = try absolute(try date())
265 let skip = Array(args.dropFirst(7))
266 // Holiday names and `holidays' need the calendar's holiday lists.
267 guard skip.allSatisfy({ $0.integer != nil }) else { throw Elisp.Unsupported(what: "org-class holidays") }
268 let week = Days.isoWeek(d)
269 guard first <= d, d <= last, Days.weekday(d) == ints[6], !skip.contains(.integer(week)) else { return .nil }
270 return try entry()
271 }
272 }
273}
Sources/OrgCore/Config/Elisp.swift added +878
@@ -0,0 +1,878 @@
1import Foundation
2
3/// A small Emacs Lisp evaluator over `Sexp` values: the special forms and functions diary
4/// sexps and table formulas use, with dynamic binding. A function or form it doesn't have
5/// throws `Unsupported`; errors Emacs would signal throw `Signal`.
6public final class Elisp {
7 public struct Unsupported: Error, Equatable, CustomStringConvertible {
8 public let what: String
9 public var description: String { what }
10 }
11
12 public struct Signal: Error, Equatable, CustomStringConvertible {
13 public let message: String
14 public var description: String { message }
15 }
16
17 public typealias Builtin = (Elisp, [Sexp]) throws -> Sexp
18
19 private var scopes: [[String: Sexp]] = [[:]]
20 private var functions: [String: Builtin] = [:]
21 private var steps = 0
22 static let stepLimit = 1_000_000
23
24 public init() {
25 Self.core(self)
26 }
27
28 /// Defines or replaces a function.
29 public func define(_ name: String, _ function: @escaping Builtin) { functions[name] = function }
30
31 /// Sets a global variable.
32 public func set(_ name: String, _ value: Sexp) { scopes[0][name] = value }
33
34 public func value(_ name: String) throws -> Sexp {
35 for scope in scopes.reversed() { if let v = scope[name] { return v } }
36 throw Signal(message: "Symbol’s value as variable is void: \(name)")
37 }
38
39 /// Evaluates `body` with `bindings` added.
40 public func binding<T>(_ bindings: [String: Sexp], _ body: () throws -> T) rethrows -> T {
41 scopes.append(bindings)
42 defer { scopes.removeLast() }
43 return try body()
44 }
45
46 private func assign(_ name: String, _ v: Sexp) {
47 for i in scopes.indices.reversed() where scopes[i][name] != nil {
48 scopes[i][name] = v
49 return
50 }
51 scopes[0][name] = v
52 }
53
54 // MARK: - Values
55
56 public static let t = Sexp.symbol("t")
57
58 public static func isNil(_ v: Sexp) -> Bool { v == .nil || v == .list([]) }
59 public static func bool(_ b: Bool) -> Sexp { b ? t : .nil }
60 static func list(_ items: [Sexp]) -> Sexp { items.isEmpty ? .nil : .list(items) }
61
62 static func cons(_ a: Sexp, _ b: Sexp) -> Sexp {
63 switch b {
64 case .list(let items): .list([a] + items)
65 case .dotted(let items, let last): .dotted([a] + items, last)
66 case _ where isNil(b): .list([a])
67 default: .dotted([a], b)
68 }
69 }
70
71 static func car(_ v: Sexp) throws -> Sexp {
72 switch v {
73 case .list(let items): items.first ?? .nil
74 case .dotted(let items, _): items[0]
75 case _ where isNil(v): .nil
76 default: throw Signal(message: "Wrong type argument: listp, \(v.description)")
77 }
78 }
79
80 static func cdr(_ v: Sexp) throws -> Sexp {
81 switch v {
82 case .list(let items): list(Array(items.dropFirst()))
83 case .dotted(let items, let last): items.count == 1 ? last : .dotted(Array(items.dropFirst()), last)
84 case _ where isNil(v): .nil
85 default: throw Signal(message: "Wrong type argument: listp, \(v.description)")
86 }
87 }
88
89 /// A proper list's elements.
90 static func elements(_ v: Sexp) throws -> [Sexp] {
91 if case .list(let items) = v { return items }
92 if isNil(v) { return [] }
93 throw Signal(message: "Wrong type argument: listp, \(v.description)")
94 }
95
96 enum Number {
97 case int(Int), float(Double)
98 var double: Double {
99 switch self {
100 case .int(let i): Double(i)
101 case .float(let d): d
102 }
103 }
104 }
105
106 static func number(_ v: Sexp) throws -> Number {
107 switch v {
108 case .integer(let i): return .int(i)
109 case .float(let d): return .float(d)
110 case .character(let c): return .int(c)
111 default: throw Signal(message: "Wrong type argument: number-or-marker-p, \(v.description)")
112 }
113 }
114
115 static func int(_ v: Sexp) throws -> Int {
116 switch v {
117 case .integer(let i): return i
118 case .character(let c): return c
119 default: throw Signal(message: "Wrong type argument: integerp, \(v.description)")
120 }
121 }
122
123 static func string(_ v: Sexp) throws -> String {
124 guard case .string(let s) = v else { throw Signal(message: "Wrong type argument: stringp, \(v.description)") }
125 return s
126 }
127
128 static func sexp(_ n: Number) -> Sexp {
129 switch n {
130 case .int(let i): .integer(i)
131 case .float(let d): .float(d)
132 }
133 }
134
135 /// `prin1-to-string` with `princ` for strings when `escape` is false.
136 public static func printed(_ v: Sexp, escape: Bool = false) -> String {
137 switch v {
138 case .string(let s): return escape ? v.description : s
139 case .float(let d): return floatText(d)
140 case .character(let c): return String(c)
141 case .list(let items): return "(" + items.map { printed($0, escape: true) }.joined(separator: " ") + ")"
142 case .dotted(let items, let last): return "(" + items.map { printed($0, escape: true) }.joined(separator: " ") + " . " + printed(last, escape: true) + ")"
143 case .vector(let items): return "[" + items.map { printed($0, escape: true) }.joined(separator: " ") + "]"
144 default: return v.description
145 }
146 }
147
148 /// Emacs's float printing: the shortest text that reads back, with `.0` on whole numbers.
149 static func floatText(_ d: Double) -> String {
150 if d.isNaN { return d.sign == .minus ? "-0.0e+NaN" : "0.0e+NaN" }
151 if d.isInfinite { return d < 0 ? "-1.0e+INF" : "1.0e+INF" }
152 var text = "\(d)"
153 if let e = text.firstIndex(of: "e") {
154 let mantissa = text[..<e]
155 var exponent = String(text[text.index(after: e)...])
156 if !exponent.hasPrefix("-"), !exponent.hasPrefix("+") { exponent = "+" + exponent }
157 text = mantissa.hasSuffix(".0") ? String(mantissa.dropLast(2)) + "e" + exponent : mantissa + "e" + exponent
158 }
159 return text
160 }
161
162 // MARK: - Evaluation
163
164 public func eval(_ form: Sexp) throws -> Sexp {
165 steps += 1
166 if steps > Self.stepLimit { throw Signal(message: "Evaluation took too long") }
167 switch form {
168 case .symbol(let name):
169 if name == "nil" || name == "t" || name.hasPrefix(":") { return form }
170 return try value(name)
171 case .character(let c): return .integer(c)
172 case .list(let items):
173 guard let head = items.first else { return .nil }
174 guard case .symbol(let name) = head else {
175 if case .list(let lambda) = head, lambda.first == .symbol("lambda") {
176 return try call(head, try items.dropFirst().map(eval))
177 }
178 throw Signal(message: "Invalid function: \(head.description)")
179 }
180 return try special(name, Array(items.dropFirst())) ?? callNamed(name, try items.dropFirst().map(eval))
181 case .dotted: throw Signal(message: "Invalid function")
182 default: return form
183 }
184 }
185
186 func progn(_ body: some Collection<Sexp>) throws -> Sexp {
187 var result = Sexp.nil
188 for form in body { result = try eval(form) }
189 return result
190 }
191
192 /// Special forms and macros; nil when `name` is an ordinary function.
193 private func special(_ name: String, _ args: [Sexp]) throws -> Sexp? {
194 switch name {
195 case "quote":
196 return args.first ?? .nil
197 case "function":
198 return args.first ?? .nil
199 case "lambda":
200 return .list([.symbol("lambda")] + args)
201 case "progn":
202 return try progn(args)
203 case "prog1":
204 guard let first = args.first else { return .nil }
205 let value = try eval(first)
206 _ = try progn(args.dropFirst())
207 return value
208 case "if":
209 guard args.count >= 2 else { throw Signal(message: "Wrong number of arguments: if") }
210 return Self.isNil(try eval(args[0])) ? try progn(args.dropFirst(2)) : try eval(args[1])
211 case "when", "unless":
212 guard let test = args.first else { return .nil }
213 return Self.isNil(try eval(test)) == (name == "unless") ? try progn(args.dropFirst()) : .nil
214 case "cond":
215 for clause in args {
216 let parts = try Self.elements(clause)
217 guard let test = parts.first else { continue }
218 let value = try eval(test)
219 if !Self.isNil(value) { return parts.count == 1 ? value : try progn(parts.dropFirst()) }
220 }
221 return .nil
222 case "and":
223 var value = Self.t
224 for form in args {
225 value = try eval(form)
226 if Self.isNil(value) { return .nil }
227 }
228 return value
229 case "or":
230 for form in args {
231 let value = try eval(form)
232 if !Self.isNil(value) { return value }
233 }
234 return .nil
235 case "let", "let*":
236 guard let first = args.first else { return .nil }
237 var bindings: [String: Sexp] = [:]
238 scopes.append([:])
239 defer { scopes.removeLast() }
240 for binding in try Self.elements(first) {
241 let (variable, value): (String, Sexp)
242 if case .symbol(let s) = binding {
243 (variable, value) = (s, .nil)
244 } else {
245 let parts = try Self.elements(binding)
246 guard case .symbol(let s)? = parts.first else { throw Signal(message: "Bad binding") }
247 (variable, value) = (s, parts.count > 1 ? try eval(parts[1]) : .nil)
248 }
249 if name == "let*" { scopes[scopes.count - 1][variable] = value } else { bindings[variable] = value }
250 }
251 if name == "let" { scopes[scopes.count - 1] = bindings }
252 return try progn(args.dropFirst())
253 case "setq":
254 var value = Sexp.nil
255 var i = 0
256 while i + 1 < args.count {
257 guard case .symbol(let variable) = args[i] else { throw Signal(message: "Bad setq") }
258 value = try eval(args[i + 1])
259 assign(variable, value)
260 i += 2
261 }
262 return value
263 case "push", "pop":
264 guard case .symbol(let variable)? = (name == "push" ? args.dropFirst().first : args.first) else { throw Unsupported(what: "\(name) on a place") }
265 let list = try value(variable)
266 if name == "pop" {
267 assign(variable, try Self.cdr(list))
268 return try Self.car(list)
269 }
270 let pushed = Self.cons(try eval(args[0]), list)
271 assign(variable, pushed)
272 return pushed
273 case "while":
274 guard let test = args.first else { return .nil }
275 while !Self.isNil(try eval(test)) { _ = try progn(args.dropFirst()) }
276 return .nil
277 case "dolist", "dotimes":
278 let spec = try args.first.map(Self.elements) ?? []
279 guard case .symbol(let variable)? = spec.first, spec.count >= 2 else { throw Signal(message: "Bad \(name)") }
280 let source = try eval(spec[1])
281 let items = name == "dolist" ? try Self.elements(source) : (0..<max(0, try Self.int(source))).map { Sexp.integer($0) }
282 scopes.append([:])
283 defer { scopes.removeLast() }
284 for item in items {
285 scopes[scopes.count - 1][variable] = item
286 _ = try progn(args.dropFirst())
287 }
288 if spec.count > 2 {
289 scopes[scopes.count - 1][variable] = name == "dotimes" ? .integer(items.count) : .nil
290 return try eval(spec[2])
291 }
292 return .nil
293 case "ignore-errors":
294 do { return try progn(args) } catch is Signal { return .nil }
295 case "condition-case":
296 guard args.count >= 2 else { throw Signal(message: "Bad condition-case") }
297 do {
298 return try eval(args[1])
299 } catch let signal as Signal {
300 for handler in args.dropFirst(2) {
301 let parts = try Self.elements(handler)
302 guard let condition = parts.first else { continue }
303 let names = (try? Self.elements(condition)) ?? [condition]
304 guard names.contains(.symbol("error")) || names.contains(.symbol("t")) else { continue }
305 let bindings: [String: Sexp] = args[0].symbol.map { $0 == "nil" ? [:] : [$0: .list([.symbol("error"), .string(signal.message)])] } ?? [:]
306 return try binding(bindings) { try progn(parts.dropFirst()) }
307 }
308 throw signal
309 }
310 default:
311 return nil
312 }
313 }
314
315 /// Calls a function value: a symbol naming one, or a lambda list.
316 public func call(_ function: Sexp, _ args: [Sexp]) throws -> Sexp {
317 switch function {
318 case .symbol(let name): return try callNamed(name, args)
319 case .list(let items) where items.first == .symbol("lambda") && items.count >= 2:
320 var bindings: [String: Sexp] = [:]
321 var mode = "required"
322 var i = 0
323 for parameter in try Self.elements(items[1]) {
324 guard case .symbol(let p) = parameter else { throw Signal(message: "Bad lambda list") }
325 if p == "&optional" || p == "&rest" {
326 mode = p
327 continue
328 }
329 if mode == "&rest" {
330 bindings[p] = Self.list(Array(args.dropFirst(i)))
331 i = args.count
332 } else if i < args.count {
333 bindings[p] = args[i]
334 i += 1
335 } else if mode == "&optional" {
336 bindings[p] = .nil
337 } else {
338 throw Signal(message: "Wrong number of arguments")
339 }
340 }
341 if i < args.count { throw Signal(message: "Wrong number of arguments") }
342 return try binding(bindings) { try progn(items.dropFirst(2)) }
343 default:
344 throw Signal(message: "Invalid function: \(function.description)")
345 }
346 }
347
348 func callNamed(_ name: String, _ args: [Sexp]) throws -> Sexp {
349 guard let function = functions[name] else { throw Unsupported(what: "function \(name)") }
350 return try function(self, args)
351 }
352
353 static func arity(_ args: [Sexp], _ range: ClosedRange<Int>, _ name: String) throws {
354 guard range.contains(args.count) else { throw Signal(message: "Wrong number of arguments: \(name), \(args.count)") }
355 }
356
357 // MARK: - Functions
358
359 private static func core(_ lisp: Elisp) {
360 func arithmetic(_ name: String, identity: Int, _ intOp: @escaping (Int, Int) -> (Int, Bool), _ floatOp: @escaping (Double, Double) -> Double) {
361 lisp.define(name) { _, args in
362 let numbers = try args.map(number)
363 guard var result = numbers.first else { return .integer(identity) }
364 if numbers.count == 1, name == "-" {
365 switch result {
366 case .int(let i): return .integer(-i)
367 case .float(let d): return .float(-d)
368 }
369 }
370 for n in numbers.dropFirst() {
371 switch (result, n) {
372 case (.int(let a), .int(let b)):
373 let (value, overflow) = intOp(a, b)
374 if overflow { throw Unsupported(what: "integer size") }
375 result = .int(value)
376 default:
377 result = .float(floatOp(result.double, n.double))
378 }
379 }
380 return sexp(result)
381 }
382 }
383 arithmetic("+", identity: 0, { $0.addingReportingOverflow($1) }, +)
384 arithmetic("-", identity: 0, { $0.subtractingReportingOverflow($1) }, -)
385 arithmetic("*", identity: 1, { $0.multipliedReportingOverflow(by: $1) }, *)
386 lisp.define("/") { _, args in
387 try arity(args, 1...Int.max, "/")
388 let numbers = try args.map(number)
389 let floating = numbers.contains { if case .float = $0 { true } else { false } }
390 if numbers.count == 1 { return try sexp(divide(.int(1), numbers[0], floating: floating)) }
391 var result = numbers[0]
392 for n in numbers.dropFirst() { result = try divide(result, n, floating: floating) }
393 return sexp(result)
394 }
395 lisp.define("%") { _, args in
396 try arity(args, 2...2, "%")
397 let b = try int(args[1])
398 guard b != 0 else { throw Signal(message: "Arithmetic error") }
399 return .integer(try int(args[0]) % b)
400 }
401 lisp.define("mod") { _, args in
402 try arity(args, 2...2, "mod")
403 switch (try number(args[0]), try number(args[1])) {
404 case (.int(let a), .int(let b)):
405 guard b != 0 else { throw Signal(message: "Arithmetic error") }
406 let r = a % b
407 return .integer(r != 0 && (r < 0) != (b < 0) ? r + b : r)
408 case (let a, let b):
409 let r = fmod(a.double, b.double)
410 return .float(r != 0 && (r < 0) != (b.double < 0) ? r + b.double : r)
411 }
412 }
413 lisp.define("1+") { _, args in try arity(args, 1...1, "1+"); return try sexp(add(number(args[0]), 1)) }
414 lisp.define("1-") { _, args in try arity(args, 1...1, "1-"); return try sexp(add(number(args[0]), -1)) }
415 lisp.define("abs") { _, args in
416 try arity(args, 1...1, "abs")
417 switch try number(args[0]) {
418 case .int(let i): return .integer(abs(i))
419 case .float(let d): return .float(abs(d))
420 }
421 }
422 for name in ["max", "min"] {
423 lisp.define(name) { _, args in
424 try arity(args, 1...Int.max, name)
425 let numbers = try args.map(number)
426 let floating = numbers.contains { if case .float = $0 { true } else { false } }
427 var best = numbers[0]
428 for n in numbers.dropFirst() where name == "max" ? n.double > best.double : n.double < best.double { best = n }
429 return floating ? .float(best.double) : sexp(best)
430 }
431 }
432 for name in ["floor", "ceiling", "round", "truncate"] {
433 lisp.define(name) { _, args in
434 try arity(args, 1...2, name)
435 var x = try number(args[0]).double
436 if args.count == 2, !isNil(args[1]) {
437 let divisor = try number(args[1]).double
438 guard divisor != 0 else { throw Signal(message: "Arithmetic error") }
439 x /= divisor
440 } else if case .int(let i) = try number(args[0]) {
441 return .integer(i)
442 }
443 let rule: FloatingPointRoundingRule = switch name {
444 case "floor": .down
445 case "ceiling": .up
446 case "round": .toNearestOrEven
447 default: .towardZero
448 }
449 guard let i = Int(exactly: x.rounded(rule)) else { throw Signal(message: "Arithmetic overflow error") }
450 return .integer(i)
451 }
452 }
453 lisp.define("float") { _, args in try arity(args, 1...1, "float"); return .float(try number(args[0]).double) }
454 for (name, op) in [("=", { (a: Double, b: Double) in a == b }), ("<", { $0 < $1 }), (">", { $0 > $1 }), ("<=", { $0 <= $1 }), (">=", { $0 >= $1 })] {
455 lisp.define(name) { _, args in
456 try arity(args, 1...Int.max, name)
457 let numbers = try args.map(number)
458 for (a, b) in zip(numbers, numbers.dropFirst()) {
459 let holds: Bool = switch (a, b) {
460 case (.int(let x), .int(let y)): op(Double(x), Double(y)) && (name != "=" || x == y)
461 default: op(a.double, b.double)
462 }
463 if !holds { return .nil }
464 }
465 return t
466 }
467 }
468 lisp.define("/=") { _, args in
469 try arity(args, 2...2, "/=")
470 return bool(try number(args[0]).double != (try number(args[1])).double)
471 }
472 lisp.define("zerop") { _, args in try arity(args, 1...1, "zerop"); return bool(try number(args[0]).double == 0) }
473 lisp.define("not") { _, args in try arity(args, 1...1, "not"); return bool(isNil(args[0])) }
474 lisp.define("null") { _, args in try arity(args, 1...1, "null"); return bool(isNil(args[0])) }
475 lisp.define("eq") { _, args in try arity(args, 2...2, "eq"); return bool(eq(args[0], args[1])) }
476 lisp.define("eql") { _, args in try arity(args, 2...2, "eql"); return bool(eq(args[0], args[1])) }
477 lisp.define("equal") { _, args in try arity(args, 2...2, "equal"); return bool(normalized(args[0]) == normalized(args[1])) }
478 lisp.define("numberp") { _, args in
479 try arity(args, 1...1, "numberp")
480 return bool((try? number(args[0])) != nil && args[0].character == nil)
481 }
482 lisp.define("integerp") { _, args in try arity(args, 1...1, "integerp"); return bool(args[0].integer != nil) }
483 lisp.define("floatp") { _, args in
484 try arity(args, 1...1, "floatp")
485 if case .float = args[0] { return t }
486 return .nil
487 }
488 lisp.define("stringp") { _, args in try arity(args, 1...1, "stringp"); return bool(args[0].string != nil) }
489 lisp.define("listp") { _, args in
490 try arity(args, 1...1, "listp")
491 switch args[0] {
492 case .list, .dotted: return t
493 default: return bool(isNil(args[0]))
494 }
495 }
496 lisp.define("consp") { _, args in
497 try arity(args, 1...1, "consp")
498 switch args[0] {
499 case .list(let items): return bool(!items.isEmpty)
500 case .dotted: return t
501 default: return .nil
502 }
503 }
504 lisp.define("symbolp") { _, args in try arity(args, 1...1, "symbolp"); return bool(args[0].symbol != nil || isNil(args[0])) }
505
506 // Lists.
507 lisp.define("car") { _, args in try arity(args, 1...1, "car"); return try car(args[0]) }
508 lisp.define("cdr") { _, args in try arity(args, 1...1, "cdr"); return try cdr(args[0]) }
509 lisp.define("cadr") { _, args in try arity(args, 1...1, "cadr"); return try car(cdr(args[0])) }
510 lisp.define("cddr") { _, args in try arity(args, 1...1, "cddr"); return try cdr(cdr(args[0])) }
511 lisp.define("cons") { _, args in try arity(args, 2...2, "cons"); return cons(args[0], args[1]) }
512 lisp.define("list") { _, args in list(args) }
513 lisp.define("nth") { _, args in
514 try arity(args, 2...2, "nth")
515 var v = args[1]
516 for _ in 0..<max(0, try int(args[0])) { v = try cdr(v) }
517 return try car(v)
518 }
519 lisp.define("nthcdr") { _, args in
520 try arity(args, 2...2, "nthcdr")
521 var v = args[1]
522 for _ in 0..<max(0, try int(args[0])) { v = try cdr(v) }
523 return v
524 }
525 lisp.define("elt") { _, args in
526 try arity(args, 2...2, "elt")
527 let items = try sequence(args[0])
528 let i = try int(args[1])
529 if case .list = args[0], i >= items.count { return .nil }
530 guard items.indices.contains(i) else { throw Signal(message: "Args out of range") }
531 return items[i]
532 }
533 lisp.define("aref") { _, args in
534 try arity(args, 2...2, "aref")
535 let items = try sequence(args[0])
536 let i = try int(args[1])
537 guard items.indices.contains(i) else { throw Signal(message: "Args out of range") }
538 return items[i]
539 }
540 lisp.define("append") { _, args in
541 guard let last = args.last else { return .nil }
542 var items: [Sexp] = []
543 for arg in args.dropLast() { items += try sequence(arg) }
544 return try items.reversed().reduce(last) { cons($1, $0) }
545 }
546 lisp.define("length") { _, args in
547 try arity(args, 1...1, "length")
548 return .integer(try sequence(args[0]).count)
549 }
550 lisp.define("reverse") { _, args in
551 try arity(args, 1...1, "reverse")
552 if case .string(let s) = args[0] { return .string(String(s.reversed())) }
553 return list(try elements(args[0]).reversed())
554 }
555 lisp.define("number-sequence") { _, args in
556 try arity(args, 1...3, "number-sequence")
557 let from = try int(args[0])
558 guard args.count > 1, !isNil(args[1]) else { return .list([.integer(from)]) }
559 let to = try int(args[1])
560 let step = args.count > 2 && !isNil(args[2]) ? try int(args[2]) : 1
561 guard step != 0 else { throw Signal(message: "The increment can not be zero") }
562 return list(Array(stride(from: from, through: to, by: step)).map { .integer($0) })
563 }
564 for name in ["memq", "member", "memql"] {
565 lisp.define(name) { _, args in
566 try arity(args, 2...2, name)
567 var rest = args[1]
568 while case .list(let items) = rest, let first = items.first {
569 if name == "member" ? normalized(first) == normalized(args[0]) : eq(first, args[0]) { return rest }
570 rest = list(Array(items.dropFirst()))
571 }
572 return .nil
573 }
574 }
575 for name in ["assoc", "assq"] {
576 lisp.define(name) { _, args in
577 try arity(args, 2...3, name)
578 for pair in try elements(args[1]) {
579 guard let key = try? car(pair) else { continue }
580 if name == "assoc" ? normalized(key) == normalized(args[0]) : eq(key, args[0]) { return pair }
581 }
582 return .nil
583 }
584 }
585 lisp.define("identity") { _, args in try arity(args, 1...1, "identity"); return args[0] }
586 lisp.define("ignore") { _, _ in .nil }
587 lisp.define("funcall") { lisp, args in
588 try arity(args, 1...Int.max, "funcall")
589 return try lisp.call(args[0], Array(args.dropFirst()))
590 }
591 lisp.define("apply") { lisp, args in
592 try arity(args, 1...Int.max, "apply")
593 let spread = args.count > 1 ? Array(args[1..<(args.count - 1)]) + (try elements(args[args.count - 1])) : []
594 return try lisp.call(args[0], spread)
595 }
596 lisp.define("mapcar") { lisp, args in
597 try arity(args, 2...2, "mapcar")
598 return list(try sequence(args[1]).map { try lisp.call(args[0], [$0]) })
599 }
600 lisp.define("mapc") { lisp, args in
601 try arity(args, 2...2, "mapc")
602 for item in try sequence(args[1]) { _ = try lisp.call(args[0], [item]) }
603 return args[1]
604 }
605 lisp.define("mapconcat") { lisp, args in
606 try arity(args, 1...3, "mapconcat")
607 let separator = args.count > 2 ? try string(args[2]) : ""
608 return .string(try sequence(args[1]).map { try printedString(lisp.call(args[0], [$0])) }.joined(separator: separator))
609 }
610 lisp.define("delq") { _, args in
611 try arity(args, 2...2, "delq")
612 return list(try elements(args[1]).filter { !eq($0, args[0]) })
613 }
614 lisp.define("delete") { _, args in
615 try arity(args, 2...2, "delete")
616 return list(try elements(args[1]).filter { normalized($0) != normalized(args[0]) })
617 }
618 lisp.define("error") { _, args in
619 let message = args.isEmpty ? "" : try format(args)
620 throw Signal(message: message)
621 }
622 lisp.define("user-error") { _, args in
623 throw Signal(message: args.isEmpty ? "" : try format(args))
624 }
625
626 // Strings.
627 lisp.define("concat") { _, args in .string(try args.map(printedString).joined()) }
628 lisp.define("format") { _, args in .string(try format(args)) }
629 lisp.define("format-message") { _, args in .string(try format(args)) }
630 lisp.define("string=") { _, args in
631 try arity(args, 2...2, "string=")
632 return bool(try stringOrSymbol(args[0]) == (try stringOrSymbol(args[1])))
633 }
634 lisp.define("string-equal") { _, args in
635 try arity(args, 2...2, "string-equal")
636 return bool(try stringOrSymbol(args[0]) == (try stringOrSymbol(args[1])))
637 }
638 lisp.define("string<") { _, args in
639 try arity(args, 2...2, "string<")
640 return bool(Array(try stringOrSymbol(args[0]).unicodeScalars.map(\.value)).lexicographicallyPrecedes(try stringOrSymbol(args[1]).unicodeScalars.map(\.value)))
641 }
642 lisp.define("string-lessp") { lisp, args in try lisp.callNamed("string<", args) }
643 lisp.define("upcase") { _, args in
644 try arity(args, 1...1, "upcase")
645 if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).uppercased().unicodeScalars.first!.value } ?? UInt32(c))) }
646 return .string(try string(args[0]).uppercased())
647 }
648 lisp.define("downcase") { _, args in
649 try arity(args, 1...1, "downcase")
650 if case .integer(let c) = args[0] { return .integer(Int(Unicode.Scalar(c).map { String($0).lowercased().unicodeScalars.first!.value } ?? UInt32(c))) }
651 return .string(try string(args[0]).lowercased())
652 }
653 lisp.define("capitalize") { _, args in
654 try arity(args, 1...1, "capitalize")
655 return .string(try string(args[0]).capitalized)
656 }
657 lisp.define("substring") { _, args in
658 try arity(args, 1...3, "substring")
659 let chars = Array(try string(args[0]))
660 func index(_ v: Sexp?, _ fallback: Int) throws -> Int {
661 guard let v, !isNil(v) else { return fallback }
662 let i = try int(v)
663 return i < 0 ? chars.count + i : i
664 }
665 let from = try index(args.count > 1 ? args[1] : nil, 0)
666 let to = try index(args.count > 2 ? args[2] : nil, chars.count)
667 guard from >= 0, to <= chars.count, from <= to else { throw Signal(message: "Args out of range") }
668 return .string(String(chars[from..<to]))
669 }
670 lisp.define("string-to-number") { _, args in
671 try arity(args, 1...2, "string-to-number")
672 return stringToNumber(try string(args[0]))
673 }
674 for name in ["number-to-string", "int-to-string"] {
675 lisp.define(name) { _, args in
676 try arity(args, 1...1, name)
677 return .string(printed(sexp(try number(args[0]))))
678 }
679 }
680 lisp.define("string-prefix-p") { _, args in
681 try arity(args, 2...3, "string-prefix-p")
682 return bool(try string(args[1]).hasPrefix(try string(args[0])))
683 }
684 lisp.define("string-suffix-p") { _, args in
685 try arity(args, 2...3, "string-suffix-p")
686 return bool(try string(args[1]).hasSuffix(try string(args[0])))
687 }
688 lisp.define("string-empty-p") { _, args in
689 try arity(args, 1...1, "string-empty-p")
690 return bool(try stringOrSymbol(args[0]).isEmpty)
691 }
692 lisp.define("string-trim") { _, args in
693 try arity(args, 1...1, "string-trim")
694 return .string(try string(args[0]).trimmingCharacters(in: .whitespacesAndNewlines))
695 }
696 lisp.define("split-string") { _, args in
697 try arity(args, 1...2, "split-string")
698 let s = try string(args[0])
699 if args.count == 2, !isNil(args[1]) {
700 let separator = try string(args[1])
701 let regex = try NSRegularExpression(pattern: separator)
702 let ns = s as NSString
703 var parts: [Sexp] = []
704 var start = 0
705 for m in regex.matches(in: s, range: NSRange(location: 0, length: ns.length)) where m.range.length > 0 {
706 parts.append(.string(ns.substring(with: NSRange(location: start, length: m.range.location - start))))
707 start = NSMaxRange(m.range)
708 }
709 parts.append(.string(ns.substring(from: start)))
710 return list(parts)
711 }
712 return list(s.split(whereSeparator: { " \t\n\r\u{0C}\u{0B}".contains($0) }).map { .string(String($0)) })
713 }
714 }
715
716 static func add(_ n: Number, _ k: Int) -> Number {
717 switch n {
718 case .int(let i): .int(i + k)
719 case .float(let d): .float(d + Double(k))
720 }
721 }
722
723 static func divide(_ a: Number, _ b: Number, floating: Bool) throws -> Number {
724 if !floating, case .int(let x) = a, case .int(let y) = b {
725 guard y != 0 else { throw Signal(message: "Arithmetic error") }
726 return .int(x / y)
727 }
728 return .float(a.double / b.double)
729 }
730
731 static func eq(_ a: Sexp, _ b: Sexp) -> Bool {
732 switch (normalized(a), normalized(b)) {
733 case (.symbol(let x), .symbol(let y)): x == y
734 case (.integer(let x), .integer(let y)): x == y
735 case (.float(let x), .float(let y)): x == y
736 default: false
737 }
738 }
739
740 /// Characters as integers and the empty list as nil, for comparing.
741 static func normalized(_ v: Sexp) -> Sexp {
742 switch v {
743 case .character(let c): .integer(c)
744 case .list(let items): items.isEmpty ? .nil : .list(items.map(normalized))
745 case .dotted(let items, let last): .dotted(items.map(normalized), normalized(last))
746 case .vector(let items): .vector(items.map(normalized))
747 default: v
748 }
749 }
750
751 static func sequence(_ v: Sexp) throws -> [Sexp] {
752 switch v {
753 case .string(let s): return s.unicodeScalars.map { .integer(Int($0.value)) }
754 case .vector(let items): return items
755 default: return try elements(v)
756 }
757 }
758
759 static func stringOrSymbol(_ v: Sexp) throws -> String {
760 if case .symbol(let s) = v { return s }
761 return try string(v)
762 }
763
764 /// What `concat` and `mapconcat` accept: strings, and lists or vectors of characters.
765 static func printedString(_ v: Sexp) throws -> String {
766 switch v {
767 case .string(let s): return s
768 case _ where isNil(v): return ""
769 case .list, .vector:
770 return String(String.UnicodeScalarView(try sequence(v).map { Unicode.Scalar(UInt32(try int($0))) ?? " " }))
771 default: throw Signal(message: "Wrong type argument: sequencep, \(v.description)")
772 }
773 }
774
775 /// `string-to-number` in base 10.
776 static func stringToNumber(_ s: String) -> Sexp {
777 let trimmed = s.drop { $0 == " " || $0 == "\t" || $0 == "\n" }
778 guard let r = trimmed.range(of: "^[-+]?([0-9]+\\.?[0-9]*|\\.[0-9]+)([eE][-+]?[0-9]+)?", options: .regularExpression) else { return .integer(0) }
779 let text = String(trimmed[r])
780 if text.range(of: "^[-+]?[0-9]+$", options: .regularExpression) != nil, let i = Int(text.hasPrefix("+") ? String(text.dropFirst()) : text) {
781 return .integer(i)
782 }
783 if text.range(of: "^[-+]?[0-9]+\\.$", options: .regularExpression) != nil, let i = Int(text.dropLast().replacingOccurrences(of: "+", with: "")) {
784 return .integer(i)
785 }
786 return .float(Double(text) ?? 0)
787 }
788
789 /// `format`.
790 static func format(_ args: [Sexp]) throws -> String {
791 let spec = Array(try string(args[0]))
792 var out = ""
793 var next = 1
794 var i = 0
795 func argument() throws -> Sexp {
796 guard next < args.count else { throw Signal(message: "Not enough arguments for format string") }
797 defer { next += 1 }
798 return args[next]
799 }
800 while i < spec.count {
801 let c = spec[i]
802 i += 1
803 guard c == "%" else {
804 out.append(c)
805 continue
806 }
807 var flags = ""
808 while i < spec.count, "-+ #0".contains(spec[i]) {
809 flags.append(spec[i])
810 i += 1
811 }
812 var width = ""
813 while i < spec.count, spec[i].isNumber {
814 width.append(spec[i])
815 i += 1
816 }
817 var precision: Int?
818 if i < spec.count, spec[i] == "." {
819 i += 1
820 var digits = ""
821 while i < spec.count, spec[i].isNumber {
822 digits.append(spec[i])
823 i += 1
824 }
825 precision = Int(digits) ?? 0
826 }
827 guard i < spec.count else { throw Signal(message: "Format string ends in middle of format specifier") }
828 let conversion = spec[i]
829 i += 1
830 var text: String
831 switch conversion {
832 case "%":
833 out.append("%")
834 continue
835 case "s", "S":
836 text = printed(try argument(), escape: conversion == "S")
837 if let precision { text = String(text.prefix(precision)) }
838 case "d", "o", "x", "X":
839 let value: Int
840 switch try number(try argument()) {
841 case .int(let n): value = n
842 case .float(let d):
843 guard let n = Int(exactly: d.rounded(.towardZero)) else { throw Signal(message: "Arithmetic overflow") }
844 value = n
845 }
846 text = String(format: "%" + flags + width + (precision.map { ".\($0)" } ?? "") + (conversion == "d" ? "ld" : "l" + String(conversion)), value)
847 out += text
848 continue
849 case "c":
850 text = Unicode.Scalar(UInt32(try int(try argument()))).map { String($0) } ?? ""
851 case "e", "f", "g":
852 let value = try number(try argument()).double
853 out += String(format: "%" + flags + width + (precision.map { ".\($0)" } ?? "") + String(conversion), value)
854 continue
855 default:
856 throw Signal(message: "Invalid format operation %\(conversion)")
857 }
858 let pad = max(0, (Int(width) ?? 0) - text.count)
859 out += flags.contains("-") ? text + String(repeating: " ", count: pad) : String(repeating: " ", count: pad) + text
860 }
861 return out
862 }
863}
864
865extension Sexp {
866 var character: Int? { if case .character(let c) = self { return c } else { return nil } }
867}
868
869extension LispReader {
870 /// The first form in `text` and the number of characters it spans, as `forward-sexp`
871 /// would read it.
872 public static func readFirst(_ text: String) throws -> (sexp: Sexp, length: Int) {
873 var reader = Reader(Array(text.unicodeScalars))
874 let sexp = try reader.form()
875 let consumed = String(String.UnicodeScalarView(reader.chars[..<reader.index]))
876 return (sexp, consumed.count)
877 }
878}
Sources/OrgCore/Parser/Inline.swift +13 −1
@@ -243,7 +243,7 @@ struct InlineScanner {
243 if !inLink, let end = timestampEnd(chars, at: i, limit: limit) { return .object(.timestamp, i..<end) } 243 if !inLink, let end = timestampEnd(chars, at: i, limit: limit) { return .object(.timestamp, i..<end) }
244 if let end = statisticsCookie(i, limit) { return .object(.statisticsCookie, i..<end) } 244 if let end = statisticsCookie(i, limit) { return .object(.statisticsCookie, i..<end) }
245 case "<": 245 case "<":
246 if !inLink, let end = timestampEnd(chars, at: i, limit: limit) { return .object(.timestamp, i..<end) } 246 if !inLink, let end = timestampEnd(chars, at: i, limit: limit) ?? diaryTimestamp(i, limit) { return .object(.timestamp, i..<end) }
247 if let end = radioTarget(i, limit) { return .object(.radioTarget, i..<end) } 247 if let end = radioTarget(i, limit) { return .object(.radioTarget, i..<end) }
248 if let end = target(i, limit) { return .object(.target, i..<end) } 248 if let end = target(i, limit) { return .object(.target, i..<end) }
249 if !inLink, let end = angleLink(i, limit) { return .object(.link, i..<end) } 249 if !inLink, let end = angleLink(i, limit) { return .object(.link, i..<end) }
@@ -625,6 +625,18 @@ struct InlineScanner {
625 } 625 }
626 626
627 /// `<<<target>>>`. 627 /// `<<<target>>>`.
628 /// A diary timestamp, `<%%(SEXP)REST>` (`org-element--timestamp-regexp`). It ends at the
629 /// first `]` or `>`, as its raw value does.
630 func diaryTimestamp(_ i: Int, _ limit: Int) -> Int? {
631 guard hasPrefix("<%%(", at: i, limit) else { return nil }
632 var close = i + 4
633 while close < limit, chars[close] != ">", chars[close] != "\n" { close += 1 }
634 guard close < limit, chars[close] == ">", close > i + 5, chars[(i + 5)..<close].contains(")") else { return nil }
635 var end = i + 1
636 while chars[end] != "]", chars[end] != ">" { end += 1 }
637 return end + 1
638 }
639
628 func radioTarget(_ i: Int, _ limit: Int) -> Int? { 640 func radioTarget(_ i: Int, _ limit: Int) -> Int? {
629 guard hasPrefix("<<<", at: i, limit) else { return nil } 641 guard hasPrefix("<<<", at: i, limit) else { return nil }
630 let start = i + 3 642 let start = i + 3
Sources/OrgstarMobile/AgendaScreen.swift +1 −1
@@ -174,7 +174,7 @@ struct AgendaScreen: View {
174 174
175 private func row(_ item: AgendaItem) -> some View { 175 private func row(_ item: AgendaItem) -> some View {
176 Group { 176 Group {
177 if let path = item.path, let offset = item.headingOffset { 177 if let path = item.path, let offset = item.headingOffset ?? item.markerOffset {
178 NavigationLink(value: ReaderRoute.offset(path, offset)) { entry(item) } 178 NavigationLink(value: ReaderRoute.offset(path, offset)) { entry(item) }
179 } else { 179 } else {
180 entry(item) 180 entry(item)
Tests/OrgCoreTests/AgendaTests.swift +1 −1
@@ -148,7 +148,7 @@ enum AgendaOracle {
148 case .timeGrid, .currentTime: "-" 148 case .timeGrid, .currentTime: "-"
149 default: item.kind.rawValue 149 default: item.kind.rawValue
150 } 150 }
151 return "\(normalize(line)) | \(type) \(item.category) \(item.timeOfDay.map(String.init) ?? "-") [\(item.extra)] \(item.urgency) \(item.path ?? "-")@\(item.headingOffset.map(String.init) ?? "-")" 151 return "\(normalize(line)) | \(type) \(item.category) \(item.timeOfDay.map(String.init) ?? "-") [\(item.extra)] \(item.urgency) \(item.headingOffset == nil ? "-" : item.path ?? "-")@\(item.headingOffset.map(String.init) ?? "-")"
152 } 152 }
153 } 153 }
154 154
Tests/OrgCoreTests/DiarySexpTests.swift added +74
@@ -0,0 +1,74 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// Diary sexps in the agenda (`org-agenda-get-sexps`, diary timestamps and planning).
6struct DiarySexpTests {
7 static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
8
9 static let file = """
10 #+FILETAGS: :ft:
11 * Holidays :hol:
12 %%(diary-float t 4 4) Fourth Thursday
13 %%(diary-anniversary 10 7 1990) Birthday %d%s
14 %%(org-anniversary 2000 10 8) Org %d years
15 %%(diary-anniversary 2 29 2000) Leap %d
16 %%(diary-block 10 5 2026 10 7 2026) Conference
17 %%(diary-cyclic 3 10 1 2026) Every third, %d%s time
18 %%(diary-date t 6 t) Sixth of month
19 %%(diary-date '(10 11) '(5 9) 2026) List date
20 %%(diary-remind '(diary-date 10 9 2026) 2) Remind me
21 %%(diary-remind '(diary-date 10 12 2026) -3) Remind range
22 %%(diary-remind '(diary-anniversary 10 14 2000) 7) Week ahead %d
23 %%(org-class 2026 9 1 2026 12 20 2 41) Class on Tuesdays
24 %%(org-block 2026 10 6 2026 10 6) Meeting at 10:30
25 %%(memq (calendar-day-of-week date) '(1 3 5)) Gym
26 %%(and (= (calendar-extract-day date) 2) "Custom; second part")
27 %%(let ((d (calendar-extract-day date))) (when (= (% d 5) 0) (format "Day %d is a fifth" d)))
28 %%(diary-float 10 1 -1) Last Monday of October
29 %%(diary-float t 0 1 8) First Sunday after the 8th
30 %%(diary-date 10 8 2026)
31 &%%(diary-date 10 9 2026) Ampersand
32 * TODO Task with a diary stamp <%%(diary-float t 2 1)>
33 * Meeting
34 <%%(diary-date t 7 t) 14:00>
35 * Due
36 DEADLINE: <%%(diary-date 10 8 2026)>
37 * Sched
38 SCHEDULED: <%%(diary-float t 5 1)>
39 * COMMENT Skipped tree
40 %%(diary-date t t t) Never shown
41 """ + "\n"
42
43 @Test func evaluatesDiaryFunctions() throws {
44 let day = Days.absolute(year: 2026, month: 10, day: 7)
45 func run(_ text: String, _ entry: String = "E") throws -> [String]? {
46 DiarySexp.entries(try LispReader.readFirst(text).sexp, entry: entry, day: day)
47 }
48 #expect(try run("(diary-anniversary 10 7 1990)", "%d%s") == ["36th"])
49 #expect(try run("(diary-date 10 8 2026)") == nil)
50 #expect(try run("(diary-float 10 3 1)") == ["E"])
51 #expect(try run("\"a; b\"") == ["a", "b"])
52 #expect(try run("(undefined)") == nil)
53 #expect(try run("(diary-ordinal-suffix -3)") == nil)
54 }
55
56 @Test func skipsSexpsBeforeTheFirstHeading() {
57 let source = AgendaSource(path: "a.org", text: "%%(diary-date t t t) Before\n* H\n%%(diary-date t t t) Under\n")
58 let day = Days.absolute(year: 2026, month: 10, day: 7)
59 #expect(Agenda.list([source], start: day, days: 1, today: day, now: (9, 0))[0].items.map(\.text) == ["Under"])
60 }
61
62 @Test(.enabled(if: oracle))
63 func agendaMatchesEmacs() throws {
64 let files = [("diary.org", Self.file)]
65 let october = Days.absolute(year: 2026, month: 10, day: 1)
66 let ours = Agenda.list([AgendaSource(path: "diary.org", text: Self.file)], start: october, days: 15, today: october + 4, now: (9, 30))
67 let kinds = Set(ours.flatMap(\.items).map(\.kind))
68 #expect(kinds.isSuperset(of: [.sexp, .timestamp, .deadline, .pastScheduled]))
69 try AgendaOracle.compare(files: files, start: october, span: 15, today: october + 4)
70 try AgendaOracle.compare(files: files, start: october + 20, span: 10, today: october + 26)
71 let leap = Days.absolute(year: 2027, month: 2, day: 27)
72 try AgendaOracle.compare(files: files, start: leap, span: 4, today: leap)
73 }
74}
Tests/OrgCoreTests/ElispTests.swift added +48
@@ -0,0 +1,48 @@
1import Foundation
2import Testing
3@testable import OrgCore
4
5/// The Lisp evaluator against Emacs: each form's `prin1` text, or `error` when it signals.
6struct ElispTests {
7 static let oracle = ProcessInfo.processInfo.environment["ORGSTAR_SKIP_ORACLE"] == nil
8
9 static let forms = [
10 "(+ 1 2 3)", "(- 10)", "(- 10 2.5)", "(* 2 3.0)", "(/ 7 2)", "(/ -7 2)", "(/ 7 2.0)", "(/ 7 0)", "(% -7 2)", "(mod -7 2)",
11 "(mod 5.5 2)", "(1+ 4)", "(abs -3.5)", "(max 1 2.0)", "(min 3 1)", "(floor 2.7)", "(floor -7 2)", "(round 2.5)",
12 "(round 3.5)", "(truncate -2.7)", "(float 3)", "(= 1 1.0)", "(< 1 2 3)", "(< 1 3 2)", "(/= 1 2)", "(zerop 0.0)",
13 "(eq 'a 'a)", "(equal '(1 \"a\") (list 1 \"a\"))", "(null nil)", "(not 0)", "(car '(1 2))", "(cdr '(1 2))",
14 "(cdr '(1 . 2))", "(cons 1 2)", "(cons 1 '(2))", "(nth 2 '(a b c))", "(nth 5 '(a))", "(length \"héllo\")",
15 "(reverse '(1 2 3))", "(append '(1) '(2) 3)", "(number-sequence 1 5 2)", "(memq 'b '(a b c))", "(member \"b\" '(\"a\" \"b\"))",
16 "(assoc \"k\" '((\"k\" . 1)))", "(mapcar #'1+ '(1 2 3))", "(mapconcat #'identity '(\"a\" \"b\") \"-\")",
17 "(apply #'+ 1 '(2 3))", "(funcall (lambda (x &optional y) (list x y)) 1)", "(let ((x 1) (y 2)) (+ x y))",
18 "(let* ((x 1) (y (1+ x))) y)", "(let ((n 0)) (dotimes (i 4) (setq n (+ n i))) n)", "(let (r) (dolist (x '(1 2)) (push x r)) r)",
19 "(cond ((= 1 2) 'a) ((= 1 1) 'b))", "(and 1 2)", "(or nil 3)", "(if nil 1 2 3)", "(when t 1 2)", "(unless t 1)",
20 "(concat \"a\" \"b\" '(99))", "(format \"%d|%5.2f|%s|%S|%-4s|%x|%c|%%\" 42 3.14159 \"s\" \"q\" \"ab\" 255 65)",
21 "(format \"%s %s\" 1.0 100.5)", "(format \"%d\" 2.9)", "(format \"%03d\" 7)", "(format \"%e\" 12345.678)", "(format \"%g\" 0.0001)",
22 "(substring \"hello\" 1 3)", "(substring \"hello\" -3)", "(string-to-number \" 12abc\")", "(string-to-number \"1.5e2\")",
23 "(string-to-number \"3.\")", "(string-to-number \"x\")", "(number-to-string 0.1)", "(number-to-string 1e21)", "(upcase \"ab\")",
24 "(string= \"a\" \"a\")", "(string< \"a\" \"b\")", "(split-string \" a b \")", "(split-string \"a,b,,c\" \",\")",
25 "(string-prefix-p \"ab\" \"abc\")", "(condition-case nil (/ 1 0) (error 'caught))", "(ignore-errors (car 1))", "(car 1)",
26 "(let ((x 0)) (while (< x 5) (setq x (1+ x))) x)", "(elt [1 2 3] 1)", "(aref \"abc\" 1)", "(length [1 2])", "(nthcdr 1 '(1 2 3))",
27 "(delq nil '(1 nil 2))", "(cadr '(1 2 3))", "(identity \"x\")", "(capitalize \"hello world\")", "(string-trim \" x \")",
28 ]
29
30 static func ours(_ form: String) -> String {
31 guard let sexp = try? LispReader.readFirst(form).sexp else { return "read-error" }
32 do {
33 return Elisp.printed(try Elisp().eval(sexp), escape: true)
34 } catch {
35 return "error"
36 }
37 }
38
39 @Test(.enabled(if: oracle))
40 func matchesEmacs() throws {
41 let list = "(list " + Self.forms.map { "(condition-case nil (prin1-to-string \($0)) (error \"error\"))" }.joined(separator: " ") + ")"
42 let emacs = try EmacsOracle.evaluate("", "(let ((print-escape-newlines t)) \(list))")
43 #expect(emacs.count == Self.forms.count)
44 for (form, expected) in zip(Self.forms, emacs) {
45 #expect(Self.ours(form) == expected, "\(form)")
46 }
47 }
48}
Tests/OrgCoreTests/ParserGapsTests.swift +2 −1
@@ -11,6 +11,7 @@ struct ParserGapsTests {
11 and on @@html:<b>bold</b>@@ or @@latex:\emph{x}@@, not @@x@@ or @@:y@@ or @@html:open. 11 and on @@html:<b>bold</b>@@ or @@latex:\emph{x}@@, not @@x@@ or @@:y@@ or @@html:open.
12 Calls: call_square(4) call_f[:results raw](x=1, y=(2))[:exports both] xcall_no(1) call_f[x] call_(1). 12 Calls: call_square(4) call_f[:results raw](x=1, y=(2))[:exports both] xcall_no(1) call_f[x] call_(1).
13 %%(diary-float t 4 2) Thanksgiving 13 %%(diary-float t 4 2) Thanksgiving
14 Diary <%%(diary-float t 4 2)>, <%%(x) 10:00>, <%%()>, <%%(a]b)>, <%%(open and <%%(y)
14 %%(indented) is text 15 %%(indented) is text
15 16
16 +----+-----+ 17 +----+-----+
@@ -47,7 +48,7 @@ struct ParserGapsTests {
47 48
48 static let kinds: [(lisp: String, ours: SyntaxKind)] = [ 49 static let kinds: [(lisp: String, ours: SyntaxKind)] = [
49 ("citation", .citation), ("citation-reference", .citationReference), ("export-snippet", .exportSnippet), 50 ("citation", .citation), ("citation-reference", .citationReference), ("export-snippet", .exportSnippet),
50 ("inline-babel-call", .inlineBabelCall), ("diary-sexp", .diarySexp), ("paragraph", .paragraph), ("item", .item), 51 ("inline-babel-call", .inlineBabelCall), ("timestamp", .timestamp), ("diary-sexp", .diarySexp), ("paragraph", .paragraph), ("item", .item),
51 ] 52 ]
52 53
53 @Test func objectsAndElementsMatchOrgElement() throws { 54 @Test func objectsAndElementsMatchOrgElement() throws {