前回話したように単位を変換する必要があるため、単位変換の関数を作成する。
ExcelにはConvert関数があるし、ほかにもライブラリはいろいろありそうだが、できるだけブラックボックスを減らすという方針なので、単位変換も自作する。
(個人的には自分で作った変換関数よりmicrosoftさんのほうを信用するが)
単位変換の方法は、とりあえずchatGPTさんに聞いてみるとこんな感じ。
単位を変換する TypeScript の関数を作成する方法はいくつかありますが、最も汎用的な方法は、関数内で単位変換のロジックを実装し、異なる単位間の変換を行うための switch 文を使用することです。以下はその例です。
function convertUnit(value: number, fromUnit: string, toUnit: string): number | string { // 各単位の変換係数を定義する(例:メートルからフィートへの変換係数) const conversionFactors: { [unit: string]: number } = { 'm': 1, // メートル 'cm': 0.01, // センチメートル 'mm': 0.001, // ミリメートル 'km': 1000, // キロメートル 'in': 0.0254, // インチ 'ft': 0.3048, // フィート // 他の単位についても追加可能 }; // 変換前の単位と変換後の単位が存在しない場合はエラーメッセージを返す if (!conversionFactors[fromUnit] || !conversionFactors[toUnit]) { return "Invalid units"; } // 変換係数を使用して値を変換する const result = value * (conversionFactors[fromUnit] / conversionFactors[toUnit]); return result; } // 使用例 console.log(convertUnit(10, 'm', 'ft')); // 32.80839895013123 console.log(convertUnit(100, 'cm', 'in')); // 39.37007874015748 console.log(convertUnit(1, 'km', 'm')); // 1000 console.log(convertUnit(500, 'mm', 'km')); // 0.0005 console.log(convertUnit(1, 'lb', 'kg')); // Invalid units
この関数は、指定された単位間で数値を変換します。単位の変換係数を定義して、それに基づいて変換を行います。不明な単位が指定された場合はエラーメッセージを返します。関数の使用例も含まれています。
これを参考に作っていく。
方針
単位の変換係数を取得し、数値に欠ける方針はそのまま用いる。ただし、前回の方針を考えると今回は通常単位→一貫性のあるSI単位か、その逆しか必要ないため、Unit.inとUnit.outメソッドを作る。
変換係数は共通なのでプライベートメソッドUnit.factorとして作成する。
ただ、全ての単位を辞書型で作るのは面倒なので、工夫して作成する。
基本の単位
基本の単位についてはChatGPTさんの提案通り、辞書型で係数を取得する。
private static factor(unit: string): number { //基本単位の係数 const standardUnits: { [unit: string]: number } = { g: 0.001, //質量(kgを基本単位とするので、0.001とする) m: 1.0, //長さ N: 1.0, //力 s: 1.0, //時間 }; if (unit in standardUnits) { return standardUnits[unit]; }
接頭辞の処理をしたいので、ここでは質量はgだが、質量の基本単位はkgであるため、gは0.001とし、kgが1.0になるように調整する。
接頭辞の対応
接頭辞がついた単位については、接頭辞を辞書型に設定し、正規表現で接頭辞+基本単位に分割して、係数をかけ合わせる。
そういえば、接頭辞って一昨年増えたんだよね。
クエタ、ロナ、クエクト、ロント…国際単位系の接頭語に新しい仲間 | Science Portal - 科学技術の最新情報サイト「サイエンスポータル」
//接頭辞がついた場合の処理 const prefixs: { [prefix: string]: number } = { Q: 1.0e30, R: 1.0e27, Y: 1.0e24, Z: 1.0e21, E: 1.0e18, P: 1.0e15, T: 1.0e12, G: 1.0e9, M: 1.0e6, k: 1.0e3, h: 1.0e2, da: 1.0e1, d: 1.0e-1, c: 1.0e-2, m: 1.0e-3, μ: 1.0e-6, n: 1.0e-9, p: 1.0e-12, f: 1.0e-15, a: 1.0e-18, z: 1.0e-21, y: 1.0e-24, r: 1.0e-27, q: 1.0e-30, }; const keyStandardUnits = Object.keys(standardUnits).join("|"); const keyPrefixs = Object.keys(prefixs).join("|"); let regexPattern = new RegExp(`\^(${keyPrefixs})(${keyStandardUnits})\$`); let matchResult = unit.match(regexPattern); if (matchResult) { return prefixs[matchResult[1]] * standardUnits[matchResult[2]]; }
組立単位
分数
ばね定数のkN/mとか速度のm/s等で利用するための組立単位。
これも正規表現で分割して掛け合わせる。分割した単位の変換係数については再起呼び出しを使って行う。
本職はC#使いのため、static関数をUnit.factoreで呼び出そうとしてエラーになって焦った。typescriptは静的メソッドもthisで呼び出すんだね。不思議な感じ。
//組立単位 //分数の単位 regexPattern = /^([^\/]+)\/([^\/]+)$/; matchResult = unit.match(regexPattern); if (matchResult) { try { return this.factor(matchResult[1]) / this.factor(matchResult[2]); } catch (error) {} }
2つ以上/が出てくる単位はいったん未実装とする。計算順序とかいろいろ考えることが多そうなので・・・。
のちのち減衰係数とかを使うとkN/(m/s)とか必要になってくるんだが・・・。あまり出てこないのでいったん無視。
累乗
面積や断面二次モーメントで多用するのでこちらも必須。分数と同じ考え方で実装。
//累乗の単位 regexPattern = /^([^\^]+)\^(\d+)$/; matchResult = unit.match(regexPattern); if (matchResult) { try { return this.factor(matchResult[1]) ** parseInt(matchResult[2]); } catch (error) {} }
気を付けないといけないのは、計算順序が累乗→分数なので、プログラムは分数→累乗の順に処理する必要がある。
(N/mm2のときにN/mmを先に計算してから2乗してしまわないよう、Nとmm2にしてから係数を取得したいため)
例外処理
最後に例外処理。ここまで来てまだ取得できないのは未実装ですよと。
throw new Error(`[${unit}]は対応していない単位です。`); }
分数や累乗のところの再起呼び出し部分でエラーをcatchしているのは、それをしないと(N/変な単位)みたいな単位を見たときのエラーメッセージが分割後の単位を表示してしまうため。
変換メソッド
変換係数を取得するメソッドができたので、変換メソッドを実装。これはシンプル。
/** * 単位付き数値を計算用数値へ変換する * @param value 単位付き数値 * @param unit_from 単位 * @returns 計算用数値 */ static in(value: number, unit_from: string): number { return value * this.factor(unit_from); } /** * 計算用の数値を単位付き数値へ変換する * @param value 計算用数値 * @param unit_to 単位 * @returns 単位付き数値 */ static out(value: number, unit_to: string): number { return value / this.factor(unit_to); }
テスト
まずfactorでテスト。しかし、プライベート関数ってテストできるの?
とおもったらプライベート関数をテストするにはこういうほうがあるらしい。
Typescriptでprivateメソッドをテストする #TypeScript - Qiita
anyにキャストして無理やり呼び出せるんだね。
さて、適当にいろいろな単位を書いて、テストを実施してみたところ、エラーが。
FAIL src/unit.test.ts ● 単位の係数取得 expect(received).toBe(expected) // Object.is equality Expected: 1e-12 Received: 1.0000000000000002e-12 17 | //組立単位(累乗) 18 | expect(unitClass.factor("mm^2")).toBe(1.0e-6); > 19 | expect(unitClass.factor("mm^4")).toBe(1.0e-12); | ^ 20 | //組立単位(累乗+割り算併用) 21 | expect(unitClass.factor("N/mm^2")).toBe(1.0e6); 22 | expect(unitClass.factor("cm/s^2")).toBe(0.01); at Object.<anonymous> (src/unit.test.ts:19:36)
前回懸念点があるといったやつで、循環小数による桁ずれが発生してしまっている。
仕方がないので、テストでは誤差を設定する。toBeの代わりにtoBeCloseToという比較があるようだ。
誤差をどのくらいにするかだが、倍精度浮動小数点数の仮数部の桁が16桁なので、一番上の位から14桁くらいまでは正しい値だろう、と判断してみる。
ということで、1.0e-12の場合は12+14=26なので、小数第26桁を誤差とするみたいな方針にしようと思う。
2024/2/15追記
+14にすると数値によってはエラーになるかも。要検討
expect(unitClass.factor("mm^4")).toBeCloseTo(1.0e-12, 26);
また、build-h-function.test.tsの方が、もともとmm単位系でテストしていたが、一貫性のあるSI単位にするという方針のため、m系に直したテストに書き換える。
そうすると懸念した通り、断面二次モーメントのほうではテストが失敗になってしまった。
先ほどと同じ方針で許容誤差を設定してtoBeCloseToに変更。
断面積がエラーにならなかったのは循環小数にならない値だったからかな?
最後に
忘れずにindex.jsでexportしてライブラリのほうは完了。
単位変換クラスの追加 · st-func/st-func-ts@ddb12e0 · GitHub
ところで * IME君、静的を性的にするのやめてくれ(辞書登録しろよって話) * CharGPT君相変わらず神。正規表現考えるのがすごい楽
Excel関数のほうの修正
Unit.inとUnit.outを使う形に修正する。今回に関しては同じ次元の話なので、関数の挙動としてはかわらないはず。
だけど、やっぱり桁落ちによる誤差が出てしまったので、こちらのテストも許容誤差を設定してOKとした。
secBuildH関数で単位を指定する仕組みに修正 · st-func/st_func_addin@afafea3 · GitHub
Excelで普通にに計算してても、桁落ちって悩むよね。四捨五入にするか切り捨てにするかとか。