CH.02 · 財金程式設計互動教材

HTML、CSS 與 JavaScript 金融互動設計

把第一章單一的複利工具,擴充成能計算終值、現值、所需報酬率、所需投資年數的多功能時間價值計算器——四個數值只要知道三個,就能反推第四個。

PV·FV·r·n 互推

本章學習目標

  • 區分現值、終值、報酬率與投資期間四個核心概念
  • 用 HTML 下拉選單與 CSS Grid 建立多欄位表單
  • 用 JavaScript 函數分工拆解四種金融計算
  • 驗證輸入並妥善處理 Infinity、負數等邊界情況

01貨幣時間價值的基本觀念

第一章完成了基本的複利終值計算器,具備「輸入→運算→輸出」結構。但實際金融決策往往不只需要計算終值——五年後想擁有 100 萬元,現在該投入多少錢?現在投入 50 萬元,十年後想成長到 100 萬元,需要多少報酬率?這些問題都與「貨幣時間價值」有關:相同金額在不同時間點並不具有相同價值,因為現在取得的資金可以投資並產生報酬。

今天 100 萬元 → 五年後 ≈ 127.6 萬元(FV = 1,000,000 × 1.05⁵)

反過來,五年後收到 100 萬元,以 5% 折現率換算為今天的價值:五年後 100 萬元 → 今天 ≈ 78.4 萬元(PV = 1,000,000 ÷ 1.05⁵)

i

同一筆錢,領取時間不同,價值就不同。金融決策必須在相同時間點比較金額,才能公平評估。

四種貨幣時間價值計算,一表看清楚——記憶口訣:四個數值(PV、FV、r、n)中,已知三個就能求第四個,這是本章工具的核心邏輯。

計算模式已知資料(輸入)求解(輸出)使用公式
計算終值(FV)
現在投入的錢,未來能累積多少?
現值 PV、報酬率 r、年數 nFVFV = PV × (1+r)ⁿ
計算現值(PV)
未來需要的錢,現在要準備多少?
終值 FV、折現率 r、年數 nPVPV = FV ÷ (1+r)ⁿ
計算所需報酬率(r)
要達到目標金額,需要多少報酬率?
現值 PV、終值 FV、年數 nrr = (FV÷PV)^(1÷n) − 1
計算所需年數(n)
要達到目標金額,需要等幾年?
現值 PV、終值 FV、報酬率 rnn = ln(FV÷PV) ÷ ln(1+r)

各公式詳解

所需報酬率:由現值、終值與年數,反推達成目標所需的年複合報酬率。例如 50 萬元希望 10 年後成長為 100 萬元:

r = (FV ÷ PV)^(1÷n) − 1 = (100÷50)^(1÷10) − 1 ≈ 7.18%

所需投資年數:由現值、終值與報酬率,反推需要等待多少年。例如 30 萬元以 6% 成長至 60 萬元:

n = ln(FV ÷ PV) ÷ ln(1 + r) = ln(2) ÷ ln(1.06) ≈ 11.90 年

折現率與報酬率的差異——數學公式相同,但金融意義不同:

名稱使用場景意義
報酬率(r)終值計算(資金成長)資金未來可能成長的速度
折現率(r)現值計算(資金折回)將未來金額折回今天的必要報酬率或資金成本

02金融互動網站的基本元件

初學者快速操作步驟

  1. 選擇計算模式——從下拉選單選:計算終值/現值/所需報酬率/所需年數
  2. 看欄位提示——灰色欄位=系統自動計算,不需填;白色欄位=需要填入資料
  3. 輸入數值——報酬率輸入 5(代表 5%),金額輸入原始數字,年數輸入正整數
  4. 點擊開始計算——或直接按 Enter 鍵觸發計算
  5. 閱讀結果與解說——系統顯示計算結果、使用公式、情境解說及風險提醒

HTML 輸入元件種類

HTML 元件程式寫法適合用途
input type="number"<input type="number">數值輸入(本金、利率、年數)
input type="date"<input type="date">日期輸入
input type="range"<input type="range">滑桿(視覺化調整數值)
input type="checkbox"<input type="checkbox">多項選擇(例如勾選計息頻率)
select + option<select><option>…</select>下拉選單(切換計算模式)

下拉選單 select 的 value 與顯示文字

<select id="calculationType">
  <option value="futureValue">計算終值</option>   ← 使用者看到「計算終值」
  <option value="presentValue">計算現值</option>  ← JavaScript 取到 "presentValue"
  <option value="requiredRate">計算所需報酬率</option>
  <option value="requiredYears">計算所需投資年數</option>
</select>
i

每個 option 有兩層:使用者看到的顯示文字,以及 JavaScript 讀取的 value。程式根據 value 決定執行哪個公式。

停用欄位 disabled 的三個作用

結果區的五項要素

03系統功能規劃

欄位停用規則一覽

計算模式現值(PV)終值(FV)年報酬率(r)投資年數(n)
計算終值需輸入 ✓自動計算 —需輸入 ✓需輸入 ✓
計算現值自動計算 —需輸入 ✓需輸入 ✓需輸入 ✓
計算所需報酬率需輸入 ✓需輸入 ✓自動計算 —需輸入 ✓
計算所需年數需輸入 ✓需輸入 ✓需輸入 ✓自動計算 —
i

