blob: 567438aee03af1984beb9b1cb81468a08e829f63 [file] [log] [blame]
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
extension MassFormatter {
public enum Unit : Int {
case gram
case kilogram
case ounce
case pound
case stone
}
}
open class MassFormatter : Formatter {
public override init() {
numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
unitStyle = .medium
isForPersonMassUse = false
super.init()
}
public required init?(coder: NSCoder) {
numberFormatter = NumberFormatter()
numberFormatter.numberStyle = .decimal
unitStyle = .medium
isForPersonMassUse = false
super.init(coder:coder)
}
/*@NSCopying*/ open var numberFormatter: NumberFormatter! // default is NumberFormatter with NumberFormatter.Style.decimal
open var unitStyle: UnitStyle // default is Formatting.UnitStyle.medium
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 {
// 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 {
//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 {
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 {
//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.usesMetricSystem {
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"]
}