swiftroundingnsdecimalnumber

Rounding NSDecimalNumber HalfDown


As we all know, when calculating with a precise number (E.g. money), it's best to use NSDecimalNumber instead of basic float or double. Then, we have rounding options:

public enum RoundingMode : UInt {

    case plain // Round up on a tie
    case down // Always down == truncate
    case up // Always up
    case bankers // on a tie round so last digit is even
}

Question:

How do I perform rounding halfDown (reverse of .plain, round down on a tie) for this NSDecimalNumber?

My original problem (you can skip this section for sure)

Setting rounding options (can change): Up, Down, HalfUp

I'm calculating var pT = price include tax from a var p = price exclude tax by this formular:

pT = roundingWithOption(p * tax)

pT is the displaying unitPrice (must include tax or business goes to jail lol) and people can custom it => I have to calculate price excludes tax, by revert the process

p = roundingWithReverseOption(pT2 / tax)

Then I can save this price exclude tax to the server and get it back for later. We only save p because tax might change and we can get tax from somewhere else => pT might differ if tax changed.

The number before and after save and continue process should be the same if nothing change (rounding, taxes...).

It's simple when options are .up and .down, but I don't know what to do with .plain.

Or can you provide a better solution for calculating priceExcludeTax from the custom price? It'll be more complicated when applied with discount and quantity, so I just ask a simple question first.


Solution

  • You can try this function in which scale define the precision wanted:

    func roundSubPlain(_ num: NSDecimalNumber, scale: Int = 0) -> NSDecimalNumber {
        let precision = (1 / pow(10, scale + 2))
        let adjustedDecimalNumber = NSDecimalNumber(decimal: num.decimalValue - precision)
        let handler = NSDecimalNumberHandler(roundingMode: .plain, scale: Int16(scale), raiseOnExactness: true, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true)
        let roundedNum = adjustedDecimalNumber.rounding(accordingToBehavior: handler)
    
        return roundedNum
    }
    

    So if you try with:

    let num1 = NSDecimalNumber(decimal: 5.5)
    let num2 = NSDecimalNumber(decimal: 6.25)
    

    You will have:

    let subNum1 = roundSubPlain(num1) // 5
    let subNum2 = roundSubPlain(num1, scale: 1) // 6.2
    

    If you want the same kind of work around for bankers, you coud use something like:

    func roundOdd(_ num: NSDecimalNumber, scale: Int = 0) -> NSDecimalNumber {
        let handler = NSDecimalNumberHandler(roundingMode: .bankers, scale: Int16(scale), raiseOnExactness: true, raiseOnOverflow: true, raiseOnUnderflow: true, raiseOnDivideByZero: true)
        let roundedNum = num.rounding(accordingToBehavior: handler)
    
        let diff = num.decimalValue - roundedNum.decimalValue
        let gap = (1 / pow(10, scale))
    
        if abs(diff / gap) == 0.5 {
            let adjust: Decimal = diff > 0 ? 1 : -1
            return NSDecimalNumber(decimal: roundedNum.decimalValue + adjust * gap)
        } else {
            return roundedNum
        }
    }
    

    This could be improved for sure but it works :)

    let num = NSDecimalNumber(decimal: 6.15)
    let odd = roundOdd(num, scale: 1) // 6.1