灰色「自動計算 —」欄位會由 JavaScript 設定為 disabled,使用者無法輸入,系統計算後也不會顯示在此欄位,而是顯示在下方結果區。

完整互動流程圖(六步驟)

① 選擇模式下拉選單選擇計算模式
② 切換表單updateFormMode() 停用欄位、更新提示
③ 輸入數值填入欄位,按計算或 Enter
④ 輸入驗證空白/負數/超出範圍檢查
⑤ 執行公式依模式分派到對應計算函數
⑥ 顯示結果結果、公式、解說、風險提醒

04建立第二章專案檔案

延續第一章的專案,在原有資料夾中新增以下兩個檔案:

financial-platform/
├─ index.html          ← 第一章(複利計算器,保留不動)
├─ time-value.html     ← 第二章(新建:多功能時間價值工具)
├─ css/
│  ├─ style.css        ← 第一章 CSS(保留)
│  └─ time-value.css   ← 第二章 CSS(新建)
├─ js/
│  ├─ calculator.js    ← 第一章 JS(保留)
│  └─ time-value.js    ← 第二章 JS(新建)
├─ data/
├─ images/
└─ README.md

檔案命名原則

建議做法範例(✓ 好)避免(✗ 不建議)
英文小寫time-value.html時間價值.html
連字號分隔單字bond-calculator.htmlbondcalculator.html
名稱能辨識內容stock-analysis.htmltest123.html
避免版本後綴risk-tool.html計算器最後版2.html

05HTML 互動表單實作

time-value.html 的完整結構如下。本頁面新增了導覽列、下拉選單、欄位提示區及風險警告區。

程式區塊 1time-value.html(完整版)
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="計算終值、現值、所需報酬率與投資年數的貨幣時間價值工具。">
  <title>多功能時間價值計算器</title>
  <link rel="stylesheet" href="css/time-value.css">
</head>
<body>

<!-- ① 頁首導覽列 -->
<header class="site-header">
  <div class="header-container">
    <a href="index.html" class="site-title">金融程式設計實作</a>
    <nav class="main-nav">
      <a href="index.html">複利工具</a>
      <a href="time-value.html" aria-current="page">時間價值</a>
    </nav>
  </div>
</header>

<!-- ② 主要內容 -->
<main class="page-container">

  <!-- ③ 工具說明區 -->
  <section class="tool-introduction">
    <p class="category-label">金融計算工具</p>
    <h1>多功能時間價值計算器</h1>
    <p>選擇計算模式,輸入相關資料,即可計算終值、現值、所需報酬率或投資年數。</p>
  </section>

  <!-- ④ 計算器主卡片 -->
  <section class="calculator-card">

    <!-- 下拉選單:選擇計算模式 -->
    <div class="form-group full-width">
      <label for="calculationType">計算模式</label>
      <select id="calculationType">
        <option value="futureValue">計算終值(FV)</option>
        <option value="presentValue">計算現值(PV)</option>
        <option value="requiredRate">計算所需報酬率(r)</option>
        <option value="requiredYears">計算所需投資年數(n)</option>
      </select>
    </div>

    <!-- 2×2 Grid 輸入欄位 -->
    <div class="form-grid">
      <div class="form-group">
        <label for="presentValue">現值/目前本金(元)</label>
        <input type="number" id="presentValue" min="0" step="1000" placeholder="例如:500000">
      </div>
      <div class="form-group">
        <label for="futureValue">終值/目標金額(元)</label>
        <input type="number" id="futureValue" min="0" step="1000" placeholder="例如:1000000">
      </div>
      <div class="form-group">
        <label for="annualRate">年報酬率或折現率(%)</label>
        <input type="number" id="annualRate" step="0.01" placeholder="例如:5">
      </div>
      <div class="form-group">
        <label for="years">投資年數</label>
        <input type="number" id="years" min="0" step="0.1" placeholder="例如:10">
      </div>
    </div>

    <div id="fieldHint" class="field-hint"></div>

    <div class="button-group">
      <button type="button" id="calculateButton" class="primary-button">開始計算</button>
      <button type="button" id="clearButton" class="secondary-button">清除資料</button>
    </div>

    <div id="message" class="message" aria-live="polite"></div>

    <section id="resultBox" class="result-box hidden" aria-live="polite">
      <p class="result-label">計算結果</p>
      <p id="mainResult" class="main-result"></p>
      <p id="formulaText"></p>
      <p id="resultExplanation"></p>
      <div id="riskNotice" class="risk-notice hidden"></div>
    </section>

  </section>

  <!-- ⑤ 使用提醒卡片 -->
  <section class="knowledge-card">
    <h2>使用提醒</h2>
    <ul>
      <li>報酬率 5% 應輸入 5,系統會自動轉換為 0.05。</li>
      <li>計算結果採固定年報酬率或折現率假設。</li>
      <li>實際投資仍可能受到市場波動、稅負及費用影響。</li>
    </ul>
  </section>

