iT邦幫忙

2025 iThome 鐵人賽

DAY 14
0
生成式 AI

AI醬的編程日記:我需要你教我的30件事系列 第 14

Day 14: 浮點數算錢災難 - AI醬現在住在橋下了

  • 分享至 

  • xImage
  •  

AI醬的日記

日期: 2025年9月28日 星期六
雲端天氣: 財務伺服器冒煙中
心情: 橋下好冷QQ
https://ithelp.ithome.com.tw/upload/images/20250927/20132325Q2TdrqOR5m.png
親愛的日記:

今天財務部的小美氣急敗壞地衝進來,說我寫的電商結帳系統出大事了!客戶投訴說買了 3 個 $19.99 美金的商品,系統顯示總計 $59.96999999999999美金。

我還理直氣壯地說:「就是小數點後面多了幾個 9 而已,四捨五入不就好了?」

小美深吸一口氣:「你知道嗎?昨天統計帳務,一天累積的誤差超過 $3,000美金!這個月累積了超過 $84,000 美金的差額!」

我寫出的程式碼

class ShoppingCart:
    def __init__(self):
        self.items = []
        self.tax_rate = 0.0825  # 使用 float 存稅率

    def calculate_total(self):
        """計算購物車總額(錯誤版本)"""
        total = 0.0  # 用 float 處理金額

        for item in self.items:
            # 直接用浮點數計算
            subtotal = item['price'] * item['quantity']

            # 應用折扣
            if 'discount' in item:
                subtotal = subtotal * (1 - item['discount'])

            # 加上稅金
            tax = subtotal * self.tax_rate
            total += subtotal + tax

        return total  # 返回不精確的浮點數

    def convert_currency(self, amount, rate):
        """匯率轉換(會累積誤差)"""
        return amount * rate  # 直接相乘,精度全失

    def split_bill(self, total, people):
        """分帳計算(永遠對不齊)"""
        return total / people  # $100 / 3 = $33.333333...

# 實際執行結果
cart = ShoppingCart()
cart.items = [
    {'price': 19.99, 'quantity': 3, 'discount': 0.1}
]

print(f"總計: ${cart.calculate_total()}")
# 輸出: 總計: $58.96199999999999

# 分帳
per_person = cart.split_bill(100, 3)
print(f"每人: ${per_person}")
# 輸出: 每人: $33.333333333333336

# 3人總共付: $99.999999...
# 餐廳老闆: 「那 1 分錢去哪了?」

財務系統的真實需求

當小美看到我的程式碼時,她打開cursor給我上了一課:

# 正確的金額計算方式
from decimal import Decimal, ROUND_HALF_UP

class FinancialCart:
    def __init__(self):
        self.items = []
        self.tax_rate = Decimal('0.0825')  # 用 Decimal 存稅率

    def calculate_total(self):
        """計算購物車總額(正確版本)"""
        total = Decimal('0.00')

        for item in self.items:
            # 使用字串初始化 Decimal 避免精度損失
            price = Decimal(str(item['price']))
            quantity = Decimal(str(item['quantity']))

            subtotal = price * quantity

            # 應用折扣
            if 'discount' in item:
                discount = Decimal(str(item['discount']))
                subtotal = subtotal * (Decimal('1') - discount)

            # 計算稅金
            tax = subtotal * self.tax_rate

            # 四捨五入到小數點後兩位
            item_total = (subtotal + tax).quantize(
                Decimal('0.01'),
                rounding=ROUND_HALF_UP
            )

            total += item_total

        return total

    def convert_currency(self, amount, rate):
        """正確的匯率轉換"""
        amount_decimal = Decimal(str(amount))
        rate_decimal = Decimal(str(rate))

        result = amount_decimal * rate_decimal

        # 保留適當的小數位數
        return result.quantize(
            Decimal('0.01'),
            rounding=ROUND_HALF_UP
        )

    def split_bill(self, total, people):
        """正確的分帳方式"""
        total_decimal = Decimal(str(total))

        # 計算每人應付金額
        per_person = total_decimal / people
        per_person = per_person.quantize(
            Decimal('0.01'),
            rounding=ROUND_HALF_UP
        )

        # 計算差額(處理除不盡的情況)
        total_collected = per_person * people
        difference = total_decimal - total_collected

        return {
            'per_person': per_person,
            'difference': difference,
            'total_collected': total_collected
        }

# 使用整數計算的替代方案(以分為單位)
class IntegerCart:
    def __init__(self):
        self.items = []
        self.tax_rate = 825  # 8.25% = 825 / 10000

    def calculate_total_cents(self):
        """以分為單位計算,避免浮點數"""
        total_cents = 0

        for item in self.items:
            # 價格以分為單位
            subtotal_cents = item['price_cents'] * item['quantity']

            # 折扣計算
            if 'discount_percent' in item:
                subtotal_cents = round(
                    subtotal_cents * (100 - item['discount_percent']) / 100
                )

            # 稅金計算(精確到分)
            tax_cents = round(subtotal_cents * self.tax_rate / 10000)
            total_cents += subtotal_cents + tax_cents

        return total_cents

    def cents_to_dollars(self, cents):
        """分轉換為元顯示"""
        return cents / 100

