0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローン計算機を作るとき、単純に「借入額 × 金利」を計算するだけでは、実際の月々の返済額は求められません。

0
Posted at

はじめに

ローン計算機を作るとき、単純に「借入額 × 金利」を計算するだけでは、実際の月々の返済額は求められません。

元利均等返済では、毎月の返済額を一定にしながら、

  • 元金
  • 利息
  • 残高
  • 返済回数

を考慮して計算する必要があります。

この記事では、Vanilla JavaScriptだけでローン返済計算機を作成します。

最終的に、以下を計算できるようにします。

  • 月々の返済額
  • 総返済額
  • 総利息
  • 元金と利息の内訳
  • 毎月の残高
  • 完全なAmortization Schedule(償還予定表)
  • 金利0%のケース
  • 入力値エラー
  • 最終回の端数調整

フレームワークや外部ライブラリは使用しません。


完成イメージ

入力する項目は3つです。

項目 例
借入額 $25,000
年利 7.5%
返済期間 5年

この条件の場合、今回の計算ロジックではおおよそ次の結果になります。

項目 結果
月々の返済額 $500.95
総返済額 $30,056.92
総利息 $5,056.92
返済回数 60回

今回は、この結果をJavaScriptで計算します。


1. 元利均等返済の計算式

元利均等返済の月々の返済額は、次の式で求められます。

$$
M =
P
\times
\frac{r(1+r)^n}
{(1+r)^n-1}
$$

別の書き方では、

$$
M =
P
\times
\frac{r}
{1-(1+r)^{-n}}
$$

です。

変数は次の意味です。

変数 意味
$M$ 月々の返済額
$P$ 借入元金
$r$ 月利
$n$ 返済回数

例えば年利が7.5%なら、

7.5% ÷ 12

なので月利は、

0.625%

JavaScriptの計算では百分率を小数に変換するため、

7.5 / 12 / 100

とします。

つまり、

const monthlyRate = annualRate / 12 / 100;

です。


2. 最小構成のJavaScript

まずは月々の返済額だけを計算する関数を作ります。

function calculateMonthlyPayment(
  principal,
  annualRate,
  months
) {
  const monthlyRate =
    annualRate / 12 / 100;

  if (monthlyRate === 0) {
    return principal / months;
  }

  return (
    principal *
    monthlyRate /
    (
      1 -
      Math.pow(
        1 + monthlyRate,
        -months
      )
    )
  );
}

例えば、

const payment =
  calculateMonthlyPayment(
    25000,
    7.5,
    60
  );

console.log(payment);

結果はおおよそ、

500.94871489058835

となります。

画面表示では、

$500.95

のように丸めます。


3. 金利0%を別処理する理由

ここはローン計算機を作るときに忘れやすいポイントです。

通常の計算式で、

monthlyRate === 0

だった場合、

1 - (1 + 0)^(-n)

は、

1 - 1

つまり、

0

になります。

結果として0除算が発生します。

そのため、

if (monthlyRate === 0) {
  return principal / months;
}

という分岐を入れています。

例えば、

$12,000
0%
12ヶ月

なら、

$12,000 ÷ 12

なので、

$1,000/月

です。


4. HTMLを作る

次に入力画面を作ります。