</main>

<footer class="site-footer">本工具僅供金融教育及情境試算使用,不構成投資建議。</footer>

<script src="js/time-value.js"></script>
</body>
</html>

HTML 重點說明

標籤 / 屬性用途說明初學者注意事項
aria-current="page"標示目前頁面的導覽連結螢幕閱讀器會告知使用者所在頁面
aria-live="polite"動態更新區塊的無障礙標記使用 JS 更新內容時,螢幕閱讀器才能讀出
select value="..."下拉選單每個選項的程式識別碼JS 讀的是 value,不是顯示文字
disabled 屬性停用輸入欄位JS 動態設定,使用者無法修改灰色欄位
class="hidden"預設隱藏元素計算完成後,JS 移除 hidden 才顯示結果
meta description頁面摘要,SEO 與搜尋結果顯示用不影響程式功能,但有助搜尋引擎理解頁面

06CSS 表單與響應式設計

程式區塊 2css/time-value.css(完整版)
/* ── 全域重設 ──────────────────────────── */
* { box-sizing: border-box; }

/* ── CSS 變數(色彩與主題設定) ─────────── */
:root {
  --primary-color: #2457a6;      /* 深藍:主按鈕、標題 */
  --primary-dark: #183f7a;       /* 深藍加深:按鈕 hover */
  --background-color: #f4f6f8;   /* 淺灰:頁面背景 */
  --card-color: #ffffff;         /* 白:卡片背景 */
  --text-color: #1f2933;         /* 深灰:內文 */
  --secondary-text: #52606d;     /* 中灰:說明文字 */
  --border-color: #bcccdc;       /* 淺藍灰:邊框 */
  --result-background: #edf5ff;  /* 淺藍:結果區背景 */
  --error-color: #b42318;        /* 紅:錯誤訊息 */
  --warning-background: #fff7e6; /* 淺黃:警告背景 */
  --warning-border: #e7a33e;     /* 橘:警告左邊框 */
}

body {
  margin: 0;
  font-family: "Microsoft JhengHei", Arial, sans-serif;
  background: var(--background-color);
  color: var(--text-color);
  line-height: 1.7;
}

/* ── 頁首導覽列 ─────────────────────────── */
.site-header { background: var(--card-color); border-bottom: 1px solid #dde3e8; }
.header-container {
  width: min(1100px, 92%); margin: 0 auto; min-height: 68px;
  display: flex; align-items: center; justify-content: space-between; gap: 24px;
}
.site-title { color: var(--text-color); text-decoration: none; font-size: 20px; font-weight: 700; }
.main-nav { display: flex; gap: 18px; }
.main-nav a { color: var(--secondary-text); text-decoration: none; font-weight: 700; }
.main-nav a:hover,
.main-nav a[aria-current='page'] { color: var(--primary-color); }

/* ── 主要內容區 ─────────────────────────── */
.page-container { width: min(900px, 92%); margin: 0 auto; padding: 48px 0; }

/* ── 卡片(計算器與知識卡) ─────────────── */
.calculator-card, .knowledge-card {
  background: var(--card-color); border-radius: 16px; padding: 32px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.07);
}
.knowledge-card { margin-top: 24px; }

/* ── 2 欄 Grid 輸入表單 ─────────────────── */
.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr)); /* 桌面版:2欄 */
  gap: 20px;
}
.form-group { margin-bottom: 20px; }
.full-width { width: 100%; }
label { display: block; margin-bottom: 8px; font-weight: 700; }

/* ── 輸入欄位與下拉選單 ─────────────────── */
input, select {
  width: 100%; padding: 13px 14px;
  border: 1px solid var(--border-color);
  border-radius: 8px; background: #ffffff; font-size: 16px;
}
input:focus, select:focus {
  outline: 2px solid #7aa7eb;
  border-color: var(--primary-color);
}
input:disabled {
  background: #e9eef3; /* 灰色:告知使用者此欄位不需填 */
  color: #7b8794;
  cursor: not-allowed;
}

/* ── 欄位提示 ───────────────────────────── */
.field-hint { min-height: 28px; margin-bottom: 16px; color: var(--secondary-text); font-size: 15px; }