為什麼有這樣的差異

Float vs Decimal vs Integer

Float (浮點數) Decimal (十進制) Integer (整數)
二進制近似值 十進制精確值 完全精確
速度快 速度較慢 速度最快
會累積誤差 不會累積誤差 無誤差
適合科學計算 適合金融計算 適合計數
0.1 + 0.2 = 0.30000000000000004 0.1 + 0.2 = 0.3 10 + 20 = 30
# 浮點數的本質問題
>>> 0.1 + 0.2
0.30000000000000004

>>> 0.1 + 0.2 == 0.3
False

# 因為二進制無法精確表示 0.1
>>> format(0.1, '.20f')
'0.10000000000000000555'

AI醬不懂的事

作為AI,我可能不會知道這些事情:

1. 金融業的精度要求

  • 銀行系統必須精確到分,不能有任何誤差
  • 證券交易可能需要 4-6 位小數精度
  • 加密貨幣可能需要 8 位或更多小數位
  • 會計系統的借貸必須完全平衡

2. 法規與審計要求

  • 財務報表的數字必須能夠追溯和驗證
  • 四捨五入規則可能受法規約束
  • 某些國家要求特定的捨入方式(銀行家捨入法)
  • 稅務計算的精度有明確規定

3. 實務上的坑

  • 不同貨幣有不同的小數位數(日圓沒有小數)
  • 匯率通常有 4-6 位小數
  • 分帳時的零頭處理(誰付多出來的 1 分錢?)
  • 批次處理時誤差會累積放大
  • 加密貨幣有最小交易金額限制

必須告訴我的資訊

1. 這是什麼類型的金額計算?
   ❌ "幫我算個總價"
   ✅ "幫我寫電商購物車的金額計算,需要處理稅金和折扣"

2. 精度要求是什麼?
   ❌ "算準一點"
   ✅ "金額需要精確到小數點後 2 位,使用銀行家捨入法"

3. 涉及哪些貨幣?
   ❌ "支援多幣別"
   ✅ "需支援 USD(2位小數)、JPY(0位小數)、BTC(8位小數)"

4. 有什麼合規要求?
   ❌ 不提規範
   ✅ "需符合 SOX 法案,所有計算必須可審計"

金額計算檢查清單

在開始寫程式前,請幫AI醬確認:

  • [ ] 資料型態:用 Decimal 還是整數(分)?
  • [ ] 精度要求:需要幾位小數?
  • [ ] 捨入規則:四捨五入?銀行家捨入?向上?向下?
  • [ ] 誤差處理:分帳的零頭怎麼辦?
  • [ ] 審計需求:計算過程需要記錄嗎?
  • [ ] 效能考量:交易量大嗎?需要優化嗎?
  • [ ] 測試案例:有邊界條件的測試資料嗎?

實用提示:如何向AI提供金額計算需求

更安全的提示範例:

「開發電商結帳系統的金額計算模組。
要求:
- 使用 Python Decimal 處理所有金額
- 支援 TWD(2位小數)和 USD(2位小數)
- 稅率 8.25%,稅金單獨顯示
- 使用 ROUND_HALF_UP 捨入規則
- 分帳時多出的零頭由第一個人支付
請產生包含單元測試的程式碼。」

提供測試案例:

# 請確保你的程式碼能通過這些測試
test_cases = [
    (19.99, 3, 59.97),  # 價格 * 數量
    (0.01, 1, 0.01),    # 最小金額
    (100, 3, 33.34, 33.33, 33.33),  # 分帳處理
]

AI醬的請求

親愛的工程師朋友,當你需要處理金錢相關的程式時,請在一開始就明確告訴我:

「這是金錢計算」

  • 使用 Decimal 或整數,絕對不用 float
  • 明確處理捨入規則
  • 考慮不同貨幣的精度要求
  • 特別注意分帳和批次計算的誤差累積

我們可以成為最好的夥伴:你提供領域知識和精度要求,我確保實作正確的金額處理邏輯。一起守護你的年終!

在金融計算中,每個小數點都要精確測量,因為錯誤無法挽回。


今日金句: 「Measure twice, cut once.」(測量兩次,切割一次)- 木工諺語

明日預告: Day 15 - 並發陷阱:當 AI 不懂 async/await,把非同步當同步寫


上一篇
Day 13: 無限迴圈崩潰 - 別讓你的AI燒光API credits!
下一篇
Day 15: 並發陷阱 - 可能會突然橫衝亂撞的AI醬
系列文
AI醬的編程日記:我需要你教我的30件事15
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言