Skip to content

Commit 587557d

Browse files
authored
Merge pull request #745 from kweinmeister/LengthFormatter
2 parents 99bc426 + dfd1d74 commit 587557d

File tree

4 files changed

+374
-18
lines changed

4 files changed

+374
-18
lines changed

Foundation.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@
311311
7900433B1CACD33E00ECCBF1 /* TestNSCompoundPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 790043391CACD33E00ECCBF1 /* TestNSCompoundPredicate.swift */; };
312312
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900433A1CACD33E00ECCBF1 /* TestNSPredicate.swift */; };
313313
AE35A1861CBAC85E0042DB84 /* SwiftFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; };
314+
BD8042161E09857800487EB8 /* TestNSLengthFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8042151E09857800487EB8 /* TestNSLengthFormatter.swift */; };
314315
BDFDF0A71DFF5B3E00C04CC5 /* TestNSPersonNameComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDFDF0A61DFF5B3E00C04CC5 /* TestNSPersonNameComponents.swift */; };
315316
BF8E65311DC3B3CB005AB5C3 /* TestNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8E65301DC3B3CB005AB5C3 /* TestNotification.swift */; };
316317
CC5249C01D341D23007CB54D /* TestUnitConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC5249BF1D341D23007CB54D /* TestUnitConverter.swift */; };
@@ -752,6 +753,7 @@
752753
88D28DE61C13AE9000494606 /* TestNSGeometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSGeometry.swift; sourceTree = "<group>"; };
753754
A5A34B551C18C85D00FD972B /* TestNSByteCountFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSByteCountFormatter.swift; sourceTree = "<group>"; };
754755
AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftFoundation.h; sourceTree = "<group>"; };
756+
BD8042151E09857800487EB8 /* TestNSLengthFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLengthFormatter.swift; sourceTree = "<group>"; };
755757
BDFDF0A61DFF5B3E00C04CC5 /* TestNSPersonNameComponents.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSPersonNameComponents.swift; sourceTree = "<group>"; };
756758
BF8E65301DC3B3CB005AB5C3 /* TestNotification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNotification.swift; sourceTree = "<group>"; };
757759
C2A9D75B1C15C08B00993803 /* TestNSUUID.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSUUID.swift; sourceTree = "<group>"; };
@@ -1351,6 +1353,7 @@
13511353
5EB6A15C1C188FC40037DCB8 /* TestNSJSONSerialization.swift */,
13521354
D3A597F11C33C68E00295652 /* TestNSKeyedArchiver.swift */,
13531355
D3A597EF1C33A9E500295652 /* TestNSKeyedUnarchiver.swift */,
1356+
BD8042151E09857800487EB8 /* TestNSLengthFormatter.swift */,
13541357
61A395F91C2484490029B337 /* TestNSLocale.swift */,
13551358
61F8AE7C1C180FC600FB62F0 /* TestNSNotificationCenter.swift */,
13561359
5EF673AB1C28B527006212A3 /* TestNSNotificationQueue.swift */,
@@ -2248,6 +2251,7 @@
22482251
5B13B3301C582D4C00651CE2 /* TestNSHTTPCookie.swift in Sources */,
22492252
5B13B3361C582D4C00651CE2 /* TestNSLocale.swift in Sources */,
22502253
5B13B3391C582D4C00651CE2 /* TestNSNull.swift in Sources */,
2254+
BD8042161E09857800487EB8 /* TestNSLengthFormatter.swift in Sources */,
22512255
5B13B3421C582D4C00651CE2 /* TestNSRunLoop.swift in Sources */,
22522256
5B13B34E1C582D4C00651CE2 /* TestNSXMLDocument.swift in Sources */,
22532257
5B13B32B1C582D4C00651CE2 /* TestNSData.swift in Sources */,

Foundation/NSLengthFormatter.swift

Lines changed: 191 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,35 @@
77
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
88
//
99

