Skip to content

Commit

Permalink
Initial implementation of MassFormatter. (swiftlang#883)
Browse files Browse the repository at this point in the history
  • Loading branch information
jszumski authored and parkera committed Jun 2, 2017
1 parent 28fe032 commit 766645d
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 25 deletions.
2 changes: 1 addition & 1 deletion Docs/Status.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ There is no _Complete_ status for test coverage because there are always additio
| `EnergyFormatter` | Unimplemented | None | |
| `ISO8601DateFormatter` | Unimplemented | None | |
| `LengthFormatter` | Complete | Substantial | |
| `MassFormatter` | Unimplemented | None | |
| `MassFormatter` | Complete | Substantial | Needs localization |
| `NumberFormatter` | Mostly Complete | Substantial | `objectValue(_:range:)` remains unimplemented |
| `PersonNameComponentsFormatter` | Unimplemented | None | |
| `ByteCountFormatter` | Mostly Complete | Substantial | `init?(coder:)` remains unimplemented |
Expand Down
6 changes: 6 additions & 0 deletions Foundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,7 @@
7900433B1CACD33E00ECCBF1 /* TestNSCompoundPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 790043391CACD33E00ECCBF1 /* TestNSCompoundPredicate.swift */; };
7900433C1CACD33E00ECCBF1 /* TestNSPredicate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7900433A1CACD33E00ECCBF1 /* TestNSPredicate.swift */; };
90E645DF1E4C89A400D0D47C /* TestNSCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90E645DE1E4C89A400D0D47C /* TestNSCache.swift */; };
A058C2021E529CF100B07AA1 /* TestMassFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */; };
AE35A1861CBAC85E0042DB84 /* SwiftFoundation.h in Headers */ = {isa = PBXBuildFile; fileRef = AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */; settings = {ATTRIBUTES = (Public, ); }; };
BD8042161E09857800487EB8 /* TestNSLengthFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8042151E09857800487EB8 /* TestNSLengthFormatter.swift */; };
BDBB65901E256BFA001A7286 /* TestNSEnergyFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDBB658F1E256BFA001A7286 /* TestNSEnergyFormatter.swift */; };
Expand Down Expand Up @@ -766,6 +767,7 @@
84BA558D1C16F90900F48C54 /* TestNSTimeZone.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSTimeZone.swift; sourceTree = "<group>"; };
88D28DE61C13AE9000494606 /* TestNSGeometry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSGeometry.swift; sourceTree = "<group>"; };
90E645DE1E4C89A400D0D47C /* TestNSCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSCache.swift; sourceTree = "<group>"; };
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestMassFormatter.swift; sourceTree = "<group>"; };
A5A34B551C18C85D00FD972B /* TestNSByteCountFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSByteCountFormatter.swift; sourceTree = "<group>"; };
AE35A1851CBAC85E0042DB84 /* SwiftFoundation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SwiftFoundation.h; sourceTree = "<group>"; };
B167A6641ED7303F0040B09A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1418,6 +1420,7 @@
D3047AEB1C38BC3300295652 /* TestNSValue.swift */,
5B6F17951C48631C00935030 /* TestNSXMLDocument.swift */,
5B40F9F11C125187000E72E3 /* TestNSXMLParser.swift */,
A058C2011E529CF100B07AA1 /* TestMassFormatter.swift */,
CC5249BF1D341D23007CB54D /* TestUnitConverter.swift */,
5B6F17961C48631C00935030 /* TestUtils.swift */,
0383A1741D2E558A0052E5D1 /* TestNSStream.swift */,
Expand Down Expand Up @@ -2267,6 +2270,9 @@
1520469B1D8AEABE00D02E36 /* HTTPServer.swift in Sources */,
5B13B3471C582D4C00651CE2 /* TestThread.swift in Sources */,
5B13B32E1C582D4C00651CE2 /* TestFileManager.swift in Sources */,
5B13B3471C582D4C00651CE2 /* TestNSThread.swift in Sources */,
5B13B32E1C582D4C00651CE2 /* TestNSFileManager.swift in Sources */,
A058C2021E529CF100B07AA1 /* TestMassFormatter.swift in Sources */,
5B13B3381C582D4C00651CE2 /* TestNSNotificationQueue.swift in Sources */,
CC5249C01D341D23007CB54D /* TestUnitConverter.swift in Sources */,
5B13B3331C582D4C00651CE2 /* TestNSJSONSerialization.swift in Sources */,
Expand Down
20 changes: 2 additions & 18 deletions Foundation/NSLengthFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ open class LengthFormatter : Formatter {
//Extract the number from the measurement
let numberInUnit = unitMeasurement.value

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

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

Expand Down Expand Up @@ -152,22 +152,6 @@ open class LengthFormatter : Formatter {
}
}

