10
10
11
11
extension MassFormatter {
12
12
public enum Unit : Int {
13
-
14
13
case gram
15
14
case kilogram
16
15
case ounce
@@ -21,28 +20,238 @@ extension MassFormatter {
21
20
22
21
open class MassFormatter : Formatter {
23
22
23
+ public override init ( ) {
24
+ numberFormatter = NumberFormatter ( )
25
+ numberFormatter. numberStyle = . decimal
26
+ unitStyle = . medium
27
+ isForPersonMassUse = false
28
+ super. init ( )
29
+ }
30
+
24
31
public required init ? ( coder: NSCoder ) {
25
- NSUnimplemented ( )
32
+ numberFormatter = NumberFormatter ( )
33
+ numberFormatter. numberStyle = . decimal
34
+ unitStyle = . medium
35
+ isForPersonMassUse = false
36
+ super. init ( coder: coder)
26
37
}
27
38
28
39
/*@NSCopying*/ open var numberFormatter : NumberFormatter ! // default is NSNumberFormatter with NSNumberFormatterDecimalStyle
29
40
open var unitStyle : UnitStyle // default is NSFormattingUnitStyleMedium
41
+
30
42
open var isForPersonMassUse : Bool // default is NO; if it is set to YES, the number argument for -stringFromKilograms: and -unitStringFromKilograms: is considered as a person’s mass
31
43
32
44
// Format a combination of a number and an unit to a localized string.
33
- open func string( fromValue value: Double , unit: Unit ) -> String { NSUnimplemented ( ) }
45
+ open func string( fromValue value: Double , unit: Unit ) -> String {
46
+ // special case: stone shows fractional values in pounds
47
+ if unit == . stone {
48
+ let stone = value. rounded ( . towardZero)
49
+ let stoneString = singlePartString ( fromValue: stone, unit: unit) // calling `string(fromValue: stone, unit: .stone)` would infinitely recur
50
+ let pounds = abs ( value. truncatingRemainder ( dividingBy: 1.0 ) ) * MassFormatter. poundsPerStone
51
+
52
+ // if we don't have any fractional component, don't append anything
53
+ if pounds == 0 {
54
+ return stoneString
55
+ } else {
56
+ let poundsString = string ( fromValue: pounds, unit: . pound)
57
+ let separator = unitStyle == MassFormatter . UnitStyle. short ? " " : " , "
58
+
59
+ return ( " \( stoneString) \( separator) \( poundsString) " )
60
+ }
61
+ }
62
+
63
+ // normal case: kilograms and pounds
64
+ return singlePartString ( fromValue: value, unit: unit)
65
+ }
34
66
35
67
// Format a number in kilograms to a localized string with the locale-appropriate unit and an appropriate scale (e.g. 1.2kg = 2.64lb in the US locale).
36
- open func string( fromKilograms numberInKilograms: Double ) -> String { NSUnimplemented ( ) }
68
+ open func string( fromKilograms numberInKilograms: Double ) -> String {
69
+ //Convert to the locale-appropriate unit
70
+ let unitFromKilograms = unit ( fromKilograms: numberInKilograms)
71
+
72
+ //Map the unit to UnitMass type for conversion later
73
+ let unitMassFromKilograms = MassFormatter . unitMass [ unitFromKilograms] !
74
+
75
+ //Create a measurement object based on the value in kilograms
76
+ let kilogramMeasurement = Measurement < UnitMass > ( value: numberInKilograms, unit: . kilograms)
77
+
78
+ //Convert the object to the locale-appropriate unit determined above
79
+ let unitMeasurement = kilogramMeasurement. converted ( to: unitMassFromKilograms)
80
+
81
+ //Extract the number from the measurement
82
+ let numberInUnit = unitMeasurement. value
83
+
84
+ return string ( fromValue: numberInUnit, unit: unitFromKilograms)
85
+ }
37
86
38
87
// Return a localized string of the given unit, and if the unit is singular or plural is based on the given number.
39
- open func unitString( fromValue value: Double , unit: Unit ) -> String { NSUnimplemented ( ) }
88
+ open func unitString( fromValue value: Double , unit: Unit ) -> String {
89
+ if unitStyle == . short {
90
+ return MassFormatter . shortSymbol [ unit] !
91
+ } else if unitStyle == . medium {
92
+ return MassFormatter . mediumSymbol [ unit] !
93
+ } else if unit == . stone { // special case, see `unitStringDisplayedAdjacent(toValue:, unit:)`
94
+ return MassFormatter . largeSingularSymbol [ unit] !
95
+ } else if value == 1.0 {
96
+ return MassFormatter . largeSingularSymbol [ unit] !
97
+ } else {
98
+ return MassFormatter . largePluralSymbol [ unit] !
99
+ }
100
+ }
40
101
41
102
// Return the locale-appropriate unit, the same unit used by -stringFromKilograms:.
42
- open func unitString( fromKilograms numberInKilograms: Double , usedUnit unitp: UnsafeMutablePointer < Unit > ? ) -> String { NSUnimplemented ( ) }
103
+ open func unitString( fromKilograms numberInKilograms: Double , usedUnit unitp: UnsafeMutablePointer < Unit > ? ) -> String {
104
+ //Convert to the locale-appropriate unit
105
+ let unitFromKilograms = unit ( fromKilograms: numberInKilograms)
106
+ unitp? . pointee = unitFromKilograms
107
+
108
+ //Map the unit to UnitMass type for conversion later
109
+ let unitMassFromKilograms = MassFormatter . unitMass [ unitFromKilograms] !
110
+
111
+ //Create a measurement object based on the value in kilograms
112
+ let kilogramMeasurement = Measurement < UnitMass > ( value: numberInKilograms, unit: . kilograms)
113
+
114
+ //Convert the object to the locale-appropriate unit determined above
115
+ let unitMeasurement = kilogramMeasurement. converted ( to: unitMassFromKilograms)
116
+
117
+ //Extract the number from the measurement
118
+ let numberInUnit = unitMeasurement. value
119
+
120
+ //Return the appropriate representation of the unit based on the selected unit style
121
+ return unitString ( fromValue: numberInUnit, unit: unitFromKilograms)
122
+ }
43
123
44
124
/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
45
125
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
46
126
open override func objectValue( _ string: String ) throws -> Any ? { return nil }
127
+
128
+
129
+ // MARK: - Private
130
+
131
+ /// This method selects the appropriate unit based on the formatter’s locale,
132
+ /// the magnitude of the value, and isForPersonMassUse property.
133
+ ///
134
+ /// - Parameter numberInKilograms: the magnitude in terms of kilograms
135
+ /// - Returns: Returns the appropriate unit
136
+ private func unit( fromKilograms numberInKilograms: Double ) -> Unit {
137
+ if numberFormatter. locale. sr3202_fix_isMetricSystemLocale ( ) {
138
+ if numberInKilograms > 1.0 || numberInKilograms <= 0.0 {
139
+ return . kilogram
140
+ } else {
141
+ return . gram
142
+ }
143
+ } else {
144
+ let metricMeasurement = Measurement < UnitMass > ( value: numberInKilograms, unit: . kilograms)
145
+ let imperialMeasurement = metricMeasurement. converted ( to: . pounds)
146
+ let numberInPounds = imperialMeasurement. value
147
+
148
+ if numberInPounds >= 1.0 || numberInPounds <= 0.0 {
149
+ return . pound
150
+ } else {
151
+ return . ounce
152
+ }
153
+ }
154
+ }
155
+
156
+ /// Formats the given value and unit into a string containing one logical
157
+ /// value. This is intended for units like kilogram and pound where
158
+ /// fractional values are represented as a decimal instead of converted
159
+ /// values in another unit.
160
+ ///
161
+ /// - Parameter value: The mass's value in the given unit.
162
+ /// - Parameter unit: The unit used in the resulting mass string.
163
+ /// - Returns: A properly formatted mass string for the given value and unit.
164
+ private func singlePartString( fromValue value: Double , unit: Unit ) -> String {
165
+ guard let formattedValue = numberFormatter. string ( from: NSNumber ( value: value) ) else {
166
+ fatalError ( " Cannot format \( value) as string " )
167
+ }
168
+
169
+ let separator = unitStyle == MassFormatter . UnitStyle. short ? " " : " "
170
+
171
+ return " \( formattedValue) \( separator) \( unitStringDisplayedAdjacent ( toValue: value, unit: unit) ) "
172
+ }
173
+
174
+ /// Return the locale-appropriate unit to be shown adjacent to the given
175
+ /// value. In most cases this will match `unitStringDisplayedAdjacent(toValue:, unit:)`
176
+ /// however there are a few special cases:
177
+ /// - Imperial pounds with a short representation use "lb" in the
178
+ /// abstract and "#" only when shown with a numeral.
179
+ /// - Stones are are singular in the abstract and only plural when
180
+ /// shown with a numeral.
181
+ ///
182
+ /// - Parameter value: The mass's value in the given unit.
183
+ /// - Parameter unit: The unit used in the resulting mass string.
184
+ /// - Returns: The locale-appropriate unit
185
+ open func unitStringDisplayedAdjacent( toValue value: Double , unit: Unit ) -> String {
186
+ if unit == . pound && unitStyle == . short {
187
+ return " # "
188
+ } else if unit == . stone && unitStyle == . long {
189
+ if value == 1.0 {
190
+ return MassFormatter . largeSingularSymbol [ unit] !
191
+ } else {
192
+ return MassFormatter . largePluralSymbol [ unit] !
193
+ }
194
+ } else {
195
+ return unitString ( fromValue: value, unit: unit)
196
+ }
197
+ }
198
+
199
+
200
+
201
+ /// The number of pounds in 1 stone
202
+ private static let poundsPerStone = 14.0
203
+
204
+ /// Maps MassFormatter.Unit enum to UnitMass class. Used for measurement conversion.
205
+ private static let unitMass : [ Unit : UnitMass ] = [ . gram: . grams,
206
+ . kilogram: . kilograms,
207
+ . ounce: . ounces,
208
+ . pound: . pounds,
209
+ . stone: . stones]
210
+
211
+ /// Maps a unit to its short symbol. Reuses strings from UnitMass.
212
+ private static let shortSymbol : [ Unit : String ] = [ . gram: UnitMass . grams. symbol,
213
+ . kilogram: UnitMass . kilograms. symbol,
214
+ . ounce: UnitMass . ounces. symbol,
215
+ . pound: UnitMass . pounds. symbol, // see `unitStringDisplayedAdjacent(toValue:, unit:)`
216
+ . stone: UnitMass . stones. symbol]
217
+
218
+ /// Maps a unit to its medium symbol. Reuses strings from UnitMass.
219
+ private static let mediumSymbol : [ Unit : String ] = [ . gram: UnitMass . grams. symbol,
220
+ . kilogram: UnitMass . kilograms. symbol,
221
+ . ounce: UnitMass . ounces. symbol,
222
+ . pound: UnitMass . pounds. symbol,
223
+ . stone: UnitMass . stones. symbol]
224
+
225
+ /// Maps a unit to its large, singular symbol.
226
+ private static let largeSingularSymbol : [ Unit : String ] = [ . gram: " gram " ,
227
+ . kilogram: " kilogram " ,
228
+ . ounce: " ounce " ,
229
+ . pound: " pound " ,
230
+ . stone: " stone " ]
231
+
232
+ /// Maps a unit to its large, plural symbol.
233
+ private static let largePluralSymbol : [ Unit : String ] = [ . gram: " grams " ,
234
+ . kilogram: " kilograms " ,
235
+ . ounce: " ounces " ,
236
+ . pound: " pounds " ,
237
+ . stone: " stones " ]
47
238
}
48
239
240
+ internal extension Locale {
241
+ /// TODO: Replace calls to the below function to use Locale.usesMetricSystem
242
+ /// Temporary workaround due to unpopulated Locale attributes
243
+ /// See https://bugs.swift.org/browse/SR-3202
244
+ internal func sr3202_fix_isMetricSystemLocale( ) -> Bool {
245
+ switch self . identifier {
246
+ case " en_US " : return false
247
+ case " en_US_POSIX " : return false
248
+ case " haw_US " : return false
249
+ case " es_US " : return false
250
+ case " chr_US " : return false
251
+ case " my_MM " : return false
252
+ case " en_LR " : return false
253
+ case " vai_LR " : return false
254
+ default : return true
255
+ }
256
+ }
257
+ }
0 commit comments