-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Initial implementation of MassFormatter. #883
Merged
+365
−25
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,7 +10,6 @@ | |
|
||
extension MassFormatter { | ||
public enum Unit : Int { | ||
|
||
case gram | ||
case kilogram | ||
case ounce | ||
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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 | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
// This source file is part of the Swift.org open source project | ||
// | ||
// Copyright (c) 2014 - 2017 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 | ||
// | ||
|
||
#if DEPLOYMENT_RUNTIME_OBJC || os(Linux) | ||
import Foundation | ||
import XCTest | ||
#else | ||
import SwiftFoundation | ||
import SwiftXCTest | ||
#endif | ||
|
||
class TestMassFormatter: XCTestCase { | ||
let formatter: MassFormatter = MassFormatter() | ||
|
||
static var allTests: [(String, (TestMassFormatter) -> () throws -> Void)] { | ||
return [ | ||
("test_stringFromKilogramsImperialRegion", test_stringFromKilogramsImperialRegion), | ||
("test_stringFromKilogramsMetricRegion", test_stringFromKilogramsMetricRegion), | ||
("test_stringFromKilogramsMetricRegionPersonMassUse", test_stringFromKilogramsMetricRegionPersonMassUse), | ||
("test_stringFromValue", test_stringFromValue), | ||
("test_unitStringFromKilograms", test_unitStringFromKilograms), | ||
("test_unitStringFromValue", test_unitStringFromValue), | ||
] | ||
} | ||
|
||
override func setUp() { | ||
formatter.numberFormatter.locale = Locale(identifier: "en_US") | ||
formatter.isForPersonMassUse = false | ||
super.setUp() | ||
} | ||
|
||
func test_stringFromKilogramsImperialRegion() { | ||
XCTAssertEqual(formatter.string(fromKilograms: -100), "-220.462 lb") | ||
XCTAssertEqual(formatter.string(fromKilograms: 0.00001), "0 oz") | ||
XCTAssertEqual(formatter.string(fromKilograms: 0.0001), "0.004 oz") | ||
XCTAssertEqual(formatter.string(fromKilograms: 1), "2.205 lb") | ||
XCTAssertEqual(formatter.string(fromKilograms: 100), "220.462 lb") | ||
} | ||
|
||
func test_stringFromKilogramsMetricRegion() { | ||
formatter.numberFormatter.locale = Locale(identifier: "de_DE") | ||
XCTAssertEqual(formatter.string(fromKilograms: -100), "-100 kg") | ||
XCTAssertEqual(formatter.string(fromKilograms: -1), "-1 kg") | ||
XCTAssertEqual(formatter.string(fromKilograms: 1000), "1.000 kg") | ||
} | ||
|
||
func test_stringFromKilogramsMetricRegionPersonMassUse() { | ||
formatter.numberFormatter.locale = Locale(identifier: "en_GB") | ||
formatter.isForPersonMassUse = true | ||
XCTAssertEqual(formatter.string(fromKilograms: -100), "-100 kg") | ||
XCTAssertEqual(formatter.string(fromKilograms: -1), "-1 kg") | ||
XCTAssertEqual(formatter.string(fromKilograms: 1000), "1,000 kg") | ||
} | ||
|
||
func test_stringFromValue() { | ||
formatter.unitStyle = Formatter.UnitStyle.long | ||
XCTAssertEqual(formatter.string(fromValue: 0.002, unit: MassFormatter.Unit.kilogram),"0.002 kilograms") | ||
XCTAssertEqual(formatter.string(fromValue: 0, unit:MassFormatter.Unit.stone), "0 stones") | ||
XCTAssertEqual(formatter.string(fromValue: 1, unit:MassFormatter.Unit.stone), "1 stone") | ||
XCTAssertEqual(formatter.string(fromValue: 2.4, unit: MassFormatter.Unit.stone), "2 stones, 5.6 pounds") | ||
|
||
formatter.unitStyle = Formatter.UnitStyle.short | ||
XCTAssertEqual(formatter.string(fromValue: 0.00000001, unit:MassFormatter.Unit.kilogram), "0kg") | ||
XCTAssertEqual(formatter.string(fromValue: 6, unit:MassFormatter.Unit.pound), "6#") | ||
XCTAssertEqual(formatter.string(fromValue: 2.4, unit: MassFormatter.Unit.stone), "2st 5.6#") | ||
XCTAssertEqual(formatter.string(fromValue: 123456, unit: MassFormatter.Unit.stone), "123,456st") | ||
|
||
formatter.unitStyle = Formatter.UnitStyle.medium | ||
XCTAssertEqual(formatter.string(fromValue: 0.00000001, unit:MassFormatter.Unit.kilogram), "0 kg") | ||
XCTAssertEqual(formatter.string(fromValue: 2.4, unit: MassFormatter.Unit.stone), "2 st, 5.6 lb") | ||
XCTAssertEqual(formatter.string(fromValue: 2.0, unit: MassFormatter.Unit.stone), "2 st") | ||
XCTAssertEqual(formatter.string(fromValue: 123456.78, unit: MassFormatter.Unit.stone), "123,456 st, 10.92 lb") | ||
} | ||
|
||
func test_unitStringFromKilograms() { | ||
var unit = MassFormatter.Unit.kilogram | ||
|
||
// imperial | ||
XCTAssertEqual(formatter.unitString(fromKilograms: -100000, usedUnit: &unit), "lb") | ||
XCTAssertEqual(unit, MassFormatter.Unit.pound) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 0, usedUnit: &unit), "lb") | ||
XCTAssertEqual(unit, MassFormatter.Unit.pound) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 0.0001, usedUnit: &unit), "oz") | ||
XCTAssertEqual(unit, MassFormatter.Unit.ounce) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 0.4535, usedUnit: &unit), "oz") | ||
XCTAssertEqual(unit, MassFormatter.Unit.ounce) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 0.4536, usedUnit: &unit), "lb") | ||
XCTAssertEqual(unit, MassFormatter.Unit.pound) | ||
|
||
// metric | ||
formatter.numberFormatter.locale = Locale(identifier: "de_DE") | ||
XCTAssertEqual(formatter.unitString(fromKilograms: -100000, usedUnit: &unit), "kg") | ||
XCTAssertEqual(unit, MassFormatter.Unit.kilogram) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 0, usedUnit: &unit), "kg") | ||
XCTAssertEqual(unit, MassFormatter.Unit.kilogram) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 0.0001, usedUnit: &unit), "g") | ||
XCTAssertEqual(unit, MassFormatter.Unit.gram) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 1.000, usedUnit: &unit), "g") | ||
XCTAssertEqual(unit, MassFormatter.Unit.gram) | ||
|
||
XCTAssertEqual(formatter.unitString(fromKilograms: 1.001, usedUnit: &unit), "kg") | ||
XCTAssertEqual(unit, MassFormatter.Unit.kilogram) | ||
} | ||
|
||
func test_unitStringFromValue() { | ||
formatter.unitStyle = Formatter.UnitStyle.long | ||
XCTAssertEqual(formatter.unitString(fromValue: 0.002, unit: MassFormatter.Unit.kilogram), "kilograms") | ||
XCTAssertEqual(formatter.unitString(fromValue: 0.100, unit: MassFormatter.Unit.gram), "grams") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.000, unit: MassFormatter.Unit.pound), "pounds") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.002, unit: MassFormatter.Unit.ounce), "ounces") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.002, unit: MassFormatter.Unit.stone), "stone") | ||
|
||
formatter.unitStyle = Formatter.UnitStyle.medium | ||
XCTAssertEqual(formatter.unitString(fromValue: 0.002, unit: MassFormatter.Unit.kilogram), "kg") | ||
XCTAssertEqual(formatter.unitString(fromValue: 0.100, unit: MassFormatter.Unit.gram), "g") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.000, unit: MassFormatter.Unit.pound), "lb") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.002, unit: MassFormatter.Unit.ounce), "oz") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.002, unit: MassFormatter.Unit.stone), "st") | ||
|
||
formatter.unitStyle = Formatter.UnitStyle.short | ||
XCTAssertEqual(formatter.unitString(fromValue: 0.002, unit: MassFormatter.Unit.kilogram), "kg") | ||
XCTAssertEqual(formatter.unitString(fromValue: 0.100, unit: MassFormatter.Unit.gram), "g") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.000, unit: MassFormatter.Unit.pound), "lb") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.002, unit: MassFormatter.Unit.ounce), "oz") | ||
XCTAssertEqual(formatter.unitString(fromValue: 2.002, unit: MassFormatter.Unit.stone), "st") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My goal was to use
Measurement<UnitMass>(value:1, unit:.stones).converted(to:.pounds)
here, but that ends up being 0.3471688213196 lb instead of the expected 14 lb.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not that I use stones regularly, but this seems like an issue :-)
Coefficient should be 6.35029? https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/Unit.swift#L1330
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@e78l Those are kilograms not pounds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was referring to the 1 stone=0.347 lb conversion problem by 'this' - changing 0.157473 on L1330 of Unit.swift to 6.35029 should solve that problem.
Appears macOS 10.12.3's Foundation has the stone conversion issue too.
Hard-coding
poundsPerStone = 14.0
looks great (and perhaps is the better way)!