10-
1110
extension LengthFormatter {
12-
public enum Unit : Int {
13-
14-
case millimeter
15-
case centimeter
16-
case meter
17-
case kilometer
18-
case inch
19-
case foot
20-
case yard
21-
case mile
11+
public enum Unit: Int {
12+
case millimeter = 8
13+
case centimeter = 9
14+
case meter = 11
15+
case kilometer = 14
16+
case inch = 1281
17+
case foot = 1282
18+
case yard = 1283
19+
case mile = 1284
2220
}
2321
}
2422

2523
open class LengthFormatter : Formatter {
2624

25+
public override init() {
26+
numberFormatter = NumberFormatter()
27+
numberFormatter.numberStyle = .decimal
28+
unitStyle = .medium
29+
isForPersonHeightUse = false
30+
super.init()
31+
}
32+
2733
public required init?(coder: NSCoder) {
28-
NSUnimplemented()
34+
numberFormatter = NumberFormatter()
35+
numberFormatter.numberStyle = .decimal
36+
unitStyle = .medium
37+
isForPersonHeightUse = false
38+
super.init(coder:coder)
2939
}
3040

3141
/*@NSCopying*/ open var numberFormatter: NumberFormatter! // default is NSNumberFormatter with NSNumberFormatterDecimalStyle
@@ -34,20 +44,183 @@ open class LengthFormatter : Formatter {
3444
open var isForPersonHeightUse: Bool // default is NO; if it is set to YES, the number argument for -stringFromMeters: and -unitStringFromMeters: is considered as a person's height
3545

3646
// Format a combination of a number and an unit to a localized string.
37-
open func string(fromValue value: Double, unit: LengthFormatter.Unit) -> String { NSUnimplemented() }
47+
open func string(fromValue value: Double, unit: LengthFormatter.Unit) -> String {
48+
guard let formattedValue = numberFormatter.string(from:NSNumber(value: value)) else {
49+
fatalError("Cannot format \(value) as string")
50+
}
51+
let separator = unitStyle == LengthFormatter.UnitStyle.short ? "" : " "
52+
return "\(formattedValue)\(separator)\(unitString(fromValue: value, unit: unit))"
53+
}
3854

3955
// Format a number in meters to a localized string with the locale-appropriate unit and an appropriate scale (e.g. 4.3m = 14.1ft in the US locale).
40-
open func string(fromMeters numberInMeters: Double) -> String { NSUnimplemented() }
56+
open func string(fromMeters numberInMeters: Double) -> String {
57+
//Convert to the locale-appropriate unit
58+
let unitFromMeters = unit(fromMeters: numberInMeters)
59+
60+
//Map the unit to UnitLength type for conversion later
61+
let unitLengthFromMeters = LengthFormatter.unitLength[unitFromMeters]!
62+
63+
//Create a measurement object based on the value in meters
64+
let meterMeasurement = Measurement<UnitLength>(value:numberInMeters, unit: .meters)
65+
66+
//Convert the object to the locale-appropriate unit determined above
67+
let unitMeasurement = meterMeasurement.converted(to: unitLengthFromMeters)
68+
69+
//Extract the number from the measurement
70+
let numberInUnit = unitMeasurement.value
71+
72+
if isForPersonHeightUse && !LengthFormatter.isMetricSystemLocale(numberFormatter.locale) {
73+
let feet = numberInUnit.rounded(.towardZero)
74+
let feetString = string(fromValue: feet, unit: .foot)
75+
76+
let inches = abs(numberInUnit.truncatingRemainder(dividingBy: 1.0)) * 12
77+
let inchesString = string(fromValue: inches, unit: .inch)
78+
79+
return ("\(feetString), \(inchesString)")
80+
}
81+
return string(fromValue: numberInUnit, unit: unitFromMeters)
82+
}
4183

4284
// Return a localized string of the given unit, and if the unit is singular or plural is based on the given number.
43-
open func unitString(fromValue value: Double, unit: Unit) -> String { NSUnimplemented() }
85+
open func unitString(fromValue value: Double, unit: Unit) -> String {
86+
if unitStyle == .short {
87+
return LengthFormatter.shortSymbol[unit]!
88+
} else if unitStyle == .medium {
89+
return LengthFormatter.mediumSymbol[unit]!
90+
} else if value == 1.0 {
91+
return LengthFormatter.largeSingularSymbol[unit]!
92+
} else {
93+
return LengthFormatter.largePluralSymbol[unit]!
94+
}
95+
}
4496

4597
// Return the locale-appropriate unit, the same unit used by -stringFromMeters:.
46-
open func unitString(fromMeters numberInMeters: Double, usedUnit unitp: UnsafeMutablePointer<Unit>?) -> String { NSUnimplemented() }
98+
open func unitString(fromMeters numberInMeters: Double, usedUnit unitp: UnsafeMutablePointer<Unit>?) -> String {
99+
100+
//Convert to the locale-appropriate unit
101+
let unitFromMeters = unit(fromMeters: numberInMeters)
102+
unitp?.pointee = unitFromMeters
103+
104+
//Map the unit to UnitLength type for conversion later
105+
let unitLengthFromMeters = LengthFormatter.unitLength[unitFromMeters]!
106+
107+
//Create a measurement object based on the value in meters
108+
let meterMeasurement = Measurement<UnitLength>(value:numberInMeters, unit: .meters)
109+
110+
//Convert the object to the locale-appropriate unit determined above
111+
let unitMeasurement = meterMeasurement.converted(to: unitLengthFromMeters)
112+
113+
//Extract the number from the measurement
114+
let numberInUnit = unitMeasurement.value
115+
116+
//Return the appropriate representation of the unit based on the selected unit style
117+
return unitString(fromValue: numberInUnit, unit: unitFromMeters)
118+
}
119+
120+
/// This method selects the appropriate unit based on the formatter’s locale,
121+
/// the magnitude of the value, and isForPersonHeightUse property.
122+
///
123+
/// - Parameter numberInMeters: the magnitude in terms of meters
124+
/// - Returns: Returns the appropriate unit
125+
private func unit(fromMeters numberInMeters: Double) -> Unit {
126+
if LengthFormatter.isMetricSystemLocale(numberFormatter.locale) {
127+
//Person height is always returned in cm for metric system
128+
if isForPersonHeightUse { return .centimeter }
129+
130+
if numberInMeters > 1000 || numberInMeters < 0.0 {
131+
return .kilometer
132+
} else if numberInMeters > 1.0 {
133+
return .meter
134+
} else if numberInMeters > 0.01 {
135+
return .centimeter
136+
} else { return .millimeter }
137+
} else {
138+
//Person height is always returned in ft for U.S. system
139+
if isForPersonHeightUse { return .foot }
140+
141+
let metricMeasurement = Measurement<UnitLength>(value:numberInMeters, unit: .meters)
142+
let usMeasurement = metricMeasurement.converted(to: .feet)
143+
let numberInFeet = usMeasurement.value
144+
145+
if numberInFeet < 0.0 || numberInFeet > 5280 {
146+
return .mile
147+
} else if numberInFeet > 3 || numberInFeet == 0.0 {
148+
return .yard
149+
} else if numberInFeet >= 1.0 {
150+
return .foot
151+
} else { return .inch }
152+
}
153+
}
154+
155+
/// TODO: Replace calls to the below function to use Locale.usesMetricSystem
156+
/// Temporary workaround due to unpopulated Locale attributes
157+
/// See https://bugs.swift.org/browse/SR-3202
158+
private static func isMetricSystemLocale(_ locale: Locale) -> Bool {
159+
switch locale.identifier {
160+
case "en_US": return false
161+
case "en_US_POSIX": return false
162+
case "haw_US": return false
163+
case "es_US": return false
164+
case "chr_US": return false
165+
case "my_MM": return false
166+
case "en_LR": return false
167+
case "vai_LR": return false
168+
default: return true
169+
}
170+
}
47171

48172
/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
49173
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
50174
open override func objectValue(_ string: String) throws -> Any? { return nil }
51-
}
52-
175+
176+
177+
/// Maps NSLengthFormatter.Unit enum to UnitLength class. Used for measurement conversion.
178+
private static let unitLength: [Unit:UnitLength] = [.millimeter:.millimeters,
179+
.centimeter:.centimeters,
180+
.meter:.meters,
181+
.kilometer:.kilometers,
182+
.inch:.inches,
183+
.foot:.feet,
184+
.yard:.yards,
185+
.mile:.miles]
53186

187+
/// Maps a unit to its short symbol. Reuses strings from UnitLength wherever possible.
188+
private static let shortSymbol: [Unit: String] = [.millimeter:UnitLength.millimeters.symbol,
189+
.centimeter:UnitLength.centimeters.symbol,
190+
.meter:UnitLength.meters.symbol,
191+
.kilometer:UnitLength.kilometers.symbol,
192+
.inch:"\"",
193+
.foot:"",
194+
.yard:UnitLength.yards.symbol,
195+
.mile:UnitLength.miles.symbol]
196+
197+
/// Maps a unit to its medium symbol. Reuses strings from UnitLength.
198+
private static let mediumSymbol: [Unit: String] = [.millimeter:UnitLength.millimeters.symbol,
199+
.centimeter:UnitLength.centimeters.symbol,
200+
.meter:UnitLength.meters.symbol,
201+
.kilometer:UnitLength.kilometers.symbol,
202+
.inch:UnitLength.inches.symbol,
203+
.foot:UnitLength.feet.symbol,
204+
.yard:UnitLength.yards.symbol,
205+
.mile:UnitLength.miles.symbol]
206+
207+
/// Maps a unit to its large, singular symbol.
208+
private static let largeSingularSymbol: [Unit: String] = [.millimeter:"millimeter",
209+
.centimeter:"centimeter",
210+
.meter:"meter",
211+
.kilometer:"kilometer",
212+
.inch:"inch",
213+
.foot:"foot",
214+
.yard:"yard",
215+
.mile:"mile"]
216+
217+
/// Maps a unit to its large, plural symbol.
218+
private static let largePluralSymbol: [Unit: String] = [.millimeter:"millimeters",
219+
.centimeter:"centimeters",
220+
.meter:"meters",
221+
.kilometer:"kilometers",
222+
.inch:"inches",
223+
.foot:"feet",
224+
.yard:"yards",
225+
.mile:"miles"]
226+
}

0 commit comments

Comments
 (0)