Sponsor Link
環境&対象
- macOS Monterey 12.3 beta4
- Xcode 13.3 beta 3
- iOS 15.2
Decimal を丸めたい
金融アプリ等を考えると、10 進数をきちんと表現できる Decimal を使うことになるのですが、Decimal には機能が揃っておらず、NSDecimal の機能を使用する必要が発生することがあります。
切り上げ・切り下げのような 数字を丸める操作もそのような NSDecimal を使うことが必要となる機能の1つです。
Float/Double での丸めかた(切り上げ・切り下げ)
Float/Double では数値を丸めるために、rounded という関数が用意されています。
print( ( 5.2).rounded() ) // 5.0
print( ( 5.5).rounded() ) // 6.0
print( (-5.2).rounded() ) // -5.0
print( (-5.5).rounded() ) // -6.0
rounded() というメソッドは、引数なしでは toNearestOrAwayFromZero というルールで丸めています。
基本的に四捨五入なのですが、ちょうど真ん中のケースでは、絶対値としての数字の大きい方に寄せています。
rounded というメソッドにはその丸め方を指定することもできるようになっていて、切り上げや切り下げは、rouded に対して、その方法を指定して行います。
let up = FloatingPointRoundingRule.up
print( ( 5.2).rounded(up) ) // 6.0
print( ( 5.5).rounded(up) ) // 6.0
print( (-5.2).rounded(up) ) // -5.0
print( (-5.5).rounded(up) ) // -5.0
let down = FloatingPointRoundingRule.down
print( ( 5.2).rounded(down) ) // 5.0
print( ( 5.5).rounded(down) ) // 5.0
print( (-5.2).rounded(down) ) // -6.0
print( (-5.5).rounded(down) ) // -6.0
.up や .down は単純な切り上げや切り下げです。数直線上の右方向もしくは左方向に一番近い整数値に丸めています。
FloatingPointRoudingRule には、.up や .down の他にも、.towardZero 等様々なオプションが用意されていますので、一度読んでみることをお勧めします。
Apple のドキュメントは、こちら。
ですが、これは、Double や Float での話であり、Decimal には適用できません。
もちろん、Decimal を Double や Float に変換して適用し、その後 Decimal に再変換すれば rounded を使って丸めることができますが、Double や Float での計算による計算誤差が発生してしまうかもしれず、せっかく Decimal を使って処理を進めていることの意味がなくなってしまいます。
Decimal を丸める
Decimal には、直接用意されてはいないのですが、Decimal には、NSDecimalNumber という Bridge されている(変換できる)型が用意されています。
Apple のドキュメントは、こちら。
この NSDecimalNumber を使用することで丸めることができます。
NSDecimalNumber を丸める時には、rouding というメソッドを使用します。
let np52 = NSDecimalNumber(decimal: Decimal(string: "5.2")!)
let np55 = NSDecimalNumber(decimal: Decimal(string: "5.5")!)
let nm52 = NSDecimalNumber(decimal: Decimal(string: "-5.2")!)
let nm55 = NSDecimalNumber(decimal: Decimal(string: "-5.5")!)
print( np52.rounding(accordingToBehavior: nil)) // 5
print( np55.rounding(accordingToBehavior: nil)) // 6
print( nm52.rounding(accordingToBehavior: nil)) // -5
print( nm55.rounding(accordingToBehavior: nil)) // -6
デフォルトでは、rounded の toNearestOrAwayFromZero と同様の動作です。
accodringToBehavior に、どのように丸めるかを指定します。指定方法は、NSDecimalNumberHandler というクラスのインスタンスを作成し、動作指定します。
Apple のドキュメントは、こちら。
丸め型を指定するオプションの型は NSDecimalNumber.RoundingMode です。rounded での .toNearestOrAwayFromZero と同様の動作を指定するには、.plain を使用します。
Apple のドキュメントは、こちら。
let np52 = NSDecimalNumber(decimal: Decimal(string: "5.2")!)
let np55 = NSDecimalNumber(decimal: Decimal(string: "5.5")!)
let nm52 = NSDecimalNumber(decimal: Decimal(string: "-5.2")!)
let nm55 = NSDecimalNumber(decimal: Decimal(string: "-5.5")!)
var plainBehavior = NSDecimalNumberHandler(roundingMode: .plain, scale: 0,
raiseOnExactness: false, raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false)
print( np52.rounding(accordingToBehavior: plainBehavior)) // 5
print( np55.rounding(accordingToBehavior: plainBehavior)) // 5
print( nm52.rounding(accordingToBehavior: plainBehavior)) // -5
print( nm55.rounding(accordingToBehavior: plainBehavior)) // -6
NSDecimalNumberHandler では、そのほかの動作も指定することができるようになっています。roudingMode の次の引数である scale は、どの桁で丸めるかを指定することができます。例えば、1を指定すると、小数点1桁で丸めてくれます。
let np524 = NSDecimalNumber(decimal: Decimal(string: "5.24")!)
var up0XBehavior = NSDecimalNumberHandler(roundingMode: .up, scale: 1,
raiseOnExactness: false, raiseOnOverflow: false,
raiseOnUnderflow: false,
raiseOnDivideByZero: false)
print( np524.rounding(accordingToBehavior: up0XBehavior)) // 5.3
NSDecimalNumberHandler の3つ目以降の引数は、計算途中で計算エラーが発生した時に例外を発生させるかどうかです。
まとめ:Decimal を丸める方法
Decimal を丸める時に Double や Float に変換せず、NSDecimalNumber を使って精度を保って計算しましょう。
- Decimal を丸める時には、Bridge されている NSDecimalNumber を使用する
- NSDecimalNumber.rounding を使って丸める
- NSDecimalNumberHandling を使って、丸める方法を指定できる
説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。
おまけ: Decimal+Extension
次のような Extension を作成して、Decimal からも rounded っぽく使えるようにしてます。
//
// Decimal+Extension.swift
//
// Created by : Tomoaki Yagishita on 2022/02/24
// © 2022 SmallDeskSoftware
//
import Foundation
extension Decimal {
func rounded(_ rule: FloatingPointRoundingRule, scale: Int16 = 0) -> Decimal {
let mode: NSDecimalNumber.RoundingMode
switch rule {
case .toNearestOrAwayFromZero:
mode = .plain
case .toNearestOrEven:
mode = .bankers
case .up:
mode = .up
case .down:
mode = .down
case .towardZero, .awayFromZero:
fatalError("unsupported rule")
default:
fatalError("unsupported rule")
}
let behavior = NSDecimalNumberHandler(roundingMode: mode, scale: scale,
raiseOnExactness: false, raiseOnOverflow: false,
raiseOnUnderflow: false, raiseOnDivideByZero: false)
return (self as NSDecimalNumber).rounding(accordingToBehavior: behavior) as Decimal
}
}
Sponsor Link