You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript找零计算函数Bug:便士找零始终少1分

找零计算代码的便士找零缺失问题修复方案

问题现象

当找零包含便士(PENNY)时总会少1分,例如支付100美元购买1.87美元的商品,应找零0.03美元便士,但实际输出仅为0.02美元。

问题根源

这是JavaScript浮点数精度缺陷导致的:十进制小数在二进制存储时存在精度损失,比如100 - 1.87的实际计算结果是98.12999999999999而非精确的98.13。循环减便士时,最后剩余的0.00999999999999会被判定为小于0.01,导致少统计一个便士。

修复方案

1. 核心修复思路

将所有金额转换为以分为单位的整数处理,彻底规避浮点数精度问题,最后再转换回美元格式展示。

2. 具体代码修改步骤

(1)调整基础金额变量和配置

  • 把价格改为分单位:let price = 187;(对应1.87美元)
  • 更新面额配置为分单位,并修正拼写错误:
    const billValues = {
      "PENNY": 1,
      "NICKEL": 5, // 原代码拼写为NICKLE,已修正
      "DIME": 10,
      "QUARTER": 25,
      "ONE": 100,
      "FIVE": 500,
      "TEN": 1000,
      "TWENTY": 2000,
      "ONE HUNDRED": 10000
    }
    

(2)重写找零计算函数

修改calculateChange,用整数计算找零,避免精度损失:

const calculateChange = (amountTendered, price) => {
  // 将输入金额转换为分(处理用户输入的字符串)
  const tenderedCents = Math.round(parseFloat(amountTendered) * 100);
  const priceCents = price;
  let changeCents = tenderedCents - priceCents;
  let totalChange = [];
  
  // 复制cid数组再反转,避免修改原数组
  const cidR = [...cid].reverse();
  
  for (let i = 0; i < cidR.length; i++) {
    const [billName, drawerAmount] = cidR[i];
    // 抽屉中的金额转为分
    const drawerCents = Math.round(drawerAmount * 100);
    const billValueCents = billValues[billName];
    
    // 计算该面额最多能取的数量:不超过找零需求,也不超过抽屉库存
    const maxCount = Math.min(
      Math.floor(changeCents / billValueCents),
      Math.floor(drawerCents / billValueCents)
    );
    
    if (maxCount > 0) {
      // 转回美元格式存入结果
      const totalForBill = (maxCount * billValueCents) / 100;
      totalChange.push([billName, totalForBill]);
      // 扣除已分配的找零
      changeCents -= maxCount * billValueCents;
    }
  }
  
  // 若仍有剩余找零,说明无法凑出
  return changeCents === 0 ? totalChange : [];
}

(3)修正展示和判断逻辑

  • 修复writeChangeDue函数的返回值问题,确保金额展示精确到分:
    const writeChangeDue = (arr) => {
      arr.forEach((item) => {
        changeDue.innerHTML += `<p class="change-due">${item[0]}: $${item[1].toFixed(2)}</p>`;
      });
    }
    
  • 调整按钮点击事件中的金额判断逻辑:
    purchaseBtn.addEventListener("click", () => {
      changeDue.innerHTML = "";
      const tendered = parseFloat(cash.value);
      const actualPrice = price / 100; // 转回美元
      
      if (isNaN(tendered) || tendered < actualPrice) {
        alert("Customer does not have enough money to purchase the item");
      } else if (tendered === actualPrice) {
        changeDue.innerHTML += `<p>No change due - customer paid with exact cash</p>`;
      } else if (tendered > parseFloat(totalCashRoundedUp)) {
        changeDue.innerHTML += `<p>Status: INSUFFICIENT_FUNDS</p>`;
      } else {
        const changeResult = calculateChange(cash.value, price);
        if (changeResult.length === 0) {
          changeDue.innerHTML += `<p>Status: INSUFFICIENT_FUNDS</p>`;
        } else {
          changeDue.innerHTML += `<p class="change-due">Status: OPEN</p>`;
          writeChangeDue(changeResult);
        }
      }
    });
    

完整修复后代码

const cash = document.getElementById("cash");
const changeDue = document.getElementById("change-due");
const purchaseBtn = document.getElementById("purchase-btn");
const cashInDrawer = document.getElementById("cash-in-drawer");
const totalSpan = document.getElementById("total")

// 价格以分为单位:187分 = 1.87美元
let price = 187;

let cid = [
  ["PENNY", 1.01],
  ["NICKEL", 2.05],
  ["DIME", 3.1],
  ["QUARTER", 4.25],
  ["ONE", 90],
  ["FIVE", 55],
  ["TEN", 20],
  ["TWENTY", 60],
  ["ONE HUNDRED", 100]
];

// 面额对应分
const billValues = {
  "PENNY": 1,
  "NICKEL": 5,
  "DIME": 10,
  "QUARTER": 25,
  "ONE": 100,
  "FIVE": 500,
  "TEN": 1000,
  "TWENTY": 2000,
  "ONE HUNDRED": 10000
}

const totalCash = cid.reduce((acc, arr) => acc + arr[1], 0);
const totalCashRoundedUp = totalCash.toFixed(2);

// 渲染现金抽屉
cid.forEach((arr) => {
 cashInDrawer.innerHTML += `<li>${arr[0]}: $${arr[1].toFixed(2)}</li>`;
});

totalSpan.textContent = `$${(price / 100).toFixed(2)}`;

const calculateChange = (amountTendered, price) => {
  const tenderedCents = Math.round(parseFloat(amountTendered) * 100);
  const priceCents = price;
  let changeCents = tenderedCents - priceCents;
  let totalChange = [];
  
  const cidR = [...cid].reverse();
  
  for (let i = 0; i < cidR.length; i++) {
    const [billName, drawerAmount] = cidR[i];
    const drawerCents = Math.round(drawerAmount * 100);
    const billValueCents = billValues[billName];
    
    const maxCount = Math.min(
      Math.floor(changeCents / billValueCents),
      Math.floor(drawerCents / billValueCents)
    );
    
    if (maxCount > 0) {
      const totalForBill = (maxCount * billValueCents) / 100;
      totalChange.push([billName, totalForBill]);
      changeCents -= maxCount * billValueCents;
    }
  }
  
  return changeCents === 0 ? totalChange : [];
}

const writeChangeDue = (arr) => {
  arr.forEach((item) => {
    changeDue.innerHTML += `<p class="change-due">${item[0]}: $${item[1].toFixed(2)}</p>`;
  });
}

purchaseBtn.addEventListener("click", () => {
  changeDue.innerHTML = "";
  const tendered = parseFloat(cash.value);
  const actualPrice = price / 100;
  
  if (isNaN(tendered) || tendered < actualPrice) {
    alert("Customer does not have enough money to purchase the item");
  } else if (tendered === actualPrice) {
    changeDue.innerHTML += `<p>No change due - customer paid with exact cash</p>`;
  } else if (tendered > parseFloat(totalCashRoundedUp)) {
    changeDue.innerHTML += `<p>Status: INSUFFICIENT_FUNDS</p>`;
  } else {
    const changeResult = calculateChange(cash.value, price);
    if (changeResult.length === 0) {
      changeDue.innerHTML += `<p>Status: INSUFFICIENT_FUNDS</p>`;
    } else {
      changeDue.innerHTML += `<p class="change-due">Status: OPEN</p>`;
      writeChangeDue(changeResult);
    }
  }
});

内容的提问来源于stack exchange,提问作者Jibril Abshir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 12:59:51