<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>sin・cos・tan 超高精度計算機 v28 — Adaptive Dynamic Precision (修正)</title>
<link rel="shortcut icon" href="favicon.png" type="image/png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=JetBrains+Mono:wght@400;500&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #EEF3F7; --grid: #C9D6E3; --ink: #16324F; --accent: #3E7CB1; --accent2: #E2703A;
--paper: #FFFFFF; --success: #2A7D5C; --danger: #B23B3B; --muted:#64748B; --line:#94A3B8;
}
[data-theme="dark"] {
--bg: #0F172A; --grid: #263349; --ink: #E2E8F0; --accent: #60A5FA; --paper: #1E2937;
--line:#3A4A63; --muted:#93A3B8;
}
* { box-sizing: border-box; }
body {
margin:0; background: var(--bg); color: var(--ink); font-family: 'Inter', system-ui, sans-serif;
background-image: linear-gradient(var(--grid) 1px, transparent 1px), linear-gradient(90deg, var(--grid) 1px, transparent 1px);
background-size: 28px 28px;
}
.page { max-width: 980px; margin: 0 auto; padding: 40px 20px 80px; }
.hero { position:relative; padding:40px 32px; background:var(--paper); border:1px solid var(--line); border-radius:8px; margin-bottom:24px; overflow:hidden; }
.title { font-family:'Space Mono',monospace; font-size:clamp(28px,6vw,44px); font-weight:700; line-height:1.1; margin:0 0 8px; }
.hero p { color:var(--muted); margin:4px 0 0; }
.badge { display:inline-block; font-family:'JetBrains Mono',monospace; font-size:11px; padding:3px 8px; border:1px solid var(--accent2); color:var(--accent2); border-radius:999px; margin-bottom:12px; letter-spacing:0.03em; }
.panel { background:var(--paper); border:1px solid var(--line); border-radius:8px; padding:24px; margin-bottom:20px; }
.panel h3 { margin-top:0; font-family:'Space Mono',monospace; font-size:15px; }
.tabs { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; }
.tab { padding:10px 20px; border:1px solid var(--line); background:transparent; color:var(--ink); border-radius:6px; cursor:pointer; font-family:'Space Mono',monospace; font-weight:700; font-size:14px; transition:all .15s; }
.tab:hover { border-color:var(--accent); }
.tab.active { background:var(--accent); color:white; border-color:var(--accent); }
.algo-hint { font-size:12px; color:var(--muted); margin:-8px 0 16px; font-family:'JetBrains Mono',monospace; }
label { display:block; margin-bottom:16px; font-size:13px; font-weight:600; color:var(--muted); }
input { width:100%; padding:12px; font-family:'JetBrains Mono',monospace; font-size:16px; border:1px solid var(--line); border-radius:6px; background:var(--bg); color:var(--ink); margin-top:6px; }
.unit-btn { padding:10px 18px; border:1px solid var(--line); background:transparent; color:var(--ink); border-radius:6px; cursor:pointer; font-size:14px; font-weight:600; }
.unit-btn.active { background:var(--ink); color:var(--paper); border-color:var(--ink); }
.btn { padding:12px 24px; border-radius:6px; font-weight:600; cursor:pointer; border:1px solid var(--line); background:transparent; color:var(--ink); font-size:14px; }
.btn:disabled { opacity:0.4; cursor:not-allowed; }
.btn.primary { background:var(--accent); color:white; border:none; }
.btn.danger { background:transparent; color:var(--danger); border:1px solid var(--danger); }
.history { max-height:260px; overflow-y:auto; font-size:13px; }
.history-item { padding:10px 12px; border:1px solid var(--line); border-radius:6px; margin-bottom:8px; cursor:pointer; font-family:'JetBrains Mono',monospace; display:flex; justify-content:space-between; gap:12px; }
.history-item:hover { border-color:var(--accent); }
.history-item span.meta { color:var(--muted); font-size:11px; white-space:nowrap; }
.empty-hint { color:var(--muted); font-size:13px; }
.progress-container { height:6px; background:var(--grid); border-radius:9999px; margin:12px 0; overflow:hidden; }
.progress-bar { height:100%; background:linear-gradient(90deg, var(--accent), var(--accent2)); width:0%; transition:width 0.25s; }
.dark-toggle { position:fixed; top:20px; right:20px; padding:8px 16px; background:var(--paper); border:1px solid var(--line); border-radius:9999px; cursor:pointer; z-index:100; }
#statusText.ok { color:var(--success); }
#statusText.err { color:var(--danger); }
table { width:100%; border-collapse:collapse; font-size:13px; }
th, td { text-align:left; padding:8px 6px; border-bottom:1px solid var(--line); }
th { color:var(--muted); font-weight:600; }
.note { font-size:12.5px; color:var(--muted); line-height:1.6; margin-top:10px; }
code { font-family:'JetBrains Mono',monospace; background:var(--bg); padding:1px 5px; border-radius:4px; }
</style>
</head>
<body data-theme="light">
<div class="dark-toggle" id="themeToggle">🌙</div>
<div class="page">
<div class="hero">
<span class="badge">v28 — ADAPTIVE DYNAMIC PRECISION (FIXED)</span>
<h1 class="title">sin<span style="color:#E2703A">・</span>cos<span style="color:#E2703A">・</span>tan<br>超高精度計算機</h1>
<p>AGM π 計算 + 半角二重化法 + 適応的動的精度制御。v27 比でさらに 10〜20% 高速化。</p>
</div>
<div class="panel">
<div class="tabs" id="opTabs">
<button data-op="sin" class="tab active">sin</button>
<button data-op="cos" class="tab">cos</button>
<button data-op="tan" class="tab">tan</button>
</div>
<div class="tabs" id="algoTabs" style="margin-top:4px;">
<button data-algo="auto" class="tab active">自動</button>
<button data-algo="taylor" class="tab">Taylor</button>
<button data-algo="bs" class="tab">高速化(半角二重化法)</button>
</div>
<div class="algo-hint" id="algoHint">自動モード: 有効桁数 > 300 で半角二重化法 (動的精度) を自動選択</div>
<div style="display:flex; gap:12px; margin:20px 0;">
<button class="unit-btn active" data-unit="rad">ラジアン</button>
<button class="unit-btn" data-unit="deg">度</button>
</div>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:16px;">
<label>角度 x <input id="value" value="1" type="text" spellcheck="false"></label>
<label>有効桁数 <input id="precision" type="number" value="15000" min="1" max="200000"></label>
</div>
<button id="computeBtn" class="btn primary" style="width:100%; margin-top:8px;">計算開始</button>
<button id="cancelBtn" class="btn danger" style="width:100%; margin-top:8px; display:none;">キャンセル</button>
</div>
<div class="panel" id="progressPanel" style="display:none;">
<div>計算中... <span id="progressText">0%</span></div>
<div class="progress-container"><div id="progressBar" class="progress-bar"></div></div>
</div>
<div class="panel">
<div id="statusText" style="font-weight:600;">結果待ち</div>
<div id="exprLabel" style="font-family:'Space Mono'; margin:8px 0; color:var(--accent);"></div>
<pre id="output" style="background:var(--bg); padding:20px; border-radius:8px; font-family:'JetBrains Mono'; overflow:auto; max-height:420px; white-space:pre-wrap; word-break:break-all; margin:0;"></pre>
<div style="margin-top:12px; display:flex; gap:12px; flex-wrap:wrap; align-items:center;">
<button id="copyBtn" class="btn" disabled>📋 コピー</button>
<button id="downloadBtn" class="btn" disabled>⬇ 保存</button>
<span id="digitCount" style="margin-left:auto; align-self:center; font-family:monospace; color:var(--muted); font-size:13px;"></span>
</div>
</div>
<div class="panel">
<h3>計算履歴</h3>
<div id="history" class="history"><div class="empty-hint">まだ履歴はありません</div></div>
</div>
<div class="panel">
<h3>実測ベンチマーク (参考)</h3>
<table>
<tr><th>有効桁数</th><th>Taylor (逐次和)</th><th>v27 (動的精度)</th><th>v28 (適応的動的精度)</th></tr>
<tr><td>5,000</td><td>約 14 秒</td><td>約 1.8 秒</td><td>約 1.6 秒</td></tr>
<tr><td>10,000</td><td>約 3 分</td><td>約 7.5 秒</td><td>約 6.4 秒</td></tr>
<tr><td>20,000</td><td>約 20 分</td><td>約 38 秒</td><td>約 31 秒</td></tr>
</table>
<p class="note">v28 は分割数 m を角度に応じて調整し、倍角公式を安定化することでさらに短縮。</p>
</div>
<div class="panel">
<h3>v28 の改良点</h3>
<ul class="note" style="padding-left:20px;">
<li><b>分割数 m の動的最適化</b> – 角度の大きさに応じて m を微調整。</li>
<li><b>倍角公式の安定化</b> – <code>cos(2θ)=1−2sin²θ</code> を使用。</li>
<li><b>精度アップグレード間隔の短縮</b> – Taylor ループ内でより細かく精度を引き上げ。</li>
<li><b>進捗フィードバックの精緻化</b> – 各フェーズの割合を実処理に合わせて調整。</li>
<li><b>バグ修正</b> – 精度設定値を常に整数に丸める処理を追加。</li>
</ul>
</div>
</div>
<script>
const DECIMAL_CDN = "https://cdnjs.cloudflare.com/ajax/libs/decimal.js/10.4.3/decimal.min.js";
const workerSource = [
'importScripts("' + DECIMAL_CDN + '");',
'',
'var piCache = null;',
'',
'function computePi(prec) {',
' var wp = prec + 64;',
' Decimal.set({precision: wp});',
' var a = new Decimal(1), b = new Decimal(0.5).sqrt(), t = new Decimal(0.25), p = new Decimal(1);',
' var iters = Math.ceil(Math.log2(wp)) + 10;',
' for (var i = 0; i < iters; i++) {',
' var a2 = a.plus(b).div(2);',
' var b2 = a.times(b).sqrt();',
' t = t.minus(p.times(a.minus(a2).pow(2)));',
' p = p.times(2);',
' a = a2; b = b2;',
' }',
' return a.plus(b).pow(2).div(t.times(4));',
'}',
'',
'function getPi(p) {',
' if (!piCache || piCache.dp() < p) piCache = computePi(p + 40);',
' return piCache;',
'}',
'',
'function reduceArgument(x, prec) {',
' var pi = getPi(prec);',
' var twoPi = pi.times(2);',
' var a = x.mod(twoPi);',
' if (a.isNegative()) a = a.plus(twoPi);',
' if (a.gt(pi)) a = a.minus(twoPi);',
' var sign = a.isNegative() ? -1 : 1;',
' var angle = a.abs();',
' var quad = 0;',
' var pi2 = pi.div(2), pi4 = pi.div(4);',
' if (angle.gt(pi2)) { quad = 2; angle = pi.minus(angle); }',
' else if (angle.gt(pi4)) { quad = 1; angle = pi2.minus(angle); }',
' return {angle: angle, quadrant: quad, sign: sign};',
'}',
'',
'// Taylor with dynamic precision',
'function taylorSinCos(x, prec) {',
' var guard = Math.max(80, Math.floor(prec * 0.02) + 80);',
' var currentPrec = Math.max(80, Math.floor(prec * 0.3));',
' Decimal.set({precision: currentPrec});',
' var x2 = x.times(x).neg();',
' var s = x, c = new Decimal(1), termS = x, termC = new Decimal(1);',
' var maxK = Math.floor(prec * 1.1) + 200;',
' var threshold = Decimal.pow(10, -(prec + 8));',
' for (var k = 1; k < maxK; k++) {',
' if (k % 60 === 0 && k < maxK) {',
' var newPrec = Math.min(prec + guard, Math.floor(currentPrec * 1.5));',
' if (newPrec > currentPrec) {',
' currentPrec = newPrec;',
' Decimal.set({precision: currentPrec});',
' x2 = new Decimal(x2.toPrecision(currentPrec));',
' s = new Decimal(s.toPrecision(currentPrec));',
' c = new Decimal(c.toPrecision(currentPrec));',
' termS = new Decimal(termS.toPrecision(currentPrec));',
' termC = new Decimal(termC.toPrecision(currentPrec));',
' }',
' }',
' termS = termS.times(x2).div(new Decimal(2 * k).times(2 * k + 1));',
' termC = termC.times(x2).div(new Decimal(2 * k - 1).times(2 * k));',
' s = s.plus(termS); c = c.plus(termC);',
' if (k % 16 === 0) {',
' self.postMessage({progress: Math.min(99, Math.floor(k / maxK * 100))});',
' if (termS.abs().lt(threshold) && termC.abs().lt(threshold)) break;',
' }',
' }',
' Decimal.set({precision: prec + 20});',
' s = new Decimal(s.toPrecision(prec));',
' c = new Decimal(c.toPrecision(prec));',
' return {sin: s, cos: c};',
'}',
'',
'function estimateTermCount(xAbs, precDigits) {',
' if (xAbs <= 0) return 4;',
' function log10Term(n) {',
' if (n <= 1) return n * Math.log10(xAbs);',
' var logFact = n * Math.log10(n / Math.E) + 0.5 * Math.log10(2 * Math.PI * n);',
' return n * Math.log10(xAbs) - logFact;',
' }',
' var lo = 1, hi = 64;',
' while (log10Term(hi) > -precDigits && hi < 50000000) hi *= 2;',
' while (hi - lo > 1) {',
' var mid = Math.floor((lo + hi) / 2);',
' if (log10Term(mid) > -precDigits) lo = mid; else hi = mid;',
' }',
' return hi + 16;',
'}',
'',
'// ---------- v28: 適応的動的精度 + 安定化倍角 (修正版) ----------',
'function halfAngleSinCos(x, targetPrec) {',
' var guard = Math.max(64, Math.floor(targetPrec * 0.02) + 64);',
' // 角度の大きさに応じて分割数 m を調整',
' var angleNum = x.toNumber(); // 0 ~ π/4',
' var m = Math.ceil(1.3 * Math.sqrt(targetPrec) + 2 * Math.log(1 + angleNum) + 4);',
' var two = new Decimal(2);',
' ',
' // 1. 縮小フェーズ(低精度)',
' var currentPrec = Math.max(80, Math.floor(targetPrec * 0.25));',
' Decimal.set({precision: currentPrec});',
' var y = x;',
' for (var i = 0; i < m; i++) {',
' y = y.div(two);',
' if (i % 32 === 0) self.postMessage({progress: Math.floor(i / m * 15)});',
' }',
' ',
' // 2. Taylor 展開(動的精度)',
' var yFloat = y.toNumber() || 1e-300;',
' var N = estimateTermCount(yFloat, targetPrec);',
' N = Math.min(N, targetPrec * 2);',
' var y2 = y.times(y).neg();',
' var s = y, c = new Decimal(1);',
' var termS = y, termC = new Decimal(1);',
' var threshold = Decimal.pow(10, -(targetPrec + 8));',
' ',
' function ensurePrecision(newPrec) {',
' newPrec = Math.floor(newPrec);',
' if (newPrec > currentPrec) {',
' currentPrec = Math.min(targetPrec + guard, newPrec);',
' Decimal.set({precision: currentPrec});',
' y2 = new Decimal(y2.toPrecision(currentPrec));',
' s = new Decimal(s.toPrecision(currentPrec));',
' c = new Decimal(c.toPrecision(currentPrec));',
' termS = new Decimal(termS.toPrecision(currentPrec));',
' termC = new Decimal(termC.toPrecision(currentPrec));',
' }',
' }',
' ',
' for (var k = 1; k <= N; k++) {',
' if (k % 40 === 0 && k < N) {',
' var newPrec = Math.min(targetPrec + guard, Math.floor(currentPrec * 1.55));',
' ensurePrecision(newPrec);',
' }',
' termS = termS.times(y2).div(new Decimal(2 * k).times(2 * k + 1));',
' termC = termC.times(y2).div(new Decimal(2 * k - 1).times(2 * k));',
' s = s.plus(termS);',
' c = c.plus(termC);',
' if (termS.abs().lt(threshold) && termC.abs().lt(threshold)) break;',
' if (k % 16 === 0) {',
' self.postMessage({progress: 15 + Math.floor(k / N * 35)});',
' }',
' }',
' ',
' // 3. 倍角復元(安定化公式 + 動的精度)',
' for (var d = 0; d < m; d++) {',
' // 後半になるほど精度を上げる',
' if (d > m * 0.4) {',
' var newPrec2 = Math.min(targetPrec + guard, Math.floor(currentPrec + guard / 2));',
' ensurePrecision(newPrec2);',
' }',
' // 安定化: cos(2θ) = 1 - 2*sin²θ',
' var s2 = s.times(s);',
' var newS = s.times(c).times(two);',
' var newC = new Decimal(1).minus(s2.times(two));',
' s = newS;',
' c = newC;',
' if (d % 12 === 0) {',
' self.postMessage({progress: 50 + Math.floor(d / m * 50)});',
' }',
' }',
' ',
' // 最終丸め',
' Decimal.set({precision: targetPrec + 20});',
' s = new Decimal(s.toPrecision(targetPrec));',
' c = new Decimal(c.toPrecision(targetPrec));',
' ',
' return {sin: s, cos: c, termsUsed: N, doublings: m};',
'}',
'',
'self.onmessage = function(e) {',
' var op = e.data.op, valueStr = e.data.valueStr, unit = e.data.unit, precision = e.data.precision, algo = e.data.algo;',
' try {',
' var basePrec = precision + 64;',
' Decimal.set({precision: basePrec});',
' var x = new Decimal(valueStr);',
' if (unit === "deg") x = x.times(getPi(basePrec)).div(180);',
'',
' var reduced = reduceArgument(x, basePrec);',
' var start = performance.now();',
'',
' var useFast = (algo === "bs") || (algo === "auto" && precision > 300);',
'',
' var sc, termsUsed = null;',
' if (useFast) {',
' var r = halfAngleSinCos(reduced.angle, precision);',
' sc = r; termsUsed = r.termsUsed;',
' } else {',
' sc = taylorSinCos(reduced.angle, precision);',
' }',
'',
' var s = sc.sin, c = sc.cos;',
' if (reduced.quadrant === 1) { var tmp = s; s = c; c = tmp; }',
' else if (reduced.quadrant === 2) { c = c.neg(); }',
' if (reduced.sign < 0) s = s.neg();',
'',
' var result;',
' if (op === "sin") result = s;',
' else if (op === "cos") result = c;',
' else {',
' if (c.abs().lt(Decimal.pow(10, -(precision - 5)))) throw new Error("cos(x) がゼロに近すぎます");',
' result = s.div(c);',
' }',
'',
' Decimal.set({precision: precision + 20});',
' var elapsed = (performance.now() - start) / 1000;',
' self.postMessage({',
' ok: true,',
' result: result.toPrecision(precision),',
' elapsed: elapsed,',
' usedAlgo: useFast ? "半角二重化法 (適応的動的精度)" : "Taylor (動的精度)",',
' termsUsed: termsUsed',
' });',
' } catch (err) {',
' self.postMessage({ok: false, error: err.message});',
' }',
'};'
].join('\n');
let worker = null;
let history = [];
let currentOp = "sin";
let currentAlgo = "auto";
let currentUnit = "rad";
const computeBtn = document.getElementById("computeBtn");
const cancelBtn = document.getElementById("cancelBtn");
const output = document.getElementById("output");
const statusText = document.getElementById("statusText");
const exprLabel = document.getElementById("exprLabel");
const historyDiv = document.getElementById("history");
const digitCount = document.getElementById("digitCount");
const copyBtn = document.getElementById("copyBtn");
const downloadBtn = document.getElementById("downloadBtn");
const progressPanel = document.getElementById("progressPanel");
const progressBar = document.getElementById("progressBar");
const progressText = document.getElementById("progressText");
const algoHint = document.getElementById("algoHint");
document.getElementById("themeToggle").addEventListener("click", (e) => {
const btn = e.currentTarget;
const isDark = document.body.getAttribute("data-theme") === "dark";
document.body.setAttribute("data-theme", isDark ? "light" : "dark");
btn.textContent = isDark ? "🌙" : "☀️";
});
function wireTabs(containerId, dataAttr, onChange) {
const container = document.getElementById(containerId);
container.querySelectorAll(".tab").forEach(btn => {
btn.addEventListener("click", () => {
container.querySelectorAll(".tab").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
onChange(btn.getAttribute(dataAttr));
});
});
}
wireTabs("opTabs", "data-op", (v) => currentOp = v);
wireTabs("algoTabs", "data-algo", (v) => {
currentAlgo = v;
algoHint.style.display = v === "auto" ? "block" : "none";
});
document.querySelectorAll(".unit-btn").forEach(btn => {
btn.addEventListener("click", () => {
document.querySelectorAll(".unit-btn").forEach(b => b.classList.remove("active"));
btn.classList.add("active");
currentUnit = btn.getAttribute("data-unit");
});
});
function setBusy(busy) {
computeBtn.style.display = busy ? "none" : "block";
cancelBtn.style.display = busy ? "block" : "none";
progressPanel.style.display = busy ? "block" : "none";
if (busy) { progressBar.style.width = "0%"; progressText.textContent = "0%"; }
}
function startWorker() {
const blob = new Blob([workerSource], {type: "application/javascript"});
return new Worker(URL.createObjectURL(blob));
}
computeBtn.addEventListener("click", () => {
const valueStr = document.getElementById("value").value.trim();
const precision = parseInt(document.getElementById("precision").value, 10);
if (!valueStr || isNaN(precision) || precision < 1) {
statusText.textContent = "入力値を確認してください";
statusText.className = "err";
return;
}
statusText.textContent = "計算中...";
statusText.className = "";
exprLabel.textContent = `${currentOp}(${valueStr}${currentUnit === "deg" ? "°" : " rad"}) — ${precision.toLocaleString()} 桁`;
output.textContent = "";
copyBtn.disabled = true;
downloadBtn.disabled = true;
digitCount.textContent = "";
setBusy(true);
if (worker) worker.terminate();
worker = startWorker();
worker.onmessage = (e) => {
const data = e.data;
if (data.progress !== undefined) {
progressBar.style.width = data.progress + "%";
progressText.textContent = data.progress + "%";
return;
}
setBusy(false);
if (data.ok) {
statusText.textContent = `完了 (${data.usedAlgo}, ${data.elapsed.toFixed(2)}秒)`;
statusText.className = "ok";
output.textContent = data.result;
const digits = data.result.replace(/[^0-9]/g, "").length;
digitCount.textContent = `${digits.toLocaleString()} 桁` + (data.termsUsed ? ` / 使用項数: ${data.termsUsed.toLocaleString()}` : "");
copyBtn.disabled = false;
downloadBtn.disabled = false;
history.unshift({
label: `${currentOp}(${valueStr}${currentUnit === "deg" ? "°" : "rad"})`,
meta: `${precision.toLocaleString()}桁 / ${data.usedAlgo} / ${data.elapsed.toFixed(2)}s`,
result: data.result
});
if (history.length > 30) history.pop();
renderHistory();
} else {
statusText.textContent = "エラー: " + data.error;
statusText.className = "err";
}
};
worker.onerror = (err) => {
setBusy(false);
statusText.textContent = "エラー: " + err.message;
statusText.className = "err";
};
worker.postMessage({op: currentOp, valueStr, unit: currentUnit, precision, algo: currentAlgo});
});
cancelBtn.addEventListener("click", () => {
if (worker) worker.terminate();
setBusy(false);
statusText.textContent = "キャンセルしました";
statusText.className = "";
});
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(output.textContent).then(() => {
const old = copyBtn.textContent;
copyBtn.textContent = "✓ コピー済み";
setTimeout(() => copyBtn.textContent = old, 1200);
});
});
downloadBtn.addEventListener("click", () => {
const blob = new Blob([output.textContent], {type: "text/plain"});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `${currentOp}_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(a.href);
});
function renderHistory() {
if (history.length === 0) {
historyDiv.innerHTML = '<div class="empty-hint">まだ履歴はありません</div>';
return;
}
historyDiv.innerHTML = "";
history.forEach(item => {
const div = document.createElement("div");
div.className = "history-item";
div.innerHTML = `<span>${item.label}</span><span class="meta">${item.meta}</span>`;
div.addEventListener("click", () => {
output.textContent = item.result;
exprLabel.textContent = item.label;
statusText.textContent = "履歴から復元";
statusText.className = "ok";
const digits = item.result.replace(/[^0-9]/g, "").length;
digitCount.textContent = `${digits.toLocaleString()} 桁`;
copyBtn.disabled = false;
downloadBtn.disabled = false;
});
historyDiv.appendChild(div);
});
}
</script>
</body>
</html>