From dd172c281ce0dcdd28c32525fc5100c40a46a5f0 Mon Sep 17 00:00:00 2001 From: Simon Evans Date: Sat, 21 Apr 2018 17:27:11 +0100 Subject: [PATCH 1/2] SR-7481: NumberFormatter inconsistency on Linux - For the numberStyle .currency, use a minimumIntegerDigits of 1. - If minimumIntegerDigits is set to any value (including 0) before the numberStyle is set, preserve the original value. (cherry picked from commit c8c16eb0e69afd073554c9ae7a959d8ec7d61af6) --- Foundation/NumberFormatter.swift | 16 +++++++++++----- TestFoundation/TestNumberFormatter.swift | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/Foundation/NumberFormatter.swift b/Foundation/NumberFormatter.swift index 61650c30fe..03f600c0c1 100644 --- a/Foundation/NumberFormatter.swift +++ b/Foundation/NumberFormatter.swift @@ -140,7 +140,7 @@ open class NumberFormatter : Formatter { _setFormatterAttribute(formatter, attributeName: kCFNumberFormatterPlusSign, value: _plusSign?._cfObject) _setFormatterAttribute(formatter, attributeName: kCFNumberFormatterCurrencySymbol, value: _currencySymbol?._cfObject) _setFormatterAttribute(formatter, attributeName: kCFNumberFormatterExponentSymbol, value: _exponentSymbol?._cfObject) - _setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMinIntegerDigits, value: _minimumIntegerDigits._bridgeToObjectiveC()._cfObject) + _setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMinIntegerDigits, value: minimumIntegerDigits._bridgeToObjectiveC()._cfObject) _setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMaxIntegerDigits, value: _maximumIntegerDigits._bridgeToObjectiveC()._cfObject) _setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMinFractionDigits, value: _minimumFractionDigits._bridgeToObjectiveC()._cfObject) if _minimumFractionDigits <= 0 { @@ -190,12 +190,15 @@ open class NumberFormatter : Formatter { case .currency, .currencyPlural, .currencyISOCode, .currencyAccounting: _usesSignificantDigits = false _usesGroupingSeparator = true + if _minimumIntegerDigits == nil { + _minimumIntegerDigits = 1 + } _minimumFractionDigits = 2 case .decimal: _usesGroupingSeparator = true _maximumFractionDigits = 3 - if _minimumIntegerDigits == 0 { + if _minimumIntegerDigits == nil { _minimumIntegerDigits = 1 } if _groupingSize == 0 { @@ -680,11 +683,14 @@ open class NumberFormatter : Formatter { _roundingIncrement = newValue } } - - internal var _minimumIntegerDigits: Int = 0 + + // Use an optional for _minimumIntegerDigits to track if the value is + // set BEFORE the .numberStyle is changed. This allows preserving a setting + // of 0. + internal var _minimumIntegerDigits: Int? open var minimumIntegerDigits: Int { get { - return _minimumIntegerDigits + return _minimumIntegerDigits ?? 0 } set { _reset() diff --git a/TestFoundation/TestNumberFormatter.swift b/TestFoundation/TestNumberFormatter.swift index c97c5ba32a..e9aa53f2f7 100644 --- a/TestFoundation/TestNumberFormatter.swift +++ b/TestFoundation/TestNumberFormatter.swift @@ -221,6 +221,28 @@ class TestNumberFormatter: XCTestCase { XCTAssertEqual(numberFormatter.minimumIntegerDigits, 3) formattedString = numberFormatter.string(from: 0.1) XCTAssertEqual(formattedString, "000.1") + + numberFormatter.numberStyle = .currency + XCTAssertEqual(numberFormatter.minimumIntegerDigits, 3) + + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let currencyFormatter = NumberFormatter() + XCTAssertEqual(currencyFormatter.minimumIntegerDigits, 0) + currencyFormatter.minimumIntegerDigits = 0 + currencyFormatter.numberStyle = .currency + XCTAssertEqual(currencyFormatter.minimumIntegerDigits, 0) + currencyFormatter.locale = Locale(identifier: "en_US") + formattedString = currencyFormatter.string(from: NSNumber(value: 0)) + XCTAssertEqual(formattedString, "$.00") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let currencyFormatter2 = NumberFormatter() + XCTAssertEqual(currencyFormatter2.minimumIntegerDigits, 0) + currencyFormatter2.numberStyle = .currency + XCTAssertEqual(currencyFormatter2.minimumIntegerDigits, 1) + currencyFormatter2.locale = Locale(identifier: "en_US") + formattedString = currencyFormatter2.string(from: NSNumber(value: 0)) + XCTAssertEqual(formattedString, "$0.00") } func test_maximumIntegerDigits() { From 3290d07b3c1a34214ca0d0b15ab063c4f0c52946 Mon Sep 17 00:00:00 2001 From: Simon Evans Date: Wed, 25 Apr 2018 00:29:18 +0100 Subject: [PATCH 2/2] NumberFormatter: More fixes for numberStyles - Fix rawValue for Style.{currencyISOCode,currencyPlural,currencyAccounting} - Fix minimumIntegerDigits for .currencyPlural - Fix usesSignificantDigits, minimumIntegerDigits and groupingSize for .percent (cherry picked from commit 3fef3a857d0302a7a919e857635601d1d31969d5) --- Foundation/NumberFormatter.swift | 54 ++++-- TestFoundation/TestNumberFormatter.swift | 210 ++++++++++++++++++++--- 2 files changed, 230 insertions(+), 34 deletions(-) diff --git a/Foundation/NumberFormatter.swift b/Foundation/NumberFormatter.swift index 03f600c0c1..8c663b7793 100644 --- a/Foundation/NumberFormatter.swift +++ b/Foundation/NumberFormatter.swift @@ -24,16 +24,16 @@ internal let kCFNumberFormatterCurrencyAccountingStyle = CFNumberFormatterStyle. extension NumberFormatter { public enum Style : UInt { - case none - case decimal - case currency - case percent - case scientific - case spellOut - case ordinal - case currencyISOCode - case currencyPlural - case currencyAccounting + case none = 0 + case decimal = 1 + case currency = 2 + case percent = 3 + case scientific = 4 + case spellOut = 5 + case ordinal = 6 + case currencyISOCode = 8 // 7 is not used + case currencyPlural = 9 + case currencyAccounting = 10 } public enum PadPosition : UInt { @@ -187,14 +187,25 @@ open class NumberFormatter : Formatter { case .none, .ordinal, .spellOut: _usesSignificantDigits = false - case .currency, .currencyPlural, .currencyISOCode, .currencyAccounting: + case .currency, .currencyISOCode, .currencyAccounting: _usesSignificantDigits = false _usesGroupingSeparator = true if _minimumIntegerDigits == nil { _minimumIntegerDigits = 1 } + if _groupingSize == 0 { + _groupingSize = 3 + } _minimumFractionDigits = 2 - + + case .currencyPlural: + _usesSignificantDigits = false + _usesGroupingSeparator = true + if _minimumIntegerDigits == nil { + _minimumIntegerDigits = 0 + } + _minimumFractionDigits = 2 + case .decimal: _usesGroupingSeparator = true _maximumFractionDigits = 3 @@ -205,9 +216,24 @@ open class NumberFormatter : Formatter { _groupingSize = 3 } - default: - _usesSignificantDigits = true + case .percent: + _usesSignificantDigits = false _usesGroupingSeparator = true + if _minimumIntegerDigits == nil { + _minimumIntegerDigits = 1 + } + if _groupingSize == 0 { + _groupingSize = 3 + } + _minimumFractionDigits = 0 + _maximumFractionDigits = 0 + + case .scientific: + _usesSignificantDigits = false + _usesGroupingSeparator = false + if _minimumIntegerDigits == nil { + _minimumIntegerDigits = 0 + } } _reset() _numberStyle = newValue diff --git a/TestFoundation/TestNumberFormatter.swift b/TestFoundation/TestNumberFormatter.swift index e9aa53f2f7..82cd593401 100644 --- a/TestFoundation/TestNumberFormatter.swift +++ b/TestFoundation/TestNumberFormatter.swift @@ -24,7 +24,15 @@ class TestNumberFormatter: XCTestCase { ("test_plusSignSymbol", test_plusSignSymbol), ("test_currencySymbol", test_currencySymbol), ("test_exponentSymbol", test_exponentSymbol), - ("test_minimumIntegerDigits", test_minimumIntegerDigits), + ("test_decimalMinimumIntegerDigits", test_decimalMinimumIntegerDigits), + ("test_currencyMinimumIntegerDigits", test_currencyMinimumIntegerDigits), + ("test_percentMinimumIntegerDigits", test_percentMinimumIntegerDigits), + ("test_scientificMinimumIntegerDigits", test_scientificMinimumIntegerDigits), + ("test_spellOutMinimumIntegerDigits", test_spellOutMinimumIntegerDigits), + ("test_ordinalMinimumIntegerDigits", test_ordinalMinimumIntegerDigits), + ("test_currencyPluralMinimumIntegerDigits", test_currencyPluralMinimumIntegerDigits), + ("test_currencyISOCodeMinimumIntegerDigits", test_currencyISOCodeMinimumIntegerDigits), + ("test_currencyAccountingMinimumIntegerDigits", test_currencyAccountingMinimumIntegerDigits), ("test_maximumIntegerDigits", test_maximumIntegerDigits), ("test_minimumFractionDigits", test_minimumFractionDigits), ("test_maximumFractionDigits", test_maximumFractionDigits), @@ -202,7 +210,7 @@ class TestNumberFormatter: XCTestCase { XCTAssertEqual(formattedString, "4.2⬆️1") } - func test_minimumIntegerDigits() { + func test_decimalMinimumIntegerDigits() { let numberFormatter1 = NumberFormatter() XCTAssertEqual(numberFormatter1.minimumIntegerDigits, 0) numberFormatter1.minimumIntegerDigits = 3 @@ -221,30 +229,192 @@ class TestNumberFormatter: XCTestCase { XCTAssertEqual(numberFormatter.minimumIntegerDigits, 3) formattedString = numberFormatter.string(from: 0.1) XCTAssertEqual(formattedString, "000.1") + } - numberFormatter.numberStyle = .currency - XCTAssertEqual(numberFormatter.minimumIntegerDigits, 3) + func test_currencyMinimumIntegerDigits() { + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .currency + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "$.00") + XCTAssertEqual(formatter.string(from: 1.23), "$1.23") + XCTAssertEqual(formatter.string(from: 123.4), "$123.40") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .currency + XCTAssertEqual(formatter2.minimumIntegerDigits, 1) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0.001), "$0.00") + XCTAssertEqual(formatter2.string(from: 1.234), "$1.23") + XCTAssertEqual(formatter2.string(from: 123456.7), "$123,456.70") + } + func test_percentMinimumIntegerDigits() { // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value - let currencyFormatter = NumberFormatter() - XCTAssertEqual(currencyFormatter.minimumIntegerDigits, 0) - currencyFormatter.minimumIntegerDigits = 0 - currencyFormatter.numberStyle = .currency - XCTAssertEqual(currencyFormatter.minimumIntegerDigits, 0) - currencyFormatter.locale = Locale(identifier: "en_US") - formattedString = currencyFormatter.string(from: NSNumber(value: 0)) - XCTAssertEqual(formattedString, "$.00") + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .percent + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "0%") + XCTAssertEqual(formatter.string(from: 1.234), "123%") + XCTAssertEqual(formatter.string(from: 123.4), "12,340%") // If .minimumIntegerDigits is not set before .numberStyle change, update the value - let currencyFormatter2 = NumberFormatter() - XCTAssertEqual(currencyFormatter2.minimumIntegerDigits, 0) - currencyFormatter2.numberStyle = .currency - XCTAssertEqual(currencyFormatter2.minimumIntegerDigits, 1) - currencyFormatter2.locale = Locale(identifier: "en_US") - formattedString = currencyFormatter2.string(from: NSNumber(value: 0)) - XCTAssertEqual(formattedString, "$0.00") + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .percent + XCTAssertEqual(formatter2.minimumIntegerDigits, 1) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0.01), "1%") + XCTAssertEqual(formatter2.string(from: 1.234), "123%") + XCTAssertEqual(formatter2.string(from: 123456.7), "12,345,670%") } - + + func test_scientificMinimumIntegerDigits() { + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .scientific + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "0E0") + XCTAssertEqual(formatter.string(from: 1.23), "1.23E0") + XCTAssertEqual(formatter.string(from: 123.4), "1.234E2") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .scientific + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0.01), "1E-2") + XCTAssertEqual(formatter2.string(from: 1.234), "1.234E0") + XCTAssertEqual(formatter2.string(from: 123456.7), "1.234567E5") + } + + func test_spellOutMinimumIntegerDigits() { + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .spellOut + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "zero") + XCTAssertEqual(formatter.string(from: 1.23), "one point two three") + XCTAssertEqual(formatter.string(from: 123.4), "one hundred twenty-three point four") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .spellOut + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0.01), "zero point zero one") + XCTAssertEqual(formatter2.string(from: 1.234), "one point two three four") + XCTAssertEqual(formatter2.string(from: 123456.7), "one hundred twenty-three thousand four hundred fifty-six point seven") + } + + func test_ordinalMinimumIntegerDigits() { + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .ordinal + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "0th") + XCTAssertEqual(formatter.string(from: 1.23), "1st") + XCTAssertEqual(formatter.string(from: 123.4), "123rd") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .ordinal + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0.01), "0th") + XCTAssertEqual(formatter2.string(from: 4.234), "4th") + XCTAssertEqual(formatter2.string(from: 42), "42nd") + } + + func test_currencyPluralMinimumIntegerDigits() { + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .currencyPlural + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "0.00 US dollars") + XCTAssertEqual(formatter.string(from: 1.23), "1.23 US dollars") + XCTAssertEqual(formatter.string(from: 123.4), "123.40 US dollars") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .currencyPlural + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0.01), "0.01 US dollars") + XCTAssertEqual(formatter2.string(from: 1.234), "1.23 US dollars") + XCTAssertEqual(formatter2.string(from: 123456.7), "123,456.70 US dollars") + } + + func test_currencyISOCodeMinimumIntegerDigits() { + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .currencyISOCode + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "USD.00") + XCTAssertEqual(formatter.string(from: 1.23), "USD1.23") + XCTAssertEqual(formatter.string(from: 123.4), "USD123.40") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .currencyISOCode + XCTAssertEqual(formatter2.minimumIntegerDigits, 1) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0.01), "USD0.01") + XCTAssertEqual(formatter2.string(from: 1.234), "USD1.23") + XCTAssertEqual(formatter2.string(from: 123456.7), "USD123,456.70") + } + + func test_currencyAccountingMinimumIntegerDigits() { + // If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value + let formatter = NumberFormatter() + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.minimumIntegerDigits = 0 + formatter.numberStyle = .currencyAccounting + XCTAssertEqual(formatter.minimumIntegerDigits, 0) + formatter.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter.string(from: 0), "$.00") + XCTAssertEqual(formatter.string(from: 1.23), "$1.23") + XCTAssertEqual(formatter.string(from: 123.4), "$123.40") + + // If .minimumIntegerDigits is not set before .numberStyle change, update the value + let formatter2 = NumberFormatter() + XCTAssertEqual(formatter2.minimumIntegerDigits, 0) + formatter2.numberStyle = .currencyAccounting + XCTAssertEqual(formatter2.minimumIntegerDigits, 1) + formatter2.locale = Locale(identifier: "en_US") + XCTAssertEqual(formatter2.string(from: 0), "$0.00") + XCTAssertEqual(formatter2.string(from: 1.23), "$1.23") + XCTAssertEqual(formatter2.string(from: 123.4), "$123.40") + } + func test_maximumIntegerDigits() { let numberFormatter = NumberFormatter() numberFormatter.maximumIntegerDigits = 3