krz/orgstar

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

Sources/OrgCore/Agenda/Days.swift

778058aafc7ddacee4a613b1da87c60aa0c8e113
orgstar/Sources/OrgCore/Agenda/Days.swift history · blame · raw

144 lines · 6952 bytes

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