💸

初めてのswift-money

に公開

決済システムやECサイトを開発していると、金額計算って意外と厄介ですよね。

「100円の商品に消費税10%をかけたら110円」なんて単純な計算ならいいんですが、実際は:

  • 浮動小数点数の誤差で0.01円ズレる
  • 割り勘で1円が消える
  • 通貨によって小数点以下の桁数が違う
  • オーバーフローのチェックが面倒

こういった問題に悩まされた経験、ありませんか?

そんなわけで、型安全で使いやすい金額計算ライブラリ MoneyKit を作りました。

https://github.com/1amageek/swift-money

特徴

整数ベースで精度バッチリ

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")
]

参考リンク

フィードバックやPRお待ちしてます!🎉

Discussion