<div class="loan-calculator">

  <h1>Loan Payment Calculator</h1>

  <p class="description">
    Calculate monthly payment,
    total interest and total repayment.
  </p>

  <div class="form-grid">

    <label>
      Loan Amount
      <input
        id="principal"
        type="number"
        value="25000"
        min="1"
        step="100"
      >
    </label>

    <label>
      Annual Interest Rate (%)
      <input
        id="annualRate"
        type="number"
        value="7.5"
        min="0"
        step="0.01"
      >
    </label>

    <label>
      Loan Term (Years)
      <input
        id="years"
        type="number"
        value="5"
        min="1"
        step="1"
      >
    </label>

  </div>

  <button id="calculateButton">
    Calculate
  </button>

  <div
    id="errorMessage"
    class="error-message"
  ></div>

  <section class="results">

    <div class="result-card">
      <span>Monthly Payment</span>
      <strong id="monthlyPayment">
        —
      </strong>
    </div>

    <div class="result-card">
      <span>Total Interest</span>
      <strong id="totalInterest">
        —
      </strong>
    </div>

    <div class="result-card">
      <span>Total Repayment</span>
      <strong id="totalRepayment">
        —
      </strong>
    </div>

  </section>

  <h2>Amortization Schedule</h2>

  <div class="table-wrapper">

    <table>

      <thead>
        <tr>
          <th>Month</th>
          <th>Payment</th>
          <th>Principal</th>
          <th>Interest</th>
          <th>Balance</th>
        </tr>
      </thead>

      <tbody id="scheduleBody">
      </tbody>

    </table>

  </div>

</div>

5. 最低限のCSS

Qiitaの記事内ではコードが本題なので、UIはシンプルにします。

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  padding: 40px 20px;
  background: #f5f7fb;
  font-family:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    sans-serif;
  color: #172033;
}

.loan-calculator {
  max-width: 1000px;
  margin: 0 auto;
  background: #ffffff;
  padding: 32px;
  border-radius: 20px;
  box-shadow:
    0 12px 40px
    rgba(0, 0, 0, 0.08);
}

h1,
h2 {
  margin-top: 0;
}

.description {
  color: #667085;
}

.form-grid {
  display: grid;
  grid-template-columns:
    repeat(
      auto-fit,
      minmax(200px, 1fr)
    );
  gap: 16px;
  margin: 28px 0;
}

label {
  display: flex;
  flex-direction: column;
  gap: 8px;
  font-weight: 600;
}

input {
  width: 100%;
  padding: 12px 14px;
  border: 1px solid #d0d5dd;
  border-radius: 10px;
  font-size: 16px;
}

button {
  border: 0;
  border-radius: 10px;
  padding: 13px 22px;
  font-size: 16px;
  cursor: pointer;
}

.results {
  display: grid;
  grid-template-columns:
    repeat(
      auto-fit,
      minmax(180px, 1fr)
    );
  gap: 16px;
  margin: 30px 0;
}

.result-card {
  padding: 20px;
  background: #f8fafc;
  border-radius: 14px;
}

.result-card span {
  display: block;
  color: #667085;
  margin-bottom: 8px;
}

.result-card strong {
  font-size: 25px;
}

.error-message {
  color: #b42318;
  margin-top: 12px;
}

.table-wrapper {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
}

th,
td {
  padding: 11px;
  border-bottom:
    1px solid #eaecf0;
  text-align: right;
}

th:first-child,
td:first-child {
  text-align: left;
}

6. 入力値を取得する

JavaScript側ではまずHTMLの要素を取得します。

const principalInput =
  document.getElementById(
    "principal"
  );

const annualRateInput =
  document.getElementById(
    "annualRate"
  );

const yearsInput =
  document.getElementById(
    "years"
  );

const calculateButton =
  document.getElementById(
    "calculateButton"
  );

const monthlyPaymentElement =
  document.getElementById(
    "monthlyPayment"
  );

const totalInterestElement =
  document.getElementById(
    "totalInterest"
  );

const totalRepaymentElement =
  document.getElementById(
    "totalRepayment"
  );

const scheduleBody =
  document.getElementById(
    "scheduleBody"
  );

const errorMessage =
  document.getElementById(
    "errorMessage"
  );

7. 入力値を検証する

金融計算では、不正な入力値をそのまま計算に渡さないようにします。

例えば、

借入額 = -1000

や、

期間 = 0

は無効です。

関数を作ります。

function validateInputs(
  principal,
  annualRate,
  years
) {
  if (
    !Number.isFinite(principal) ||
    !Number.isFinite(annualRate) ||
    !Number.isFinite(years)
  ) {
    return "Please enter valid numbers.";
  }

  if (principal <= 0) {
    return (
      "Loan amount must be greater than 0."
    );
  }

  if (annualRate < 0) {
    return (
      "Interest rate cannot be negative."
    );
  }

  if (years <= 0) {
    return (
      "Loan term must be greater than 0."
    );
  }

  return null;
}

