Packages/MoonriseKit/Sources/Answers/Phrasing.swift
79 lines · 3032 bytes
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}