mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
Arithmetic: drop divide-/modulo-by-zero short-circuit
The previous implementation intercepted `n / 0` and `n % 0` and returned `.null`, on the rationale that `null` is friendlier for rule authors and matches the engine's "missing value" convention. That's the last remaining intentional deviation from `json-logic-js` in this file — remove it for full spec parity. `json-logic-js` delegates `/` and `%` to native JS arithmetic, which follows IEEE 754: `n / 0` is `±Infinity` (sign matches the dividend), `0 / 0` is `NaN`, and any `n % 0` is `NaN`. Swift's `Double` operators and `truncatingRemainder(dividingBy:)` already produce these IEEE values, so the fix is just dropping the divisor==0 short-circuit and wrapping the result in `.float`. Tests: - Replace `testDivByZeroReturnsNull` with `testDivByZeroFollowsIeee754` pinning all three cases (`+Infinity`, `-Infinity`, `NaN`). - Replace `testModByZeroReturnsNull` with `testModByZeroIsNan` covering `7 % 0` and `0 % 0`. - Update the `1 / null` / `1 % null` cases inside `testSubDivAndModUseToNumberPerSpec` (divisor coerces to 0 → `+Infinity` / `NaN` instead of `.null`). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
a1ad0bce09
commit
e32933780b
2 files changed
+37
-43
No files matched your search
@@ -33,11 +33,10 @@ import Foundation
|
||||
/// become `Double.nan` and propagate naturally — the final result is
|
||||
/// `.float(nan)`, which `isTruthy` reports as falsy.
|
||||
///
|
||||
/// Division and modulo by zero return `.null` instead of `infinity` /
|
||||
/// `nan`. JSON Logic JS produces the IEEE values, but for rule authoring
|
||||
/// `null` is friendlier: it short-circuits comparisons in a predictable
|
||||
/// way and matches the engine's "missing value" convention. This is the
|
||||
/// one intentional deviation from the spec in this file.
|
||||
/// Division and modulo by zero produce the IEEE 754 values (`±Infinity`
|
||||
/// for `n / 0` with `n ≠ 0`, `NaN` for `0 / 0` and any `n % 0`), matching
|
||||
/// `json-logic-js` exactly. Result is wrapped in `.float`, which means
|
||||
/// `isTruthy` correctly reports `Infinity` as truthy and `NaN` as falsy.
|
||||
enum ArithmeticOperators {
|
||||
|
||||
/// `{"+": [a, b, ...]}` — variadic sum. Each operand is coerced via
|
||||
@@ -83,25 +82,18 @@ enum ArithmeticOperators {
|
||||
}
|
||||
|
||||
/// `{"/": [a, b]}` — division. Operands are coerced via JS `Number()`
|
||||
/// (`asNumber`). Division by zero returns `.null` (see type docs).
|
||||
/// (`asNumber`). Division by zero follows IEEE 754: `n / 0` is
|
||||
/// `±Infinity` (sign matches the dividend), `0 / 0` is `NaN`.
|
||||
static func opDiv(args: Value, vars: Value) throws -> Value {
|
||||
let (lhs, rhs) = try Operators.evalTwo(args, vars: vars, opName: "/")
|
||||
let divisor = asDouble(rhs)
|
||||
if divisor == 0.0 {
|
||||
return .null
|
||||
}
|
||||
return .float(asDouble(lhs) / divisor)
|
||||
return .float(asDouble(lhs) / asDouble(rhs))
|
||||
}
|
||||
|
||||
/// `{"%": [a, b]}` — modulo. Operands are coerced via JS `Number()`
|
||||
/// (`asNumber`). Modulo by zero returns `.null` (see type docs).
|
||||
/// (`asNumber`). `n % 0` follows IEEE 754 and is `NaN`.
|
||||
static func opMod(args: Value, vars: Value) throws -> Value {
|
||||
let (lhs, rhs) = try Operators.evalTwo(args, vars: vars, opName: "%")
|
||||
let divisor = asDouble(rhs)
|
||||
if divisor == 0.0 {
|
||||
return .null
|
||||
}
|
||||
return .float(asDouble(lhs).truncatingRemainder(dividingBy: divisor))
|
||||
return .float(asDouble(lhs).truncatingRemainder(dividingBy: asDouble(rhs)))
|
||||
}
|
||||
|
||||
/// `Number(value)`-style coercion for `-`, `/`, `%`. Falls back to
|
||||
|
||||
@@ -179,22 +179,26 @@ final class ArithmeticOperatorsTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testDivByZeroReturnsNull() throws {
|
||||
// Both .int(0) and .float(0.0) divisors → .null
|
||||
XCTAssertEqual(
|
||||
try run(ArithmeticOperators.opDiv, args: arr(.int(1), .int(0))),
|
||||
.null
|
||||
/// `n / 0` follows IEEE 754: positive dividend → `+Infinity`, negative
|
||||
/// dividend → `-Infinity`, `0 / 0` → `NaN`. Matches `json-logic-js`,
|
||||
/// which delegates to native JS `/` (no short-circuit).
|
||||
func testDivByZeroFollowsIeee754() throws {
|
||||
// 1 / 0 → +Infinity (covers both .int(0) and .float(0.0) divisors).
|
||||
let posIntZero = unwrapFloat(
|
||||
try run(ArithmeticOperators.opDiv, args: arr(.int(1), .int(0)))
|
||||
)
|
||||
XCTAssertEqual(
|
||||
try run(ArithmeticOperators.opDiv, args: arr(.int(1), .float(0.0))),
|
||||
.null
|
||||
XCTAssertEqual(posIntZero, .infinity)
|
||||
let posFloatZero = unwrapFloat(
|
||||
try run(ArithmeticOperators.opDiv, args: arr(.int(1), .float(0.0)))
|
||||
)
|
||||
// Even 0/0 returns .null (not NaN) — explicit short-circuit before
|
||||
// arithmetic.
|
||||
XCTAssertEqual(
|
||||
try run(ArithmeticOperators.opDiv, args: arr(.int(0), .int(0))),
|
||||
.null
|
||||
XCTAssertEqual(posFloatZero, .infinity)
|
||||
// -1 / 0 → -Infinity.
|
||||
let neg = unwrapFloat(
|
||||
try run(ArithmeticOperators.opDiv, args: arr(.int(-1), .int(0)))
|
||||
)
|
||||
XCTAssertEqual(neg, -.infinity)
|
||||
// 0 / 0 → NaN.
|
||||
assertNaN(try run(ArithmeticOperators.opDiv, args: arr(.int(0), .int(0))))
|
||||
}
|
||||
|
||||
func testDivWrongArityIsTypeError() {
|
||||
@@ -217,11 +221,11 @@ final class ArithmeticOperatorsTests: XCTestCase {
|
||||
)
|
||||
}
|
||||
|
||||
func testModByZeroReturnsNull() throws {
|
||||
XCTAssertEqual(
|
||||
try run(ArithmeticOperators.opMod, args: arr(.int(7), .int(0))),
|
||||
.null
|
||||
)
|
||||
/// `n % 0` is always `NaN` per IEEE 754. Matches `json-logic-js`,
|
||||
/// which delegates to native JS `%` (no short-circuit).
|
||||
func testModByZeroIsNan() throws {
|
||||
assertNaN(try run(ArithmeticOperators.opMod, args: arr(.int(7), .int(0))))
|
||||
assertNaN(try run(ArithmeticOperators.opMod, args: arr(.int(0), .int(0))))
|
||||
}
|
||||
|
||||
func testModWrongArityIsTypeError() {
|
||||
@@ -297,16 +301,14 @@ final class ArithmeticOperatorsTests: XCTestCase {
|
||||
.float(0.0)
|
||||
)
|
||||
|
||||
// 1 / null and 1 % null → divisor coerces to 0 → null (engine's
|
||||
// intentional div/mod-by-zero short-circuit; see type docs).
|
||||
// 1 / null → divisor coerces to 0 → +Infinity (IEEE 754, see
|
||||
// testDivByZeroFollowsIeee754 for the broader pinning).
|
||||
XCTAssertEqual(
|
||||
try run(ArithmeticOperators.opDiv, args: arr(.int(1), .null)),
|
||||
.null
|
||||
)
|
||||
XCTAssertEqual(
|
||||
try run(ArithmeticOperators.opMod, args: arr(.int(1), .null)),
|
||||
.null
|
||||
unwrapFloat(try run(ArithmeticOperators.opDiv, args: arr(.int(1), .null))),
|
||||
.infinity
|
||||
)
|
||||
// 1 % null → divisor coerces to 0 → NaN.
|
||||
assertNaN(try run(ArithmeticOperators.opMod, args: arr(.int(1), .null)))
|
||||
|
||||
// Bools coerce to 0 / 1 (Number(true) === 1, Number(false) === 0).
|
||||
XCTAssertEqual(
|
||||
|
||||
Reference in new issue
Block a user