8. 通貨表示を整える

JavaScriptのIntl.NumberFormatを使えば、

500.94871489

を、

$500.95

のように表示できます。

const currencyFormatter =
  new Intl.NumberFormat(
    "en-US",
    {
      style: "currency",
      currency: "USD"
    }
  );

function formatCurrency(value) {
  return currencyFormatter.format(value);
}

今回は米国向けのローン計算を想定しているためUSDにしています。

日本円なら、

new Intl.NumberFormat(
  "ja-JP",
  {
    style: "currency",
    currency: "JPY"
  }
);

に変更できます。


9. 償還予定表を作る

ここからが今回の中心部分です。

毎月、

残高 × 月利

で利息を計算します。

そして、

月々の返済額 - 利息

が、その月に返済される元金です。

つまり、

interest =
  balance * monthlyRate;

principalPaid =
  payment - interest;

です。

元金を残高から引きます。

balance -= principalPaid;

これを完済するまで繰り返します。


10. Amortization Schedule関数

function buildAmortizationSchedule(
  principal,
  annualRate,
  months,
  monthlyPayment
) {
  const monthlyRate =
    annualRate / 12 / 100;

  let balance = principal;

  const schedule = [];

  for (
    let month = 1;
    month <= months;
    month++
  ) {

    const interest =
      monthlyRate === 0
        ? 0
        : balance * monthlyRate;

    let principalPaid =
      monthlyPayment - interest;

    let actualPayment =
      monthlyPayment;

    /*
     * 最終回は丸め誤差や端数により
     * 元金返済額が残高を超える可能性がある。
     */
    if (
      principalPaid > balance ||
      month === months
    ) {
      principalPaid = balance;

      actualPayment =
        principalPaid +
        interest;
    }

    balance -= principalPaid;

    /*
     * 浮動小数点誤差で
     * -0.0000001 のようになるのを防ぐ。
     */
    if (
      Math.abs(balance) < 1e-8
    ) {
      balance = 0;
    }

    schedule.push({
      month,
      payment:
        actualPayment,
      principal:
        principalPaid,
      interest,
      balance
    });

    if (balance <= 0) {
      break;
    }
  }

  return schedule;
}

11. なぜ最終回を調整するのか

JavaScriptのNumberは浮動小数点数です。

例えば数学的には、

0.1 + 0.2 = 0.3

ですが、JavaScriptでは、

console.log(
  0.1 + 0.2
);

結果が、

0.30000000000000004

になることがあります。

ローン計算でも同様に、

残高 = -0.00000003

のような小さな誤差が残る場合があります。

そのため、

if (
  Math.abs(balance) < 1e-8
) {
  balance = 0;
}

として処理しています。

また、最後の支払いでは、

principalPaid = balance;

として、残っている元金を正確に完済します。


12. 表をHTMLに表示する

function renderSchedule(schedule) {
  scheduleBody.innerHTML = "";

  for (const row of schedule) {

    const tr =
      document.createElement("tr");

    tr.innerHTML = `
      <td>${row.month}</td>

      <td>
        ${formatCurrency(
          row.payment
        )}
      </td>

      <td>
        ${formatCurrency(
          row.principal
        )}
      </td>

      <td>
        ${formatCurrency(
          row.interest
        )}
      </td>

      <td>
        ${formatCurrency(
          row.balance
        )}
      </td>
    `;

    scheduleBody.appendChild(tr);
  }
}

これで、

Month
Payment
Principal
Interest
Balance

を毎月表示できます。


13. 総利息を計算する

償還予定表があるので、各月の利息を合計できます。

function getTotalInterest(
  schedule
) {
  return schedule.reduce(
    (sum, row) =>
      sum + row.interest,
    0
  );
}

総返済額も同じです。

