Home > Blockchain >  Round decimal to nearest increment given a number
Round decimal to nearest increment given a number

Time:09-16

I would like to round down a decimal to the nearest increment of another number. For example, given a value of 2.23678301 and an increment of 0.0001, I would like to round this to 2.2367. Sometimes the increment could be something like 0.00022, in which case the value would be rounded down to 2.23674.

I tried to do this, but sometimes the result is not correct and tests aren't passing:

extension Decimal {
    func rounded(byIncrement increment: Self) -> Self {
        var multipleOfValue = self / increment
        var roundedMultipleOfValue = Decimal()
        NSDecimalRound(&roundedMultipleOfValue, &multipleOfValue, 0, .down)
        return roundedMultipleOfValue * increment
    }
}

/// Tests

class DecimalTests: XCTestCase {
    func testRoundedByIncrement() {
        // Given
        let value: Decimal = 2.2367830187654

        // Then
        XCTAssertEqual(value.rounded(byIncrement: 0.00010000), 2.2367)
        XCTAssertEqual(value.rounded(byIncrement: 0.00022), 2.23674)
        XCTAssertEqual(value.rounded(byIncrement: 0.0000001), 2.236783)
        XCTAssertEqual(value.rounded(byIncrement: 0.00000001), 2.23678301) // XCTAssertEqual failed: ("2.23678301") is not equal to ("2.236783009999999744")
        XCTAssertEqual(value.rounded(byIncrement: 3.5), 0)
        XCTAssertEqual(value.rounded(byIncrement: 0.000000000000001), 2.2367830187654) // XCTAssertEqual failed: ("2.2367830187653998323726489726140416") is not equal to ("2.236783018765400576")
    }
}

I'm not sure why the decimal calculations are making up numbers that were never there, like the last assertion. Is there a cleaner or more accurate way to do this?

CodePudding user response:

Your code is fine. You're just calling it incorrectly. This line doesn't do what you think:

let value: Decimal = 2.2367830187654

This is equivalent to:

let value = Decimal(double: Double(2.2367830187654))

The value is first converted to a Double, binary rounding it to 2.236783018765400576. That value is then converted to a Decimal.

You need to use the string initializer everywhere you want a Decimal from a digit string:

let value = Decimal(string: "2.2367830187654")!

XCTAssertEqual(value.rounded(byIncrement: Decimal(string: "0.00000001")!), Decimal(string: "2.23678301")!)

etc.

Or you can use the integer-based initializers:

let value = Decimal(sign: .plus, exponent: -13, significand: 22367830187654)

In iOS 15 there are some new initializers that don't return optionals (init(_:format:lenient:) for example), but you're still going to need to pass Strings, not floating point literals.

You could also do this, though it may be confusing to readers, and might lead to bugs if folks take the quotes away:

extension Decimal: ExpressibleByStringLiteral {
    public init(stringLiteral value: String) {
        self.init(string: value)!
    }
}

let value: Decimal = "2.2367830187654"

XCTAssertEqual(value.rounded(byIncrement: "0.00000001"), "2.23678301")

For test code, that's probably nice, but I'd be very careful about using it in production code.

  • Related