Skip to content

Commit 660f795

Browse files
authored
Implement negative power support to pow(_ x: Decimal, _ y: Int). Historically Decimal has never supported negative power and will reproduce unexpected results (#895)
resolves: rdar://133875543
1 parent bd6e770 commit 660f795

File tree

3 files changed

+60
-10
lines changed

3 files changed

+60
-10
lines changed

Sources/FoundationEssentials/Decimal/Decimal+Compatibility.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ extension Decimal : _ObjectiveCBridgeable {
8484
@available(macOS 10.10, iOS 8.0, watchOS 2.0, tvOS 9.0, *)
8585
public func pow(_ x: Decimal, _ y: Int) -> Decimal {
8686
let result = try? x._power(
87-
exponent: UInt(y), roundingMode: .plain
87+
exponent: y, roundingMode: .plain
8888
)
8989
return result ?? .nan
9090
}
9191
#else
9292
@_spi(SwiftCorelibsFoundation)
9393
public func _pow(_ x: Decimal, _ y: Int) -> Decimal {
9494
let result = try? x._power(
95-
exponent: UInt(y), roundingMode: .plain
95+
exponent: y, roundingMode: .plain
9696
)
9797
return result ?? .nan
9898
}
@@ -233,7 +233,9 @@ private func __NSDecimalPower(
233233
_ roundingMode: Decimal.RoundingMode
234234
) -> Decimal.CalculationError {
235235
do {
236-
let power = try decimal.pointee._power(exponent: UInt(exponent), roundingMode: roundingMode)
236+
let power = try decimal.pointee._power(
237+
exponent: exponent, roundingMode: roundingMode
238+
)
237239
result.pointee = power
238240
return .noError
239241
} catch {

Sources/FoundationEssentials/Decimal/Decimal+Math.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,15 +367,19 @@ extension Decimal {
367367
}
368368

369369
internal func _power(
370-
exponent: UInt, roundingMode: RoundingMode
370+
exponent: Int, roundingMode: RoundingMode
371371
) throws -> Decimal {
372372
if self.isNaN {
373373
throw _CalculationError.overflow
374374
}
375375
if exponent == 0 {
376376
return Decimal(1)
377377
}
378-
var power = exponent
378+
if self == .zero {
379+
// Technically 0^-n is undefined, return NaN
380+
return exponent > 0 ? Decimal(0) : .nan
381+
}
382+
var power = abs(exponent)
379383
var result = self
380384
var temporary = Decimal(1)
381385
while power > 1 {
@@ -395,6 +399,14 @@ extension Decimal {
395399
result = try temporary._multiply(
396400
by: result, roundingMode: roundingMode
397401
)
402+
// Negative Exponent Rule
403+
// x^-n = 1/(x^n)
404+
if exponent < 0 {
405+
result = try Decimal(1)._divide(
406+
by: result,
407+
roundingMode: roundingMode
408+
)
409+
}
398410
return result
399411
}
400412

Tests/FoundationEssentialsTests/DecimalTests.swift

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -657,20 +657,20 @@ final class DecimalTests : XCTestCase {
657657
// Positive base
658658
let six = Decimal(6)
659659
for exponent in 1 ..< 10 {
660-
result = try six._power(exponent: UInt(exponent), roundingMode: .plain)
660+
result = try six._power(exponent: exponent, roundingMode: .plain)
661661
XCTAssertEqual(result.doubleValue, pow(6.0, Double(exponent)))
662662
}
663663
// Negative base
664664
let negativeSix = Decimal(-6)
665665
for exponent in 1 ..< 10 {
666-
result = try negativeSix._power(exponent: UInt(exponent), roundingMode: .plain)
666+
result = try negativeSix._power(exponent: exponent, roundingMode: .plain)
667667
XCTAssertEqual(result.doubleValue, pow(-6.0, Double(exponent)))
668668
}
669669
for i in -2 ... 10 {
670670
for j in 0 ... 5 {
671671
let actual = Decimal(i)
672672
let result = try actual._power(
673-
exponent: UInt(j), roundingMode: .plain
673+
exponent: j, roundingMode: .plain
674674
)
675675
let expected = Decimal(pow(Double(i), Double(j)))
676676
XCTAssertEqual(expected, result, "\(result) == \(i)^\(j)")
@@ -1008,10 +1008,10 @@ final class DecimalTests : XCTestCase {
10081008
for i in -2...10 {
10091009
for j in 0...5 {
10101010
let power = Decimal(i)
1011-
let actual = try power._power(exponent: UInt(j), roundingMode: .plain)
1011+
let actual = try power._power(exponent: j, roundingMode: .plain)
10121012
let expected = Decimal(pow(Double(i), Double(j)))
10131013
XCTAssertEqual(expected, actual, "\(actual) == \(i)^\(j)")
1014-
XCTAssertEqual(expected, try power._power(exponent: UInt(j), roundingMode: .plain))
1014+
XCTAssertEqual(expected, try power._power(exponent: j, roundingMode: .plain))
10151015
}
10161016
}
10171017

@@ -1301,4 +1301,40 @@ final class DecimalTests : XCTestCase {
13011301
XCTAssertEqual(length, 3)
13021302
}
13031303
#endif
1304+
1305+
func testNegativePower() {
1306+
func test(withBase base: Decimal, power: Int) {
1307+
XCTAssertEqual(
1308+
try base._power(exponent: -power, roundingMode: .plain),
1309+
try Decimal(1)/base._power(exponent: power, roundingMode: .plain),
1310+
"Base: \(base), Power: \(power)"
1311+
)
1312+
}
1313+
// Negative Exponent Rule
1314+
// x^-n = 1/(x^n)
1315+
for power in 2 ..< 10 {
1316+
// Positive Integer base
1317+
test(withBase: Decimal(Int.random(in: 1 ..< 10)), power: power)
1318+
1319+
// Negative Integer base
1320+
test(withBase: Decimal(Int.random(in: -10 ..< -1)), power: power)
1321+
1322+
// Postive Double base
1323+
test(withBase: Decimal(Double.random(in: 0 ..< 1.0)), power: power)
1324+
1325+
// Negative Double base
1326+
test(withBase: Decimal(Double.random(in: -1.0 ..< 0.0)), power: power)
1327+
1328+
// For zero base: 0^n = 0; 0^(-n) = nan
1329+
XCTAssertEqual(
1330+
try Decimal(0)._power(exponent: power, roundingMode: .plain),
1331+
Decimal(0)
1332+
)
1333+
XCTAssertEqual(
1334+
try Decimal(0)._power(exponent: -power, roundingMode: .plain),
1335+
Decimal.nan
1336+
)
1337+
}
1338+
1339+
}
13041340
}

0 commit comments

Comments
 (0)