Skip to content

Commit ff9b072

Browse files
authored
Merge pull request swiftlang#1975 from spevans/pr_sr_7481_42
[4.2] SR-7481: NumberFormatter inconsistency on Linux
2 parents 684bb66 + 3290d07 commit ff9b072

File tree

2 files changed

+246
-22
lines changed

2 files changed

+246
-22
lines changed

Foundation/NumberFormatter.swift

Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ internal let kCFNumberFormatterCurrencyAccountingStyle = CFNumberFormatterStyle.
2424

2525
extension NumberFormatter {
2626
public enum Style : UInt {
27-
case none
28-
case decimal
29-
case currency
30-
case percent
31-
case scientific
32-
case spellOut
33-
case ordinal
34-
case currencyISOCode
35-
case currencyPlural
36-
case currencyAccounting
27+
case none = 0
28+
case decimal = 1
29+
case currency = 2
30+
case percent = 3
31+
case scientific = 4
32+
case spellOut = 5
33+
case ordinal = 6
34+
case currencyISOCode = 8 // 7 is not used
35+
case currencyPlural = 9
36+
case currencyAccounting = 10
3737
}
3838

3939
public enum PadPosition : UInt {
@@ -140,7 +140,7 @@ open class NumberFormatter : Formatter {
140140
_setFormatterAttribute(formatter, attributeName: kCFNumberFormatterPlusSign, value: _plusSign?._cfObject)
141141
_setFormatterAttribute(formatter, attributeName: kCFNumberFormatterCurrencySymbol, value: _currencySymbol?._cfObject)
142142
_setFormatterAttribute(formatter, attributeName: kCFNumberFormatterExponentSymbol, value: _exponentSymbol?._cfObject)
143-
_setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMinIntegerDigits, value: _minimumIntegerDigits._bridgeToObjectiveC()._cfObject)
143+
_setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMinIntegerDigits, value: minimumIntegerDigits._bridgeToObjectiveC()._cfObject)
144144
_setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMaxIntegerDigits, value: _maximumIntegerDigits._bridgeToObjectiveC()._cfObject)
145145
_setFormatterAttribute(formatter, attributeName: kCFNumberFormatterMinFractionDigits, value: _minimumFractionDigits._bridgeToObjectiveC()._cfObject)
146146
if _minimumFractionDigits <= 0 {
@@ -187,24 +187,53 @@ open class NumberFormatter : Formatter {
187187
case .none, .ordinal, .spellOut:
188188
_usesSignificantDigits = false
189189

190-
case .currency, .currencyPlural, .currencyISOCode, .currencyAccounting:
190+
case .currency, .currencyISOCode, .currencyAccounting:
191191
_usesSignificantDigits = false
192192
_usesGroupingSeparator = true
193+
if _minimumIntegerDigits == nil {
194+
_minimumIntegerDigits = 1
195+
}
196+
if _groupingSize == 0 {
197+
_groupingSize = 3
198+
}
193199
_minimumFractionDigits = 2
194-
200+
201+
case .currencyPlural:
202+
_usesSignificantDigits = false
203+
_usesGroupingSeparator = true
204+
if _minimumIntegerDigits == nil {
205+
_minimumIntegerDigits = 0
206+
}
207+
_minimumFractionDigits = 2
208+
195209
case .decimal:
196210
_usesGroupingSeparator = true
197211
_maximumFractionDigits = 3
198-
if _minimumIntegerDigits == 0 {
212+
if _minimumIntegerDigits == nil {
199213
_minimumIntegerDigits = 1
200214
}
201215
if _groupingSize == 0 {
202216
_groupingSize = 3
203217
}
204218

205-
default:
206-
_usesSignificantDigits = true
219+
case .percent:
220+
_usesSignificantDigits = false
207221
_usesGroupingSeparator = true
222+
if _minimumIntegerDigits == nil {
223+
_minimumIntegerDigits = 1
224+
}
225+
if _groupingSize == 0 {
226+
_groupingSize = 3
227+
}
228+
_minimumFractionDigits = 0
229+
_maximumFractionDigits = 0
230+
231+
case .scientific:
232+
_usesSignificantDigits = false
233+
_usesGroupingSeparator = false
234+
if _minimumIntegerDigits == nil {
235+
_minimumIntegerDigits = 0
236+
}
208237
}
209238
_reset()
210239
_numberStyle = newValue
@@ -680,11 +709,14 @@ open class NumberFormatter : Formatter {
680709
_roundingIncrement = newValue
681710
}
682711
}
683-
684-
internal var _minimumIntegerDigits: Int = 0
712+
713+
// Use an optional for _minimumIntegerDigits to track if the value is
714+
// set BEFORE the .numberStyle is changed. This allows preserving a setting
715+
// of 0.
716+
internal var _minimumIntegerDigits: Int?
685717
open var minimumIntegerDigits: Int {
686718
get {
687-
return _minimumIntegerDigits
719+
return _minimumIntegerDigits ?? 0
688720
}
689721
set {
690722
_reset()

TestFoundation/TestNumberFormatter.swift

Lines changed: 195 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,15 @@ class TestNumberFormatter: XCTestCase {
2424
("test_plusSignSymbol", test_plusSignSymbol),
2525
("test_currencySymbol", test_currencySymbol),
2626
("test_exponentSymbol", test_exponentSymbol),
27-
("test_minimumIntegerDigits", test_minimumIntegerDigits),
27+
("test_decimalMinimumIntegerDigits", test_decimalMinimumIntegerDigits),
28+
("test_currencyMinimumIntegerDigits", test_currencyMinimumIntegerDigits),
29+
("test_percentMinimumIntegerDigits", test_percentMinimumIntegerDigits),
30+
("test_scientificMinimumIntegerDigits", test_scientificMinimumIntegerDigits),
31+
("test_spellOutMinimumIntegerDigits", test_spellOutMinimumIntegerDigits),
32+
("test_ordinalMinimumIntegerDigits", test_ordinalMinimumIntegerDigits),
33+
("test_currencyPluralMinimumIntegerDigits", test_currencyPluralMinimumIntegerDigits),
34+
("test_currencyISOCodeMinimumIntegerDigits", test_currencyISOCodeMinimumIntegerDigits),
35+
("test_currencyAccountingMinimumIntegerDigits", test_currencyAccountingMinimumIntegerDigits),
2836
("test_maximumIntegerDigits", test_maximumIntegerDigits),
2937
("test_minimumFractionDigits", test_minimumFractionDigits),
3038
("test_maximumFractionDigits", test_maximumFractionDigits),
@@ -202,7 +210,7 @@ class TestNumberFormatter: XCTestCase {
202210
XCTAssertEqual(formattedString, "4.2⬆️1")
203211
}
204212

205-
func test_minimumIntegerDigits() {
213+
func test_decimalMinimumIntegerDigits() {
206214
let numberFormatter1 = NumberFormatter()
207215
XCTAssertEqual(numberFormatter1.minimumIntegerDigits, 0)
208216
numberFormatter1.minimumIntegerDigits = 3
@@ -222,7 +230,191 @@ class TestNumberFormatter: XCTestCase {
222230
formattedString = numberFormatter.string(from: 0.1)
223231
XCTAssertEqual(formattedString, "000.1")
224232
}
225-
233+
234+
func test_currencyMinimumIntegerDigits() {
235+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
236+
let formatter = NumberFormatter()
237+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
238+
formatter.minimumIntegerDigits = 0
239+
formatter.numberStyle = .currency
240+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
241+
formatter.locale = Locale(identifier: "en_US")
242+
XCTAssertEqual(formatter.string(from: 0), "$.00")
243+
XCTAssertEqual(formatter.string(from: 1.23), "$1.23")
244+
XCTAssertEqual(formatter.string(from: 123.4), "$123.40")
245+
246+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
247+
let formatter2 = NumberFormatter()
248+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
249+
formatter2.numberStyle = .currency
250+
XCTAssertEqual(formatter2.minimumIntegerDigits, 1)
251+
formatter2.locale = Locale(identifier: "en_US")
252+
XCTAssertEqual(formatter2.string(from: 0.001), "$0.00")
253+
XCTAssertEqual(formatter2.string(from: 1.234), "$1.23")
254+
XCTAssertEqual(formatter2.string(from: 123456.7), "$123,456.70")
255+
}
256+
257+
func test_percentMinimumIntegerDigits() {
258+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
259+
let formatter = NumberFormatter()
260+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
261+
formatter.minimumIntegerDigits = 0
262+
formatter.numberStyle = .percent
263+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
264+
formatter.locale = Locale(identifier: "en_US")
265+
XCTAssertEqual(formatter.string(from: 0), "0%")
266+
XCTAssertEqual(formatter.string(from: 1.234), "123%")
267+
XCTAssertEqual(formatter.string(from: 123.4), "12,340%")
268+
269+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
270+
let formatter2 = NumberFormatter()
271+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
272+
formatter2.numberStyle = .percent
273+
XCTAssertEqual(formatter2.minimumIntegerDigits, 1)
274+
formatter2.locale = Locale(identifier: "en_US")
275+
XCTAssertEqual(formatter2.string(from: 0.01), "1%")
276+
XCTAssertEqual(formatter2.string(from: 1.234), "123%")
277+
XCTAssertEqual(formatter2.string(from: 123456.7), "12,345,670%")
278+
}
279+
280+
func test_scientificMinimumIntegerDigits() {
281+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
282+
let formatter = NumberFormatter()
283+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
284+
formatter.minimumIntegerDigits = 0
285+
formatter.numberStyle = .scientific
286+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
287+
formatter.locale = Locale(identifier: "en_US")
288+
XCTAssertEqual(formatter.string(from: 0), "0E0")
289+
XCTAssertEqual(formatter.string(from: 1.23), "1.23E0")
290+
XCTAssertEqual(formatter.string(from: 123.4), "1.234E2")
291+
292+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
293+
let formatter2 = NumberFormatter()
294+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
295+
formatter2.numberStyle = .scientific
296+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
297+
formatter2.locale = Locale(identifier: "en_US")
298+
XCTAssertEqual(formatter2.string(from: 0.01), "1E-2")
299+
XCTAssertEqual(formatter2.string(from: 1.234), "1.234E0")
300+
XCTAssertEqual(formatter2.string(from: 123456.7), "1.234567E5")
301+
}
302+
303+
func test_spellOutMinimumIntegerDigits() {
304+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
305+
let formatter = NumberFormatter()
306+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
307+
formatter.minimumIntegerDigits = 0
308+
formatter.numberStyle = .spellOut
309+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
310+
formatter.locale = Locale(identifier: "en_US")
311+
XCTAssertEqual(formatter.string(from: 0), "zero")
312+
XCTAssertEqual(formatter.string(from: 1.23), "one point two three")
313+
XCTAssertEqual(formatter.string(from: 123.4), "one hundred twenty-three point four")
314+
315+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
316+
let formatter2 = NumberFormatter()
317+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
318+
formatter2.numberStyle = .spellOut
319+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
320+
formatter2.locale = Locale(identifier: "en_US")
321+
XCTAssertEqual(formatter2.string(from: 0.01), "zero point zero one")
322+
XCTAssertEqual(formatter2.string(from: 1.234), "one point two three four")
323+
XCTAssertEqual(formatter2.string(from: 123456.7), "one hundred twenty-three thousand four hundred fifty-six point seven")
324+
}
325+
326+
func test_ordinalMinimumIntegerDigits() {
327+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
328+
let formatter = NumberFormatter()
329+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
330+
formatter.minimumIntegerDigits = 0
331+
formatter.numberStyle = .ordinal
332+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
333+
formatter.locale = Locale(identifier: "en_US")
334+
XCTAssertEqual(formatter.string(from: 0), "0th")
335+
XCTAssertEqual(formatter.string(from: 1.23), "1st")
336+
XCTAssertEqual(formatter.string(from: 123.4), "123rd")
337+
338+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
339+
let formatter2 = NumberFormatter()
340+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
341+
formatter2.numberStyle = .ordinal
342+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
343+
formatter2.locale = Locale(identifier: "en_US")
344+
XCTAssertEqual(formatter2.string(from: 0.01), "0th")
345+
XCTAssertEqual(formatter2.string(from: 4.234), "4th")
346+
XCTAssertEqual(formatter2.string(from: 42), "42nd")
347+
}
348+
349+
func test_currencyPluralMinimumIntegerDigits() {
350+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
351+
let formatter = NumberFormatter()
352+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
353+
formatter.minimumIntegerDigits = 0
354+
formatter.numberStyle = .currencyPlural
355+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
356+
formatter.locale = Locale(identifier: "en_US")
357+
XCTAssertEqual(formatter.string(from: 0), "0.00 US dollars")
358+
XCTAssertEqual(formatter.string(from: 1.23), "1.23 US dollars")
359+
XCTAssertEqual(formatter.string(from: 123.4), "123.40 US dollars")
360+
361+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
362+
let formatter2 = NumberFormatter()
363+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
364+
formatter2.numberStyle = .currencyPlural
365+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
366+
formatter2.locale = Locale(identifier: "en_US")
367+
XCTAssertEqual(formatter2.string(from: 0.01), "0.01 US dollars")
368+
XCTAssertEqual(formatter2.string(from: 1.234), "1.23 US dollars")
369+
XCTAssertEqual(formatter2.string(from: 123456.7), "123,456.70 US dollars")
370+
}
371+
372+
func test_currencyISOCodeMinimumIntegerDigits() {
373+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
374+
let formatter = NumberFormatter()
375+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
376+
formatter.minimumIntegerDigits = 0
377+
formatter.numberStyle = .currencyISOCode
378+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
379+
formatter.locale = Locale(identifier: "en_US")
380+
XCTAssertEqual(formatter.string(from: 0), "USD.00")
381+
XCTAssertEqual(formatter.string(from: 1.23), "USD1.23")
382+
XCTAssertEqual(formatter.string(from: 123.4), "USD123.40")
383+
384+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
385+
let formatter2 = NumberFormatter()
386+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
387+
formatter2.numberStyle = .currencyISOCode
388+
XCTAssertEqual(formatter2.minimumIntegerDigits, 1)
389+
formatter2.locale = Locale(identifier: "en_US")
390+
XCTAssertEqual(formatter2.string(from: 0.01), "USD0.01")
391+
XCTAssertEqual(formatter2.string(from: 1.234), "USD1.23")
392+
XCTAssertEqual(formatter2.string(from: 123456.7), "USD123,456.70")
393+
}
394+
395+
func test_currencyAccountingMinimumIntegerDigits() {
396+
// If .minimumIntegerDigits is set to 0 before .numberStyle change, preserve the value
397+
let formatter = NumberFormatter()
398+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
399+
formatter.minimumIntegerDigits = 0
400+
formatter.numberStyle = .currencyAccounting
401+
XCTAssertEqual(formatter.minimumIntegerDigits, 0)
402+
formatter.locale = Locale(identifier: "en_US")
403+
XCTAssertEqual(formatter.string(from: 0), "$.00")
404+
XCTAssertEqual(formatter.string(from: 1.23), "$1.23")
405+
XCTAssertEqual(formatter.string(from: 123.4), "$123.40")
406+
407+
// If .minimumIntegerDigits is not set before .numberStyle change, update the value
408+
let formatter2 = NumberFormatter()
409+
XCTAssertEqual(formatter2.minimumIntegerDigits, 0)
410+
formatter2.numberStyle = .currencyAccounting
411+
XCTAssertEqual(formatter2.minimumIntegerDigits, 1)
412+
formatter2.locale = Locale(identifier: "en_US")
413+
XCTAssertEqual(formatter2.string(from: 0), "$0.00")
414+
XCTAssertEqual(formatter2.string(from: 1.23), "$1.23")
415+
XCTAssertEqual(formatter2.string(from: 123.4), "$123.40")
416+
}
417+
226418
func test_maximumIntegerDigits() {
227419
let numberFormatter = NumberFormatter()
228420
numberFormatter.maximumIntegerDigits = 3

0 commit comments

Comments
 (0)