function getTotalRepayment(
  schedule
) {
  return schedule.reduce(
    (sum, row) =>
      sum + row.payment,
    0
  );
}

単純に、

monthlyPayment * months

とする方法もあります。

ただし最終回を端数調整しているため、今回は実際のScheduleを合計する方法を使います。


14. すべてをまとめる

function calculateLoan() {

  errorMessage.textContent = "";

  const principal =
    Number(
      principalInput.value
    );

  const annualRate =
    Number(
      annualRateInput.value
    );

  const years =
    Number(
      yearsInput.value
    );

  const error =
    validateInputs(
      principal,
      annualRate,
      years
    );

  if (error) {
    errorMessage.textContent =
      error;

    return;
  }

  const months =
    Math.round(
      years * 12
    );

  const monthlyPayment =
    calculateMonthlyPayment(
      principal,
      annualRate,
      months
    );

  const schedule =
    buildAmortizationSchedule(
      principal,
      annualRate,
      months,
      monthlyPayment
    );

  const totalInterest =
    getTotalInterest(
      schedule
    );

  const totalRepayment =
    getTotalRepayment(
      schedule
    );

  monthlyPaymentElement.textContent =
    formatCurrency(
      monthlyPayment
    );

  totalInterestElement.textContent =
    formatCurrency(
      totalInterest
    );

  totalRepaymentElement.textContent =
    formatCurrency(
      totalRepayment
    );

  renderSchedule(schedule);
}

ボタンにイベントを登録します。

calculateButton.addEventListener(
  "click",
  calculateLoan
);

初期表示でも計算したいので、

calculateLoan();

を最後に実行します。


15. 完成版JavaScript

ここまでをまとめると次のようになります。

const principalInput =
  document.getElementById("principal");

const annualRateInput =
  document.getElementById("annualRate");

const yearsInput =
  document.getElementById("years");

const calculateButton =
  document.getElementById(
    "calculateButton"
  );

const monthlyPaymentElement =
  document.getElementById(
    "monthlyPayment"
  );

const totalInterestElement =
  document.getElementById(
    "totalInterest"
  );

const totalRepaymentElement =
  document.getElementById(
    "totalRepayment"
  );

const scheduleBody =
  document.getElementById(
    "scheduleBody"
  );

const errorMessage =
  document.getElementById(
    "errorMessage"
  );


const currencyFormatter =
  new Intl.NumberFormat(
    "en-US",
    {
      style: "currency",
      currency: "USD"
    }
  );


function formatCurrency(value) {
  return currencyFormatter.format(
    value
  );
}


function validateInputs(
  principal,
  annualRate,
  years
) {

  if (
    !Number.isFinite(principal) ||
    !Number.isFinite(annualRate) ||
    !Number.isFinite(years)
  ) {
    return "Please enter valid numbers.";
  }

  if (principal <= 0) {
    return (
      "Loan amount must be greater than 0."
    );
  }

  if (annualRate < 0) {
    return (
      "Interest rate cannot be negative."
    );
  }

  if (years <= 0) {
    return (
      "Loan term must be greater than 0."
    );
  }

  return null;
}


function calculateMonthlyPayment(
  principal,
  annualRate,
  months
) {

  const monthlyRate =
    annualRate / 12 / 100;

  if (monthlyRate === 0) {
    return principal / months;
  }

  return (
    principal *
    monthlyRate /
    (
      1 -
      Math.pow(
        1 + monthlyRate,
        -months
      )
    )
  );
}


function buildAmortizationSchedule(
  principal,
  annualRate,
  months,
  monthlyPayment
) {

  const monthlyRate =
    annualRate / 12 / 100;

  let balance = principal;

  const schedule = [];

  for (
    let month = 1;
    month <= months;
    month++
  ) {

    const interest =
      monthlyRate === 0
        ? 0
        : balance *
          monthlyRate;

    let principalPaid =
      monthlyPayment -
      interest;

    let actualPayment =
      monthlyPayment;

    if (
      principalPaid > balance ||
      month === months
    ) {

      principalPaid =
        balance;

      actualPayment =
        principalPaid +
        interest;
    }

    balance -=
      principalPaid;

    if (
      Math.abs(balance) < 1e-8
    ) {
      balance = 0;
    }

    schedule.push({
      month,
      payment:
        actualPayment,
      principal:
        principalPaid,
      interest,
      balance
    });

    if (balance <= 0) {
      break;
    }
  }

  return schedule;
}


