Skip to content

Commit 8f05bbb

Browse files
authored
Merge pull request #2155 from spevans/pr_sr_10525
SR-10525: Decimal(string:locale:) different results macOS vs Linux
2 parents 9ec23d4 + ee5022f commit 8f05bbb

File tree

2 files changed

+81
-62
lines changed

2 files changed

+81
-62
lines changed

Foundation/Decimal.swift

Lines changed: 29 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -631,15 +631,17 @@ extension Decimal {
631631
}
632632
}
633633

634-
extension Decimal : CustomStringConvertible {
634+
extension Decimal: CustomStringConvertible {
635635
public init?(string: String, locale: Locale? = nil) {
636636
let scan = Scanner(string: string)
637+
scan.locale = locale
637638
var theDecimal = Decimal()
638639
if !scan.scanDecimal(&theDecimal) {
639640
return nil
640641
}
641642
self = theDecimal
642643
}
644+
643645
public var description: String {
644646
if self.isNaN {
645647
return "NaN"
@@ -2055,34 +2057,6 @@ fileprivate let pow10 = [
20552057
/*^39 is on 9 shorts. */
20562058
]
20572059

2058-
// Copied from Scanner.swift
2059-
private func decimalSep(_ locale: Locale?) -> String {
2060-
if let loc = locale {
2061-
if let sep = loc._bridgeToObjectiveC().object(forKey: .decimalSeparator) as? NSString {
2062-
return sep._swiftObject
2063-
}
2064-
return "."
2065-
} else {
2066-
return decimalSep(Locale.current)
2067-
}
2068-
}
2069-
2070-
// Copied from Scanner.swift
2071-
private func isADigit(_ ch: unichar) -> Bool {
2072-
struct Local {
2073-
static let set = CharacterSet.decimalDigits
2074-
}
2075-
return Local.set.contains(UnicodeScalar(ch)!)
2076-
}
2077-
2078-
// Copied from Scanner.swift
2079-
private func numericValue(_ ch: unichar) -> Int {
2080-
if (ch >= unichar(unicodeScalarLiteral: "0") && ch <= unichar(unicodeScalarLiteral: "9")) {
2081-
return Int(ch) - Int(unichar(unicodeScalarLiteral: "0"))
2082-
} else {
2083-
return __CFCharDigitValue(UniChar(ch))
2084-
}
2085-
}
20862060

20872061
// Could be silently inexact for float and double.
20882062
extension Scanner {
@@ -2094,78 +2068,66 @@ extension Scanner {
20942068
} else {
20952069
return false
20962070
}
2097-
20982071
}
20992072

21002073
public func scanDecimal() -> Decimal? {
21012074

2102-
var result = Decimal()
2103-
2075+
var result = Decimal.zero
21042076
let string = self._scanString
21052077
let length = string.length
21062078
var buf = _NSStringBuffer(string: string, start: self._scanLocation, end: length)
2107-
2108-
let ds_chars = decimalSep(locale as? Locale).utf16
2109-
let ds = ds_chars[ds_chars.startIndex]
2079+
var tooBig = false
2080+
let ds = (locale as? Locale ?? Locale.current).decimalSeparator?.first ?? Character(".")
21102081
buf.skip(_skipSet)
21112082
var neg = false
2083+
var ok = false
21122084

21132085
if buf.currentCharacter == unichar(unicodeScalarLiteral: "-") || buf.currentCharacter == unichar(unicodeScalarLiteral: "+") {
2086+
ok = true
21142087
neg = buf.currentCharacter == unichar(unicodeScalarLiteral: "-")
21152088
buf.advance()
21162089
buf.skip(_skipSet)
21172090
}
2118-
guard isADigit(buf.currentCharacter) else {
2119-
return nil
2120-
}
2121-
2122-
var tooBig = false
21232091

21242092
// build the mantissa
2125-
repeat {
2126-
let numeral = numericValue(buf.currentCharacter)
2127-
if numeral == -1 {
2128-
break
2129-
}
2130-
2093+
while let numeral = decimalValue(buf.currentCharacter) {
2094+
ok = true
21312095
if tooBig || multiplyBy10(&result,andAdd:numeral) != .noError {
21322096
tooBig = true
21332097
if result._exponent == Int32(Int8.max) {
21342098
repeat {
21352099
buf.advance()
2136-
} while isADigit(buf.currentCharacter)
2100+
} while decimalValue(buf.currentCharacter) != nil
21372101
return Decimal.nan
21382102
}
21392103
result._exponent += 1
21402104
}
21412105
buf.advance()
2142-
} while isADigit(buf.currentCharacter)
2106+
}
21432107

21442108
// get the decimal point
2145-
if buf.currentCharacter == ds {
2109+
if let us = UnicodeScalar(buf.currentCharacter), Character(us) == ds {
2110+
ok = true
21462111
buf.advance()
21472112
// continue to build the mantissa
2148-
repeat {
2149-
let numeral = numericValue(buf.currentCharacter)
2150-
if numeral == -1 {
2151-
break
2152-
}
2113+
while let numeral = decimalValue(buf.currentCharacter) {
21532114
if tooBig || multiplyBy10(&result,andAdd:numeral) != .noError {
21542115
tooBig = true
21552116
} else {
21562117
if result._exponent == Int32(Int8.min) {
21572118
repeat {
21582119
buf.advance()
2159-
} while isADigit(buf.currentCharacter)
2120+
} while decimalValue(buf.currentCharacter) != nil
21602121
return Decimal.nan
21612122
}
21622123
result._exponent -= 1
21632124
}
21642125
buf.advance()
2165-
} while isADigit(buf.currentCharacter)
2126+
}
21662127
}
21672128

21682129
if buf.currentCharacter == unichar(unicodeScalarLiteral: "e") || buf.currentCharacter == unichar(unicodeScalarLiteral: "E") {
2130+
ok = true
21692131
var exponentIsNegative = false
21702132
var exponent: Int32 = 0
21712133

@@ -2177,18 +2139,14 @@ extension Scanner {
21772139
buf.advance()
21782140
}
21792141

2180-
repeat {
2181-
let numeral = numericValue(buf.currentCharacter)
2182-
if numeral == -1 {
2183-
break
2184-
}
2142+
while let numeral = decimalValue(buf.currentCharacter) {
21852143
exponent = 10 * exponent + Int32(numeral)
21862144
guard exponent <= 2*Int32(Int8.max) else {
21872145
return Decimal.nan
21882146
}
21892147

21902148
buf.advance()
2191-
} while isADigit(buf.currentCharacter)
2149+
}
21922150

21932151
if exponentIsNegative {
21942152
exponent = -exponent
@@ -2200,6 +2158,9 @@ extension Scanner {
22002158
result._exponent = exponent
22012159
}
22022160

2161+
// No valid characters have been seen upto this point so error out.
2162+
guard ok == true else { return nil }
2163+
22032164
result.isNegative = neg
22042165

22052166
// if we get to this point, and have NaN, then the input string was probably "-0"
@@ -2212,6 +2173,12 @@ extension Scanner {
22122173
self._scanLocation = buf.location
22132174
return result
22142175
}
2176+
2177+
// Copied from Scanner.swift
2178+
private func decimalValue(_ ch: unichar) -> Int? {
2179+
guard let s = UnicodeScalar(ch), s.isASCII else { return nil }
2180+
return Character(s).wholeNumberValue
2181+
}
22152182
}
22162183

22172184
extension Decimal : Codable {

TestFoundation/TestDecimal.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class TestDecimal: XCTestCase {
3434
("test_doubleValue", test_doubleValue),
3535
("test_NSDecimalNumberValues", test_NSDecimalNumberValues),
3636
("test_bridging", test_bridging),
37+
("test_stringWithLocale", test_stringWithLocale),
3738
]
3839
}
3940

@@ -1053,4 +1054,55 @@ class TestDecimal: XCTestCase {
10531054
// NSNumber does NOT bridge to Decimal
10541055
XCTAssertNil(NSNumber(value: 1) as? Decimal)
10551056
}
1057+
1058+
func test_stringWithLocale() {
1059+
1060+
let en_US = Locale(identifier: "en_US")
1061+
let fr_FR = Locale(identifier: "fr_FR")
1062+
1063+
XCTAssertEqual(Decimal(string: "1,234.56")! * 1000, Decimal(1000))
1064+
XCTAssertEqual(Decimal(string: "1,234.56", locale: en_US)! * 1000, Decimal(1000))
1065+
XCTAssertEqual(Decimal(string: "1,234.56", locale: fr_FR)! * 1000, Decimal(1234))
1066+
XCTAssertEqual(Decimal(string: "1.234,56", locale: en_US)! * 1000, Decimal(1234))
1067+
XCTAssertEqual(Decimal(string: "1.234,56", locale: fr_FR)! * 1000, Decimal(1000))
1068+
1069+
XCTAssertEqual(Decimal(string: "-1,234.56")! * 1000, Decimal(-1000))
1070+
XCTAssertEqual(Decimal(string: "+1,234.56")! * 1000, Decimal(1000))
1071+
XCTAssertEqual(Decimal(string: "+1234.56e3"), Decimal(1234560))
1072+
XCTAssertEqual(Decimal(string: "+1234.56E3"), Decimal(1234560))
1073+
XCTAssertEqual(Decimal(string: "+123456000E-3"), Decimal(123456))
1074+
1075+
XCTAssertNil(Decimal(string: ""))
1076+
XCTAssertNil(Decimal(string: "x"))
1077+
XCTAssertEqual(Decimal(string: "-x"), Decimal.zero)
1078+
XCTAssertEqual(Decimal(string: "+x"), Decimal.zero)
1079+
XCTAssertEqual(Decimal(string: "-"), Decimal.zero)
1080+
XCTAssertEqual(Decimal(string: "+"), Decimal.zero)
1081+
XCTAssertEqual(Decimal(string: "-."), Decimal.zero)
1082+
XCTAssertEqual(Decimal(string: "+."), Decimal.zero)
1083+
1084+
XCTAssertEqual(Decimal(string: "-0"), Decimal.zero)
1085+
XCTAssertEqual(Decimal(string: "+0"), Decimal.zero)
1086+
XCTAssertEqual(Decimal(string: "-0."), Decimal.zero)
1087+
XCTAssertEqual(Decimal(string: "+0."), Decimal.zero)
1088+
XCTAssertEqual(Decimal(string: "e1"), Decimal.zero)
1089+
XCTAssertEqual(Decimal(string: "e-5"), Decimal.zero)
1090+
XCTAssertEqual(Decimal(string: ".3e1"), Decimal(3))
1091+
1092+
XCTAssertEqual(Decimal(string: "."), Decimal.zero)
1093+
XCTAssertEqual(Decimal(string: ".", locale: en_US), Decimal.zero)
1094+
XCTAssertNil(Decimal(string: ".", locale: fr_FR))
1095+
1096+
XCTAssertNil(Decimal(string: ","))
1097+
XCTAssertEqual(Decimal(string: ",", locale: fr_FR), Decimal.zero)
1098+
XCTAssertNil(Decimal(string: ",", locale: en_US))
1099+
1100+
let s1 = "1234.5678"
1101+
XCTAssertEqual(Decimal(string: s1, locale: en_US)?.description, s1)
1102+
XCTAssertEqual(Decimal(string: s1, locale: fr_FR)?.description, "1234")
1103+
1104+
let s2 = "1234,5678"
1105+
XCTAssertEqual(Decimal(string: s2, locale: en_US)?.description, "1234")
1106+
XCTAssertEqual(Decimal(string: s2, locale: fr_FR)?.description, s1)
1107+
}
10561108
}

0 commit comments

Comments
 (0)