1
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?

単位変換ツールを作って分かった「温度だけが特別」な理由 — 線形係数とアフィン変換

1
Posted at

単位変換ツールは無数にあるが、自分で作ってみると設計に 1 つだけ面白い分岐がある: ほぼ全ての単位は「基準単位への係数」1 つで表せるのに、温度だけはそれができない。1km = 1000m、1in = 0.0254m のように線形単位は係数だけで N×N の変換表なしに任意ペアを変換できる。でも 0°C は 0°F ではない (32°F)。摂氏・華氏・ケルビンは y = a·x + bアフィン変換で、係数だけでは表せない。この「温度だけ特例」をどう設計するかが、地味だが本質的なポイント。長さ・質量・温度・面積・速度・データ量を横断する変換器を作った。

🌐 デモ: https://sen.ltd/portfolio/unit-converter/
📦 GitHub: https://github.com/sen-ltd/unit-converter

スクリーンショット

線形単位: 係数 1 つで N×N 表を消す

長さの変換を素朴に考えると「mm↔cm, mm↔m, mm↔km, cm↔m...」と全ペアの変換式が要りそうに見える。11 単位なら 11×10 = 110 通り。これは破綻する。

解は基準単位を 1 つ決めて、各単位を「基準への係数」で表すこと。長さの基準を m にすると:

{ id: "mm", factor: 0.001 },   // 1mm = 0.001m
{ id: "m",  factor: 1 },        // 基準
{ id: "km", factor: 1000 },     // 1km = 1000m
{ id: "in", factor: 0.0254 },   // 1in = 0.0254m

変換は基準単位を経由する 1 本道:

export function convert(categoryId, fromId, toId, value) {
  // ...
  const base = value * from.factor;  // 任意単位 → 基準単位
  return base / to.factor;           // 基準単位 → 任意単位
}

12 in → ft なら 12 × 0.0254 = 0.3048m0.3048 / 0.3048 = 1ftN 個の単位に対して係数 N 個を持つだけで、N×N の変換表が消える。これがディメンショナル変換の基本形。

温度: 係数では表せない

ところが温度で同じことをやろうとすると破綻する。「摂氏の係数は?」が定義できない。なぜなら摂氏と華氏は原点がずれているから:

  • 0°C = 32°F (氷点)
  • 100°C = 212°F (沸点)
  • 差分の比は 100:180 = 5:9 だが、0 同士が対応しない

これは比例 (y = a·x) ではなくアフィン変換 (y = a·x + b)。係数 a だけでなく切片 b が要る。線形単位の factor 1 つでは表現できない。

解決策: 温度カテゴリだけ、各単位に基準への変換関数のペア (toBase / fromBase) を持たせる。基準はケルビン:

{
  id: "temperature", base: "K", affine: true,
  units: [
    { id: "C", toBase: (c) => c + 273.15,          fromBase: (k) => k - 273.15 },
    { id: "F", toBase: (f) => (f - 32) * 5/9 + 273.15, fromBase: (k) => (k - 273.15) * 9/5 + 32 },
    { id: "K", toBase: (k) => k,                    fromBase: (k) => k },
  ],
}

変換エンジンは affine フラグで分岐:

if (cat.affine) {
  const base = from.toBase(value);  // 任意温度 → K
  return to.fromBase(base);         // K → 任意温度
}
// 線形は係数ルート
const base = value * from.factor;
return base / to.factor;

構造は線形と同じ「基準経由の 1 本道」だが、係数の掛け算が関数呼び出しに変わっただけ。抽象を揃えつつ、表現力だけ上げる設計。

テストで「温度は単純比ではない」を固定

温度の特例性をテストで明示する。一番有名な事実、-40°C = -40°F の交差点:

test("-40°C = -40°F (the crossover)", () =>
  approx(convert("temperature", "C", "F", -40), -40));

