Skip to content

Commit 2b6543f

Browse files
authored
Decimal.init(sign:exponent:significand:) should clamp exponent (#747)
While I was here: imported more Decimal tests from swift-corelibs-foundation resolves: rdar://132019894
1 parent 6d71f17 commit 2b6543f

File tree

4 files changed

+212
-13
lines changed

4 files changed

+212
-13
lines changed

Sources/FoundationEssentials/Decimal/Decimal+Conformances.swift

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ internal import _ForSwiftFoundation
1717
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
1818
extension Decimal : CustomStringConvertible {
1919
public init?(string: __shared String, locale: __shared Locale? = nil) {
20-
// Substitute the decimal sign if needed
21-
var decimalString = string
22-
if let decimalSeparator = locale?.decimalSeparator {
23-
decimalString = decimalString.replacing(decimalSeparator, with: ".")
24-
}
25-
guard let value = Decimal.decimal(from: decimalString.utf8, matchEntireString: false).result else {
20+
let decimalSeparator = locale?.decimalSeparator ?? "."
21+
guard let value = Decimal.decimal(
22+
from: string.utf8,
23+
decimalSeparator: decimalSeparator.utf8,
24+
matchEntireString: false
25+
).result else {
2626
return nil
2727
}
2828
self = value
@@ -239,7 +239,7 @@ extension Decimal /* : FloatingPoint */ {
239239
self = significand
240240
do {
241241
self = try significand._multiplyByPowerOfTen(
242-
power: exponent, roundingMode: .plain)
242+
power: Int(Int16(clamping: exponent)), roundingMode: .plain)
243243
} catch {
244244
guard let actual = error as? Decimal._CalculationError else {
245245
self = .nan

Sources/FoundationEssentials/Decimal/Decimal.swift

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ extension Decimal {
223223

224224
internal static func decimal(
225225
from stringView: String.UTF8View,
226+
decimalSeparator: String.UTF8View,
226227
matchEntireString: Bool
227228
) -> (result: Decimal?, processedLength: Int) {
228229
func multiplyBy10AndAdd(
@@ -247,6 +248,20 @@ extension Decimal {
247248
return i
248249
}
249250

251+
func stringViewContainsDecimalSeparator(at index: String.UTF8View.Index) -> Bool {
252+
for indexOffset in 0 ..< decimalSeparator.count {
253+
let stringIndex = stringView.index(index, offsetBy: indexOffset)
254+
let decimalIndex = decimalSeparator.index(
255+
decimalSeparator.startIndex,
256+
offsetBy: indexOffset
257+
)
258+
if stringView[stringIndex] != decimalSeparator[decimalIndex] {
259+
return false
260+
}
261+
}
262+
return true
263+
}
264+
250265
var result = Decimal()
251266
var index = stringView.startIndex
252267
index = skipWhiteSpaces(from: index)
@@ -298,8 +313,8 @@ extension Decimal {
298313
result = product
299314
}
300315
// Get the decimal point
301-
if index != stringView.endIndex && stringView[index] == UInt8._period {
302-
stringView.formIndex(after: &index)
316+
if index != stringView.endIndex && stringViewContainsDecimalSeparator(at: index) {
317+
stringView.formIndex(&index, offsetBy: decimalSeparator.count)
303318
// Continue to build the mantissa
304319
while index != stringView.endIndex,
305320
let digitValue = stringView[index].digitValue {

Sources/FoundationEssentials/JSON/JSONDecoder.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,9 @@ extension FixedWidthInteger {
10751075
extension Decimal {
10761076
init?(entire string: String) {
10771077
guard let value = Decimal.decimal(
1078-
from: string.utf8, matchEntireString: true
1078+
from: string.utf8,
1079+
decimalSeparator: ".".utf8,
1080+
matchEntireString: true
10791081
).result else {
10801082
return nil
10811083
}

Tests/FoundationEssentialsTests/DecimalTests.swift

Lines changed: 185 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ final class DecimalTests : XCTestCase {
109109
XCTAssertEqual("-5", Decimal(signOf: Decimal(-3), magnitudeOf: Decimal(-5)).description)
110110
}
111111

112+
func test_DescriptionWithLocale() {
113+
let decimal = Decimal(string: "-123456.789")!
114+
XCTAssertEqual(decimal.toString(with: nil), "-123456.789")
115+
let en = decimal.toString(with: Locale(identifier: "en_GB"))
116+
XCTAssertEqual(en, "-123456.789")
117+
let fr = decimal.toString(with: Locale(identifier: "fr_FR"))
118+
XCTAssertEqual(fr, "-123456,789")
119+
}
120+
112121
func test_BasicConstruction() {
113122
let zero = Decimal()
114123
XCTAssertEqual(20, MemoryLayout<Decimal>.size)
@@ -252,6 +261,57 @@ final class DecimalTests : XCTestCase {
252261
XCTAssertEqual(zero3.description, "0")
253262
}
254263

264+
func test_stringWithLocale() {
265+
266+
let en_US = Locale(identifier: "en_US")
267+
let fr_FR = Locale(identifier: "fr_FR")
268+
269+
XCTAssertEqual(Decimal(string: "1,234.56")! * 1000, Decimal(1000))
270+
XCTAssertEqual(Decimal(string: "1,234.56", locale: en_US)! * 1000, Decimal(1000))
271+
XCTAssertEqual(Decimal(string: "1,234.56", locale: fr_FR)! * 1000, Decimal(1234))
272+
XCTAssertEqual(Decimal(string: "1.234,56", locale: en_US)! * 1000, Decimal(1234))
273+
XCTAssertEqual(Decimal(string: "1.234,56", locale: fr_FR)! * 1000, Decimal(1000))
274+
275+
XCTAssertEqual(Decimal(string: "-1,234.56")! * 1000, Decimal(-1000))
276+
XCTAssertEqual(Decimal(string: "+1,234.56")! * 1000, Decimal(1000))
277+
XCTAssertEqual(Decimal(string: "+1234.56e3"), Decimal(1234560))
278+
XCTAssertEqual(Decimal(string: "+1234.56E3"), Decimal(1234560))
279+
XCTAssertEqual(Decimal(string: "+123456000E-3"), Decimal(123456))
280+
281+
XCTAssertNil(Decimal(string: ""))
282+
XCTAssertNil(Decimal(string: "x"))
283+
XCTAssertEqual(Decimal(string: "-x"), Decimal.zero)
284+
XCTAssertEqual(Decimal(string: "+x"), Decimal.zero)
285+
XCTAssertEqual(Decimal(string: "-"), Decimal.zero)
286+
XCTAssertEqual(Decimal(string: "+"), Decimal.zero)
287+
XCTAssertEqual(Decimal(string: "-."), Decimal.zero)
288+
XCTAssertEqual(Decimal(string: "+."), Decimal.zero)
289+
290+
XCTAssertEqual(Decimal(string: "-0"), Decimal.zero)
291+
XCTAssertEqual(Decimal(string: "+0"), Decimal.zero)
292+
XCTAssertEqual(Decimal(string: "-0."), Decimal.zero)
293+
XCTAssertEqual(Decimal(string: "+0."), Decimal.zero)
294+
XCTAssertEqual(Decimal(string: "e1"), Decimal.zero)
295+
XCTAssertEqual(Decimal(string: "e-5"), Decimal.zero)
296+
XCTAssertEqual(Decimal(string: ".3e1"), Decimal(3))
297+
298+
XCTAssertEqual(Decimal(string: "."), Decimal.zero)
299+
XCTAssertEqual(Decimal(string: ".", locale: en_US), Decimal.zero)
300+
XCTAssertNil(Decimal(string: ".", locale: fr_FR))
301+
302+
XCTAssertNil(Decimal(string: ","))
303+
XCTAssertEqual(Decimal(string: ",", locale: fr_FR), Decimal.zero)
304+
XCTAssertNil(Decimal(string: ",", locale: en_US))
305+
306+
let s1 = "1234.5678"
307+
XCTAssertEqual(Decimal(string: s1, locale: en_US)?.description, s1)
308+
XCTAssertEqual(Decimal(string: s1, locale: fr_FR)?.description, "1234")
309+
310+
let s2 = "1234,5678"
311+
XCTAssertEqual(Decimal(string: s2, locale: en_US)?.description, "1234")
312+
XCTAssertEqual(Decimal(string: s2, locale: fr_FR)?.description, s1)
313+
}
314+
255315
func testStringPartialMatch() {
256316
// This tests makes sure Decimal still has the
257317
// same behavior that it only requires the beginning
@@ -736,6 +796,37 @@ final class DecimalTests : XCTestCase {
736796
XCTAssertEqual(-4.98, result.doubleValue, accuracy: 0.0001)
737797
}
738798

799+
func test_Round() throws {
800+
let testCases: [(Double, Double, Int, Decimal.RoundingMode)] = [
801+
// expected, start, scale, round
802+
( 0, 0.5, 0, .down ),
803+
( 1, 0.5, 0, .up ),
804+
( 2, 2.5, 0, .bankers ),
805+
( 4, 3.5, 0, .bankers ),
806+
( 5, 5.2, 0, .plain ),
807+
( 4.5, 4.5, 1, .down ),
808+
( 5.5, 5.5, 1, .up ),
809+
( 6.5, 6.5, 1, .plain ),
810+
( 7.5, 7.5, 1, .bankers ),
811+
812+
( -1, -0.5, 0, .down ),
813+
( -2, -2.5, 0, .up ),
814+
( -2, -2.5, 0, .bankers ),
815+
( -4, -3.5, 0, .bankers ),
816+
( -5, -5.2, 0, .plain ),
817+
( -4.5, -4.5, 1, .down ),
818+
( -5.5, -5.5, 1, .up ),
819+
( -6.5, -6.5, 1, .plain ),
820+
( -7.5, -7.5, 1, .bankers ),
821+
]
822+
for testCase in testCases {
823+
let (expected, start, scale, mode) = testCase
824+
let num = Decimal(start)
825+
let actual = try num._round(scale: scale, roundingMode: mode)
826+
XCTAssertEqual(Decimal(expected), actual, "Failed test case: \(testCase)")
827+
}
828+
}
829+
739830
func test_Maths() {
740831
for i in -2...10 {
741832
for j in 0...5 {
@@ -767,6 +858,25 @@ final class DecimalTests : XCTestCase {
767858
}
768859
}
769860
}
861+
862+
XCTAssertEqual(Decimal(186243 * 15673 as Int64), Decimal(186243) * Decimal(15673))
863+
864+
XCTAssertEqual(Decimal(string: "5538")! + Decimal(string: "2880.4")!, Decimal(string: "8418.4")!)
865+
866+
XCTAssertEqual(Decimal(string: "5538.0")! - Decimal(string: "2880.4")!, Decimal(string: "2657.6")!)
867+
XCTAssertEqual(Decimal(string: "2880.4")! - Decimal(5538), Decimal(string: "-2657.6")!)
868+
XCTAssertEqual(Decimal(0x10000) - Decimal(0x1000), Decimal(0xf000))
869+
#if !os(watchOS)
870+
XCTAssertEqual(Decimal(0x1_0000_0000) - Decimal(0x1000), Decimal(0xFFFFF000))
871+
XCTAssertEqual(Decimal(0x1_0000_0000_0000) - Decimal(0x1000), Decimal(0xFFFFFFFFF000))
872+
#endif
873+
XCTAssertEqual(Decimal(1234_5678_9012_3456_7899 as UInt64) - Decimal(1234_5678_9012_3456_7890 as UInt64), Decimal(9))
874+
XCTAssertEqual(Decimal(0xffdd_bb00_8866_4422 as UInt64) - Decimal(0x7777_7777), Decimal(0xFFDD_BB00_10EE_CCAB as UInt64))
875+
876+
let highBit = Decimal(_exponent: 0, _length: 8, _isNegative: 0, _isCompact: 1, _reserved: 0, _mantissa: (0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x8000))
877+
let otherBits = Decimal(_exponent: 0, _length: 8, _isNegative: 0, _isCompact: 1, _reserved: 0, _mantissa: (0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0xffff, 0x7fff))
878+
XCTAssertEqual(highBit - otherBits, Decimal(1))
879+
XCTAssertEqual(otherBits + Decimal(1), highBit)
770880
}
771881

772882
func testMisc() throws {
@@ -805,6 +915,67 @@ final class DecimalTests : XCTestCase {
805915
XCTAssertTrue(Decimal.nan.magnitude.isNaN)
806916
XCTAssertEqual(Decimal.leastFiniteMagnitude.magnitude, -Decimal.leastFiniteMagnitude)
807917

918+
XCTAssertEqual(Decimal(-9), Decimal(1) - Decimal(10))
919+
XCTAssertEqual(Decimal(1.234), abs(Decimal(1.234)))
920+
XCTAssertEqual(Decimal(1.234), abs(Decimal(-1.234)))
921+
XCTAssertEqual((0 as Decimal).magnitude, 0 as Decimal)
922+
XCTAssertEqual((1 as Decimal).magnitude, 1 as Decimal)
923+
XCTAssertEqual((1 as Decimal).magnitude, abs(1 as Decimal))
924+
XCTAssertEqual((1 as Decimal).magnitude, abs(-1 as Decimal))
925+
XCTAssertEqual((-1 as Decimal).magnitude, abs(-1 as Decimal))
926+
XCTAssertEqual((-1 as Decimal).magnitude, abs(1 as Decimal))
927+
XCTAssertEqual(Decimal.greatestFiniteMagnitude.magnitude, Decimal.greatestFiniteMagnitude)
928+
929+
var a = Decimal(1234)
930+
var result = try a._multiplyByPowerOfTen(power: 1, roundingMode: .plain)
931+
XCTAssertEqual(Decimal(12340), result)
932+
a = Decimal(1234)
933+
result = try a._multiplyByPowerOfTen(power: 2, roundingMode: .plain)
934+
XCTAssertEqual(Decimal(123400), result)
935+
a = result
936+
do {
937+
result = try a._multiplyByPowerOfTen(power: 128, roundingMode: .plain)
938+
XCTFail("Expected to throw _CalcuationError.overflow")
939+
} catch {
940+
guard let calculationError = error as? Decimal._CalculationError else {
941+
XCTFail("Expected Decimal._CalculationError, got \(error)")
942+
return
943+
}
944+
XCTAssertEqual(.overflow, calculationError)
945+
}
946+
a = Decimal(1234)
947+
result = try a._multiplyByPowerOfTen(power: -2, roundingMode: .plain)
948+
XCTAssertEqual(Decimal(12.34), result)
949+
a = result
950+
do {
951+
result = try a._multiplyByPowerOfTen(power: -128, roundingMode: .plain)
952+
XCTFail("Expected to throw _CalcuationError.underflow")
953+
} catch {
954+
guard let calculationError = error as? Decimal._CalculationError else {
955+
XCTFail("Expected Decimal._CalculationError, got \(error)")
956+
return
957+
}
958+
XCTAssertEqual(.underflow, calculationError)
959+
}
960+
a = Decimal(1234)
961+
result = try a._power(exponent: 0, roundingMode: .plain)
962+
XCTAssertEqual(Decimal(1), result)
963+
a = Decimal(8)
964+
result = try a._power(exponent: 2, roundingMode: .plain)
965+
XCTAssertEqual(Decimal(64), result)
966+
a = Decimal(-2)
967+
result = try a._power(exponent: 3, roundingMode: .plain)
968+
XCTAssertEqual(Decimal(-8), result)
969+
for i in -2...10 {
970+
for j in 0...5 {
971+
let power = Decimal(i)
972+
let actual = try power._power(exponent: UInt(j), roundingMode: .plain)
973+
let expected = Decimal(pow(Double(i), Double(j)))
974+
XCTAssertEqual(expected, actual, "\(actual) == \(i)^\(j)")
975+
XCTAssertEqual(expected, try power._power(exponent: UInt(j), roundingMode: .plain))
976+
}
977+
}
978+
808979
do {
809980
// SR-13015
810981
let a = try XCTUnwrap(Decimal(string: "119.993"))
@@ -946,15 +1117,26 @@ final class DecimalTests : XCTestCase {
9461117
}
9471118

9481119
func test_Significand() {
949-
let x = -42 as Decimal
1120+
var x = -42 as Decimal
1121+
XCTAssertEqual(x.significand.sign, .plus)
1122+
var y = Decimal(sign: .plus, exponent: 0, significand: x)
1123+
XCTAssertEqual(y, -42)
1124+
y = Decimal(sign: .minus, exponent: 0, significand: x)
1125+
XCTAssertEqual(y, 42)
1126+
1127+
x = 42 as Decimal
9501128
XCTAssertEqual(x.significand.sign, .plus)
951-
let y = Decimal(sign: .plus, exponent: 0, significand: x)
952-
XCTAssertEqual(y.sign, .minus)
1129+
y = Decimal(sign: .plus, exponent: 0, significand: x)
1130+
XCTAssertEqual(y, 42)
1131+
y = Decimal(sign: .minus, exponent: 0, significand: x)
1132+
XCTAssertEqual(y, -42)
9531133

9541134
let a = Decimal.leastNonzeroMagnitude
9551135
XCTAssertEqual(Decimal(sign: .plus, exponent: -10, significand: a), 0)
1136+
XCTAssertEqual(Decimal(sign: .plus, exponent: .min, significand: a), 0)
9561137
let b = Decimal.greatestFiniteMagnitude
9571138
XCTAssertTrue(Decimal(sign: .plus, exponent: 10, significand: b).isNaN)
1139+
XCTAssertTrue(Decimal(sign: .plus, exponent: .max, significand: b).isNaN)
9581140
}
9591141

9601142
func test_ULP() {

0 commit comments

Comments
 (0)