Sources/OrgCore/Agenda/Days.swift
144 lines · 6952 bytes
1import Foundation
2
3/// Calendar days as Emacs numbers them (`calendar-absolute-from-gregorian`): 1 is
4/// 1 January of year 1, proleptic Gregorian.
5public enum Days {
6 /// Out-of-range days and months roll over, as with `encode-time`.
7 public static func absolute(year: Int, month: Int, day: Int) -> Int {
8 let monthIndex = month - 1
9 let y = year + Int((Double(monthIndex) / 12).rounded(.down))
10 let m = monthIndex - 12 * Int((Double(monthIndex) / 12).rounded(.down)) + 1
11 // Days from civil (Howard Hinnant), with 1970-01-01 = 0.
12 let yy = m <= 2 ? y - 1 : y
13 let era = (yy >= 0 ? yy : yy - 399) / 400
14 let yoe = yy - era * 400
15 let doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5
16 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy
17 return era * 146097 + doe - 719468 + 719163 + day - 1
18 }
19
20 public static func date(_ absolute: Int) -> (year: Int, month: Int, day: Int) {
21 let z = absolute - 719163 + 719468
22 let era = (z >= 0 ? z : z - 146096) / 146097
23 let doe = z - era * 146097
24 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365
25 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100)
26 let mp = (5 * doy + 2) / 153
27 let day = doy - (153 * mp + 2) / 5 + 1
28 let month = mp < 10 ? mp + 3 : mp - 9
29 return (yoe + era * 400 + (month <= 2 ? 1 : 0), month, day)
30 }
31
32 /// 0 is Sunday, as `calendar-day-of-week`.
33 public static func weekday(_ absolute: Int) -> Int {
34 ((absolute % 7) + 7) % 7
35 }
36
37 /// `org-days-to-iso-week`.
38 public static func isoWeek(_ absolute: Int) -> Int {
39 let monday = absolute - (weekday(absolute) + 6) % 7
40 let thursday = monday + 3
41 let year = date(thursday).year
42 return (thursday - Days.absolute(year: year, month: 1, day: 1)) / 7 + 1
43 }
44
45 /// The day of a `Date` in `calendar`'s time zone.
46 public static func absolute(_ date: Date, calendar: Calendar) -> Int {
47 let c = calendar.dateComponents([.year, .month, .day], from: date)
48 return absolute(year: c.year!, month: c.month!, day: c.day!)
49 }
50
51 static let dateRegex = try! NSRegularExpression(pattern: "([0-9]{4})-([0-9]{2})-([0-9]{2})")
52 static let timeRegex = try! NSRegularExpression(pattern: "([0-9]{1,2}):([0-9]{2})")
53
54 /// `time-to-days` of `org-time-string-to-time`: the first date in the string.
55 static func absolute(of string: String) -> Int? {
56 let ns = string as NSString
57 guard let m = dateRegex.firstMatch(in: string, range: NSRange(location: 0, length: ns.length)) else { return nil }
58 func int(_ i: Int) -> Int { Int(ns.substring(with: m.range(at: i)))! }
59 return absolute(year: int(1), month: int(2), day: int(3))
60 }
61
62 /// The hour of `org-parse-time-string`, 0 when there is none.
63 static func hour(of string: String) -> Int {
64 let ns = string as NSString
65 guard let date = dateRegex.firstMatch(in: string, range: NSRange(location: 0, length: ns.length)) else { return 0 }
66 let rest = NSRange(location: NSMaxRange(date.range), length: ns.length - NSMaxRange(date.range))
67 // org-ts-regexp0: a day name may sit between the date and the time.
68 let pattern = try! NSRegularExpression(pattern: "^( +[^\\]+0-9>\\r\\n -]+)?( +([0-9]{1,2}):([0-9]{2}))")
69 guard let m = pattern.firstMatch(in: string, range: rest) else { return 0 }
70 return Int(ns.substring(with: m.range(at: 3)))!
71 }
72
73 public enum Prefer { case past, future, nearest }
74
75 static let repeatRegex = try! NSRegularExpression(pattern: "\\+([0-9]+)([hdwmy])")
76
77 /// `org-closest-date`: the occurrence of a repeating timestamp `start` closest to `current`.
78 /// Timestamps without a repeater give their own day.
79 static func closest(_ start: String, to current: Int, prefer: Prefer) -> Int? {
80 guard let sday = absolute(of: start) else { return nil }
81 let ns = start as NSString
82 guard let m = repeatRegex.firstMatch(in: start, range: NSRange(location: 0, length: ns.length)) else { return sday }
83 let value = Int(ns.substring(with: m.range(at: 1)))!
84 let unit = ns.substring(with: m.range(at: 2))
85 if value == 0 { return sday }
86 let cday = current
87 if cday <= sday { return sday }
88 let base = date(sday)
89 let target = date(cday)
90 var n1: Int
91 var n2: Int
92 switch unit {
93 case "h":
94 let missing = mod(24 * (cday - sday) - hour(of: start), value)
95 n1 = missing == 0 ? cday : cday - (1 + missing / 24)
96 n2 = cday + (value - missing) / 24
97 case "d", "w":
98 let step = unit == "w" ? 7 * value : value
99 n1 = sday + step * ((cday - sday) / step)
100 n2 = n1 + step
101 case "m":
102 func add(_ d: (year: Int, month: Int, day: Int), _ n: Int) -> (year: Int, month: Int, day: Int) {
103 (d.year, d.month + n, d.day)
104 }
105 let months = ((12 * (target.year - base.year) + (target.month - base.month) + (target.day > base.day ? 0 : -1)) / value) * value
106 let before = add(base, months)
107 n1 = absolute(year: before.year, month: before.month, day: before.day)
108 let after = add(before, value)
109 n2 = absolute(year: after.year, month: after.month, day: after.day)
110 default:
111 let later = target.month > base.month || (target.month == base.month && target.day > base.day)
112 let years = ((target.year - base.year - (later ? 0 : 1)) / value) * value
113 n1 = absolute(year: base.year + years, month: base.month, day: base.day)
114 n2 = absolute(year: base.year + years + value, month: base.month, day: base.day)
115 }
116 switch prefer {
117 case .past: return cday == n2 ? n2 : n1
118 case .future: return cday == n1 ? n1 : n2
119 case .nearest: return abs(cday - n1) > abs(cday - n2) ? n2 : n1
120 }
121 }
122
123 private static func mod(_ a: Int, _ b: Int) -> Int { ((a % b) + b) % b }
124
125 static let wdaysRegex = try! NSRegularExpression(pattern: "-([0-9]+)([hdwmy])(\\z|>| )")
126
127 /// `org-get-wdays`: the warning days of a deadline, or with `delay` the delay days of a
128 /// scheduled timestamp.
129 static func warningDays(_ stamp: String, delay: Bool, defaultDays: Int) -> Int {
130 let tv = defaultDays
131 if (delay && tv < 0) || (!delay && tv <= 0) { return -tv }
132 let ns = stamp as NSString
133 guard let m = wdaysRegex.firstMatch(in: stamp, range: NSRange(location: 0, length: ns.length)) else { return tv }
134 let n = Double(ns.substring(with: m.range(at: 1)))!
135 let factor: Double = switch ns.substring(with: m.range(at: 2)) {
136 case "d": 1
137 case "w": 7
138 case "m": 30.4
139 case "y": 365.25
140 default: 0.041667
141 }
142 return Int((n * factor).rounded(.down))
143 }
144}