単位変換ツール

Prev Next

「単位変換ツール:質量」をダウンロードするには、以下にアクセスしてください:[tulip](https://<span translate=).co/library/apps/unit-converter--mass">Library

「単位変換ツール:体積」をダウンロードするには、[tulip.co/library/apps/unit-converter--volume">Library](https://<span translate=)にアクセスしてください。

この記事では、当社のライブラリ開発者の一人であるタマシュが、ライブラリで利用可能な「単位変換」ビルディングブロックアプリの開発における判断基準について解説します。

なぜテーブルではなくオブジェクトリストを採用したのか

こんにちは、Tulipのタマスです。

この単位変換コンポーネントを構築する際、私が最初に下したアーキテクチャ上の決定の一つは、テーブルを使用しないことでした。その代わりに、すべてをデフォルトのコンポーネント変数として初期化されたオブジェクトリスト内に格納することを選びました**。**

その理由は単純です。この文脈における単位変換は、操作対象となるデータではなく、数学的な定数のように振る舞うからです。 関係性は純粋に乗算的であり、めったに変化せず、実行時の編集、集計、フィルタリング、あるいはリレーショナルクエリを必要としません。テーブルを導入した場合、以下のことが必要になったでしょう:* 追加のインフラストラクチャの作成と維持、* クエリやaggregations の設定、* コンポーネントを使用するすべてのアプリに別の依存関係レイヤーを追加すること。

それにより、機能は向上しないまま複雑さが増すことになっていたでしょう。

そこで代わりに、データセットをコンポーネント内に直接埋め込みました:* 標準ベースデータセット: * _lengthToBaseDefaults * _massToBaseDefaults * _volumeToBaseDefaults

  • 同義語辞書:
    • _lengthUnitAliases
    • _massUnitAliases
    • _volumeUnitAliases

これらはすべてデフォルトの変数値として扱われます。これにより、コンポーネントは以下の特性を維持できます:* 移植性が高い* 自己完結型* 決定論的* アプリ間のコピーが容易

データセットが定数のように振る舞う場合、私はそれを外部化するのではなく、コンポーネント内で保護することを好みます。

なぜテーブルではないのか?

テーブルベースの解決策を採用すると、この問題の性質に見合わない構造的な依存関係が生じてしまいます。

単位の定義はトランザクションデータではありません。それらは構造的な定義です。それらを操作的なレコードのように扱うことは、問題の規模に対して不釣り合いです。

ここでの私の目標は、アーキテクチャ上の比例性でした。つまり、問題が真に必要とする分だけのインフラストラクチャのみを使用することです。

設計原則:保護されたロジック、柔軟な構成

このコンポーネントは、次の2つの核心的な考え方に基づいています:1. 標準的な基本正規化 2. 同義語解決による入力許容性

私は意図的に、数学的な正しさとユーザーの柔軟性を分離しました。* 標準ベースのデータセットは、数学的な関係を定義します。* 同義語辞書は、柔軟なユーザー入力がどのように解釈されるかを定義します。

どちらのレイヤーも設計の一部であり、後付けの要素ではありません。

標準ベースモデルでは、各単位はそのドメイン内の単一の参照単位にマッピングされます。すべての変換は、その参照単位を経由します。これにより、単一の正規化パスが形成され、すべての単位が同じ数学的基準を共有することが保証されます。

ドメインの基底単位

ドメインごとに基本単位を明示的に固定しました:* 長さ → m* 質量 → kg* 体積 → L

これらは意図的な選択です:* これらは業界標準です。* 丸め誤差を最小限に抑えます。* エンジニアにとってシステムが直感的に理解できるようになります。

正規化単位キー(サポートされる単位)

すべてのエイリアスのマッピングは、これらの正規キーのいずれかに解決されなければなりません。

  • **質量(基本単位:kg)**µg, mg, g, kg, t, lb, oz
  • **長さ(基本単位:m)**mm, cm, m, km, in, ft, yd, mi
  • **体積(基本単位:L)**ml、cl、µl、mm3、cm3、l、m3、fl_oz_us、pt_us、qt_us、gal_us

各ドメインには、独自の基本データセット・オブジェクト・リストがあります。各エントリは、単位とその factorToBase を定義します。

変換は常に同じパターンに従います:* 入力 → 基本単位* 基本単位 → 変換先

分岐なし。ペアの維持なし。

保護された内部インターフェース

意図しないロジックの変更を防ぐため、コア変数を隔離しました:* _inputValue* _inputUnit* _targetUnit* _conversionResult

コアエンジンはこれらにのみ依存しています。

これを2つの薄い設定レイヤーが囲んでいます:*01 – ユーザー設定:入力ステージ外部ソースを内部変数にマッピングします。*03 – ユーザー設定:出力ステージ計算結果を外部へ出力します。

これにより、計算コアを安定させつつ、柔軟な統合が可能になります。

トリガーの内訳 – MAGICの仕組み

保護された MAGIC トリガーは、2 つの論理演算を実行します:1. 単位の同義語を解決する 2. 標準ベースの正規化を適用する

エイリアス解決層

各ドメインには同義語辞書があります。例:* _lengthUnitAliases

各オブジェクトには以下が含まれます:* raw* canonical

例:{ "raw": "mm", "canonical": "mm" }{ "raw": "millimeter", "canonical": "mm" }{ "raw": "m³", "canonical": "m3" }これにより、複数の表記形式を単一の正規化キーにマッピングできます。重要な点として、ベースデータセット内でのエントリの重複は発生していません。同義語レイヤーは、数学的な定義を増やしすぎることなく、入力のばらつきを吸収するためにのみ存在します。

正規化基本式

正規化単位が確定すると、式は次のようになります:result = inputValue × factorToBase(canonicalInput) ÷ factorToBase(canonicalTarget)

トリガーの例(長さドメイン)

(@Variable._inputValue * array_value_at_index( map_to_number_list(@Variable._lengthToBaseDefaults , 'factorToBase'),      array_index_of( map_to_text_list(@Variable._lengthToBaseDefaults , 'unit'), array_value_at_index( map_to_text_list(@Variable._lengthUnitAliases , 'canonical'),          array_index_of( map_to_text_list(@Variable._lengthUnitAliases , 'raw'), @Variable._inputUnit ) ) )    ))/array_value_at_index( map_to_number_list(@Variable._lengthToBaseDefaults , 'factorToBase'), array_index_of( map_to_text_list(@Variable._lengthToBaseDefaults , 'unit'),    array_value_at_index( map_to_text_list(@Variable._lengthUnitAliases , 'canonical'),      array_index_of( map_to_text_list(@Variable._lengthUnitAliases , 'raw'), @Variable._targetUnit ) ) ))構造的には、これは階層化されたロジックです:* raw → canonical への変換* canonical → factor への変換* ベース単位による正規化

一見複雑に見えますが、エンジンを自己完結型に保ち、中間状態を排除するために意図的にインライン化されています。

この構造が選ばれた理由

3つの保証が欲しかったからです:* ペアの爆発が起こらないこと* 複数のパスによる丸め誤差が生じないこと* 単位を拡張する際にトリガーの書き換えが発生しないこと

正規化されたベースモデルは、これら3つすべてを満たしてくれます。新しいユニットを追加する際は、factorToBaseエントリを正確に1つ追加します。エンジン自体は変更されません。新しい記述形式をサポートする際は、エイリアスエントリを正確に1つ追加します。計算式は変更されません。この分離こそが、アーキテクチャ上の核心的な決定事項です。

マルチドメインでの再利用:同じエンジン、異なるデータセット

もう一つの意図的な決定:エンジンロジックはドメインに依存しない。長さ、質量、体積は同じ構造的トリガーを共有する。変化するのはデータセット変数だけだ。3つの異なるコンバータを構築する代わりに、1つの標準的なエンジンを構築し、データセットにドメインを定義させた。これにより、認知的負荷が軽減され、コンポーネントの挙動が予測可能になる。

なぜスケーラブルなのか

スケーリングはデータセットレベルで行われます。単位を追加する際、以下の作業は不要です:* トリガー式の変更、* 条件分岐の追加、* ロジックの複製。

アルゴリズムは安定したままです。進化するのはデータセットの方です。

コンポーネントの拡張

新しい標準単位の追加

  • 関連する _ToBaseDefaults データセットを拡張します。
  • unit および factorToBase を追加します。
  • トリガーは変更しないでください。

新しい表記形式の追加

  • 関連する _UnitAliases 辞書を拡張します。
  • raw と、それに対応する canonical キーを追加します。
  • ベースデータセットは変更しないでください。

これは意図的なものです。数学的な定義を増やしすぎることなく、ユーザーフレンドリーな柔軟性を実現したかったからです。

エラー処理(セーフルックアップパターン)

2つの失敗ケースが存在します:1. エイリアス辞書に「raw」単位が見つからない場合 2. ベースデータセットに正規単位が見つからない場合

いずれかのルックアップが失敗した場合、計算は実行されません。

これにより、以下が保証されます:* 古い値が存在しない* 気付かれない誤計算がない* 決定論的な動作

なぜ単位と係数を別々の配列にしないのか?

単位と因子を別々の配列に格納すると、位置依存性のリスクが生じます。一方のリストの順序が変わり、もう一方が変わらない場合、システムは黙って動作しなくなります。単位とfactorToBaseを構造化されたオブジェクト内に一緒に格納することで、私はその構造的な脆弱性を排除しました。

中核となる哲学

このコンポーネントは、意図的に構造をシンプルにし、動作を厳格に設計しています。* 計算処理は一元化されています。* エンジンは保護されています。* 柔軟性は周辺部分に存在します。

サポートされる単位セットが安全に進化できる一方で、ロジックが長期にわたり安定するよう設計しました。エンジン自体は変更されず、データセットが拡張されていきます。

参加する

コミュニティに参加して、改善案の共有、追加単位の提案、問題の報告、あるいはアーキテクチャ上の決定事項について議論しましょう