function getTotalInterest(
  schedule
) {

  return schedule.reduce(
    (sum, row) =>
      sum + row.interest,
    0
  );
}


function getTotalRepayment(
  schedule
) {

  return schedule.reduce(
    (sum, row) =>
      sum + row.payment,
    0
  );
}


function renderSchedule(
  schedule
) {

  scheduleBody.innerHTML =
    "";

  for (const row of schedule) {

    const tr =
      document.createElement(
        "tr"
      );

    tr.innerHTML = `
      <td>
        ${row.month}
      </td>

      <td>
        ${formatCurrency(
          row.payment
        )}
      </td>

      <td>
        ${formatCurrency(
          row.principal
        )}
      </td>

      <td>
        ${formatCurrency(
          row.interest
        )}
      </td>

      <td>
        ${formatCurrency(
          row.balance
        )}
      </td>
    `;

    scheduleBody.appendChild(
      tr
    );
  }
}


function calculateLoan() {

  errorMessage.textContent =
    "";

  const principal =
    Number(
      principalInput.value
    );

  const annualRate =
    Number(
      annualRateInput.value
    );

  const years =
    Number(
      yearsInput.value
    );

  const error =
    validateInputs(
      principal,
      annualRate,
      years
    );

  if (error) {

    errorMessage.textContent =
      error;

    return;
  }

  const months =
    Math.round(
      years * 12
    );

  const monthlyPayment =
    calculateMonthlyPayment(
      principal,
      annualRate,
      months
    );

  const schedule =
    buildAmortizationSchedule(
      principal,
      annualRate,
      months,
      monthlyPayment
    );

  const totalInterest =
    getTotalInterest(
      schedule
    );

  const totalRepayment =
    getTotalRepayment(
      schedule
    );

  monthlyPaymentElement
    .textContent =
    formatCurrency(
      monthlyPayment
    );

  totalInterestElement
    .textContent =
    formatCurrency(
      totalInterest
    );

  totalRepaymentElement
    .textContent =
    formatCurrency(
      totalRepayment
    );

  renderSchedule(
    schedule
  );
}


calculateButton.addEventListener(
  "click",
  calculateLoan
);


calculateLoan();

16. $25,000 / 7.5% / 5年で確認する

入力値を、

Loan Amount:
25000

Annual Rate:
7.5

Term:
5 years

とします。

返済回数は、

5 * 12

なので、

60回

です。

今回の計算では、おおよそ、

Monthly Payment:
$500.95

Total Interest:
$5,056.92

Total Repayment:
$30,056.92

となります。


17. 最初の月の中身を見る

借入残高は、

$25,000

月利は、

7.5% ÷ 12
= 0.625%

なので、最初の月の利息は、

$25,000 × 0.00625

つまり、

$156.25

です。

月々の返済額がおよそ、

$500.95

なので、元金部分は、

$500.95 - $156.25

およそ、

$344.70

になります。

その結果、次月の残高はおよそ、

$25,000 - $344.70

つまり、

$24,655.30

です。

翌月はこの残高に対して利息が計算されます。

そのため返済が進むにつれて、

利息部分は徐々に小さくなり、元金返済部分が徐々に大きくなる

という動きになります。


18. なぜ月々の返済額だけでは不十分なのか

ローン計算ツールを作っていると、

Monthly Payment

だけを表示したくなります。

しかし、利用者の意思決定を考えると、

Monthly Payment

だけでは情報が足りません。

例えば長い返済期間を選択すると、月々の返済額は下がる可能性があります。

一方で借入期間が長くなるため、総利息が増える可能性があります。

そのため計算機では少なくとも、

