ST_FUNC開発日記

建築構造設計Excelアドイン開発の記録

単位変換関数の作成

前回話したように単位を変換する必要があるため、単位変換の関数を作成する。

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で普通にに計算してても、桁落ちって悩むよね。四捨五入にするか切り捨てにするかとか。

電卓での計算って基本桁落ちさせるから、Excelと結果が見た目乗変わったりしちゃったり面倒である。