/* ── 按鈕組 ─────────────────────────────── */
.button-group { display: flex; gap: 12px; }
button { min-height: 48px; padding: 12px 20px; border-radius: 8px; font-size: 16px; font-weight: 700; cursor: pointer; }
.primary-button { flex: 1; border: none; background: var(--primary-color); color: #ffffff; }
.primary-button:hover { background: var(--primary-dark); }
.secondary-button { border: 1px solid var(--border-color); background: #ffffff; color: var(--text-color); }
.secondary-button:hover { background: #eef2f5; }

/* ── 錯誤訊息 ───────────────────────────── */
.message { min-height: 28px; margin-top: 18px; font-weight: 700; }
.error-message { color: var(--error-color); }

/* ── 計算結果區 ─────────────────────────── */
.result-box { margin-top: 8px; padding: 24px; border-radius: 12px; background: var(--result-background); }
.result-label { margin: 0; color: var(--secondary-text); font-weight: 700; }
.main-result { margin: 6px 0 14px; color: var(--primary-dark); font-size: 30px; font-weight: 700; }

/* ── 風險警告區 ─────────────────────────── */
.risk-notice { margin-top: 16px; padding: 14px; border-left: 4px solid var(--warning-border); background: var(--warning-background); }

/* ── 隱藏工具類別 ───────────────────────── */
.hidden { display: none; }

/* ── 頁尾 ───────────────────────────────── */
.site-footer { padding: 28px; text-align: center; color: var(--secondary-text); font-size: 14px; }

/* ── 手機版(寬度 ≤ 700px)─────────────── */
@media (max-width: 700px) {
  .header-container { flex-direction: column; align-items: flex-start; padding: 16px 0; gap: 8px; }
  .form-grid { grid-template-columns: 1fr; gap: 0; } /* 手機版:1欄 */
  .calculator-card, .knowledge-card { padding: 22px; }
  .button-group { flex-direction: column; } /* 按鈕上下排列 */
  .main-result { font-size: 25px; }
}

CSS 重點說明

CSS 概念說明金融工具應用
CSS 變數 :root定義全域色彩,一處修改全站生效統一主色 #2457a6,換主題只改一行
display: grid二維版面(欄+列)輸入表單分 2 欄,手機版自動改 1 欄
display: flex一維版面(水平或垂直)按鈕並排;手機版改 flex-direction: column
input:disabled停用欄位的視覺樣式灰色背景告知使用者此欄不需填
min(900px, 92%)限制最大寬度同時保留側邊距大螢幕不會撐太寬,小螢幕保留 4% 邊距
@media (max-width:700px)RWD 響應式斷點手機版自動改為單欄、按鈕縮排

07JavaScript 金融函數設計

函數分工原則:一個函數只做一件事——好處是每個公式可以單獨測試;發生錯誤時較容易定位;未來可在其他頁面重複使用。

函數名稱職責輸入參數回傳值
calculateFutureValue()計算終值PV、annualRate、yearsFV(終值)
calculatePresentValue()計算現值FV、annualRate、yearsPV(現值)
calculateRequiredRate()計算所需報酬率PV、FV、yearsr(小數,需×100轉%)
calculateRequiredYears()計算所需年數PV、FV、annualRaten(年數,含小數)
formatCurrency()格式化新臺幣amount(數字)字串(例如 NT$162,889)
formatPercent()格式化百分比rate(小數,如 0.0718)字串(例如 7.18%)
validatePositiveNumber()驗證正數value、fieldName錯誤訊息字串或空字串
validateRate()驗證報酬率範圍ratePercent錯誤訊息字串或空字串
// ① 終值:FV = PV × (1 + r)^n
function calculateFutureValue(presentValue, annualRate, years) {
  return presentValue * Math.pow(1 + annualRate, years);
}

// ② 現值:PV = FV ÷ (1 + r)^n
function calculatePresentValue(futureValue, annualRate, years) {
  return futureValue / Math.pow(1 + annualRate, years);
}

// ③ 所需報酬率:r = (FV ÷ PV)^(1÷n) - 1
function calculateRequiredRate(presentValue, futureValue, years) {
  return Math.pow(futureValue / presentValue, 1 / years) - 1;
}

// ④ 所需年數:n = ln(FV ÷ PV) ÷ ln(1 + r)
// Math.log() = 自然對數 ln
function calculateRequiredYears(presentValue, futureValue, annualRate) {
  return Math.log(futureValue / presentValue) / Math.log(1 + annualRate);
}
i

Math.log() 在 JavaScript 是自然對數(ln),不是以 10 為底的對數。所需年數公式 ln(FV÷PV) ÷ ln(1+r) 正是由此推導。

08JavaScript 完整互動程式

程式區塊 3js/time-value.js(完整版)
"use strict";

// ── ① 取得所有網頁元素 ──────────────────────────────
const calculationType = document.getElementById("calculationType");
const presentValueInput = document.getElementById("presentValue");
const futureValueInput = document.getElementById("futureValue");
const annualRateInput = document.getElementById("annualRate");
const yearsInput = document.getElementById("years");
const fieldHint = document.getElementById("fieldHint");
const calculateButton = document.getElementById("calculateButton");
const clearButton = document.getElementById("clearButton");
const messageBox = document.getElementById("message");
const resultBox = document.getElementById("resultBox");
const mainResult = document.getElementById("mainResult");
const formulaText = document.getElementById("formulaText");
const resultExplanation = document.getElementById("resultExplanation");
const riskNotice = document.getElementById("riskNotice");

// ── ② 四個金融計算函數 ──────────────────────────────
function calculateFutureValue(pv, r, n) { return pv * Math.pow(1 + r, n); }
function calculatePresentValue(fv, r, n) { return fv / Math.pow(1 + r, n); }
function calculateRequiredRate(pv, fv, n) { return Math.pow(fv / pv, 1 / n) - 1; }
function calculateRequiredYears(pv, fv, r) { return Math.log(fv / pv) / Math.log(1 + r); }

// ── ③ 格式化函數 ────────────────────────────────────
function formatCurrency(amount) {
  return new Intl.NumberFormat("zh-TW", {
    style: "currency", currency: "TWD", maximumFractionDigits: 0
  }).format(amount);
}
function formatPercent(rate) {
  return new Intl.NumberFormat("zh-TW", {
    style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2
  }).format(rate);
}

// ── ④ 輸入讀取:空白自動回傳 NaN ────────────────────
function getNumber(inputEl) {
  const v = inputEl.value.trim();
  return v === "" ? Number.NaN : Number(v);
}

// ── ⑤ 輸入驗證函數 ──────────────────────────────────
function validatePositiveNumber(value, fieldName) {
  if (Number.isNaN(value)) return `請輸入${fieldName}。`;
  if (value <= 0) return `${fieldName}必須大於 0。`;
  return "";
}
function validateRate(ratePercent) {
  if (Number.isNaN(ratePercent)) return "請輸入年報酬率或折現率。";
  if (ratePercent <= -100) return "利率不得小於或等於 -100%。";
  if (ratePercent > 100) return "利率高於 100%,請確認輸入是否正確。";
  return "";
}
function getHighRateWarning(ratePercent) {
  if (ratePercent > 20)
    return "⚠️ 目前年報酬率高於 20%。長期維持高報酬通常伴隨較高風險,請將結果視為情境試算。";
  return "";
}

// ── ⑥ 顯示 / 清除輔助函數 ──────────────────────────
function showError(msg) {
  messageBox.textContent = msg;
  messageBox.className = "message error-message";
  resultBox.classList.add("hidden");
}
function clearOutput() {
  messageBox.textContent = ""; messageBox.className = "message";
  resultBox.classList.add("hidden");
  mainResult.textContent = formulaText.textContent = resultExplanation.textContent = "";
  riskNotice.textContent = ""; riskNotice.classList.add("hidden");
}
function showRiskNotice(msg) {
  if (!msg) { riskNotice.textContent = ""; riskNotice.classList.add("hidden"); return; }
  riskNotice.textContent = msg;
  riskNotice.classList.remove("hidden");
}

// ── ⑦ 表單模式切換(下拉選單改變時執行) ───────────
function updateFormMode() {
  // 先恢復所有欄位
  [presentValueInput, futureValueInput, annualRateInput, yearsInput]
    .forEach(el => { el.disabled = false; });
  clearOutput();

  const mode = calculationType.value;
  const hints = {
    futureValue: { disable: futureValueInput, hint: "請輸入現值、年報酬率與投資年數。" },
    presentValue: { disable: presentValueInput, hint: "請輸入未來終值、年折現率與投資年數。" },
    requiredRate: { disable: annualRateInput, hint: "請輸入現值、目標終值與投資年數。" },
    requiredYears: { disable: yearsInput, hint: "請輸入現值、目標終值與年報酬率。" },
  };
  hints[mode].disable.disabled = true;
  hints[mode].disable.value = "";
  fieldHint.textContent = hints[mode].hint;
}

// ── ⑧ 四種模式的計算處理函數 ───────────────────────
function handleFutureValue() {
  const pv = getNumber(presentValueInput);
  const rp = getNumber(annualRateInput);
  const n = getNumber(yearsInput);
  let err = validatePositiveNumber(pv, '現值') ||
            validateRate(rp) ||
            validatePositiveNumber(n, '投資年數');
  if (err) { showError(err); return; }
  const r = rp / 100;
  const fv = calculateFutureValue(pv, r, n);
  mainResult.textContent = `預估終值:${formatCurrency(fv)}`;
  formulaText.textContent = 'FV = PV × (1 + r)ⁿ';
  resultExplanation.textContent =
    `投入 ${formatCurrency(pv)},以年報酬率 ${rp}% 投資 ${n} 年,` +
    `預估增加 ${formatCurrency(fv - pv)}。`;
  showRiskNotice(getHighRateWarning(rp));
  resultBox.classList.remove("hidden");
}

function handlePresentValue() {
  const fv = getNumber(futureValueInput);
  const rp = getNumber(annualRateInput);
  const n = getNumber(yearsInput);
  let err = validatePositiveNumber(fv, '終值') ||
            validateRate(rp) ||
            validatePositiveNumber(n, '投資年數');
  if (err) { showError(err); return; }
  const r = rp / 100;
  const pv = calculatePresentValue(fv, r, n);
  mainResult.textContent = `目前現值:${formatCurrency(pv)}`;
  formulaText.textContent = 'PV = FV ÷ (1 + r)ⁿ';
  resultExplanation.textContent =
    `${n} 年後需要 ${formatCurrency(fv)},以折現率 ${rp}% 計算,` +
    `目前約需準備 ${formatCurrency(pv)}。`;
  showRiskNotice(getHighRateWarning(rp));
  resultBox.classList.remove("hidden");
}

function handleRequiredRate() {
  const pv = getNumber(presentValueInput);
  const fv = getNumber(futureValueInput);
  const n = getNumber(yearsInput);
  let err = validatePositiveNumber(pv, '現值') ||
            validatePositiveNumber(fv, '終值') ||
            validatePositiveNumber(n, '投資年數');
  if (err) { showError(err); return; }
  const r = calculateRequiredRate(pv, fv, n);
  mainResult.textContent = `所需年報酬率:${formatPercent(r)}`;
  formulaText.textContent = 'r = (FV ÷ PV)^(1÷n) − 1';
  resultExplanation.textContent =
    `將 ${formatCurrency(pv)} 在 ${n} 年後成長至 ${formatCurrency(fv)},` +
    `每年需達到約 ${formatPercent(r)} 的複合報酬率。`;
  showRiskNotice(getHighRateWarning(r * 100));
  resultBox.classList.remove("hidden");
}

function handleRequiredYears() {
  const pv = getNumber(presentValueInput);
  const fv = getNumber(futureValueInput);
  const rp = getNumber(annualRateInput);
  let err = validatePositiveNumber(pv, '現值') ||
            validatePositiveNumber(fv, '終值') ||
            validateRate(rp);
  if (err) { showError(err); return; }
  if (rp === 0) { showError("報酬率為 0% 時,資金不會自行成長,無法計算達成目標的年數。"); return; }
  const r = rp / 100;
  const n = calculateRequiredYears(pv, fv, r);
  if (!Number.isFinite(n) || n < 0) {
    showError("計算結果不合理,請確認現值、目標金額與報酬率的方向是否正確。");
    return;
  }
  mainResult.textContent = `所需投資年數:約 ${n.toFixed(2)} 年`;
  formulaText.textContent = 'n = ln(FV ÷ PV) ÷ ln(1 + r)';
  resultExplanation.textContent =
    `將 ${formatCurrency(pv)} 以年報酬率 ${rp}% 投資,` +
    `約需 ${n.toFixed(2)} 年才能達到 ${formatCurrency(fv)}。`;
  showRiskNotice(getHighRateWarning(rp));
  resultBox.classList.remove("hidden");
}

// ── ⑨ 主計算入口:依模式分派 ───────────────────────
function handleCalculation() {
  clearOutput();
  const dispatch = {
    futureValue: handleFutureValue,
    presentValue: handlePresentValue,
    requiredRate: handleRequiredRate,
    requiredYears: handleRequiredYears,
  };
  (dispatch[calculationType.value] || (() => {}))();
}

// ── ⑩ 清除按鈕 ───────────────────────────────────
function clearForm() {
  [presentValueInput, futureValueInput, annualRateInput, yearsInput]
    .forEach(el => { el.value = ""; });
  clearOutput();
  updateFormMode();
}

// ── ⑪ 事件監聽:三種事件 ────────────────────────
calculationType.addEventListener("change", updateFormMode);   // 切換模式
calculateButton.addEventListener("click", handleCalculation); // 點擊計算
clearButton.addEventListener("click", clearForm);             // 點擊清除
document.addEventListener("keydown", e => {                   // 按 Enter 計算
  if (e.key === "Enter") handleCalculation();
});

// ── ⑫ 頁面載入時初始化 ──────────────────────────
updateFormMode();

09JavaScript 程式逐步解析

三種事件說明

事件名稱觸發時機本章用途
change使用者更換下拉選單或輸入內容後離開欄位切換計算模式,更新停用欄位與提示文字
click使用者點擊按鈕點擊「開始計算」或「清除資料」
keydown使用者按下任何鍵盤按鍵偵測 Enter 鍵,直接觸發計算(免點按鈕)

條件判斷 === vs ==

// === 嚴格相等:同時比較值與型態(建議使用)
"5" === 5  // false(字串 vs 數字)
5 === 5    // true

// == 寬鬆相等:自動轉換型態(容易出錯)
"5" == 5   // true(危險!型態不同卻相等)

// 本章程式統一使用 ===
if (mode === "futureValue") { ... }

Number.isFinite 的必要性

// 當 ratePercent = 0 時:
Math.log(1 + 0) = Math.log(1) = 0
ln(FV/PV) / 0 = Infinity
→ 必須先排除 ratePercent === 0 的情況

// 當 fv < pv 且 r > 0 時:
Math.log(fv/pv) 為負數 → n 為負數
→ 必須檢查 n < 0,提示「方向不一致」

// 防護程式碼
if (!Number.isFinite(n) || n < 0) {
  showError("計算結果不合理,請確認資料方向。");
  return;
}

formatPercent 的注意事項

// calculateRequiredRate() 回傳小數(如 0.0718 代表 7.18%)
const r = calculateRequiredRate(pv, fv, n); // 例如 0.0718

// ✓ 正確:直接傳入小數
formatPercent(r) // 顯示 "7.18%"

// ✗ 錯誤:先乘 100 再格式化
formatPercent(r * 100) // 顯示 "718.00%"(錯誤!)

// 風險提醒函數傳入時才乘 100
showRiskNotice(getHighRateWarning(r * 100)); // 傳百分比數字(7.18)比較 > 20

10操作實務與測試

四種模式標準測試案例

計算模式輸入值預期結果金融公式驗算
計算終值PV=500,000、r=6%、n=10≈ 895,424 元500,000 × 1.06¹⁰
計算現值FV=1,000,000、r=5%、n=5≈ 783,526 元1,000,000 ÷ 1.05⁵
計算所需報酬率PV=500,000、FV=1,000,000、n=10≈ 7.18%(2)^(1/10)−1
計算所需年數PV=300,000、FV=600,000、r=6%≈ 11.90 年ln(2)÷ln(1.06)

邊界測試

測試項目輸入預期反應
空白現值現值未輸入請輸入現值
負本金現值 −100,000現值必須大於 0
零年數年數 0投資年數必須大於 0
報酬率 −100%利率 −100利率不得小於或等於 −100%
高報酬率利率 30顯示高報酬率警告(橘色框)
零報酬率反推年數利率 0、FV>PV資金不會自行成長,無法計算
目標低於本金PV=100萬、FV=80萬(計算報酬率)得到負報酬率(正常結果)

11實務案例

(一)現在領 100 萬 vs 五年後領 130 萬

甲方案:現在領取 100 萬元。乙方案:五年後領取 130 萬元。若年折現率為 5%,乙方案現值為:

PV = 1,300,000 ÷ (1.05)⁵ ≈ 1,018,491 元

乙方案現值略高於 100 萬元,5% 折現率下乙方案較有利。但若折現率提高至 8%:

PV = 1,300,000 ÷ (1.08)⁵ ≈ 884,845 元

此時甲方案較有利。這說明折現率的高低會改變方案比較的結果。

(二)十年資產翻倍需要多少報酬率

投資人有 80 萬元,希望十年後成長為 160 萬元,所需年複合報酬率:

r = (160÷80)^(1÷10) − 1 = 2^(0.1) − 1 ≈ 7.18%

此結果不代表選擇「平均報酬率 7.18%」的商品就一定能達成。實際投資還可能受到報酬率順序風險、市場波動、費用與稅負等因素影響。

(三)退休目標的現值

某人預估二十年後需要 1,000 萬元退休金,以年報酬率 4% 折現,目前一次準備金額:

PV = 10,000,000 ÷ (1.04)²⁰ ≈ 4,563,869 元

此計算未考慮後續每月投入,也未考慮通貨膨脹,僅適合作為單筆資金的簡化試算。

(四)不同報酬率下的等待時間比較
年報酬率所需年數(100萬→200萬)金融觀察
3%≈ 23.45 年保守型投資,等待較久
6%≈ 11.90 年中性報酬,約 12 年翻倍
10%≈ 7.27 年較積極型,但風險相對較高

「72 法則」速算:年報酬率 r%,資產翻倍約需 72÷r 年。例如 6% 約需 12 年,10% 約需 7.2 年,與公式計算結果相近。

12常見錯誤與除錯

症狀可能原因修正方式
切換模式後欄位沒有停用select 的 id 或 JS 的 value 不一致確認 id="calculationType",value 值拼字正確
按計算後結果沒顯示JS 路徑錯誤、id 拼字不符、程式語法錯誤F12 → Console 查看紅色錯誤訊息
所需年數顯示 Infinity報酬率為 0%,公式分母 ln(1)=0加入 ratePercent===0 的特殊判斷
報酬率顯示 718% 而非 7.18%formatPercent(r*100) 重複轉換確認傳入 formatPercent 的是小數(0.0718)
結果顯示負年數現值>終值但報酬率為正,方向矛盾加入 n<0 的驗證,提示使用者確認條件
Cannot read properties of nullJS 找不到指定 id 的 HTML 元素確認 HTML 的 id 與 JS 的 getElementById 完全一致

如何使用 F12 開發者工具

  1. 在網頁任意位置按 F12(或右鍵 →「檢查」)
  2. 點選上方「Console」分頁
  3. 重新操作觸發錯誤的動作
  4. 查看紅色錯誤訊息
i

「Cannot read properties of null (reading "value")」→ document.getElementById() 找不到指定 id 的元素 → 確認 HTML 中的 id 與 JS 程式中的字串完全一致(含大小寫)。

「SyntaxError: Unexpected token」→ 程式碼有語法錯誤(缺少括號、引號或大括號)→ 查看錯誤訊息後的檔案名稱與行數。

13金融工具的使用者體驗原則

原則不建議做法建議做法
欄位名稱具體金額現值/目前本金(元)
單位標示清楚利率欄無說明年報酬率或折現率(%)
結果有解說顯示 7.18%「將 50 萬在 10 年後成長至 100 萬,每年需約 7.18%」
錯誤訊息不責備你輸入錯了投資年數必須大於 0,請重新輸入
揭露模型假設不說明假設計算結果採固定年報酬率假設,不計費用與稅負
高報酬率警告不加提醒超過 20% 自動顯示橘色警告框

14關鍵字

貨幣時間價值現值(PV)終值(FV) 報酬率折現率投資年數 HTML 表單inputselect optiondisabledCSS Grid FlexboxCSS 變數響應式網頁(RWD) JavaScript 函數條件判斷=== 事件監聽changeclick keydownMath.powMath.log Number.isNaNNumber.isFiniteformatCurrency formatPercent72 法則F12 開發者工具

15實作練習

(一)基礎操作
  • 將工具名稱改為「貨幣時間價值試算平台」
  • 計算現值 300,000 元、年報酬率 5%、投資 10 年的終值
  • 測試零報酬率下的終值與現值(結果應等於輸入本金)
  • 將網站頁尾加入製作者名稱
  • 確認按 Enter 鍵可觸發計算
(二)功能修改
  • 增加計算終值時顯示「累積成長倍數」(FV ÷ PV)
  • 增加計算終值時顯示「總投資獲利」(FV − PV)
  • 增加報酬率超過 15% 時的提醒文字
  • 增加年數超過 50 年時的長期試算提醒
  • 增加「複製結果」按鈕,點擊後將結果複製到剪貼簿
  • 增加按下 Esc 鍵清除資料的功能
(三)進階挑戰
  • 增加每月複利模式(將年利率轉換為月利率,期數改為月數)
  • 增加名目年利率與有效年利率換算(EAR = (1 + r/m)^m − 1)
  • 增加通膨調整後的實質終值(使用費雪方程式)
  • 增加單利與複利比較表格
  • 使用 Chart.js 繪製「不同報酬率下的終值成長曲線」
  • 將計算紀錄匯出為 JSON 格式下載

上面「功能修改」的前兩項,正是本章挑戰任務(04 分頁)任務一的兩個變體;「常見錯誤與除錯」表格中的三種症狀,則對應任務二、三——先在這裡讀懂需求與陷阱,再到挑戰任務分頁動手實作與除錯。

本章結語——本章將第一章的單一複利工具擴充為具備四種計算模式的多功能時間價值計算器。讀者學習了貨幣時間價值的四個核心概念(FV、PV、r、n),以及如何用 HTML 下拉選單、CSS Grid 版面、JavaScript 函數分工與三種事件監聽,實現一個友善的金融互動工具。

本章重要學習成果:四種公式的數學邏輯與反推關係;停用欄位引導使用者的 UX 設計;函數分工使程式易讀易測試;三種邊界情況(零報酬率、負年數、Infinity)的防護處理;以及 F12 開發者工具的除錯方法。

下一章將學習 JSON 與 Markdown 的金融資料管理。當工具的資料(如債券商品資訊、題庫)需要大量更新時,把資料與程式分開儲存,才能有效維護金融網站。

多功能時間價值計算器(教材原始範例)

從下拉選單切換「計算終值/現值/所需報酬率/所需投資年數」四種模式,欄位會自動停用不需要的項目。這就是課本第五~八節的 time-value.html/time-value.css/time-value.js 完全對應的成品——等一下的挑戰任務就是在這份程式碼上動手修改。

  time-value.html — 在瀏覽器中執行

觀念測驗

共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。

0 / 15 已作答

挑戰任務:修改程式碼,讓工具自己告訴你哪裡錯

下面三個練習都是「真的會執行」的程式碼。修改完成後按下「執行測試」,系統會把你的程式碼放進真實瀏覽器環境中運行,並用白話文告訴你哪一行邏輯不對、哪個語法漏了什麼。

任務一:功能修改

對應教材「十五、實作練習(二)功能修改」

難度:入門
  • 在「計算終值」模式,新增顯示「累積成長倍數」(FV ÷ PV)
  • HTML 已經留好 #growthMultiple 空欄位,只需要在 JavaScript 中賦值
js/time-value.js — 可直接編輯
提示(卡住了再打開)
handleFutureValue() 算出 fv 之後、resultBox.classList.remove("hidden") 之前,加入一行把 (fv / pv).toFixed(2) 組成的文字寫進 #growthMultiple
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己動手做一次,或按「換一題再練習」挑戰另一個類似的任務。

任務二:抓出隱藏的 id 錯誤

對應教材「十二、常見錯誤與除錯」— Cannot read properties of null

難度:除錯
  • 這份程式碼交接自另一位同學,執行後完全沒有反應(或一載入就壞掉)
  • 找出問題並修正,讓計算器恢復正常運作
js/time-value.js — 藏有 1 個 bug
提示(卡住了再打開)
瀏覽器主控台(F12 → Console)如果出現「Cannot read properties of null」,代表 document.getElementById("...") 抓到的是 null。逐一比對 HTML 裡每個 id="..." 跟 JS 裡 getElementById("...") 的拼字是否一模一樣。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一個 id 錯誤。

任務三:算出來了,但答案不合理

對應教材「十二、常見錯誤與除錯」— 百分比重複轉換 / 方向矛盾的負數年份

難度:除錯
  • 這份程式碼可以正常執行、不會報錯,但算出來的數字不合理(可能是百分比爆增,或是出現負數年份)
  • 找出問題所在並修正
js/time-value.js — 數字不合理,但沒有紅字錯誤
提示(卡住了再打開)
這是「跑得動、但答案錯」的類型,找不到紅字錯誤時,回頭檢查 formatPercent() 傳入的到底是小數還是已經乘過 100 的數字,以及計算出來的年數是不是負的、有沒有在顯示前先做合理性檢查。
💡 查看範例解答(建議自己先試過再打開)


            

看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一種不合理答案的成因。