iOS数字计算及小数位截取

参考

前言

日常项目的开发中,经常会遇到数字计算、小数位截取等操作,如果使用标准的Double类型操作,则会丢失精度,需要用到专门用于计算的NSDecimalNumber

一、对Double数字类型进行小数位控制处理

public extension Double {
    func roundTo(decimalCount: Int32, roundType: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> Double {
        // 一些特殊的数据会存在精度丢失的问题 "0.14" -double-> 0.14000000000000001 -> [roundType: up, decimalCount:2] -> 0.14999999999999999
        // 一些特殊的数据会存在精度丢失的问题 "0.14" -double-> 0.14000000000000001 -> [roundType: up, decimalCount:3] -> 0.14000000000000001

        // 乘以10的要保留的小数位的长度,10^n, 最小只能是保留0位小数
        let divisor = pow(10.0, Double(max(decimalCount, 0)))
        // 获取整数
        let bigDouble: Double = (self * divisor)
        // 截取,通过type,向上截取、向下截取、四舍五入截取只保留整数位
        let integerDouble = bigDouble.rounded(roundType)
        // 回归要保留的小数位
        let result = integerDouble / divisor
        return result
    }

    func roundToStr(decimalCount: Int32, roundType: FloatingPointRoundingRule = .toNearestOrAwayFromZero) -> String {
        // 一些特殊的数据会存在精度丢失的问题 "0.14" -double-> 0.14000000000000001 -> [roundType: up, decimalCount:2] -> 0.14999999999999999 -string-> "0.15"
        // 一些特殊的数据会存在精度丢失的问题 "0.14" -double-> 0.14000000000000001 -> [roundType: up, decimalCount:3] -> 0.14000000000000001 -string-> "0.14"
        let result = self.roundTo(decimalCount: decimalCount, roundType: roundType)
        return "\(result)"
    }
}

针对NSDecimalNumber的相关处理及全部代码

import Foundation

public enum RoundType: Int32 {
    case round // 下一位四舍五入
    case up // 下一位不是0就进一位
    case down // 下一位是任何东西都丢掉
    
