searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

房贷月供和利息到底怎么算?用 decimal.js 正确实现等额本息、等额本金与提前还款(含 Typ

2026-07-13 17:04:10
2
0
代码取自一个上线中的房贷计算器核心(纯 TS、零平台依赖),在线DEMO:[房贷计算器](https://jsq3000.com/finance/mortgage)工具可对照结果。

##  金额必须走 Decimal,在边界统一舍入到分

**用 JavaScript 的浮点 `number` 直接算钱一定会出错——`0.1 + 0.2` 在 JS 里等于 `0.30000000000000004`,几百期累加后误差会放大到肉眼可见。** 正确做法是:所有中间计算用高精度的 `decimal.js`,只在输出时统一四舍五入到「分」(2 位小数)。这是房贷、个税、工资这类金融计算的第一条铁律。

封装一个金额工具,把"高精度计算 + 边界舍入"固定下来:

```ts
import Decimal from "decimal.js";
// 中间计算用 40 位精度,输出显式舍入
Decimal.set({ precision: 40, rounding: Decimal.ROUND_HALF_UP });

export function dec(value: Decimal.Value): Decimal {
  return new Decimal(value);
}
// 舍入到分(2 位),ROUND_HALF_UP,返回普通 number
export function round2(value: Decimal.Value): number {
  return new Decimal(value).toDecimalPlaces(2, Decimal.ROUND_HALF_UP).toNumber();
}

下面两种还款方式都建立在这个基础上。

一、等额本息:每月还固定金额

等额本息(equal installment)指还款期内每月偿还相同金额,其中本金逐月递增、利息逐月递减。 月供 M 的公式是:

M = P · r · (1+r)ⁿ / ((1+r)ⁿ − 1)

其中 P 是贷款本金,r 是月利率(年利率 ÷ 100 ÷ 12),n 是总期数(30 年 = 360 期)。用 decimal.js 实现,注意先处理 r=0(无息)的退化情况避免除零:

const r = dec(annualRatePercent).div(100).div(12);
let monthly: Decimal;
if (r.isZero()) {
  monthly = dec(principal).div(n);
} else {
  const factor = r.plus(1).pow(n); // (1+r)^n
  monthly = dec(principal).mul(r).mul(factor).div(factor.minus(1));
}
const monthlyRounded = round2(monthly);

然后逐期生成还款计划:每期利息 = 当期剩余本金 × 月利率(先舍入到分),本金 = 月供 − 利息:

let remaining = dec(principal);
for (let period = 1; period <= n; period++) {
  const interest = dec(round2(remaining.mul(r)));   // 当期利息,舍入到分
  let principalPart: Decimal, payment: Decimal;
  if (period === n) {
    principalPart = remaining;                       // 末期:还清全部剩余本金
    payment = principalPart.plus(interest);
  } else {
    principalPart = dec(monthlyRounded).minus(interest);
    payment = dec(monthlyRounded);
  }
  remaining = remaining.minus(principalPart);
  // push 到 schedule...
}

以**贷款 100 万元、年利率 3.5%、30 年(360 期)**为例,这套实现算出:月供约 4490.45 元,还款总额约 1,616,560.07 元,支付利息总额约 616,560.07 元

二、等额本金:每月还固定本金,月供递减

等额本金(equal principal)指每月偿还相同的本金,利息按剩余本金递减,所以月供逐月减少。 实现更直接:每期本金 = 本金 ÷ 期数(固定),利息 = 剩余本金 × 月利率:

const basePrincipal = round2(dec(principal).div(n));
let remaining = dec(principal);
for (let period = 1; period <= n; period++) {
  const interest = dec(round2(remaining.mul(r)));
  const principalPart = period === n ? remaining : dec(basePrincipal);
  const payment = principalPart.plus(interest);
  remaining = remaining.minus(principalPart);
}

同样 100 万 / 3.5% / 30 年:首月月供约 5694.45 元,末月约 2785.08 元,支付利息总额约 526,457.92 元

对比一下:等额本金比等额本息总共少付利息约 9 万元(616,560 − 526,458 ≈ 90,102 元),代价是前期月供更高、还款压力更大。这就是"提前还得多、总利息少"的直观体现,也是工具要把两种方式并排算给用户看的意义。

三、最关键的工程细节:末期吸收舍入误差

每期利息都舍入到分,几百期累加后必然和理论值产生几分钱的漂移,如果不处理,最后一期会出现"本金没还清"或"多还几分"的 bug。 解决办法是让末期不按公式算本金,而是直接还清当期剩余的全部本金

if (period === n) {
  principalPart = remaining;            // 末期 = 剩余本金,吸收所有舍入漂移
  payment = principalPart.plus(interest);
}

这样无论前面 359 期累计了多少分的舍入误差,最后一期都会精确把账平掉,保证「逐期本金之和 = 贷款本金」严格成立。这是金融计算里一个朴素但必须的处理——对账要对得上,不能差一分钱

四、为什么把这套逻辑做成纯函数

计算逻辑写成零依赖的纯 TypeScript 函数(不碰 DOM、不依赖框架),才能同时被 Web、小程序和单元测试复用,也方便和银行口径逐项核对。 输入用强类型约束,并在入口做校验(本金为正、利率非负、期数为正整数):

export interface MortgageInput {
  principal: number;          // 贷款本金(元)
  annualRatePercent: number;  // 年利率(%),如 3.5
  termMonths: number;         // 期数(月),30 年 = 360
  method: "equal-installment" | "equal-principal";
}

提前还款(缩短年限 vs 减少月供)也复用同一套月利率和还款计划逻辑,只是把"提前还款时点之后"的剩余本金重新按新期数或新月供再算一遍,再和原计划对比省息。核心仍是上面这套 decimal.js 的精确账。

常见问题(FAQ)

Q:房贷月供怎么算?
等额本息月供用公式 M = P·r·(1+r)ⁿ/((1+r)ⁿ−1),P 为本金、r 为月利率(年利率÷12)、n 为期数。等额本金月供 = 本金÷期数 + 当期剩余本金×月利率,逐月递减。

Q:等额本息和等额本金哪个划算?
等额本金支付的总利息更少。以 100 万、3.5%、30 年为例,等额本息总利息约 61.7 万、等额本金约 52.6 万,等额本金少付约 9 万,但前期月供更高、还款压力更大。怎么选取决于现金流,不是单看利息。

Q:为什么算房贷不能用普通的浮点数?
JavaScript 浮点有精度误差(0.1+0.2=0.30000000000000004),房贷有几百期累加,误差会放大导致月供、利息、剩余本金对不上账。正确做法是用 decimal.js 高精度计算,只在输出时舍入到分。

Q:房贷计算里最后一期为什么要单独处理?
因为每期利息舍入到分会累积几分钱漂移。末期直接偿还剩余的全部本金来吸收误差,保证逐期本金之和精确等于贷款本金,账才对得上。

Q:贷款 100 万、利率 3.5%、30 年,月供和利息是多少?
等额本息月供约 4490.45 元,总利息约 61.66 万元;等额本金首月约 5694.45 元、末月约 2785.08 元,总利息约 52.65 万元。利率或年限变化时需重新计算。

小结

房贷计算的工程要点不在公式本身,而在精度:金额全程走 decimal.js、每期利息舍入到分、末期吸收累计舍入误差保证本金还清;逻辑做成强类型纯函数便于复用和对账。等额本息和等额本金的差别是"利息多少 vs 前期压力大小"的取舍。

想按自己的本金、利率、年限直接算(并对比等额本息/等额本金和提前还款省息),可以用在线版:房贷计算器,输出月供、还款总额、总利息和逐期还款计划。本文与工具仅用于计算方法学习与参考,实际贷款利率、还款方式以银行合同为准,不构成任何投资或借贷建议。


参考来源:等额本息/等额本金为通行还款方式,月供公式为标准年金公式;文中示例(100 万 / 3.5% / 30 年)经 Python decimal 按同一舍入口径复核;实现取自笔者上线的房贷计算核心。利率请以中国人民银行 LPR 及银行实际执行为准。


0条评论
0 / 1000
c****6
1文章数
0粉丝数
c****6
1 文章 | 0 粉丝
c****6
1文章数
0粉丝数
c****6
1 文章 | 0 粉丝
原创

房贷月供和利息到底怎么算?用 decimal.js 正确实现等额本息、等额本金与提前还款(含 Typ

2026-07-13 17:04:10
2
0
代码取自一个上线中的房贷计算器核心(纯 TS、零平台依赖),在线DEMO:[房贷计算器](https://jsq3000.com/finance/mortgage)工具可对照结果。

##  金额必须走 Decimal,在边界统一舍入到分

**用 JavaScript 的浮点 `number` 直接算钱一定会出错——`0.1 + 0.2` 在 JS 里等于 `0.30000000000000004`,几百期累加后误差会放大到肉眼可见。** 正确做法是:所有中间计算用高精度的 `decimal.js`,只在输出时统一四舍五入到「分」(2 位小数)。这是房贷、个税、工资这类金融计算的第一条铁律。

封装一个金额工具,把"高精度计算 + 边界舍入"固定下来:

```ts
import Decimal from "decimal.js";
// 中间计算用 40 位精度,输出显式舍入
Decimal.set({ precision: 40, rounding: Decimal.ROUND_HALF_UP });

export function dec(value: Decimal.Value): Decimal {
  return new Decimal(value);
}
// 舍入到分(2 位),ROUND_HALF_UP,返回普通 number
export function round2(value: Decimal.Value): number {
  return new Decimal(value).toDecimalPlaces(2, Decimal.ROUND_HALF_UP).toNumber();
}

下面两种还款方式都建立在这个基础上。

一、等额本息:每月还固定金额

等额本息(equal installment)指还款期内每月偿还相同金额,其中本金逐月递增、利息逐月递减。 月供 M 的公式是:

M = P · r · (1+r)ⁿ / ((1+r)ⁿ − 1)

其中 P 是贷款本金,r 是月利率(年利率 ÷ 100 ÷ 12),n 是总期数(30 年 = 360 期)。用 decimal.js 实现,注意先处理 r=0(无息)的退化情况避免除零:

const r = dec(annualRatePercent).div(100).div(12);
let monthly: Decimal;
if (r.isZero()) {
  monthly = dec(principal).div(n);
} else {
  const factor = r.plus(1).pow(n); // (1+r)^n
  monthly = dec(principal).mul(r).mul(factor).div(factor.minus(1));
}
const monthlyRounded = round2(monthly);

然后逐期生成还款计划:每期利息 = 当期剩余本金 × 月利率(先舍入到分),本金 = 月供 − 利息:

let remaining = dec(principal);
for (let period = 1; period <= n; period++) {
  const interest = dec(round2(remaining.mul(r)));   // 当期利息,舍入到分
  let principalPart: Decimal, payment: Decimal;
  if (period === n) {
    principalPart = remaining;                       // 末期:还清全部剩余本金
    payment = principalPart.plus(interest);
  } else {
    principalPart = dec(monthlyRounded).minus(interest);
    payment = dec(monthlyRounded);
  }
  remaining = remaining.minus(principalPart);
  // push 到 schedule...
}

以**贷款 100 万元、年利率 3.5%、30 年(360 期)**为例,这套实现算出:月供约 4490.45 元,还款总额约 1,616,560.07 元,支付利息总额约 616,560.07 元

二、等额本金:每月还固定本金,月供递减

等额本金(equal principal)指每月偿还相同的本金,利息按剩余本金递减,所以月供逐月减少。 实现更直接:每期本金 = 本金 ÷ 期数(固定),利息 = 剩余本金 × 月利率:

const basePrincipal = round2(dec(principal).div(n));
let remaining = dec(principal);
for (let period = 1; period <= n; period++) {
  const interest = dec(round2(remaining.mul(r)));
  const principalPart = period === n ? remaining : dec(basePrincipal);
  const payment = principalPart.plus(interest);
  remaining = remaining.minus(principalPart);
}

同样 100 万 / 3.5% / 30 年:首月月供约 5694.45 元,末月约 2785.08 元,支付利息总额约 526,457.92 元

对比一下:等额本金比等额本息总共少付利息约 9 万元(616,560 − 526,458 ≈ 90,102 元),代价是前期月供更高、还款压力更大。这就是"提前还得多、总利息少"的直观体现,也是工具要把两种方式并排算给用户看的意义。

三、最关键的工程细节:末期吸收舍入误差

每期利息都舍入到分,几百期累加后必然和理论值产生几分钱的漂移,如果不处理,最后一期会出现"本金没还清"或"多还几分"的 bug。 解决办法是让末期不按公式算本金,而是直接还清当期剩余的全部本金

if (period === n) {
  principalPart = remaining;            // 末期 = 剩余本金,吸收所有舍入漂移
  payment = principalPart.plus(interest);
}

这样无论前面 359 期累计了多少分的舍入误差,最后一期都会精确把账平掉,保证「逐期本金之和 = 贷款本金」严格成立。这是金融计算里一个朴素但必须的处理——对账要对得上,不能差一分钱

四、为什么把这套逻辑做成纯函数

计算逻辑写成零依赖的纯 TypeScript 函数(不碰 DOM、不依赖框架),才能同时被 Web、小程序和单元测试复用,也方便和银行口径逐项核对。 输入用强类型约束,并在入口做校验(本金为正、利率非负、期数为正整数):

export interface MortgageInput {
  principal: number;          // 贷款本金(元)
  annualRatePercent: number;  // 年利率(%),如 3.5
  termMonths: number;         // 期数(月),30 年 = 360
  method: "equal-installment" | "equal-principal";
}

提前还款(缩短年限 vs 减少月供)也复用同一套月利率和还款计划逻辑,只是把"提前还款时点之后"的剩余本金重新按新期数或新月供再算一遍,再和原计划对比省息。核心仍是上面这套 decimal.js 的精确账。

常见问题(FAQ)

Q:房贷月供怎么算?
等额本息月供用公式 M = P·r·(1+r)ⁿ/((1+r)ⁿ−1),P 为本金、r 为月利率(年利率÷12)、n 为期数。等额本金月供 = 本金÷期数 + 当期剩余本金×月利率,逐月递减。

Q:等额本息和等额本金哪个划算?
等额本金支付的总利息更少。以 100 万、3.5%、30 年为例,等额本息总利息约 61.7 万、等额本金约 52.6 万,等额本金少付约 9 万,但前期月供更高、还款压力更大。怎么选取决于现金流,不是单看利息。

Q:为什么算房贷不能用普通的浮点数?
JavaScript 浮点有精度误差(0.1+0.2=0.30000000000000004),房贷有几百期累加,误差会放大导致月供、利息、剩余本金对不上账。正确做法是用 decimal.js 高精度计算,只在输出时舍入到分。

Q:房贷计算里最后一期为什么要单独处理?
因为每期利息舍入到分会累积几分钱漂移。末期直接偿还剩余的全部本金来吸收误差,保证逐期本金之和精确等于贷款本金,账才对得上。

Q:贷款 100 万、利率 3.5%、30 年,月供和利息是多少?
等额本息月供约 4490.45 元,总利息约 61.66 万元;等额本金首月约 5694.45 元、末月约 2785.08 元,总利息约 52.65 万元。利率或年限变化时需重新计算。

小结

房贷计算的工程要点不在公式本身,而在精度:金额全程走 decimal.js、每期利息舍入到分、末期吸收累计舍入误差保证本金还清;逻辑做成强类型纯函数便于复用和对账。等额本息和等额本金的差别是"利息多少 vs 前期压力大小"的取舍。

想按自己的本金、利率、年限直接算(并对比等额本息/等额本金和提前还款省息),可以用在线版:房贷计算器,输出月供、还款总额、总利息和逐期还款计划。本文与工具仅用于计算方法学习与参考,实际贷款利率、还款方式以银行合同为准,不构成任何投资或借贷建议。


参考来源:等额本息/等额本金为通行还款方式,月供公式为标准年金公式;文中示例(100 万 / 3.5% / 30 年)经 Python decimal 按同一舍入口径复核;实现取自笔者上线的房贷计算核心。利率请以中国人民银行 LPR 及银行实际执行为准。


文章来自个人专栏
文章 | 订阅
0条评论
0 / 1000
请输入你的评论
0
0