Skip to content

Commit 766645d

Browse files
jszumskiparkera
authored andcommitted
Initial implementation of MassFormatter. (#883)
1 parent 28fe032 commit 766645d

File tree

6 files changed

+365
-25
lines changed

6 files changed

+365
-25
lines changed

Docs/Status.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ There is no _Complete_ status for test coverage because there are always additio
9898
| `EnergyFormatter` | Unimplemented | None | |
9999
| `ISO8601DateFormatter` | Unimplemented | None | |
100100
| `LengthFormatter` | Complete | Substantial | |
101-
| `MassFormatter` | Unimplemented | None | |
101+
| `MassFormatter` | Complete | Substantial | Needs localization |
102102
| `NumberFormatter` | Mostly Complete | Substantial | `objectValue(_:range:)` remains unimplemented |
103103
| `PersonNameComponentsFormatter` | Unimplemented | None | |
104104
| `ByteCountFormatter` | Mostly Complete | Substantial | `init?(coder:)` remains unimplemented |

Foundation.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@
317317
7900433B1CACD33E00ECCBF1 /* TestNSCompoundPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 790043391CACD33E00ECCBF1 /* TestNSCompoundPredicate.swift */; };
318318
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900433A1CACD33E00ECCBF1 /* TestNSPredicate.swift */; };
319319
90E645DF1E4C89A400D0D47C /* TestNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E645DE1E4C89A400D0D47C /* TestNSCache.swift */; };
320+
A058C2021E529CF100B07AA1 /* TestMassFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */; };
320321
AE35A1861CBAC85E0042DB84 /* SwiftFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; };
321322
BD8042161E09857800487EB8 /* TestNSLengthFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8042151E09857800487EB8 /* TestNSLengthFormatter.swift */; };
322323
BDBB65901E256BFA001A7286 /* TestNSEnergyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBB658F1E256BFA001A7286 /* TestNSEnergyFormatter.swift */; };
@@ -766,6 +767,7 @@
766767
84BA558D1C16F90900F48C54 /* TestNSTimeZone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSTimeZone.swift; sourceTree = "<group>"; };
767768
88D28DE61C13AE9000494606 /* TestNSGeometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSGeometry.swift; sourceTree = "<group>"; };
768769
90E645DE1E4C89A400D0D47C /* TestNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSCache.swift; sourceTree = "<group>"; };
770+
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestMassFormatter.swift; sourceTree = "<group>"; };
769771
A5A34B551C18C85D00FD972B /* TestNSByteCountFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSByteCountFormatter.swift; sourceTree = "<group>"; };
770772
AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftFoundation.h; sourceTree = "<group>"; };
771773
B167A6641ED7303F0040B09A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
@@ -1418,6 +1420,7 @@
14181420
D3047AEB1C38BC3300295652 /* TestNSValue.swift */,
14191421
5B6F17951C48631C00935030 /* TestNSXMLDocument.swift */,
14201422
5B40F9F11C125187000E72E3 /* TestNSXMLParser.swift */,
1423+
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */,
14211424
CC5249BF1D341D23007CB54D /* TestUnitConverter.swift */,
14221425
5B6F17961C48631C00935030 /* TestUtils.swift */,
14231426
0383A1741D2E558A0052E5D1 /* TestNSStream.swift */,
@@ -2267,6 +2270,9 @@
22672270
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */,
22682271
5B13B3471C582D4C00651CE2 /* TestThread.swift in Sources */,
22692272
5B13B32E1C582D4C00651CE2 /* TestFileManager.swift in Sources */,
2273+
5B13B3471C582D4C00651CE2 /* TestNSThread.swift in Sources */,
2274+
5B13B32E1C582D4C00651CE2 /* TestNSFileManager.swift in Sources */,
2275+
A058C2021E529CF100B07AA1 /* TestMassFormatter.swift in Sources */,
22702276
5B13B3381C582D4C00651CE2 /* TestNSNotificationQueue.swift in Sources */,
22712277
CC5249C01D341D23007CB54D /* TestUnitConverter.swift in Sources */,
22722278
5B13B3331C582D4C00651CE2 /* TestNSJSONSerialization.swift in Sources */,

Foundation/NSLengthFormatter.swift

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ open class LengthFormatter : Formatter {
6969
//Extract the number from the measurement
7070
let numberInUnit = unitMeasurement.value
7171

72-
if isForPersonHeightUse && !LengthFormatter.isMetricSystemLocale(numberFormatter.locale) {
72+
if isForPersonHeightUse && !numberFormatter.locale.sr3202_fix_isMetricSystemLocale() {
7373
let feet = numberInUnit.rounded(.towardZero)
7474
let feetString = string(fromValue: feet, unit: .foot)
7575

@@ -123,7 +123,7 @@ open class LengthFormatter : Formatter {
123123
/// - Parameter numberInMeters: the magnitude in terms of meters
124124
/// - Returns: Returns the appropriate unit
125125
private func unit(fromMeters numberInMeters: Double) -> Unit {
126-
if LengthFormatter.isMetricSystemLocale(numberFormatter.locale) {
126+
if numberFormatter.locale.sr3202_fix_isMetricSystemLocale() {
127127
//Person height is always returned in cm for metric system
128128
if isForPersonHeightUse { return .centimeter }
129129

@@ -152,22 +152,6 @@ open class LengthFormatter : Formatter {
152152
}
153153
}
154154

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-
}
171155

172156
/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
173157
/// - Note: Since this API is under consideration it may be either removed or revised in the near future

Foundation/NSMassFormatter.swift

Lines changed: 215 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010

1111
extension MassFormatter {
1212
public enum Unit : Int {
13-
1413
case gram
1514
case kilogram
1615
case ounce
@@ -21,28 +20,238 @@ extension MassFormatter {
2120

2221
open class MassFormatter : Formatter {
2322

23+
public override init() {
24+
numberFormatter = NumberFormatter()
25+
numberFormatter.numberStyle = .decimal
26+
unitStyle = .medium
27+
isForPersonMassUse = false
28+
super.init()
29+
}
30+
2431
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)
2637
}
2738

2839
/*@NSCopying*/ open var numberFormatter: NumberFormatter! // default is NSNumberFormatter with NSNumberFormatterDecimalStyle
2940
open var unitStyle: UnitStyle // default is NSFormattingUnitStyleMedium
41+
3042
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
3143

3244
// 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+
}
3466

3567
// 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 = convertedUnit(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+
}
3786

3887
// 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+
}
40101

41102
// 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 = convertedUnit(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+
}
43123

44124
/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
45125
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
46126
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 convertedUnit(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"]
47238
}
48239

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

Comments
 (0)