-
Notifications
You must be signed in to change notification settings - Fork 316
/
DistanceFormatter.swift
332 lines (281 loc) · 13 KB
/
DistanceFormatter.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
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
import CoreLocation
import MapboxDirections
/**
:nodoc:
An object responsible for the rounding behavior of distances according to locale.
*/
public struct RoundingTable {
/**
:nodoc:
`Threshold` supplies rounding behavior for a given maximum distance.
*/
public struct Threshold {
/**
:nodoc:
The maximum distance that the `Threshold` is applicable.
*/
public let maximumDistance: Measurement<UnitLength>
/**
:nodoc:
The increment that a given distance with be rounded to.
*/
public let roundingIncrement: Double
/**
:nodoc:
The maximum number of digits following the decimal point.
*/
public let maximumFractionDigits: Int
/**
:nodoc:
Initializes a `Threshold` object with a given maximum distance, rounding increment, and maximum fraction of digits.
*/
public init(maximumDistance: Measurement<UnitLength>, roundingIncrement: Double, maximumFractionDigits: Int) {
self.maximumDistance = maximumDistance
self.roundingIncrement = roundingIncrement
self.maximumFractionDigits = maximumFractionDigits
}
/**
:nodoc:
Returns a rounded `Measurement<UnitLength>` for a given distance.
*/
public func measurement(of distance: CLLocationDistance) -> Measurement<UnitLength> {
var measurement = Measurement(value: distance, unit: .meters).converted(to: maximumDistance.unit)
measurement.value.round(roundingIncrement: roundingIncrement)
measurement.value.round(precision: pow(10, Double(maximumFractionDigits)))
return measurement
}
}
/**
:nodoc:
An array of `Threshold`s that detail the rounding behavior.
*/
public let thresholds: [Threshold]
/**
Returns the most applicable threshold for the given distance, falling back to the last threshold.
*/
public func threshold(for distance: CLLocationDistance) -> Threshold {
return thresholds.first {
distance < $0.maximumDistance.distance
} ?? thresholds.last!
}
/**
:nodoc:
Initializes a `RoundingTable` with the given thresholds.
- parameter thresholds: An array of `Threshold`s that dictate rounding behavior.
*/
public init(thresholds: [Threshold]) {
self.thresholds = thresholds
}
/**
:nodoc:
The rounding behavior for locales where the metric system is used.
*/
public static var metric: RoundingTable = RoundingTable(thresholds: [
.init(maximumDistance: Measurement(value: 25, unit: .meters), roundingIncrement: 5, maximumFractionDigits: 0),
.init(maximumDistance: Measurement(value: 100, unit: .meters), roundingIncrement: 25, maximumFractionDigits: 0),
.init(maximumDistance: Measurement(value: 999, unit: .meters), roundingIncrement: 50, maximumFractionDigits: 0),
// The rounding increment is a small value rather than 0 because of floating-point imprecision that causes 0.5 to round down.
.init(maximumDistance: Measurement(value: 3, unit: .kilometers), roundingIncrement: 0.0001, maximumFractionDigits: 1),
.init(maximumDistance: Measurement(value: 5, unit: .kilometers), roundingIncrement: 0.0001, maximumFractionDigits: 0)
])
/**
:nodoc:
The rounding behavior used by the UK.
*/
public static var uk: RoundingTable = RoundingTable(thresholds: [
.init(maximumDistance: Measurement(value: 20, unit: .yards), roundingIncrement: 10, maximumFractionDigits: 0),
.init(maximumDistance: Measurement(value: 100, unit: .yards), roundingIncrement: 25, maximumFractionDigits: 0),
.init(maximumDistance: Measurement(value: 0.1, unit: .miles).converted(to: .yards), roundingIncrement: 50, maximumFractionDigits: 1),
.init(maximumDistance: Measurement(value: 3, unit: .miles), roundingIncrement: 0.1, maximumFractionDigits: 1),
.init(maximumDistance: Measurement(value: 5, unit: .miles), roundingIncrement: 0.0001, maximumFractionDigits: 0)
])
/**
:nodoc:
The rounding behavior for locales where the imperial system is used.
*/
public static var us: RoundingTable = RoundingTable(thresholds: [
.init(maximumDistance: Measurement(value: 0.1, unit: .miles).converted(to: .feet), roundingIncrement: 50, maximumFractionDigits: 0),
.init(maximumDistance: Measurement(value: 3, unit: .miles), roundingIncrement: 0.1, maximumFractionDigits: 1),
.init(maximumDistance: Measurement(value: 5, unit: .miles), roundingIncrement: 0.0001, maximumFractionDigits: 0)
])
}
extension Measurement where UnitType == UnitLength {
/**
Initializes a measurement from the given distance.
- parameter distance: The distance being measured.
*/
public init(distance: CLLocationDistance) {
self.init(value: distance, unit: .meters)
}
/**
The distance in meters.
*/
public var distance: CLLocationDistance {
return converted(to: .meters).value
}
/**
Returns a length measurement equivalent to the receiver but converted to the most appropriate unit based on the given locale and rounded based on the unit.
- parameter locale: The locale that determines the chosen unit.
*/
public func localized(into locale: Locale = .nationalizedCurrent) -> Measurement<UnitLength> {
let threshold: RoundingTable
if MeasurementSystem(NavigationSettings.shared.distanceUnit) == .metric {
threshold = .metric
} else if locale.languageCode == "en" && locale.regionCode == "GB" {
threshold = .uk
} else {
threshold = .us
}
return threshold.threshold(for: distance).measurement(of: distance)
}
}
extension NSAttributedString.Key {
/**
An `NSNumber` containing the numeric quantity represented by the localized substring.
*/
public static let quantity = NSAttributedString.Key(rawValue: "MBQuantity")
}
/**
A formatter that provides localized representations of distance units and measurements.
This class is limited to `UnitLength` and its behavior is more specific to distances than `MeasurementFormatter`. By default, the class automatically localizes and rounds the measurement using `Measurement.localized(into:)` and `Locale.nationalizedCurrent`. Measurements can be formatted into either strings or attributed strings.
*/
open class DistanceFormatter: Formatter, NSSecureCoding {
// MARK: Configuring the Formatting
public static var supportsSecureCoding = true
/**
Options for choosing and formatting the unit.
- seealso: `MeasurementFormatter.unitOptions`
*/
open var unitOptions: MeasurementFormatter.UnitOptions {
get {
return measurementFormatter.unitOptions
}
set {
measurementFormatter.unitOptions = newValue
}
}
/**
The unit style.
- seealso: `MeasurementFormatter.unitStyle`
*/
open var unitStyle: Formatter.UnitStyle {
get {
return measurementFormatter.unitStyle
}
set {
measurementFormatter.unitStyle = newValue
}
}
/**
The locale that determines the chosen unit, name of the unit, and number formatting.
- seealso: `MeasurementFormatter.locale`
*/
open var locale: Locale {
get {
return measurementFormatter.locale
}
set {
measurementFormatter.locale = newValue
}
}
/**
The underlying measurement formatter.
*/
@NSCopying open var measurementFormatter = MeasurementFormatter()
public override init() {
super.init()
unitOptions = .providedUnit
locale = .nationalizedCurrent
}
public required init?(coder decoder: NSCoder) {
super.init(coder: decoder)
}
// MARK: Getting String Representation of Values
/**
Creates and returns a localized, formatted string representation of the given distance in meters.
The distance is converted from meters to the most appropriate unit based on the locale and quantity.
- parameter distance: The distance, measured in meters, to localize and format.
- returns: A localized, formatted representation of the distance.
- seealso: `MeasurementFormatter.string(from:)`
*/
open func string(from distance: CLLocationDistance) -> String {
return string(from: Measurement(distance: distance))
}
/**
Creates and returns a localized, formatted attributed string representation of the given distance in meters.
The distance is converted from meters to the most appropriate unit based on the locale and quantity. `NSAttributedString.Key.quantity` is applied to the range representing the quantity. For example, the “5” in “5 km” has a quantity attribute set to 5.
- parameter distance: The distance, measured in meters, to localize and format.
- parameter defaultAttributes: The default attributes to apply to the resulting attributed string.
- returns: A localized, formatted representation of the distance.
*/
open func attributedString(from distance: CLLocationDistance, defaultAttributes attributes: [NSAttributedString.Key: Any]? = nil) -> NSAttributedString {
return attributedString(from: Measurement(distance: distance), defaultAttributes: attributes)
}
/**
Creates and returns a localized, formatted string representation of the given measurement.
- parameter measurement: The measurement to localize and format.
- returns: A localized, formatted representation of the measurement.
- seealso: `MeasurementFormatter.string(from:)`
*/
open func string(from measurement: Measurement<UnitLength>) -> String {
return measurementFormatter.string(from: measurement.localized(into: locale))
}
/**
Creates and returns a localized, formatted attributed string representation of the given measurement.
`NSAttributedString.Key.quantity` is applied to the range representing the quantity. For example, the “5” in “5 km” has a quantity attribute set to 5.
- parameter measurement: The measurement to localize and format.
- parameter defaultAttributes: The default attributes to apply to the resulting attributed string.
- returns: A localized, formatted representation of the measurement.
*/
open func attributedString(from measurement: Measurement<UnitLength>, defaultAttributes attributes: [NSAttributedString.Key: Any]? = nil) -> NSAttributedString {
let string = self.string(from: measurement)
let localizedMeasurement = measurement.localized(into: locale)
let attributedString = NSMutableAttributedString(string: string, attributes: attributes)
if let quantityString = measurementFormatter.numberFormatter.string(from: localizedMeasurement.value as NSNumber) {
// NSMutableAttributedString methods accept NSRange, not Range.
let quantityRange = (string as NSString).range(of: quantityString)
if quantityRange.location != NSNotFound {
attributedString.addAttribute(.quantity, value: localizedMeasurement.value as NSNumber, range: quantityRange)
}
}
return attributedString
}
open override func string(for obj: Any?) -> String? {
if let distanceFromObj = obj as? CLLocationDistance {
return self.string(from: distanceFromObj)
} else if let measurementFromObj = obj as? Measurement<UnitLength> {
return self.string(from: measurementFromObj)
} else {
return nil
}
}
open override func attributedString(for obj: Any, withDefaultAttributes attrs: [NSAttributedString.Key : Any]? = nil) -> NSAttributedString? {
if let distanceFromObj = obj as? CLLocationDistance {
return self.attributedString(from: distanceFromObj, defaultAttributes: attrs)
} else if let measurementFromObj = obj as? Measurement<UnitLength> {
return self.attributedString(from: measurementFromObj, defaultAttributes: attrs)
} else {
return nil
}
}
}
extension Double {
func rounded(precision: Double) -> Double {
if precision == 0 {
return Double(Int(rounded()))
}
return (self * precision).rounded() / precision
}
mutating func round(precision: Double) {
self = rounded(precision: precision)
}
func rounded(roundingIncrement: Double) -> Double {
if roundingIncrement == 0 {
return self
}
return (self / roundingIncrement).rounded() * roundingIncrement
}
mutating func round(roundingIncrement: Double) {
self = rounded(roundingIncrement: roundingIncrement)
}
}