/// TODO: Replace calls to the below function to use Locale.usesMetricSystem
/// Temporary workaround due to unpopulated Locale attributes
/// See https://bugs.swift.org/browse/SR-3202
private static func isMetricSystemLocale(_ locale: Locale) -> Bool {
switch locale.identifier {
case "en_US": return false
case "en_US_POSIX": return false
case "haw_US": return false
case "es_US": return false
case "chr_US": return false
case "my_MM": return false
case "en_LR": return false
case "vai_LR": return false
default: return true
}
}

/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
Expand Down
221 changes: 215 additions & 6 deletions Foundation/NSMassFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@

extension MassFormatter {
public enum Unit : Int {

case gram
case kilogram
case ounce
Expand All @@ -21,28 +20,238 @@ extension MassFormatter {

open class MassFormatter : Formatter {

public override init() {
numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
unitStyle = .medium
isForPersonMassUse = false
super.init()
}

public required init?(coder: NSCoder) {
NSUnimplemented()
numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
unitStyle = .medium
isForPersonMassUse = false
super.init(coder:coder)
}

/*@NSCopying*/ open var numberFormatter: NumberFormatter! // default is NSNumberFormatter with NSNumberFormatterDecimalStyle
open var unitStyle: UnitStyle // default is NSFormattingUnitStyleMedium

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

// Format a combination of a number and an unit to a localized string.
open func string(fromValue value: Double, unit: Unit) -> String { NSUnimplemented() }
open func string(fromValue value: Double, unit: Unit) -> String {
// special case: stone shows fractional values in pounds
if unit == .stone {
let stone = value.rounded(.towardZero)
let stoneString = singlePartString(fromValue: stone, unit: unit) // calling `string(fromValue: stone, unit: .stone)` would infinitely recur
let pounds = abs(value.truncatingRemainder(dividingBy: 1.0)) * MassFormatter.poundsPerStone

// if we don't have any fractional component, don't append anything
if pounds == 0 {
return stoneString
} else {
let poundsString = string(fromValue: pounds, unit: .pound)
let separator = unitStyle == MassFormatter.UnitStyle.short ? " " : ", "

return ("\(stoneString)\(separator)\(poundsString)")
}
}

// normal case: kilograms and pounds
return singlePartString(fromValue: value, unit: unit)
}

// 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).
open func string(fromKilograms numberInKilograms: Double) -> String { NSUnimplemented() }
open func string(fromKilograms numberInKilograms: Double) -> String {
//Convert to the locale-appropriate unit
let unitFromKilograms = convertedUnit(fromKilograms: numberInKilograms)

//Map the unit to UnitMass type for conversion later
let unitMassFromKilograms = MassFormatter.unitMass[unitFromKilograms]!

//Create a measurement object based on the value in kilograms
let kilogramMeasurement = Measurement<UnitMass>(value:numberInKilograms, unit: .kilograms)

//Convert the object to the locale-appropriate unit determined above
let unitMeasurement = kilogramMeasurement.converted(to: unitMassFromKilograms)

//Extract the number from the measurement
let numberInUnit = unitMeasurement.value

return string(fromValue: numberInUnit, unit: unitFromKilograms)
}

// Return a localized string of the given unit, and if the unit is singular or plural is based on the given number.
open func unitString(fromValue value: Double, unit: Unit) -> String { NSUnimplemented() }
open func unitString(fromValue value: Double, unit: Unit) -> String {
if unitStyle == .short {
return MassFormatter.shortSymbol[unit]!
} else if unitStyle == .medium {
return MassFormatter.mediumSymbol[unit]!
} else if unit == .stone { // special case, see `unitStringDisplayedAdjacent(toValue:, unit:)`
return MassFormatter.largeSingularSymbol[unit]!
} else if value == 1.0 {
return MassFormatter.largeSingularSymbol[unit]!
} else {
return MassFormatter.largePluralSymbol[unit]!
}
}

// Return the locale-appropriate unit, the same unit used by -stringFromKilograms:.
open func unitString(fromKilograms numberInKilograms: Double, usedUnit unitp: UnsafeMutablePointer<Unit>?) -> String { NSUnimplemented() }
open func unitString(fromKilograms numberInKilograms: Double, usedUnit unitp: UnsafeMutablePointer<Unit>?) -> String {
//Convert to the locale-appropriate unit
let unitFromKilograms = convertedUnit(fromKilograms: numberInKilograms)
unitp?.pointee = unitFromKilograms

//Map the unit to UnitMass type for conversion later
let unitMassFromKilograms = MassFormatter.unitMass[unitFromKilograms]!

//Create a measurement object based on the value in kilograms
let kilogramMeasurement = Measurement<UnitMass>(value:numberInKilograms, unit: .kilograms)

//Convert the object to the locale-appropriate unit determined above
let unitMeasurement = kilogramMeasurement.converted(to: unitMassFromKilograms)

//Extract the number from the measurement
let numberInUnit = unitMeasurement.value

//Return the appropriate representation of the unit based on the selected unit style
return unitString(fromValue: numberInUnit, unit: unitFromKilograms)
}

/// - Experiment: This is a draft API currently under consideration for official import into Foundation as a suitable alternative
/// - Note: Since this API is under consideration it may be either removed or revised in the near future
open override func objectValue(_ string: String) throws -> Any? { return nil }


// MARK: - Private

/// This method selects the appropriate unit based on the formatter’s locale,
/// the magnitude of the value, and isForPersonMassUse property.
///
/// - Parameter numberInKilograms: the magnitude in terms of kilograms
/// - Returns: Returns the appropriate unit
private func convertedUnit(fromKilograms numberInKilograms: Double) -> Unit {
if numberFormatter.locale.sr3202_fix_isMetricSystemLocale() {
if numberInKilograms > 1.0 || numberInKilograms <= 0.0 {
return .kilogram
} else {
return .gram
}
} else {
let metricMeasurement = Measurement<UnitMass>(value:numberInKilograms, unit: .kilograms)
let imperialMeasurement = metricMeasurement.converted(to: .pounds)
let numberInPounds = imperialMeasurement.value

if numberInPounds >= 1.0 || numberInPounds <= 0.0 {
return .pound
} else {
return .ounce
}
}
}

/// Formats the given value and unit into a string containing one logical
/// value. This is intended for units like kilogram and pound where
/// fractional values are represented as a decimal instead of converted
/// values in another unit.
///
/// - Parameter value: The mass's value in the given unit.
/// - Parameter unit: The unit used in the resulting mass string.
/// - Returns: A properly formatted mass string for the given value and unit.
private func singlePartString(fromValue value: Double, unit: Unit) -> String {
guard let formattedValue = numberFormatter.string(from:NSNumber(value: value)) else {
fatalError("Cannot format \(value) as string")
}

let separator = unitStyle == MassFormatter.UnitStyle.short ? "" : " "

return "\(formattedValue)\(separator)\(unitStringDisplayedAdjacent(toValue: value, unit: unit))"
}

/// Return the locale-appropriate unit to be shown adjacent to the given
/// value. In most cases this will match `unitStringDisplayedAdjacent(toValue:, unit:)`
/// however there are a few special cases:
/// - Imperial pounds with a short representation use "lb" in the
/// abstract and "#" only when shown with a numeral.
/// - Stones are are singular in the abstract and only plural when
/// shown with a numeral.
///
/// - Parameter value: The mass's value in the given unit.
/// - Parameter unit: The unit used in the resulting mass string.
/// - Returns: The locale-appropriate unit
open func unitStringDisplayedAdjacent(toValue value: Double, unit: Unit) -> String {
if unit == .pound && unitStyle == .short {
return "#"
} else if unit == .stone && unitStyle == .long {
if value == 1.0 {
return MassFormatter.largeSingularSymbol[unit]!
} else {
return MassFormatter.largePluralSymbol[unit]!
}
} else {
return unitString(fromValue: value, unit: unit)
}
}



/// The number of pounds in 1 stone
private static let poundsPerStone = 14.0

/// Maps MassFormatter.Unit enum to UnitMass class. Used for measurement conversion.
private static let unitMass: [Unit: UnitMass] = [.gram: .grams,
.kilogram: .kilograms,
.ounce: .ounces,
.pound: .pounds,
.stone: .stones]

/// Maps a unit to its short symbol. Reuses strings from UnitMass.
private static let shortSymbol: [Unit: String] = [.gram: UnitMass.grams.symbol,
.kilogram: UnitMass.kilograms.symbol,
.ounce: UnitMass.ounces.symbol,
.pound: UnitMass.pounds.symbol, // see `unitStringDisplayedAdjacent(toValue:, unit:)`
.stone: UnitMass.stones.symbol]

/// Maps a unit to its medium symbol. Reuses strings from UnitMass.
private static let mediumSymbol: [Unit: String] = [.gram: UnitMass.grams.symbol,
.kilogram: UnitMass.kilograms.symbol,
.ounce: UnitMass.ounces.symbol,
.pound: UnitMass.pounds.symbol,
.stone: UnitMass.stones.symbol]

/// Maps a unit to its large, singular symbol.
private static let largeSingularSymbol: [Unit: String] = [.gram: "gram",
.kilogram: "kilogram",
.ounce: "ounce",
.pound: "pound",
.stone: "stone"]

/// Maps a unit to its large, plural symbol.
private static let largePluralSymbol: [Unit: String] = [.gram: "grams",
.kilogram: "kilograms",
.ounce: "ounces",
.pound: "pounds",
.stone: "stones"]
}

internal extension Locale {
/// TODO: Replace calls to the below function to use Locale.usesMetricSystem
/// Temporary workaround due to unpopulated Locale attributes
/// See https://bugs.swift.org/browse/SR-3202
internal func sr3202_fix_isMetricSystemLocale() -> Bool {
switch self.identifier {
case "en_US": return false
case "en_US_POSIX": return false
case "haw_US": return false
case "es_US": return false
case "chr_US": return false
case "my_MM": return false
case "en_LR": return false
case "vai_LR": return false
default: return true
}
}
}
Loading

0 comments on commit 766645d

Please sign in to comment.