JavaScript

JavaScriptのMath.expm1():小さな指数変化を精度よく計算する

この記事でわかること

Math.expm1()で指数関数から1を引く計算の精度低下を抑える方法を解説。引数・戻り値、ブラウザーとNode.jsの実行手順、期待出力、入力検証と値の平滑化への応用を学べます。

目的:小さな変化を計算から消さない

Math.expm1()は、自然対数の底である約2.718の数をeとしたとき、eのx乗から1を引いた数を返す関数です。指数関数による増加量や、目標値へ徐々に近づける計算で利用できます。

数学的にはMath.exp(x) - 1と同じ式ですが、コンピューター上では計算の途中で丸めが入ります。xが非常に小さいと、指数関数の結果は1に近くなり、その時点で小さな差が失われることがあります。あとから1を引いても、失われた情報は戻りません。

そこで、最終的に必要な「1との差」を求めるためのMath.expm1()を使います。ゼロに近い入力を扱うコードで、式をそのまま書くより数値的に安定した計算を選べることが、この関数を学ぶ目的です。MDNのMath.expm1()でも、この精度の違いが説明されています。

動作環境と対応バージョン

2026年10月11日時点で、MDNはMath.expm1()を広く利用可能な機能として扱い、2015年7月からブラウザー間で利用できるとしています。現在のChrome、Edge、Firefox、Safariで利用できる標準組み込み関数です。古いブラウザーや組み込みWebViewでは、使用前に存在を確認してください。

Node.jsでも利用できます。本記事の再現用環境はNode.js 22系以降とします。これは記事で使用する環境の指定であり、機能が初めて導入されたバージョンを意味しません。Node.js 22がV8 12.4を採用していることは公式リリース案内で確認できます。

この関数はDOMに依存しません。以下のサンプルはブラウザーとNode.jsの両方で同じコードを使え、追加パッケージや通信も不要です。

JavaScript
console.log(typeof Math.expm1);

対応環境ではfunctionと表示されます。undefinedなら、その環境で後続のコードを実行せず、対応する環境に切り替えてください。単にMath.exp(x) - 1へ置き換えると、この記事で扱う精度上の利点が失われます。

引数と戻り値

呼び出し方はMath.expm1(x)です。Mathは生成するクラスではなく、関数を直接呼び出すための組み込みオブジェクトです。

  • 引数xには、指数として使う数値を渡します。
  • 戻り値はeのx乗から1を引いたnumberです。
  • xが0なら0、正なら正の値、負なら負の値を返します。
  • InfinityにはInfinity、-Infinityには-1を返します。
  • NaNにはNaNを返し、通常の数値計算の失敗を例外として通知する関数ではありません。

数値以外の入力に注意する

JavaScriptの数値を期待する組み込み処理では、入力の数値変換が起こります。数値文字列が計算できても、それを入力検証として利用すると、空文字列などの意図しない値まで受け入れかねません。BigIntやSymbolの暗黙の数値変換はTypeErrorになります。変換規則はMDNのNumberを参照してください。

アプリケーションの関数では、最初に有限のnumberだけを受け付けると、入力の問題と計算結果を区別しやすくなります。

コピーして動かす基本例

次のコードはそのまま実行できます。出力を小数6桁にそろえ、環境ごとの末尾の丸め差を読み取りやすくしています。

JavaScript
(() => {
  "use strict";

  if (typeof Math.expm1 !== "function") {
    console.log("この環境はMath.expm1に対応していません");
    return;
  }

  function calculateChange(x) {
    if (typeof x !== "number" || !Number.isFinite(x)) {
      throw new TypeError("xは有限の数値にしてください");
    }

    const result = Math.expm1(x);
    if (!Number.isFinite(result)) {
      throw new RangeError("計算結果が有限の範囲を超えました");
    }
    return result;
  }

  for (const x of [0, 1, -1]) {
    console.log(`${x}: ${calculateChange(x).toFixed(6)}`);
  }

  try {
    calculateChange("1");
  } catch (error) {
    console.log(`${error.name}: ${error.message}`);
  }
})();

対応環境での期待出力は次のとおりです。ここで示す結果は公式例と計算の確認によるもので、実機での実行検証は行っていません。

CODE
0: 0.000000
1: 1.718282
-1: -0.632121
TypeError: xは有限の数値にしてください

ブラウザーでの実行方法

ブラウザーで空のタブを開き、開発者ツールのConsoleを選びます。コード全体を貼り付けて実行すると、Consoleに結果が出ます。貼り付け制限が表示される場合は、ブラウザーの案内を確認するか、コードを手入力してください。

コード全体を即時実行関数で囲んでいるため、繰り返し実行してもConsoleのトップレベルで同じ変数名を再宣言する問題を避けられます。ページ内の要素を用意する必要はありません。

Node.jsでの実行方法

