krz/moonrise

Night-sky companion for iPhone, Apple Watch and Vision Pro: what's up tonight, the Moon, planets, launches and missions, and a Library of the solar system. SwiftUI app with a Cloudflare Workers backend. astronomy cloudflare-workers ios night-sky space swiftui visionos watchos

Packages/MoonriseKit/Sources/Answers/Phrasing.swift

79 lines · 3032 bytes

12 symbols in this file
 1import Foundation
 2
 3/// One label and value in an answer's result view. `alert` values are drawn in the accent red.
 4public struct AnswerRow: Sendable, Hashable {
 5    public var label: String
 6    public var value: String
 7    public var alert = false
 8
 9    public init(_ label: String, _ value: String, alert: Bool = false) {
10        self.label = label
11        self.value = value
12        self.alert = alert
13    }
14}
15
16/// Times, days, durations and numbers for spoken answers and their rows.
17struct Phrasing {
18    var locale: Locale
19    var timeZone: TimeZone
20    var calendar: Calendar
21
22    init(locale: Locale, timeZone: TimeZone) {
23        self.locale = locale
24        self.timeZone = timeZone
25        calendar = Calendar(identifier: .gregorian)
26        calendar.timeZone = timeZone
27        calendar.locale = locale
28    }
29
30    private var style: Date.FormatStyle { Date.FormatStyle(locale: locale, calendar: calendar, timeZone: timeZone) }
31
32    /// "19:42" or "7:42 PM".
33    func time(_ d: Date) -> String {
34        d.formatted(Date.FormatStyle(date: .omitted, time: .shortened, locale: locale, timeZone: timeZone))
35    }
36
37    /// "today at 19:42", "tomorrow at 05:10", "Thursday at 03:12" or "on 28 October at 03:12".
38    func when(_ d: Date, from now: Date) -> String {
39        let at = "at \(time(d))"
40        if calendar.isDate(d, inSameDayAs: now) { return "today \(at)" }
41        if let next = calendar.date(byAdding: .day, value: 1, to: now), calendar.isDate(d, inSameDayAs: next) { return "tomorrow \(at)" }
42        if let week = calendar.date(byAdding: .day, value: 6, to: calendar.startOfDay(for: now)), d < week {
43            return "\(d.formatted(style.weekday(.wide))) \(at)"
44        }
45        return "on \(d.formatted(style.day().month(.wide))) \(at)"
46    }
47
48    /// "19:42" today, else "Thu 22 Oct 19:42", for rows.
49    func short(_ d: Date, from now: Date) -> String {
50        calendar.isDate(d, inSameDayAs: now) ? time(d) : d.formatted(style.weekday(.abbreviated).day().month(.abbreviated).hour().minute())
51    }
52
53    /// "11 hours, 30 minutes" (spoken) or "11 hr, 30 min" (rows).
54    func duration(_ seconds: TimeInterval, spoken: Bool) -> String {
55        Duration.seconds(seconds.rounded()).formatted(.units(allowed: [.days, .hours, .minutes], width: spoken ? .wide : .abbreviated, maximumUnitCount: 2).locale(locale))
56    }
57
58    func number(_ x: Double, digits: Int) -> String {
59        x.formatted(.number.precision(.fractionLength(digits)).locale(locale))
60    }
61
62    func list(_ items: [String]) -> String {
63        items.formatted(.list(type: .and).locale(locale))
64    }
65}
66
67enum Compass {
68    static func point(_ azimuth: Double) -> String {
69        ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][index(azimuth)]
70    }
71
72    static func name(_ azimuth: Double) -> String {
73        ["north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest"][index(azimuth)]
74    }
75
76    private static func index(_ azimuth: Double) -> Int {
77        Int(((azimuth + 22.5) / 45).rounded(.down)) % 8
78    }
79}