/
Fixed+Conversion.swift
155 lines (130 loc) · 8.3 KB
/
Fixed+Conversion.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import Foundation
/// An enum dictating how the conversion of a fixed value from one region to another should behave
///
/// - SeeAlso: ``Fixed/converted(to:behavior:)-6z6df``
/// - SeeAlso: ``Fixed/converted(to:behavior:)-3fufq``
/// - SeeAlso: ``Fixed/converted(to:behavior:)-3meoh``
public enum ConversionBehavior {
/// When converting a fixed value, the [components](<doc:Terminology>) (day, hour, minute, etc) should be preserved.
///
/// This is a failable operation and may result in a ``TimeError`` being thrown, as the value's
/// components may not exist in the new region. For example, a 2 AM value in a European region may
/// throw an error when converted to a US region, if that fixed value happens to occur during a daylight
/// saving time transition.
///
/// Similarly, a fixed value representing a 13th month on a lunisolar calendar would fail
/// when converted to a gregorian region.
///
/// - Warning: In general, this operation only makes sense to perform on fixed values that represent a day (or larger) range.
case preservingComponents
/// When converting a fixed value, the ``Fixed/range`` should be preserved.
///
/// This operation represents answering the question "if it's 2 PM in Los Angeles, what time is it in Rome?".
///
/// - Warning: In general, this operation only makes sense to perform on fixed values that represent a day (or smaller) range.
/// Attempting to use this on larger units (months, years, and eras) will likely result in a thrown ``TimeError``, since most calendars
/// do not have the same month, year, or era boundaries as other calendars.
case preservingRange
}
extension Fixed {
/// Convert a fixed value to a new region.
///
/// - Parameters:
/// - region: The region to which the new fixed value should belong
/// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen
/// - Returns: A new fixed value that has been converted to the specified time zone.
/// - Throws: A ``TimeError`` if the conversion could not be completed
/// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``.
public func converted(to newRegion: Region, behavior: ConversionBehavior) throws -> Self {
if newRegion.isEquivalent(to: self.region) { return self }
switch behavior {
case .preservingComponents:
return try Fixed(region: newRegion, strictDateComponents: self.dateComponents)
case .preservingRange:
let currentRange = self.range
let midPointValue = Fixed(region: newRegion, instant: self.approximateMidPoint)
if midPointValue.range == currentRange { return midPointValue }
let startValue = Fixed(region: newRegion, instant: currentRange.lowerBound)
if startValue.range == currentRange { return startValue }
if self.instant != currentRange.lowerBound {
let instantValue = Fixed(region: newRegion, instant: self.instant)
if instantValue.range == currentRange { return instantValue }
}
throw TimeError.invalidDateComponents(self.dateComponents, in: newRegion)
}
}
/// Convert a fixed value to a new time zone.
///
/// - Parameters:
/// - timeZone: The time zone to which the new fixed value should belong
/// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen
/// - Returns: A new fixed value that has been converted to the specified time zone.
/// - Throws: A ``TimeError`` if the conversion could not be completed
/// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``.
public func converted(to timeZone: TimeZone, behavior: ConversionBehavior) throws -> Self {
let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale)
return try self.converted(to: newRegion, behavior: behavior)
}
/// Convert a fixed value to a new calendar.
///
/// - Parameters:
/// - calendar: The calendar to which the new fixed value should belong
/// - behavior: The ``ConversionBehavior`` specifying how the conversion should happen
/// - Returns: A new fixed value that has been converted to the specified calendar.
/// - Throws: A ``TimeError`` if the conversion could not be completed
/// - Warning: This operation may fail for many possible reasons and should be used with care. For full details, see ``ConversionBehavior``.
public func converted(to calendar: Calendar, behavior: ConversionBehavior) throws -> Self {
let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale)
return try self.converted(to: newRegion, behavior: behavior)
}
/// Construct a new `Fixed` value by converting this fixed value to a new `Locale`.
///
/// Changing a fixed value's locale affects how the value is formatted. It does not change the underlying components.
public func converted(to locale: Locale) -> Self {
let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale)
return Self(region: newRegion, instant: self.instant, components: self.dateComponents)
}
}
extension Fixed where Granularity: GTOEDay {
/// Convert a fixed date to another time zone
///
/// This works by transitioning the underlying *components* to a new time zone. If successful, the resulting value
/// will have the same `.year`, `.month`, etc as the original value. However, the resulting `.range` will be different.
///
/// - Parameter timeZone: The new time zone of the resulting fixed value
/// - Returns: A new fixed value with the same underlying components
/// - Throws: Throws a ``TimeError`` if the underlying components do not exist in the specified `timeZone`. For example,
/// converting "30 December 2011" to the `Pacific/Apia` time zone throws an error, because that day did not exist in that time zone.
public func converted(to timeZone: TimeZone) throws -> Self {
let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale)
return try Self(region: newRegion, strictDateComponents: self.dateComponents)
}
}
extension Fixed where Granularity: LTOEDay {
/// Construct a new `Fixed` value by converting this fixed value to a new `Calendar`.
///
/// - Note: This functionality is only available when dealing with fixed values that represent a day or smaller. All
/// supported calendars have the same basic definition of a day, being roughly `00:00:00 ... 23:59:59.999`.
/// Therefore, converting such a value to another calendar will result in the old temporal range being equivalent to
/// the new temporal range. This is not true for eras, years, and months: calendars have different definitions of when years start
/// and when months change, etc. Therefore, it is not possible to map "February 2024" to a non-gregorian calendar, since
/// there is not a guaranteed correspondance between their underlying `Range<Instant>` values.
public func converted(to calendar: Calendar) -> Self {
let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale)
return Self(region: newRegion, instant: self.approximateMidPoint)
}
}
extension Fixed where Granularity: LTOEHour {
/// Convert a fixed time to another time zone.
///
/// This works by transitioning the underlying time range to the new time zone. Therefore, the resulting components
/// (`.hour`, `.minute`, etc) will be *different* from the original components. However, the resulting `.range` will
/// be the same.
///
/// - Parameter timeZone: The new time zone of the resulting fixed value
/// - Returns: A fixed value representing the same range of time in a different `TimeZone`.
public func converted(to timeZone: TimeZone) -> Self {
let newRegion = Region(calendar: calendar, timeZone: timeZone, locale: locale)
return Self(region: newRegion, instant: self.firstInstant)
}
}