Node.jsをインストールした環境で、コードをUTF-8のexpm1-demo.jsとして保存します。ターミナルでそのファイルのあるディレクトリーへ移動し、次のコマンドを実行してください。

Shell
node --version
node expm1-demo.js

最初のコマンドで22系以降の環境であることを確認し、次のコマンドで標準出力へ結果を表示します。ファイル名をnodeへ渡す実行方法はNode.js公式の実行ガイドに沿っています。

失敗例:指数を計算してから1を引く

次のコードは、小さな入力で計算方法を比較します。前のサンプルと独立して、同じ手順で実行できます。

JavaScript
(() => {
  const x = 1.1111111111e-15;
  const direct = Math.exp(x) - 1;
  const stable = Math.expm1(x);

  console.log(`直接計算: ${direct.toExponential(10)}`);
  console.log(`expm1: ${stable.toExponential(10)}`);
})();

MDNに掲載された計算値に基づく期待出力は次のとおりです。指数関数の近似計算では、実装によって末尾に差が出る場合があります。

CODE
直接計算: 1.1102230246e-15
expm1: 1.1111111111e-15

ゼロに非常に近いxでは、求める結果もおおむねxに近くなります。比較すると、直接計算した値は入力から相対的に大きくずれています。処理を何度も繰り返す用途では、こうした誤差が計算全体に影響する可能性があります。MDNのMath.exp()も、ゼロに近い引数で1との差を求める場合にMath.expm1()を案内しています。

活用例:目標値へ滑らかに近づける

表示値を急に切り替えず、目標との差を少しずつ縮めたい場合を考えます。経過時間をdt、追従の強さをrateとすると、更新割合を1 - eの-rate * dt乗として計算できます。これを-Math.expm1(-rate * dt)と書けば、経過時間が小さい場合にも1との差を直接求められます。

次は、現在値0を目標値100へ近づける自己完結した計算例です。実際のアニメーションではなく、更新1回分の割合と結果を表示します。

JavaScript
(() => {
  function smoothingWeight(rate, dt) {
    if (!Number.isFinite(rate) || !Number.isFinite(dt)) {
      throw new TypeError("rateとdtは有限の数値にしてください");
    }
    if (rate < 0 || dt < 0) {
      throw new RangeError("rateとdtは0以上にしてください");
    }

    const product = rate * dt;
    if (!Number.isFinite(product)) {
      throw new RangeError("rateとdtの積が大きすぎます");
    }
    return -Math.expm1(-product);
  }

  const current = 0;
  const target = 100;
  const rate = 2;
  const dt = 0.5;
  const weight = smoothingWeight(rate, dt);
  const next = current + (target - current) * weight;

  console.log(`更新割合: ${weight.toFixed(6)}`);
  console.log(`更新後: ${next.toFixed(6)}`);
  console.log(`経過時間0: ${smoothingWeight(rate, 0).toFixed(6)}`);
})();

期待出力は次のとおりです。

CODE
更新割合: 0.632121
更新後: 63.212056
経過時間0: 0.000000

この例では、0.5秒で差の約63.2%を埋めます。dtを秒で渡すなら、rateも秒を基準に設定します。ミリ秒を混ぜると追従速度が大きく変わるため、呼び出し側で単位を統一してください。

応用先にはメーターの表示値や、計測値の急な変化を和らげる処理があります。追従が遅れる性質もあるため、瞬間的なピークを把握したい値では、元の値も残す設計が適しています。

制約と実装時の確認点

Math.expm1()を使っても任意精度計算にはなりません。JavaScriptのnumberは64ビットの浮動小数点数で、表現できる範囲と精度に限界があります。入力の乗算ですでに情報が失われた場合、その後にこの関数を呼んでも復元できません。

大きな正の入力では結果がInfinityになる可能性があります。基本例では戻り値にも有限性の検査を入れ、こうした状況を独自のRangeErrorへ変換しています。一方、平滑化の例では入力の積を検査しています。どの値を異常として扱うかは、用途ごとに決める必要があります。

また、toFixed()は表示用の文字列を作ります。後続の計算には整形前の数値を渡してください。毎回表示用に丸めた値を計算へ戻すと、この関数で抑えた誤差とは別の丸め誤差が加わります。

このAPIは同期処理で、Promiseを返しません。awaitは不要です。入力検証で投げた例外は呼び出し側で扱い、NaNやInfinityの結果を例外が自動的に拾うと考えないことが大切です。

公式参考資料

PREFERRED SOURCES

Googleの優先するニュース提供元にSEの部屋を追加

Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。

Googleの設定画面で確認する 新しいタブで開きます

候補にSEの部屋が表示されない場合は、まだ追加できません。

y.
WRITTEN BY

y_ymo10

SEの部屋で、HTML・CSSからJavaScript・React・Next.jsまで、Web制作の開発ノートを公開しています。

ほかの開発ノートを読む →