DEV Community

Cover image for 国際データの数値正規化:実践ガイド
circobit
circobit

Posted on

国際データの数値正規化:実践ガイド

ドイツのWebサイトからテーブルをエクスポートした。売上列には「1.234,56 €」と表示されている。

それをExcelに貼り付けると、「1.23456E」になるか、もっと悪い場合は日付になる。

国際的な数値フォーマット問題へようこそ。

根本的な問題:「1.234」とは何を意味するか?

米国と英国では:1.234 = 1.234(小数)

ドイツ、スペイン、ラテンアメリカの大部分では:1.234 = 1234(千の位の区切り)

同じ文字。まったく異なる値。

国際的なWebサイトからデータを抽出する際、どちらのフォーマットか仮定することはできません。間違えると、分析結果が桁違いにずれます。

3桁の曖昧さ

最も難しいケースは、区切り文字の後にちょうど3桁ある場合です。

入力 US式の解釈 EU式の解釈
1.234 1.234(小数) 1,234(千の位)
1,234 1,234(千の位) 1.234(小数)
1.234.567 無効 1,234,567
1,234,567 1,234,567 無効

区切り文字の後が2桁なら、明らかに小数:「1.23」や「1,23」。
4桁以上なら、明らかに小数:「3.14159」。
3桁の場合は?どちらにもなりえます。

検出アルゴリズム

ほとんどの実世界のケースに対応するヒューリスティックアプローチ:

function normalizeNumberString(value) {
  if (typeof value !== "string") return value;
  const v = value.trim();
  if (!v || !/[0-9]/.test(v)) return value;

  // ステップ1:通貨記号、パーセント、空白をクリーニング
  let cleaned = v
    .replace(/ /g, " ")
    .replace(/^(USD|EUR|GBP|R\$|MXN)\s*/i, "")
    .replace(/[$€£¥₹₽₩]/g, "")
    .replace(/%/g, "")
    .replace(/\s+/g, "");

  // ステップ2:マイナス記号の処理
  const isNegative = cleaned.match(/^-/);
  if (isNegative) {
    cleaned = cleaned.substring(1).trim();
  }

  // ステップ3:純粋な数字のみ?完了
  if (/^\d+$/.test(cleaned)) {
    return isNegative ? `-${cleaned}` : cleaned;
  }

  // ステップ4:区切り文字を検出
  const hasComma = cleaned.includes(",");
  const hasDot = cleaned.includes(".");

  // ステップ5:ヒューリスティックを適用
  let decimalSeparator = ".";
  let thousandsSeparator = ",";

  if (hasComma && hasDot) {
    // 両方存在:最後のものが小数点
    const lastComma = cleaned.lastIndexOf(",");
    const lastDot = cleaned.lastIndexOf(".");

    if (lastComma > lastDot) {
      decimalSeparator = ",";
      thousandsSeparator = ".";
    }
  } else if (hasComma && !hasDot) {
    const parts = cleaned.split(",");

    if (parts.length === 2 && parts[1].length <= 2) {
      // "1,23" → 小数
      decimalSeparator = ",";
    } else {
      // "1,234" or "1,234,567" → 千の位
      decimalSeparator = null;
      thousandsSeparator = ",";
    }
  } else if (hasDot && !hasComma) {
    const parts = cleaned.split(".");

    if (parts.length === 2) {
      const afterDot = parts[1];

      if (afterDot.length <= 2 || afterDot.length >= 4) {
        // "1.23" or "3.14159" → 小数
        decimalSeparator = ".";
      } else {
        // "1.234" → 千の位(テーブル向けの実用的な選択)
        decimalSeparator = null;
        thousandsSeparator = ".";
      }
    } else {
      // 複数のドット → 千の位
      decimalSeparator = null;
      thousandsSeparator = ".";
    }
  }

  // ステップ6:千の位区切りを除去
  if (thousandsSeparator) {
    cleaned = cleaned.replace(new RegExp(`\\${thousandsSeparator}`, "g"), "");
  }

  // ステップ7:小数点をドットに正規化
  if (decimalSeparator && decimalSeparator !== ".") {
    cleaned = cleaned.replace(decimalSeparator, ".");
  }

  // ステップ8:検証
  const num = Number(cleaned);
  if (!Number.isFinite(num)) return value;

  return isNegative ? `-${cleaned}` : cleaned;
}
Enter fullscreen mode Exit fullscreen mode

なぜ3桁 = 千の位なのか

重要な判断:区切り文字の後にちょうど3桁ある場合(例:「1.234」)、小数ではなく千の位として解釈します。

なぜか?HTMLテーブルでは:

  • 千の位を含む金融データは非常に一般的:$1.234、€1.234
  • ちょうど3桁の小数は実際にはまれ
  • 3桁の小数を持つ科学的データ(例:「3.141」)は通常「3.14159」または「3.14」と書かれる