    var decimalRoundType: NSDecimalNumber.RoundingMode {
        /**
        plain: 保留位数的下一位四舍五入
        down: 保留位数的下一位直接舍去
        up: 保留位数的下一位直接进一位
        bankers: 当保留位数的下一位不是5时,四舍五入,当保留位数的下一位是5时,其前一位是偶数直接舍去,是奇数直接进位(如果5后面还有数字则直接进位)
         */
        switch self {
        case .round:
            return .plain
        case .up:
            return .up
        case .down:
            return .down
        }
    }
    var doubleRoundType: FloatingPointRoundingRule {
        /**
         toNearestOrAwayFromZero: 保留位数的下一位四舍五入
         down: 保留位数的下一位直接舍去
         up: 保留位数的下一位直接进一位
         */
        switch self {
        case .round:
            return .toNearestOrAwayFromZero
        case .up:
            return .up
        case .down:
            return .down
        }
    }
}
public extension Double {
    func roundTo(decimalCount: Int32, roundType: RoundType = .round) -> Double {
        // 乘以10的要保留的小数位的长度,10^n, 最小只能是保留0位小数
        let divisor = pow(10.0, Double(max(decimalCount, 0)))
        // 获取整数
        let bigDouble: Double = (self * divisor)
        // 截取,通过type,向上截取、向下截取、四舍五入截取只保留整数位
        let integerDouble = bigDouble.rounded(roundType.doubleRoundType)
        // 回归要保留的小数位
        let result = integerDouble / divisor
        return result
    }
    func roundToStr(decimalCount: Int32, roundType: RoundType = .round, minZeroCount: Int = 2) -> String {
        let result = self.roundTo(decimalCount: decimalCount, roundType: roundType)
        var resultStr = "\(result)"
        if decimalCount > 0 {
            // 如果不是整数,设计小数位
            // 小数位不足,补齐 10.1 -> 10.10
            var nowCount: Int = 0
            let list = resultStr.components(separatedBy: ".")
            if list.count == 2 {
                let lastStr = list.last ?? ""
                nowCount = lastStr.count
            }
            let disCount = minZeroCount - nowCount
            if disCount > 0 {
                if list.count == 2 {
                    // 之前有小数位
                    resultStr = resultStr + Array(repeating: "0", count: disCount).joined()
                } else {
                    // 之前没有小数位
                    resultStr = resultStr + "." + Array(repeating: "0", count: disCount).joined()
                }
            }
        } else {
            // 整数
            let list = resultStr.components(separatedBy: ".")
            return list.first ?? "0"
        }
        return resultStr
    }
}

public extension String {
    var numberText: String {
        // 去掉文本里的一些特殊字符,防止转移成数字时出错
        var txt = self
        // 去掉千分位
        txt = txt.replacingOccurrences(of: ",", with: "")
        txt = txt.replacingOccurrences(of: ",", with: "")
        // 去掉金额符号
        txt = txt.replacingOccurrences(of: "$", with: "")
        txt = txt.replacingOccurrences(of: "¥", with: "")
        // 去掉空格
        txt = txt.replacingOccurrences(of: " ", with: "")
        // 去掉百分比符号
        txt = txt.replacingOccurrences(of: "%", with: "")
        // 去掉数字里的正数符号+
        txt = txt.replacingOccurrences(of: "+", with: "")
        return txt
    }
    var doubleValue: Double {
        return Double(self.numberText) ?? 0
    }
    var floatValue: Float {
        return Float(self.doubleValue)
    }
    var intValue: Int {
        return Int(self.numberText) ?? 0
    }
    var int32Value: Int32 {
        return Int32(self.numberText) ?? 0
    }
    var int64Value: Int64 {
        return Int64(self.numberText) ?? 0
    }
    var decimalNumber: NSDecimalNumber {
        let txt = self.numberText
        if txt.count == 0 {
            // 空字符串会生成NaN,与其他值计算会崩溃
            return .zero
        }
        let res = NSDecimalNumber(string: txt)
        if res == .notANumber {
            // 空字符串会生成NaN,与其他值计算会崩溃
            return .zero
        }
        return res
    }
    func roundToStr(decimalCount: Int16 = 2, roundType: RoundType = .round) -> NSDecimalNumber {
        return self.decimalNumber.roundTo(decimalCount: decimalCount, roundType: roundType)
    }
    func roundToStr(decimalCount: Int16 = 2, roundType: RoundType = .round, minZeroCount: Int = 2) -> String {
        return self.decimalNumber.roundToStr(decimalCount: decimalCount, roundType: roundType, minZeroCount: minZeroCount)
    }
}

public extension NSDecimalNumber {
    // MARK: - 加减乘除
    static func + (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
        return lhs.adding(rhs)
    }
    
    static func - (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
        return lhs.subtracting(rhs)
    }
    
    static func * (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
        return lhs.multiplying(by: rhs)
    }
    