test("0°C = 32°F", () => approx(convert("temperature", "C", "F", 0), 32));
test("100°C = 212°F", () => approx(convert("temperature", "C", "F", 100), 212));

// 「もし係数だけで実装してたら 0°C→0°F になってしまう」を防ぐ回帰テスト
test("affine is NOT a simple ratio: 0°C→F is 32, not 0", () => {
  assert.notEqual(convert("temperature", "C", "F", 0), 0);
});

最後のテストが効く。うっかり温度を線形係数で実装すると 0°C → 0°F になる。それを「0 ではないこと」で固定しておくと、リファクタで温度を線形扱いに戻してしまう事故を防げる。

カタログの自己検証

データ駆動なので、カタログ自体の整合性もテストする:

test("linear base unit has factor 1", () => {
  for (const cat of CATEGORIES) {
    if (cat.affine) continue;
    assert.equal(getUnit(cat, cat.base).factor, 1);
  }
});

test("affine units' toBase/fromBase round-trip", () => {
  const temp = getCategory("temperature");
  for (const u of temp.units) {
    for (const v of [-40, 0, 37, 100]) {
      approx(u.fromBase(u.toBase(v)), v);  // 往復で元に戻る
    }
  }
});

「基準単位の係数は 1」は線形カテゴリの不変条件。これを破ると全変換がずれるので、カタログに新単位を追加したときの安全網になる。アフィンは「fromBase(toBase(x)) === x」のラウンドトリップで関数ペアの整合性を担保。

ついでに対応した細かい単位

汎用変換器なので、日常で「あれ、これいくつ?」となる単位を入れた:

  • 尺貫法: 尺 (0.303m)・里 (3927m)・坪 (3.306m²)・畳 (1.653m²)・匁 (3.75g)・貫 (3.75kg)
  • データ量の 10³ vs 2¹⁰: KB (1000) と KiB (1024) を別単位として区別。1 KB ≠ 1 KiB もテスト
  • 速度のマッハ: 海面・15℃ の音速 340.29 m/s
test("1 KB (10³) ≠ 1 KiB (2¹⁰)", () => {
  assert.notEqual(
    convert("data", "kb", "byte", 1),
    convert("data", "kib", "byte", 1));
});
test("2 tatami ≈ 1 tsubo", () =>
  approx(convert("area", "tatami", "tsubo", 2), 1, 1e-3));

「畳 2 枚 = 1 坪」が変換で出るのは、データに正しい係数が入っている証拠。

設計

units.js   ← 単位カタログ (係数 + アフィン関数) (DOM-free)
convert.js ← 変換エンジン + 数値フォーマット (DOM-free, 40 tests)
app.js     ← UI glue

UI はカテゴリタブ + 2 つのドロップダウン + 「全単位への変換」テーブル。convert.js は DOM 非依存なので 40 個のテストが全部 Node で走る。

試してみる

温度タブで -40 を入れると、摂氏も華氏も -40 になる交差点が見られる。データ量タブで 1 GB と 1 GiB の差 (約 7.4%) も。

まとめ

  • 線形単位は基準への係数 1 つで表すと N×N 変換表が消える。値 × from係数 ÷ to係数
  • 温度はアフィン変換 (y = a·x + b)。原点がずれている (0°C ≠ 0°F) ので係数では表せず、toBase/fromBase 関数を持たせる。
  • 構造は線形・アフィンとも「基準経由の 1 本道」で揃え、係数の掛け算を関数呼び出しに置き換えるだけ。
  • 0°C→F が 0 ではない」を回帰テストにすると、温度を線形に戻す事故を防げる。
  • カタログ駆動ならカタログ自体の不変条件 (基準係数=1、往復一致) もテストする。
  • KB/KiB、尺貫法など「単位の細かい違い」こそ汎用ツールの価値。

これは SEN 合同会社の OSS ポートフォリオ #265 です。https://sen.ltd/portfolio/

1
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
1
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?