Monthly Payment
Total Interest
Total Repayment
Loan Term

を一緒に表示したほうが分かりやすいと考えています。

金利だけではなく、手数料や返済条件も含めてローンを比較するときの考え方については、EasyLoanWorldの以下の記事も参考になります。

How to Compare Small Loan Offers Beyond the Interest Rate - EasyLoanWorld

今回のJavaScript計算機は入力した年利を使った元利均等返済シミュレーションであり、Origination Feeなどを含めた正式なAPRを計算するものではありません。


19. APRと単純な年利を混同しない

これは金融計算ツールを作るときに重要です。

今回入力している、

Annual Interest Rate

は、

annualRate / 12 / 100

として月利に変換しています。

しかし実際の金融商品で表示されるAPRには、商品によって一定の手数料などが含まれる場合があります。

したがって、このツールの入力欄を安易に、

APR

と名前変更するのは適切ではありません。

正式にAPRを扱うなら、

  • 手数料
  • 実際に受け取る金額
  • 支払いスケジュール
  • 支払回数
  • キャッシュフロー

などを考慮した別の計算ロジックが必要になります。

今回はあくまで、

元金、年利、期間から元利均等返済を計算する

ことに限定しています。


20. テストケースを用意する

金融計算では、画面上で1回動いただけでは不十分です。

最低限いくつかのケースを確認しておきます。

通常ケース

console.log(
  calculateMonthlyPayment(
    25000,
    7.5,
    60
  )
);

期待値:

約500.95

金利0%

console.log(
  calculateMonthlyPayment(
    12000,
    0,
    12
  )
);

期待値:

1000

1年間

console.log(
  calculateMonthlyPayment(
    10000,
    5,
    12
  )
);

正常な正数が返ることを確認します。


21. Node.jsで簡単なテストを書く

計算部分をUIから分離しておくと、テストしやすくなります。

例えば、

import assert from "node:assert";
import test from "node:test";

function calculateMonthlyPayment(
  principal,
  annualRate,
  months
) {

  const r =
    annualRate / 12 / 100;

  if (r === 0) {
    return principal / months;
  }

  return (
    principal *
    r /
    (
      1 -
      Math.pow(
        1 + r,
        -months
      )
    )
  );
}


test(
  "0% interest",
  () => {

    const payment =
      calculateMonthlyPayment(
        12000,
        0,
        12
      );

    assert.equal(
      payment,
      1000
    );
  }
);


test(
  "25000 at 7.5% for 60 months",
  () => {

    const payment =
      calculateMonthlyPayment(
        25000,
        7.5,
        60
      );

    assert.ok(
      Math.abs(
        payment -
        500.94871489058835
      ) < 0.000001
    );
  }
);

実行します。

node --test

計算処理とDOM処理を分離すると、後からテストケースを増やしやすくなります。


22. 関数をUIから分離する

実際にプロジェクトとして育てるなら、

loanCalculator.js

に、

calculateMonthlyPayment()
buildAmortizationSchedule()
getTotalInterest()
getTotalRepayment()

をまとめます。

そして、

app.js

では、

DOM取得
イベント処理
画面描画

だけを担当させます。

例えば、

src/
├── loanCalculator.js
├── app.js
└── formatters.js

のように分けると管理しやすくなります。


23. 将来的に追加できる機能

この計算機を発展させるなら、次のような機能を追加できます。

Extra Payment

毎月追加返済した場合に、完済まで何ヶ月短縮できるか計算する。

One-Time Extra Payment

特定の月だけ追加返済する。

Different Loan Terms

3年、5年、7年などを比較する。

Chart.js

元金と利息の割合をグラフで表示する。

CSV Export

償還予定表をCSVで出力する。

PDF Export

返済スケジュールをPDFにする。

Multiple Currencies

USDだけでなくJPY、GBP、EURなどに対応する。

URL Parameters

?amount=25000&rate=7.5&years=5

のように条件をURLで共有できるようにする。


24. Extra Paymentを実装する場合

例えば毎月、