このヒューリスティックは、実世界のテーブルの大多数に対して機能する実用的なトレードオフです。

完全なフォーマット対応表

入力 解釈 正規化後の出力
1.234,56 EU式小数 1234.56
1,234.56 US式小数 1234.56
1.234.567,89 EU式千の位 + 小数 1234567.89
1,234,567.89 US式千の位 + 小数 1234567.89
1.234 千の位 1234
1,234 千の位 1234
3.14 小数 3.14
3,14 小数 3.14
3.14159 小数(4桁以上) 3.14159
$ 1.200,50 通貨 + EU式 1200.50
-5.000 負の千の位 -5000
12.5% パーセンテージ 12.5

通貨コードの処理

国際データには通貨の接頭辞と接尾辞が付きます。先にクリーニングしましょう:

// 記号の前に通貨コードを除去
.replace(/^(USD|EUR|GBP|JPY|CHF|CAD|AUD|CNY|INR|BRL|R\$|MXN|KRW)\s*/i, "")
// その後に記号を除去
.replace(/[$€£¥₹₽₩₪฿₫₴₦]/g, "")
Enter fullscreen mode Exit fullscreen mode

順序が重要です:「R$」(ブラジルレアル)には「$」が含まれるため、通貨コードを先に除去することで部分一致を防ぎます。

ヒューリスティックが失敗する場合

完璧なヒューリスティックはありません。以下の場合に不正確な結果になります:

  1. ちょうど3桁の小数を持つ科学データ:「3.141」が「3141」になる
  2. 3桁の小数を持つ10ドル未満の価格:「$1.234」が「$1234」になる
  3. 1つの列にフォーマットが混在:一部の行がEU式、一部がUS式

ケース3では、列レベルのフォーマット検出が必要です:

function detectColumnFormat(values) {
  let euIndicators = 0;
  let usIndicators = 0;

  for (const v of values) {
    if (/\d\.\d{3},\d{2}$/.test(v)) euIndicators++;
    if (/\d,\d{3}\.\d{2}$/.test(v)) usIndicators++;
  }

  if (euIndicators > usIndicators) return "eu";
  if (usIndicators > euIndicators) return "us";
  return "auto"; // セルごとのヒューリスティックを使用
}
Enter fullscreen mode Exit fullscreen mode

地域別のエクスポートプロファイル

ソース国がわかっている場合、地域固有のプロファイルを使うことで曖昧さを完全に回避できます:

  • ヨーロッパ式:カンマを小数点として仮定、CSVのデリミタにセミコロンを使用
  • 米国/英国式:ドットを小数点として仮定、CSVのデリミタにカンマを使用

HTML Table Exporterには両方のフォーマット用のプリセットプロファイルと、PandasやDuckDB用の専用プロファイルが含まれています。

よくある落とし穴

精度を失わない

// 間違い:大きな数値で精度を失う
const num = parseFloat("12345678901234567890");
// 12345678901234567000(JavaScriptの限界)

// 正しい:最終計算まで文字列として保持
const cleaned = normalizeNumberString(value);
// 計算が必要な時だけparseFloat
Enter fullscreen mode Exit fullscreen mode

HTMLエンティティに注意

Webテーブルには数値内に &nbsp;(ノーブレークスペース)が含まれることがあります:

// "1&nbsp;234&nbsp;567" は "1234567" になるべき
.replace(/&nbsp;/g, " ")
.replace(/\s+/g, "")
Enter fullscreen mode Exit fullscreen mode

不確実な場合は元の値を保持

値が数値に見えない場合は、変更せずに返す:

if (!/[0-9]/.test(v)) return value;
// ...
if (!Number.isFinite(num)) return value;
Enter fullscreen mode Exit fullscreen mode

「N/A」を「N/A」のまま残す方が、パースを試みるよりも良い。

まとめ

シナリオ 検出方法 備考
. と , の両方 最後のもの = 小数点 普遍的
カンマのみ、後ろ2桁以下 小数 "1,23"
カンマのみ、後ろ3桁以上 千の位 "1,234"
ドットのみ、後ろ2桁以下または4桁以上 小数 "1.23", "3.14159"
ドットのみ、後ろちょうど3桁 千の位 "1.234"

3桁のケースが実用的なトレードオフです。ほとんどの実データに対して機能します。迷ったら、ソース地域に合ったエクスポートプロファイルを使いましょう。

WebサイトからExcelにデータをフォーマットの問題なくコピーする方法については、WebサイトからExcelにテーブルをコピーする方法のガイドをご覧ください。


推測なしでクリーンな数値エクスポートが必要ですか?gauchogrid.com/ja/html-table-exporterで詳細を確認するか、Chrome ウェブストアで無料でお試しください。

Top comments (0)