    static func / (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
        if rhs == .zero {
            // 防止崩溃
            return .zero
        }
        return lhs.dividing(by: rhs)
    }
    // MARK: - 比较大小
    static func > (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
        let result = lhs.compare(rhs)
        return result == .orderedDescending
    }
    static func >= (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
        let result = lhs.compare(rhs)
        return result == .orderedDescending || result == .orderedSame
    }
    static func < (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
        let result = lhs.compare(rhs)
        return result == .orderedAscending
    }
    static func <= (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
        let result = lhs.compare(rhs)
        return result == .orderedAscending || result == .orderedSame
    }
    static func == (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
        let result = lhs.compare(rhs)
        return result == .orderedSame
    }
    // MARK: - 最大最小值
    func min(_ val: NSDecimalNumber) -> NSDecimalNumber {
        if self > val {
            return val
        }
        return self
    }
    func max(_ val: NSDecimalNumber) -> NSDecimalNumber {
        if self < val {
            return val
        }
        return self
    }
    static func min(_ nums: NSDecimalNumber...) -> NSDecimalNumber {
        return nums.reduce(nums.first ?? .zero, { ($0 > $1) ? $1 : $0 })
    }
    static func max(_ nums: NSDecimalNumber...) -> NSDecimalNumber {
        return nums.reduce(nums.first ?? .zero, { ($0 < $1) ? $1 : $0 })
    }
    // MARK: - 限制小数位
    func roundTo(decimalCount: Int16, roundType: RoundType = .round) -> NSDecimalNumber {
        /**
        plain: 保留位数的下一位四舍五入
        down: 保留位数的下一位直接舍去
        up: 保留位数的下一位直接进一位
        bankers: 当保留位数的下一位不是5时,四舍五入,当保留位数的下一位是5时,其前一位是偶数直接舍去,是奇数直接进位(如果5后面还有数字则直接进位)
         */
        /**
        raiseOnExactness: 发生精确错误时是否抛出异常,一般为false
        raiseOnOverflow: 发生溢出错误时是否抛出异常,一般为false
        raiseOnUnderflow: 发生不足错误时是否抛出异常,一般为false
        raiseOnDivideByZero: 除数是0时是否抛出异常,一般为true
         */

        let behavior = NSDecimalNumberHandler(
            roundingMode: roundType.decimalRoundType,
            scale: decimalCount,
            raiseOnExactness: false,
            raiseOnOverflow: false,
            raiseOnUnderflow: false,
            raiseOnDivideByZero: true)
        let product = multiplying(by: .one, withBehavior: behavior)
        return product
    }
    func roundToStr(decimalCount: Int16 = 2, roundType: RoundType = .round, minZeroCount: Int = 2) -> String {
        /**
        plain: 保留位数的下一位四舍五入
        down: 保留位数的下一位直接舍去
        up: 保留位数的下一位直接进一位
        bankers: 当保留位数的下一位不是5时,四舍五入,当保留位数的下一位是5时,其前一位是偶数直接舍去,是奇数直接进位(如果5后面还有数字则直接进位)
         */
        /**
        raiseOnExactness: 发生精确错误时是否抛出异常,一般为false
        raiseOnOverflow: 发生溢出错误时是否抛出异常,一般为false
        raiseOnUnderflow: 发生不足错误时是否抛出异常,一般为false
        raiseOnDivideByZero: 除数是0时是否抛出异常,一般为true
         */
        let result = self.roundTo(decimalCount: decimalCount, roundType: roundType)
        var resultStr = result.stringValue
        if decimalCount > 0 {
            // 如果不是整数,设计小数位
            // 小数位不足,补齐 10.1 -> 10.10
            var nowCount: Int = 0
            let list = resultStr.components(separatedBy: ".")
            if list.count == 2 {
                let lastStr = list.last ?? ""
                nowCount = lastStr.count
            }
            let disCount = minZeroCount - nowCount
            if disCount > 0 {
                if list.count == 2 {
                    // 之前有小数位
                    resultStr = resultStr + Array(repeating: "0", count: disCount).joined()
                } else {
                    // 之前没有小数位
                    resultStr = resultStr + "." + Array(repeating: "0", count: disCount).joined()
                }
            }
        }
        return resultStr
    }
    
    // MARK: - 快捷数字
    static var num100: NSDecimalNumber {
        return NSDecimalNumber(value: 100)
    }
}

测试代码

let numStr1 = "0.14"
let db2 = Double(numStr1.numberText) ?? 0
let roundType: RoundType = .up
let a0 = numStr1.roundToStr(decimalCount: 0, roundType: roundType, minZeroCount: 2) // "1"
let a1 = numStr1.roundToStr(decimalCount: 1, roundType: roundType, minZeroCount: 2) // "0.20"
let a2 = numStr1.roundToStr(decimalCount: 2, roundType: roundType, minZeroCount: 2) // "0.14"
let a3 = numStr1.roundToStr(decimalCount: 3, roundType: roundType, minZeroCount: 2) // "0.14"

let c0 = db2.roundToStr(decimalCount: 0, roundType: roundType, minZeroCount: 2) // "1"
let c1 = db2.roundToStr(decimalCount: 1, roundType: roundType, minZeroCount: 2) // "0.20"
let c2 = db2.roundToStr(decimalCount: 2, roundType: roundType, minZeroCount: 2) // "0.15"
let c3 = db2.roundToStr(decimalCount: 3, roundType: roundType, minZeroCount: 2) // "0.14"
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容