初めてのswift-money
決済システムやECサイトを開発していると、金額計算って意外と厄介ですよね。
「100円の商品に消費税10%をかけたら110円」なんて単純な計算ならいいんですが、実際は:
- 浮動小数点数の誤差で0.01円ズレる
- 割り勘で1円が消える
- 通貨によって小数点以下の桁数が違う
- オーバーフローのチェックが面倒
こういった問題に悩まされた経験、ありませんか?
そんなわけで、型安全で使いやすい金額計算ライブラリ MoneyKit を作りました。
特徴
整数ベースで精度バッチリ
Stripeとかの決済APIって、金額を「4999」(49.99ドル)みたいに最小単位で送りますよね。MoneyKitも同じアプローチです。
// 整数で管理するから精度の問題なし
let price = try Money(amount: 4999, currency: .USD) // $49.99
print(price.formatted) // "$49.99"
// Decimal型からも作れる
let shipping = try Money(decimalAmount: 5.00, currency: .USD)
通貨の型安全性
異なる通貨の足し算をコンパイル時にエラーにできたら最高なんですが、Swiftの型システムではちょっと難しい。でも実行時にはちゃんとチェックします:
let usd = try Money(amount: 100, currency: .USD)
let jpy = try Money(amount: 100, currency: .JPY)
// これはエラーになる
let total = try usd + jpy // ❌ MoneyError.currencyMismatch
エラーを投げたくない場合は、safe系メソッドも用意してます:
if let result = usd.tryAdd(jpy) {
print(result)
} else {
print("通貨が違います") // こっちが実行される
}
丸め方を自由に選べる
消費税の計算って、切り捨て・切り上げ・四捨五入のどれを使うかで結果が変わりますよね。
let price = try Money(amount: 1234, currency: .JPY) // ¥1,234
// 10%税込み計算(1357.4円)
let calc = price * 1.1
// 丸め方を選べる
let roundDown = try calc.roundedDown // ¥1,357(切り捨て)
let roundUp = try calc.roundedUp // ¥1,358(切り上げ)
let halfUp = try calc.halfUp // ¥1,357(四捨五入)
let bankers = try calc.bankers // ¥1,357(銀行丸め)
日本の消費税は基本的に切り捨てなので:
let taxIncluded = try (price * 1.1).roundedDown // ¥1,357
便利メソッドも用意してます:
// デフォルトで10%税込み、銀行丸め
let withTax = try price.withTax()
// 軽減税率8%、切り捨て
let withTax8 = try price.withTax(rate: 1.08, rounding: .down)
割り勘も完璧
3人で1万円を割り勘するとき、普通に割ると3333.33...円ですよね。でも現金払いだと1円が行方不明に。
MoneyKitなら1円も無駄にしません:
let bill = try Money(amount: 10000, currency: .JPY) // ¥10,000
let split = try bill.distribute(into: 3)
// [¥3,334, ¥3,333, ¥3,333]
// 合計はちゃんと ¥10,000 ✨
最初の人が1円多く払う仕組みです。
重み付けの分配も簡単:
let revenue = try Money(amount: 100000, currency: .USD) // $1,000
// 70% : 20% : 10% で分配
let shares = try revenue.allocate(by: [70, 20, 10])
// [$700.00, $200.00, $100.00]
通貨ごとの小数点桁数を自動判定
JPY(円)は小数点なし、USD(ドル)は2桁、KWD(クウェートディナール)は3桁。ISO 4217の仕様に従って自動で判定します:
// 円は小数点なし
let yen = try Money(amount: 1000, currency: .JPY)
print(yen.decimalAmount) // 1000.0
// ドルは2桁
let usd = try Money(amount: 4999, currency: .USD)
print(usd.decimalAmount) // 49.99
// クウェートディナールは3桁
let kwd = try Money(amount: 1234, currency: .KWD)
print(kwd.decimalAmount) // 1.234
カスタム通貨も作れます:
let custom = try Currency(code: "XXX", decimalPlaces: 4)
オーバーフロー検出
金額計算でInt.maxを超えることって普通はないですが、万が一のために:
let huge = try Money(amount: Int.max - 100, currency: .JPY)
let overflow = try Money(amount: 200, currency: .JPY)
// オーバーフローを検出してエラー
let result = try huge + overflow // ❌ MoneyError.amountOverflow
safe系メソッドならnilを返します:
if let result = huge.tryAdd(overflow) {
print(result)
} else {
print("オーバーフローしました") // こっちが実行される
}
痒いところに手が届く機能
計算を途中で止めない
複雑な計算をするとき、途中で丸めると誤差が大きくなります。MoneyKitなら最後まで計算してから丸められます:
let price = try Money(amount: 10000, currency: .JPY) // ¥10,000
// 20%引き → 10%税込み
let calc = price * 0.8 * 1.1 // まだ丸めない
// 最後に丸める
let final = try calc.roundedDown // ¥8,800
計算を組み合わせることもできます:
let item1 = try Money(amount: 1000, currency: .JPY)
let item2 = try Money(amount: 500, currency: .JPY)
let calc1 = item1 * 1.1
let calc2 = item2 * 1.1
// 計算同士を足してから丸める
let total = try (calc1 + calc2).roundedDown // ¥1,650
パーセンテージ計算
手数料計算とか、よくやりますよね:
let amount = try Money(amount: 10000, currency: .USD) // $100.00
// 2.9%の手数料
let fee = try amount.percentage(0.029) // $2.90
// 固定費も足す
let fixedFee = try Money(amount: 30, currency: .USD) // $0.30
let totalFee = try fee + fixedFee // $3.20
JSON対応
CodableなのでそのままJSONにできます:
struct Product: Codable {
let name: String
let price: Money
}
let product = Product(
name: "Widget",
price: try Money(amount: 2999, currency: .USD)
)
let data = try JSONEncoder().encode(product)
// {"name":"Widget","price":{"amount":2999,"currencyCode":"USD"}}
Swift Concurrency対応
Sendable準拠なので、Actor間でも安心:
actor PaymentProcessor {
func process(_ amount: Money) async throws {
// Money は Sendable なので問題なし
let fee = try amount.percentage(0.029)
// ...
}
}
ロケールに応じたフォーマット
表示用の文字列も簡単:
let money = try Money(amount: 123456, currency: .USD) // $1,234.56
// デフォルトロケール
print(money.formatted) // "$1,234.56"
// 日本のロケール
print(money.formatted(locale: Locale(identifier: "ja_JP"))) // "$1,234.56"
// ドイツのロケール
print(money.formatted(locale: Locale(identifier: "de_DE"))) // "1.234,56 $"
// 数値だけ
print(money.numericString) // "1234.56"
実用例
ECサイトの決済
// 商品: ¥1,234
let product = try Money(amount: 1234, currency: .JPY)
// 送料: ¥500
let shipping = try Money(amount: 500, currency: .JPY)
// 小計
let subtotal = try product + shipping // ¥1,734
// 消費税10%(切り捨て)
let total = try (subtotal * 1.1).roundedDown // ¥1,907
print("お支払い金額: \(total.formatted)") // "¥1,907"
決済手数料の計算
let orderAmount = try Money(decimalAmount: 49.99, currency: .USD)
// 2.9% + $0.30 の手数料
let percentFee = try orderAmount.percentage(0.029) // $1.45
let fixedFee = try Money(amount: 30, currency: .USD) // $0.30
let totalFee = try percentFee + fixedFee // $1.75
// 手数料を引いた入金額
let net = try orderAmount - totalFee // $48.24
// 決済APIに送る
let params = [
"amount": orderAmount.amount, // 4999
"currency": orderAmount.currency.code.lowercased() // "usd"
]
サブスクの日割り計算
// 月額 $29.99
let monthly = try Money(amount: 2999, currency: .USD)
let daysInMonth = 30
let daysUsed = 15
// 1日あたりの料金
let daily = try monthly / daysInMonth // $1.00
// 日割り金額
let prorated = try daily * daysUsed // $15.00
複雑な割引計算
let price = try Money(amount: 10000, currency: .USD) // $100.00
// 20%引き
let discount = try price.percentage(0.20) // $20.00
let salePrice = try price - discount // $80.00
// さらにBlack Fridayで15%引き
let finalCalc = salePrice * 0.85
let final = try finalCalc.roundedDown // $68.00
まとめ
金額計算、地味に難しいですよね。特に:
- 浮動小数点数の誤差
- 通貨ごとの違い
- 丸め処理
- オーバーフロー
- 1円単位の端数処理
こういった問題を気にせず使えるライブラリを目指しました。
決済システムやECサイトを作る際は、ぜひ使ってみてください!
インストール
Swift Package Managerで簡単に導入できます:
dependencies: [
.package(url: "https://github.com/1amageek/swift-money.git", from: "1.0.0")
]
参考リンク
- GitHub: https://github.com/1amageek/swift-money
- Swift 6対応
- MIT License
フィードバックやPRお待ちしてます!🎉
Discussion