DEV Community

Cover image for 국제 데이터를 위한 숫자 정규화: 실용 가이드
circobit
circobit

Posted on

국제 데이터를 위한 숫자 정규화: 실용 가이드

독일 웹사이트에서 테이블을 내보냅니다. 매출 열에 "1.234,56 €"가 표시됩니다.

Excel에 붙여넣습니다. "1.23456E"가 되거나, 더 나쁜 경우 날짜가 됩니다.

국제 숫자 형식 문제에 오신 것을 환영합니다.

핵심 문제: "1.234"는 무엇을 의미하는가?

미국과 영국: 1.234 = 일 점 이삼사 (소수)

독일, 스페인, 대부분의 라틴 아메리카: 1.234 = 천이백삼십사 (천 단위)

같은 문자. 완전히 다른 값.

국제 웹사이트에서 데이터를 추출할 때, 어떤 형식을 다루고 있는지 가정할 수 없습니다. 그리고 잘못 판단하면 분석이 자릿수 단위로 틀어집니다.

세 자리 모호성

구분자 뒤에 정확히 세 자리가 있는 경우가 가장 어렵습니다.

입력 미국식 해석 유럽식 해석
1.234 1.234 (소수) 1,234 (천 단위)
1,234 1,234 (천 단위) 1.234 (소수)
1.234.567 유효하지 않음 1,234,567
1,234,567 1,234,567 유효하지 않음

뒤에 두 자리면 명확히 소수: "1.23" 또는 "1,23".
뒤에 네 자리 이상이면 명확히 소수: "3.14159".
세 자리면? 어느 쪽이든 될 수 있습니다.

감지 알고리즘

대부분의 실제 상황을 처리하는 휴리스틱 접근 방식:

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" 또는 "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" 또는 "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

세 자리 = 천 단위인 이유

핵심 결정: 단일 구분자 뒤에 정확히 세 자리가 있을 때 ("1.234"처럼), 소수가 아닌 천 단위로 해석합니다.

이유는? HTML 테이블에서:

  • 천 단위가 있는 재무 데이터가 매우 일반적: $1.234, €1.234
  • 정확히 세 자리 소수는 실제로 드묾
  • 세 자리 소수가 있는 과학 데이터 (예: "3.141")는 보통 "3.14159"나 "3.14"로 작성

이 휴리스틱은 대부분의 실제 테이블에서 작동하는 실용적 타협입니다.

전체 형식 매트릭스

입력 해석 정규화된 출력
1.234,56 유럽식 소수 1234.56
1,234.56 미국식 소수 1234.56
1.234.567,89 유럽식 천 단위 + 소수 1234567.89
1,234,567.89 미국식 천 단위 + 소수 1234567.89
1.234 천 단위 1234
1,234 천 단위 1234
3.14 소수 3.14
3,14 소수 3.14
3.14159 소수 (4자리 이상) 3.14159
$ 1.200,50 통화 + 유럽식 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. 하나의 열에 혼합된 형식: 일부 행은 유럽식, 일부는 미국식

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 엔티티 주의

웹 테이블에는 숫자 안에 &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"

세 자리의 경우가 실용적 타협입니다. 대부분의 실제 데이터에서 작동합니다. 확신이 없을 때는 소스 지역에 맞는 내보내기 프로필을 사용하세요.

웹사이트에서 Excel로 서식 문제 없이 데이터를 복사하는 방법에 대해 자세히 알아보려면 웹사이트에서 Excel로 테이블 복사하기 가이드를 참조하세요.


추측 없이 깔끔한 숫자 내보내기가 필요하신가요? gauchogrid.com/ko/html-table-exporter에서 자세히 알아보거나 Chrome 웹 스토어에서 무료로 사용해 보세요.

Top comments (0)