$100

追加返済するとします。

考え方は単純で、

const principalPaid =
  monthlyPayment +
  extraPayment -
  interest;

とします。

ただし、追加返済によって予定より早く残高が0になるため、

for (
  let month = 1;
  balance > 0;
  month++
)

のようなループのほうが扱いやすくなります。

例えば、

function buildScheduleWithExtraPayment(
  principal,
  annualRate,
  monthlyPayment,
  extraPayment
) {

  const monthlyRate =
    annualRate / 12 / 100;

  let balance =
    principal;

  let month =
    1;

  const schedule =
    [];

  while (
    balance > 0 &&
    month <= 1200
  ) {

    const interest =
      balance *
      monthlyRate;

    let principalPaid =
      monthlyPayment +
      extraPayment -
      interest;

    if (
      principalPaid >
      balance
    ) {
      principalPaid =
        balance;
    }

    const actualPayment =
      principalPaid +
      interest;

    balance -=
      principalPaid;

    schedule.push({
      month,
      payment:
        actualPayment,
      principal:
        principalPaid,
      interest,
      balance:
        Math.max(
          balance,
          0
        )
    });

    month++;
  }

  return schedule;
}

month <= 1200を入れているのは、予期しない入力やバグによる無限ループを防ぐためです。


25. 金額計算でNumberを使うときの注意

今回のサンプルでは分かりやすさを優先してJavaScriptのNumberを使いました。

ただし、本番の金融システムでは浮動小数点誤差に注意する必要があります。

例えばセント単位に変換して整数で保持する方法があります。

const amountInCents =
  Math.round(
    amount * 100
  );

例えば、

$25.99

なら、

2599 cents

として扱います。

また、本格的な金融システムではdecimal型を扱えるライブラリや、金融機関固有の丸めルールも検討する必要があります。

この点は、

ブラウザ上の教育用シミュレーター

と、

実際の金融取引処理

を分けて考える必要があります。


26. UIと金融計算ロジックを分離するメリット

今回の記事で一番重要だと感じたのは、計算式そのものよりも、

金融計算ロジックをUIから独立させること

です。

例えば、

calculateMonthlyPayment()

はDOMを一切操作していません。

そのため同じ関数を、

  • Webサイト
  • React
  • Vue
  • Node.js
  • REST API
  • npm package
  • テスト
  • Mobile Web App

などから利用できます。

金融ツールを複数作る場合、この構造にしておくと再利用しやすくなります。


27. 実際のローン商品とは結果が異なる場合がある

今回のコードは教育・シミュレーション用途です。

現実のローンでは、

  • 手数料
  • 日割り利息
  • 実際の日数
  • 支払日
  • 初回支払日
  • 休日
  • 丸め規則
  • 変動金利
  • 保険料
  • 税金
  • 繰上返済条件

などによって結果が変わる可能性があります。

そのため、実際の金融契約では貸し手から提示された正式な返済予定表や契約条件を確認する必要があります。


まとめ

今回はVanilla JavaScriptを使って、元利均等返済方式のローン計算機を作りました。

基本となる月々の返済額は、

$$
M =
P
\times
\frac{r}
{1-(1+r)^{-n}}
$$

で計算できます。

ただし実装では公式だけではなく、

0%金利
入力値検証
浮動小数点誤差
最終回の端数調整
Amortization Schedule
総利息
総返済額

なども考える必要があります。

特にローン計算機の場合、

月々の返済額だけを表示するより、総利息や総返済額まで見せたほうが利用者にとって理解しやすい

と感じました。

今後はこの実装をベースに、

追加返済
複数ローン比較
グラフ
CSV Export
API化

なども実装していきたいと思います。


参考

ローン返済額だけでなく、金利以外の手数料や返済条件を比較するときの考え方:

How to Compare Small Loan Offers Beyond the Interest Rate - EasyLoanWorld

※ 本記事のコードは学習・シミュレーション目的です。実際の金融商品の返済額やAPR、手数料、契約条件を保証するものではありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?