From 10091ea0639c879f6f93e852e8e11711882e9f16 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Tue, 17 Dec 2024 09:36:45 +0800 Subject: [PATCH 1/6] Fix parsing foreign currency strings Currently parsing a currency string would fail if the currency code does not match `FormatStyle`'s locale region. For example, ```swift let style = Decimal.FormatStyle.Currency(code: "GBP", locale: .init(identifier: "en_US")).presentation(.isoCode) ``` This formats 3.14 into "GBP\u{0xa0}3.14", but parsing such string fails. Fix this by always set the ICU formatter's currency code. Resolves rdar://138179737 --- .../Number/Decimal+ParseStrategy.swift | 2 +- .../Number/FloatingPointParseStrategy.swift | 2 +- .../Number/ICULegacyNumberFormatter.swift | 7 +- .../Number/IntegerParseStrategy.swift | 2 +- .../ICU/ICU+Enums.swift | 1 + .../Formatting/NumberFormatStyleTests.swift | 69 +++++++++++++++---- .../Formatting/NumberParseStrategyTests.swift | 63 ++++++++++++++++- 7 files changed, 126 insertions(+), 20 deletions(-) diff --git a/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift index a7cd9ae16..1f6f48b38 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift @@ -54,7 +54,7 @@ extension Decimal.ParseStrategy { numberFormatType = .percent(format.collection) locale = format.locale } else if let format = formatStyle as? Decimal.FormatStyle.Currency { - numberFormatType = .currency(format.collection) + numberFormatType = .currency(format.collection, format.currencyCode) locale = format.locale } else { // For some reason we've managed to accept a format style of a type that we don't own, which shouldn't happen. Fallback to the default decimal style and try anyways. diff --git a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift index 5c9350e30..ed22a88d3 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift @@ -79,7 +79,7 @@ public extension FloatingPointParseStrategy { self.formatStyle = format self.lenient = lenient self.locale = format.locale - self.numberFormatType = .currency(format.collection) + self.numberFormatType = .currency(format.collection, format.currencyCode) } } diff --git a/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift b/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift index d2f321e79..fdd203c6c 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift @@ -123,7 +123,7 @@ internal final class ICULegacyNumberFormatter : @unchecked Sendable { enum NumberFormatType : Hashable, Codable { case number(NumberFormatStyleConfiguration.Collection) case percent(NumberFormatStyleConfiguration.Collection) - case currency(CurrencyFormatStyleConfiguration.Collection) + case currency(CurrencyFormatStyleConfiguration.Collection, /*currency code*/ String) case descriptive(DescriptiveNumberFormatConfiguration.Collection) } @@ -143,7 +143,7 @@ internal final class ICULegacyNumberFormatter : @unchecked Sendable { } case .percent(_): icuType = .percent - case .currency(let config): + case .currency(let config, let _): icuType = config.icuNumberFormatStyle case .descriptive(let config): icuType = config.icuNumberFormatStyle @@ -178,12 +178,13 @@ internal final class ICULegacyNumberFormatter : @unchecked Sendable { } } - case .currency(let config): + case .currency(let config, let currencyCode): setMultiplier(config.scale, formatter: formatter) setPrecision(config.precision, formatter: formatter) setGrouping(config.group, formatter: formatter) setDecimalSeparator(config.decimalSeparatorStrategy, formatter: formatter) setRoundingIncrement(config.roundingIncrement, formatter: formatter) + try setTextAttribute(.currencyCode, formatter: formatter, value: currencyCode) // Currency specific attributes if let sign = config.signDisplayStrategy { diff --git a/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift index a6ad0b735..9180ed103 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift @@ -103,6 +103,6 @@ public extension IntegerParseStrategy { self.formatStyle = format self.lenient = lenient self.locale = format.locale - self.numberFormatType = .currency(format.collection) + self.numberFormatType = .currency(format.collection, format.currencyCode) } } diff --git a/Sources/FoundationInternationalization/ICU/ICU+Enums.swift b/Sources/FoundationInternationalization/ICU/ICU+Enums.swift index 65b7e061c..1f1a99cb6 100644 --- a/Sources/FoundationInternationalization/ICU/ICU+Enums.swift +++ b/Sources/FoundationInternationalization/ICU/ICU+Enums.swift @@ -109,6 +109,7 @@ extension UNumberFormatAttribute { extension UNumberFormatTextAttribute { static let defaultRuleSet = UNUM_DEFAULT_RULESET + static let currencyCode = UNUM_CURRENCY_CODE } extension UDateRelativeDateTimeFormatterStyle { diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift index a45f6ccef..1afb7822f 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift @@ -595,6 +595,39 @@ final class NumberFormatStyleTests: XCTestCase { XCTAssertEqual((-922337203685477 as Decimal).formatted(baseStyle.rounded(rule: .awayFromZero, increment: 100)), "-$1000T") } + func testCurrency_Codable() throws { + let gbpInUS = Decimal.FormatStyle.Currency(code: "GBP", locale: enUSLocale) + let encoded = try JSONEncoder().encode(gbpInUS) + let json_gbp = String(data: encoded, encoding: String.Encoding.utf8)! + print(json_gbp) + + let previouslyEncoded = """ +{ + "collection": + { + "presentation": + { + "option": 1 + } + }, + "currencyCode": "GBP", + "locale": + { + "current": 0, + "identifier": "en_US" + } +} +""".data(using: .utf8) + + guard let previouslyEncoded else { + XCTFail() + return + } + + let decoded = try JSONDecoder().decode(Decimal.FormatStyle.Currency, from: previouslyEncoded) + XCTAssertEqual(decoded, gbpInUS) + } + func testCurrency_scientific() throws { let baseStyle = Decimal.FormatStyle.Currency(code: "USD", locale: Locale(identifier: "en_US")).notation(.scientific) @@ -1905,23 +1938,33 @@ final class FormatStylePatternMatchingTests : XCTestCase { _verifyMatching("€52,249", formatStyle: floatStyle, expectedUpperBound: nil, expectedValue: nil) _verifyMatching("€52,249", formatStyle: decimalStyle, expectedUpperBound: nil, expectedValue: nil) - // Different locale let frenchStyle: IntegerFormatStyle.Currency = .init(code: "EUR", locale: frFR) - - let frenchPrice = "57 379 €" - _verifyMatching(frenchPrice, formatStyle: frenchStyle, expectedUpperBound: frenchPrice.endIndex, expectedValue: 57379) - _verifyMatching(frenchPrice, formatStyle: floatStyle.locale(frFR), expectedUpperBound: frenchPrice.endIndex, expectedValue: 57379) - _verifyMatching(frenchPrice, formatStyle: decimalStyle.locale(frFR), expectedUpperBound: frenchPrice.endIndex, expectedValue: 57379) - - _verifyMatching("\(frenchPrice) semble beaucoup", formatStyle: frenchStyle, expectedUpperBound: frenchPrice.endIndex, expectedValue: 57379) - _verifyMatching("\(frenchPrice) semble beaucoup", formatStyle: floatStyle.locale(frFR), expectedUpperBound: frenchPrice.endIndex, expectedValue: 57379) - _verifyMatching("\(frenchPrice) semble beaucoup", formatStyle: decimalStyle.locale(frFR), expectedUpperBound: frenchPrice.endIndex, expectedValue: 57379) - + let frenchPrice = frenchStyle.format(57379) + XCTAssertEqual(frenchPrice, "57 379,00 €") + _verifyMatching("57 379,00 €", formatStyle: frenchStyle, expectedUpperBound: "57 379,00 €".endIndex, expectedValue: 57379) + _verifyMatching("57 379 €", formatStyle: frenchStyle, expectedUpperBound: "57 379 €".endIndex, expectedValue: 57379) + _verifyMatching("57 379,00 € semble beaucoup", formatStyle: frenchStyle, expectedUpperBound: "57 379,00 €".endIndex, expectedValue: 57379) + + // Does not match when matching with USD style + _verifyMatching("57 379,00 €", formatStyle: floatStyle.locale(frFR), expectedUpperBound: nil, expectedValue: nil) + _verifyMatching("57 379,00 €", formatStyle: decimalStyle.locale(frFR), expectedUpperBound: nil, expectedValue: nil) + + // Mix currency and locale + _verifyMatching("57 379,00 $US", formatStyle: floatStyle.locale(frFR), expectedUpperBound: "57 379,00 $US".endIndex, expectedValue: 57379) + _verifyMatching("57 379,00 $US", formatStyle: decimalStyle.locale(frFR), expectedUpperBound: "57 379,00 $US".endIndex, expectedValue: 57379) + _verifyMatching("57 379,00 $US semble beaucoup", formatStyle: floatStyle.locale(frFR), expectedUpperBound: "57 379,00 $US".endIndex, expectedValue: 57379) + _verifyMatching("57 379,00 $US semble beaucoup", formatStyle: decimalStyle.locale(frFR), expectedUpperBound: "57 379,00 $US".endIndex, expectedValue: 57379) + + // Range tests let newFrenchStr = " coûte \(frenchPrice)" let frenchMatchRange = newFrenchStr.firstIndex(of: "5")! ..< newFrenchStr.endIndex _verifyMatching(newFrenchStr, formatStyle: frenchStyle, range: frenchMatchRange, expectedUpperBound: newFrenchStr.endIndex, expectedValue: 57379) - _verifyMatching(newFrenchStr, formatStyle: floatStyle.locale(frFR), range: frenchMatchRange, expectedUpperBound: newFrenchStr.endIndex, expectedValue: 57379) - _verifyMatching(newFrenchStr, formatStyle: decimalStyle.locale(frFR), range: frenchMatchRange, expectedUpperBound: newFrenchStr.endIndex, expectedValue: 57379) + + // Mix currency and locale range tests + let newFrenchUSDStr = " coûte 57 379,00 $US" + let usdPriceRange = newFrenchUSDStr.firstIndex(of: "5")! ..< newFrenchUSDStr.endIndex + _verifyMatching(newFrenchUSDStr, formatStyle: floatStyle.locale(frFR), range: usdPriceRange, expectedUpperBound: newFrenchUSDStr.endIndex, expectedValue: 57379) + _verifyMatching(newFrenchUSDStr, formatStyle: decimalStyle.locale(frFR), range: usdPriceRange, expectedUpperBound: newFrenchUSDStr.endIndex, expectedValue: 57379) // Sign tests let signTests = [ diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift index becd50269..2b65d922b 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift @@ -157,6 +157,67 @@ final class NumberParseStrategyTests : XCTestCase { _verifyRoundtripCurrency(negativeData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") } + func testParseCurrencyWithDifferentCodes() throws { + let enUS = Locale(identifier: "en_US") + // Decimal + let style = Decimal.FormatStyle.Currency(code: "GBP", locale: enUS).presentation(.isoCode) + XCTAssertEqual(style.format(3.14), "GBP 3.14") + + let parsed = try style.parseStrategy.parse("GBP 3.14") + XCTAssertEqual(parsed, 3.14) + + // Floating point + let floatingPointStyle: FloatingPointFormatStyle.Currency = .init(code: "GBP", locale: enUS).presentation(.isoCode) + XCTAssertEqual(floatingPointStyle.format(3.14), "GBP 3.14") + + let parsedFloatingPoint = try floatingPointStyle.parseStrategy.parse("GBP 3.14") + XCTAssertEqual(parsedFloatingPoint, 3.14) + + // Integer + let integerStyle: IntegerFormatStyle.Currency = .init(code: "GBP", locale: enUS).presentation(.isoCode) + XCTAssertEqual(integerStyle.format(32), "GBP 32.00") + + let parsedInt = try integerStyle.parseStrategy.parse("GBP 32.00") + XCTAssertEqual(parsedInt, 32) + } + + func test_roundtripForeignCurrency() { + let testData: [Int] = [ + 87650000, 8765000, 876500, 87650, 8765, 876, 87, 8, 0 + ] + let negativeData: [Int] = [ + -87650000, -8765000, -876500, -87650, -8765, -876, -87, -8 + ] + + func _verifyRoundtripCurrency(_ testData: [Int], _ style: IntegerFormatStyle.Currency, _ testName: String = "", file: StaticString = #filePath, line: UInt = #line) { + for value in testData { + let str = style.format(value) + let parsed = try! Int(str, strategy: style.parseStrategy) + XCTAssertEqual(value, parsed, "\(testName): formatted string: \(str) parsed: \(parsed)", file: file, line: line) + + let nonLenientParsed = try! Int(str, format: style, lenient: false) + XCTAssertEqual(value, nonLenientParsed, file: file, line: line) + } + } + + let currencyStyle: IntegerFormatStyle.Currency = .init(code: "EUR", locale: Locale(identifier: "en_US")) + _verifyRoundtripCurrency(testData, currencyStyle, "currency style") + _verifyRoundtripCurrency(testData, currencyStyle.sign(strategy: .always()), "currency style, sign: always") + _verifyRoundtripCurrency(testData, currencyStyle.grouping(.never), "currency style, grouping: never") + _verifyRoundtripCurrency(testData, currencyStyle.presentation(.isoCode), "currency style, presentation: iso code") + _verifyRoundtripCurrency(testData, currencyStyle.presentation(.fullName), "currency style, presentation: iso code") + _verifyRoundtripCurrency(testData, currencyStyle.presentation(.narrow), "currency style, presentation: iso code") + _verifyRoundtripCurrency(testData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") + + _verifyRoundtripCurrency(negativeData, currencyStyle, "currency style") + _verifyRoundtripCurrency(negativeData, currencyStyle.sign(strategy: .accountingAlways()), "currency style, sign: always") + _verifyRoundtripCurrency(negativeData, currencyStyle.grouping(.never), "currency style, grouping: never") + _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.isoCode), "currency style, presentation: iso code") + _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.fullName), "currency style, presentation: iso code") + _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.narrow), "currency style, presentation: iso code") + _verifyRoundtripCurrency(negativeData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") + } + let testNegativePositiveDecimalData: [Decimal] = [ Decimal(string:"87650")!, Decimal(string:"8765")!, Decimal(string:"876.5")!, Decimal(string:"87.65")!, Decimal(string:"8.765")!, Decimal(string:"0.8765")!, Decimal(string:"0.08765")!, Decimal(string:"0.008765")!, Decimal(string:"0")!, Decimal(string:"-0.008765")!, Decimal(string:"-876.5")!, Decimal(string:"-87650")! ] @@ -188,7 +249,7 @@ final class NumberParseStrategyTests : XCTestCase { XCTAssertEqual(try! strategy.parse("-1,234.56 US dollars"), Decimal(string: "-1234.56")!) XCTAssertEqual(try! strategy.parse("-USD\u{00A0}1,234.56"), Decimal(string: "-1234.56")!) } - + func testNumericBoundsParsing() throws { let locale = Locale(identifier: "en_US") do { From 6b31a6305d463a1ded64592bf2794692e6dfab5e Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Tue, 17 Dec 2024 09:47:46 +0800 Subject: [PATCH 2/6] Update the test to throw properly instead of force unwrap --- .../Formatting/NumberParseStrategyTests.swift | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift index 2b65d922b..567e9d43a 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift @@ -181,7 +181,7 @@ final class NumberParseStrategyTests : XCTestCase { XCTAssertEqual(parsedInt, 32) } - func test_roundtripForeignCurrency() { + func test_roundtripForeignCurrency() throws { let testData: [Int] = [ 87650000, 8765000, 876500, 87650, 8765, 876, 87, 8, 0 ] @@ -189,10 +189,10 @@ final class NumberParseStrategyTests : XCTestCase { -87650000, -8765000, -876500, -87650, -8765, -876, -87, -8 ] - func _verifyRoundtripCurrency(_ testData: [Int], _ style: IntegerFormatStyle.Currency, _ testName: String = "", file: StaticString = #filePath, line: UInt = #line) { + func _verifyRoundtripCurrency(_ testData: [Int], _ style: IntegerFormatStyle.Currency, _ testName: String = "", file: StaticString = #filePath, line: UInt = #line) throws { for value in testData { let str = style.format(value) - let parsed = try! Int(str, strategy: style.parseStrategy) + let parsed = try Int(str, strategy: style.parseStrategy) XCTAssertEqual(value, parsed, "\(testName): formatted string: \(str) parsed: \(parsed)", file: file, line: line) let nonLenientParsed = try! Int(str, format: style, lenient: false) @@ -201,21 +201,21 @@ final class NumberParseStrategyTests : XCTestCase { } let currencyStyle: IntegerFormatStyle.Currency = .init(code: "EUR", locale: Locale(identifier: "en_US")) - _verifyRoundtripCurrency(testData, currencyStyle, "currency style") - _verifyRoundtripCurrency(testData, currencyStyle.sign(strategy: .always()), "currency style, sign: always") - _verifyRoundtripCurrency(testData, currencyStyle.grouping(.never), "currency style, grouping: never") - _verifyRoundtripCurrency(testData, currencyStyle.presentation(.isoCode), "currency style, presentation: iso code") - _verifyRoundtripCurrency(testData, currencyStyle.presentation(.fullName), "currency style, presentation: iso code") - _verifyRoundtripCurrency(testData, currencyStyle.presentation(.narrow), "currency style, presentation: iso code") - _verifyRoundtripCurrency(testData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") - - _verifyRoundtripCurrency(negativeData, currencyStyle, "currency style") - _verifyRoundtripCurrency(negativeData, currencyStyle.sign(strategy: .accountingAlways()), "currency style, sign: always") - _verifyRoundtripCurrency(negativeData, currencyStyle.grouping(.never), "currency style, grouping: never") - _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.isoCode), "currency style, presentation: iso code") - _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.fullName), "currency style, presentation: iso code") - _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.narrow), "currency style, presentation: iso code") - _verifyRoundtripCurrency(negativeData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") + try _verifyRoundtripCurrency(testData, currencyStyle, "currency style") + try _verifyRoundtripCurrency(testData, currencyStyle.sign(strategy: .always()), "currency style, sign: always") + try _verifyRoundtripCurrency(testData, currencyStyle.grouping(.never), "currency style, grouping: never") + try _verifyRoundtripCurrency(testData, currencyStyle.presentation(.isoCode), "currency style, presentation: iso code") + try _verifyRoundtripCurrency(testData, currencyStyle.presentation(.fullName), "currency style, presentation: iso code") + try _verifyRoundtripCurrency(testData, currencyStyle.presentation(.narrow), "currency style, presentation: iso code") + try _verifyRoundtripCurrency(testData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") + + try _verifyRoundtripCurrency(negativeData, currencyStyle, "currency style") + try _verifyRoundtripCurrency(negativeData, currencyStyle.sign(strategy: .accountingAlways()), "currency style, sign: always") + try _verifyRoundtripCurrency(negativeData, currencyStyle.grouping(.never), "currency style, grouping: never") + try _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.isoCode), "currency style, presentation: iso code") + try _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.fullName), "currency style, presentation: iso code") + try _verifyRoundtripCurrency(negativeData, currencyStyle.presentation(.narrow), "currency style, presentation: iso code") + try _verifyRoundtripCurrency(negativeData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") } let testNegativePositiveDecimalData: [Decimal] = [ Decimal(string:"87650")!, Decimal(string:"8765")!, From 85616ad4e3c5c47032ba18bedd5c8d666dc7cc88 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Tue, 17 Dec 2024 11:43:46 +0800 Subject: [PATCH 3/6] Remove another force try --- .../Formatting/NumberParseStrategyTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift index 567e9d43a..246847a9f 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift @@ -195,7 +195,7 @@ final class NumberParseStrategyTests : XCTestCase { let parsed = try Int(str, strategy: style.parseStrategy) XCTAssertEqual(value, parsed, "\(testName): formatted string: \(str) parsed: \(parsed)", file: file, line: line) - let nonLenientParsed = try! Int(str, format: style, lenient: false) + let nonLenientParsed = try Int(str, format: style, lenient: false) XCTAssertEqual(value, nonLenientParsed, file: file, line: line) } } From 9a1e617c6affea242c994cdbf45fddb686f6115b Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Wed, 18 Dec 2024 14:29:07 +0800 Subject: [PATCH 4/6] Remove the stored `numberFormatType` and `locale` inside `IntegerParseStrategy` and `FloatingPointParseStrategy` These properties are redundant as the information is already available through the public variable `formatStyle`. --- .../Number/FloatingPointFormatStyle.swift | 6 +- .../Number/FloatingPointParseStrategy.swift | 41 ++++++----- .../Number/IntegerFormatStyle.swift | 6 +- .../Number/IntegerParseStrategy.swift | 70 ++++++++++--------- Sources/TestSupport/TestSupport.swift | 4 ++ .../Formatting/NumberParseStrategyTests.swift | 39 +++++++++++ 6 files changed, 110 insertions(+), 56 deletions(-) diff --git a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointFormatStyle.swift b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointFormatStyle.swift index ce6b3720d..047efe4e6 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointFormatStyle.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointFormatStyle.swift @@ -418,7 +418,7 @@ extension FloatingPointFormatStyle { extension FloatingPointFormatStyle : CustomConsumingRegexComponent { public typealias RegexOutput = Value public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Value)? { - FloatingPointParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) + try FloatingPointParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) } } @@ -426,7 +426,7 @@ extension FloatingPointFormatStyle : CustomConsumingRegexComponent { extension FloatingPointFormatStyle.Percent : CustomConsumingRegexComponent { public typealias RegexOutput = Value public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Value)? { - FloatingPointParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) + try FloatingPointParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) } } @@ -434,7 +434,7 @@ extension FloatingPointFormatStyle.Percent : CustomConsumingRegexComponent { extension FloatingPointFormatStyle.Currency : CustomConsumingRegexComponent { public typealias RegexOutput = Value public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Value)? { - FloatingPointParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) + try FloatingPointParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) } } diff --git a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift index ed22a88d3..5e7d4662a 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift @@ -18,8 +18,6 @@ import FoundationEssentials public struct FloatingPointParseStrategy : Codable, Hashable where Format : FormatStyle, Format.FormatInput : BinaryFloatingPoint { public var formatStyle: Format public var lenient: Bool - var numberFormatType: ICULegacyNumberFormatter.NumberFormatType - var locale: Locale } @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) @@ -29,27 +27,42 @@ extension FloatingPointParseStrategy : Sendable where Format : Sendable {} extension FloatingPointParseStrategy: ParseStrategy { @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public func parse(_ value: String) throws -> Format.FormatInput { - guard let parser = ICULegacyNumberFormatter.formatter(for: numberFormatType, locale: locale, lenient: lenient) else { - throw CocoaError(CocoaError.formatting, userInfo: [ - NSDebugDescriptionErrorKey: "Cannot parse \(value), unable to create formatter" ]) - } - if let v = parser.parseAsDouble(value._trimmingWhitespace()) { - return Format.FormatInput(v) - } else { + let trimmedString = value._trimmingWhitespace() + guard let result = try parse(trimmedString, startingAt: trimmedString.startIndex, in: trimmedString.startIndex..) -> (String.Index, Format.FormatInput)? { + internal func parse(_ value: String, startingAt index: String.Index, in range: Range) throws -> (String.Index, Format.FormatInput)? { guard index < range.upperBound else { return nil } + let numberFormatType: ICULegacyNumberFormatter.NumberFormatType + let locale: Locale + + if let format = formatStyle as? FloatingPointFormatStyle { + numberFormatType = .number(format.collection) + locale = format.locale + } else if let format = formatStyle as? FloatingPointFormatStyle.Percent { + numberFormatType = .percent(format.collection) + locale = format.locale + } else if let format = formatStyle as? FloatingPointFormatStyle.Currency { + numberFormatType = .currency(format.collection, format.currencyCode) + locale = format.locale + } else { + // For some reason we've managed to accept a format style of a type that we don't own, which shouldn't happen. Fallback to the default decimal style and try anyways. + numberFormatType = .number(.init()) + locale = .autoupdatingCurrent + } + guard let parser = ICULegacyNumberFormatter.formatter(for: numberFormatType, locale: locale, lenient: lenient) else { - return nil + throw CocoaError(CocoaError.formatting, userInfo: [ + NSDebugDescriptionErrorKey: "Cannot parse \(value), unable to create formatter" ]) } let substr = value[index..(format: Format, lenient: Bool = true) where Format == FloatingPointFormatStyle { self.formatStyle = format self.lenient = lenient - self.locale = format.locale - self.numberFormatType = .number(format.collection) } } @@ -78,8 +89,6 @@ public extension FloatingPointParseStrategy { init(format: Format, lenient: Bool = true) where Format == FloatingPointFormatStyle.Currency { self.formatStyle = format self.lenient = lenient - self.locale = format.locale - self.numberFormatType = .currency(format.collection, format.currencyCode) } } @@ -88,7 +97,5 @@ public extension FloatingPointParseStrategy { init(format: Format, lenient: Bool = true) where Format == FloatingPointFormatStyle.Percent { self.formatStyle = format self.lenient = lenient - self.locale = format.locale - self.numberFormatType = .percent(format.collection) } } diff --git a/Sources/FoundationInternationalization/Formatting/Number/IntegerFormatStyle.swift b/Sources/FoundationInternationalization/Formatting/Number/IntegerFormatStyle.swift index 421a10902..23f9b7e0a 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/IntegerFormatStyle.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/IntegerFormatStyle.swift @@ -563,7 +563,7 @@ extension IntegerFormatStyle { extension IntegerFormatStyle : CustomConsumingRegexComponent { public typealias RegexOutput = Value public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Value)? { - IntegerParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) + try IntegerParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) } } @@ -571,7 +571,7 @@ extension IntegerFormatStyle : CustomConsumingRegexComponent { extension IntegerFormatStyle.Percent : CustomConsumingRegexComponent { public typealias RegexOutput = Value public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Value)? { - IntegerParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) + try IntegerParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) } } @@ -579,7 +579,7 @@ extension IntegerFormatStyle.Percent : CustomConsumingRegexComponent { extension IntegerFormatStyle.Currency : CustomConsumingRegexComponent { public typealias RegexOutput = Value public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range) throws -> (upperBound: String.Index, output: Value)? { - IntegerParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) + try IntegerParseStrategy(format: self, lenient: false).parse(input, startingAt: index, in: bounds) } } diff --git a/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift index 9180ed103..7c6ea518f 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift @@ -18,8 +18,6 @@ import FoundationEssentials public struct IntegerParseStrategy : Codable, Hashable where Format : FormatStyle, Format.FormatInput : BinaryInteger { public var formatStyle: Format public var lenient: Bool - var numberFormatType: ICULegacyNumberFormatter.NumberFormatType - var locale: Locale } @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) @@ -28,51 +26,63 @@ extension IntegerParseStrategy : Sendable where Format : Sendable {} @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) extension IntegerParseStrategy: ParseStrategy { public func parse(_ value: String) throws -> Format.FormatInput { - guard let parser = ICULegacyNumberFormatter.formatter(for: numberFormatType, locale: locale, lenient: lenient) else { - throw CocoaError(CocoaError.formatting, userInfo: [ - NSDebugDescriptionErrorKey: "Cannot parse \(value). Could not create parser." ]) - } let trimmedString = value._trimmingWhitespace() - if let v = parser.parseAsInt(trimmedString) { - guard let exact = Format.FormatInput(exactly: v) else { - throw CocoaError(CocoaError.formatting, userInfo: [ - NSDebugDescriptionErrorKey: "Cannot parse \(value). The number does not fall within the valid bounds of the specified output type" ]) - } - return exact - } else if let v = parser.parseAsDouble(trimmedString) { - guard v.magnitude < Double(sign: .plus, exponent: Double.significandBitCount + 1, significand: 1) else { - throw CocoaError(CocoaError.formatting, userInfo: [ - NSDebugDescriptionErrorKey: "Cannot parse \(value). The number does not fall within the lossless floating-point range" ]) - } - guard let exact = Format.FormatInput(exactly: v) else { - throw CocoaError(CocoaError.formatting, userInfo: [ - NSDebugDescriptionErrorKey: "Cannot parse \(value). The number does not fall within the valid bounds of the specified output type" ]) - } - return exact - } else { + guard let result = try parse(trimmedString, startingAt: trimmedString.startIndex, in: trimmedString.startIndex..) -> (String.Index, Format.FormatInput)? { + internal func parse(_ value: String, startingAt index: String.Index, in range: Range) throws -> (String.Index, Format.FormatInput)? { guard index < range.upperBound else { return nil } + let numberFormatType: ICULegacyNumberFormatter.NumberFormatType + let locale: Locale + + if let format = formatStyle as? IntegerFormatStyle { + numberFormatType = .number(format.collection) + locale = format.locale + } else if let format = formatStyle as? IntegerFormatStyle.Percent { + numberFormatType = .percent(format.collection) + locale = format.locale + } else if let format = formatStyle as? IntegerFormatStyle.Currency { + numberFormatType = .currency(format.collection, format.currencyCode) + locale = format.locale + } else { + // For some reason we've managed to accept a format style of a type that we don't own, which shouldn't happen. Fallback to the default decimal style and try anyways. + numberFormatType = .number(.init()) + locale = .autoupdatingCurrent + } + guard let parser = ICULegacyNumberFormatter.formatter(for: numberFormatType, locale: locale, lenient: lenient) else { return nil } let substr = value[index..(format: Format, lenient: Bool = true) where Format == IntegerFormatStyle { self.formatStyle = format self.lenient = lenient - self.locale = format.locale - self.numberFormatType = .number(format.collection) } } @@ -92,8 +100,6 @@ public extension IntegerParseStrategy { init(format: Format, lenient: Bool = true) where Format == IntegerFormatStyle.Percent { self.formatStyle = format self.lenient = lenient - self.locale = format.locale - self.numberFormatType = .percent(format.collection) } } @@ -102,7 +108,5 @@ public extension IntegerParseStrategy { init(format: Format, lenient: Bool = true) where Format == IntegerFormatStyle.Currency { self.formatStyle = format self.lenient = lenient - self.locale = format.locale - self.numberFormatType = .currency(format.collection, format.currencyCode) } } diff --git a/Sources/TestSupport/TestSupport.swift b/Sources/TestSupport/TestSupport.swift index 04c5a2db3..740a51ca3 100644 --- a/Sources/TestSupport/TestSupport.swift +++ b/Sources/TestSupport/TestSupport.swift @@ -49,6 +49,8 @@ public typealias FloatingPointFormatStyle = Foundation.FloatingPointFormatStyle public typealias NumberFormatStyleConfiguration = Foundation.NumberFormatStyleConfiguration @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public typealias CurrencyFormatStyleConfiguration = Foundation.CurrencyFormatStyleConfiguration +@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) +public typealias IntegerParseStrategy = FoundationInternationalization.IntegerParseStrategy @available(FoundationPreview 0.4, *) public typealias DiscreteFormatStyle = Foundation.DiscreteFormatStyle @@ -147,6 +149,8 @@ public typealias FloatingPointFormatStyle = FoundationInternationalization.Float public typealias NumberFormatStyleConfiguration = FoundationInternationalization.NumberFormatStyleConfiguration @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public typealias CurrencyFormatStyleConfiguration = FoundationInternationalization.CurrencyFormatStyleConfiguration +@available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) +public typealias IntegerParseStrategy = FoundationInternationalization.IntegerParseStrategy @available(FoundationPreview 0.4, *) public typealias DiscreteFormatStyle = FoundationEssentials.DiscreteFormatStyle diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift index 246847a9f..02b1fab40 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberParseStrategyTests.swift @@ -218,6 +218,44 @@ final class NumberParseStrategyTests : XCTestCase { try _verifyRoundtripCurrency(negativeData, currencyStyle.decimalSeparator(strategy: .always), "currency style, decimal display: always") } + func test_parseStategyCodable_sameCurrency() throws { + // same currency code + let fs: IntegerFormatStyle.Currency = .init(code: "USD", locale: Locale(identifier:"en_US")) + let p = IntegerParseStrategy(format: fs) + // Valid JSON representation for `p` + let existingSerializedParseStrategy = """ + {"formatStyle":{"locale":{"current":0,"identifier":"en_US"},"collection":{"presentation":{"option":1}},"currencyCode":"USD"},"numberFormatType":{"currency":{"_0":{"presentation":{"option":1}}}},"lenient":true,"locale":{"identifier":"en_US","current":0}} + """ + + guard let existingData = existingSerializedParseStrategy.data(using: .utf8) else { + XCTFail("Unable to get data from JSON string") + return + } + + let decoded: IntegerParseStrategy.Currency> = try JSONDecoder().decode(IntegerParseStrategy.Currency>.self, from: existingData) + XCTAssertEqual(decoded, p) + XCTAssertEqual(decoded.formatStyle, fs) + XCTAssertEqual(decoded.formatStyle.currencyCode, "USD") + } + + func test_parseStategyCodable_differentCurrency() throws { + let fs: IntegerFormatStyle.Currency = .init(code: "GBP", locale: Locale(identifier:"en_US")) + let p = IntegerParseStrategy(format: fs) + // Valid JSON representation for `p` + let existingSerializedParseStrategy = """ + {"formatStyle":{"collection":{"presentation":{"option":1}},"locale":{"current":0,"identifier":"en_US"},"currencyCode":"GBP"},"lenient":true,"locale":{"current":0,"identifier":"en_US"},"numberFormatType":{"currency":{"_0":{"presentation":{"option":1}}}}} + """ + + guard let existingData = existingSerializedParseStrategy.data(using: .utf8) else { + XCTFail("Unable to get data from JSON string") + return + } + let decoded: IntegerParseStrategy.Currency> = try JSONDecoder().decode(IntegerParseStrategy.Currency>.self, from: existingData) + XCTAssertEqual(decoded, p) + XCTAssertEqual(decoded.formatStyle, fs) + XCTAssertEqual(decoded.formatStyle.currencyCode, "GBP") + } + let testNegativePositiveDecimalData: [Decimal] = [ Decimal(string:"87650")!, Decimal(string:"8765")!, Decimal(string:"876.5")!, Decimal(string:"87.65")!, Decimal(string:"8.765")!, Decimal(string:"0.8765")!, Decimal(string:"0.08765")!, Decimal(string:"0.008765")!, Decimal(string:"0")!, Decimal(string:"-0.008765")!, Decimal(string:"-876.5")!, Decimal(string:"-87650")! ] @@ -368,3 +406,4 @@ final class NumberExtensionParseStrategyTests: XCTestCase { XCTAssertEqual(try Decimal("-$3000.0000014", format: .currency(code: "USD")), Decimal(string: "-3000.0000014")!) } } + From 876db352a5e537d35953e788c58879b95e3c3341 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Wed, 18 Dec 2024 14:33:19 +0800 Subject: [PATCH 5/6] Address feedback --- .../Number/Decimal+ParseStrategy.swift | 2 +- .../Number/FloatingPointParseStrategy.swift | 2 +- .../Number/ICULegacyNumberFormatter.swift | 4 +-- .../Number/IntegerParseStrategy.swift | 2 +- .../Formatting/NumberFormatStyleTests.swift | 28 +++++++++---------- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift index 1f6f48b38..23af6ff3f 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/Decimal+ParseStrategy.swift @@ -54,7 +54,7 @@ extension Decimal.ParseStrategy { numberFormatType = .percent(format.collection) locale = format.locale } else if let format = formatStyle as? Decimal.FormatStyle.Currency { - numberFormatType = .currency(format.collection, format.currencyCode) + numberFormatType = .currency(format.collection, currencyCode: format.currencyCode) locale = format.locale } else { // For some reason we've managed to accept a format style of a type that we don't own, which shouldn't happen. Fallback to the default decimal style and try anyways. diff --git a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift index 5e7d4662a..92f241149 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/FloatingPointParseStrategy.swift @@ -52,7 +52,7 @@ extension FloatingPointParseStrategy: ParseStrategy { numberFormatType = .percent(format.collection) locale = format.locale } else if let format = formatStyle as? FloatingPointFormatStyle.Currency { - numberFormatType = .currency(format.collection, format.currencyCode) + numberFormatType = .currency(format.collection, currencyCode: format.currencyCode) locale = format.locale } else { // For some reason we've managed to accept a format style of a type that we don't own, which shouldn't happen. Fallback to the default decimal style and try anyways. diff --git a/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift b/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift index fdd203c6c..a40004c37 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/ICULegacyNumberFormatter.swift @@ -123,7 +123,7 @@ internal final class ICULegacyNumberFormatter : @unchecked Sendable { enum NumberFormatType : Hashable, Codable { case number(NumberFormatStyleConfiguration.Collection) case percent(NumberFormatStyleConfiguration.Collection) - case currency(CurrencyFormatStyleConfiguration.Collection, /*currency code*/ String) + case currency(CurrencyFormatStyleConfiguration.Collection, currencyCode: String) case descriptive(DescriptiveNumberFormatConfiguration.Collection) } @@ -143,7 +143,7 @@ internal final class ICULegacyNumberFormatter : @unchecked Sendable { } case .percent(_): icuType = .percent - case .currency(let config, let _): + case .currency(let config, _): icuType = config.icuNumberFormatStyle case .descriptive(let config): icuType = config.icuNumberFormatStyle diff --git a/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift b/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift index 7c6ea518f..078683b7c 100644 --- a/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift +++ b/Sources/FoundationInternationalization/Formatting/Number/IntegerParseStrategy.swift @@ -50,7 +50,7 @@ extension IntegerParseStrategy: ParseStrategy { numberFormatType = .percent(format.collection) locale = format.locale } else if let format = formatStyle as? IntegerFormatStyle.Currency { - numberFormatType = .currency(format.collection, format.currencyCode) + numberFormatType = .currency(format.collection, currencyCode: format.currencyCode) locale = format.locale } else { // For some reason we've managed to accept a format style of a type that we don't own, which shouldn't happen. Fallback to the default decimal style and try anyways. diff --git a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift index 1afb7822f..c9764ad37 100644 --- a/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift +++ b/Tests/FoundationInternationalizationTests/Formatting/NumberFormatStyleTests.swift @@ -599,24 +599,22 @@ final class NumberFormatStyleTests: XCTestCase { let gbpInUS = Decimal.FormatStyle.Currency(code: "GBP", locale: enUSLocale) let encoded = try JSONEncoder().encode(gbpInUS) let json_gbp = String(data: encoded, encoding: String.Encoding.utf8)! - print(json_gbp) - let previouslyEncoded = """ -{ - "collection": - { - "presentation": { - "option": 1 + "collection": + { + "presentation": + { + "option": 1 + } + }, + "currencyCode": "GBP", + "locale": + { + "current": 0, + "identifier": "en_US" + } } - }, - "currencyCode": "GBP", - "locale": - { - "current": 0, - "identifier": "en_US" - } -} """.data(using: .utf8) guard let previouslyEncoded else { From a245a417c7b713eae4c746b383b03f80597dd5d7 Mon Sep 17 00:00:00 2001 From: Tina Liu Date: Thu, 19 Dec 2024 13:30:31 +0800 Subject: [PATCH 6/6] Fix a typo --- Sources/TestSupport/TestSupport.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/TestSupport/TestSupport.swift b/Sources/TestSupport/TestSupport.swift index 740a51ca3..780d0dc06 100644 --- a/Sources/TestSupport/TestSupport.swift +++ b/Sources/TestSupport/TestSupport.swift @@ -50,7 +50,7 @@ public typealias NumberFormatStyleConfiguration = Foundation.NumberFormatStyleCo @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) public typealias CurrencyFormatStyleConfiguration = Foundation.CurrencyFormatStyleConfiguration @available(macOS 12.0, iOS 15.0, tvOS 15.0, watchOS 8.0, *) -public typealias IntegerParseStrategy = FoundationInternationalization.IntegerParseStrategy +public typealias IntegerParseStrategy = Foundation.IntegerParseStrategy @available(FoundationPreview 0.4, *) public typealias DiscreteFormatStyle = Foundation.DiscreteFormatStyle