Use Cases

Implementing Currency Formatting with Claude Code

Learn about implementing currency formatting using Claude Code. Includes practical code examples.

通貨フォーマットをClaude Codeで正しく実装する

ECサイト、会計アプリ、国際対応サービスでは、通貨の正確なフォーマットが不可欠です。小数点、桁区切り、通貨記号のルールは国・通貨ごとに異なるため、自前でフォーマットすると不具合の温床になります。Claude Codeを使えば、Intl.NumberFormatや専用ライブラリを活用した堅牢な実装を効率的に構築できます。

Intl.NumberFormatの活用

> 多通貨対応の通貨フォーマットユーティリティを作って。
> 日本円、USドル、ユーロに対応して。
> ロケール別表示、為替変換、金額のバリデーションも含めて。
// src/lib/currency.ts

export type CurrencyCode = 'JPY' | 'USD' | 'EUR' | 'GBP' | 'CNY' | 'KRW';

interface FormatOptions {
  currency: CurrencyCode;
  locale?: string;
  compact?: boolean;      // "1.2万" のような短縮表示
  showSign?: boolean;     // +/- 符号を表示
}

/**
 * 通貨をフォーマットする
 */
export function formatCurrency(
  amount: number,
  options: FormatOptions
): string {
  const { currency, locale = 'en-US', compact = false, showSign = false } = options;

  const formatter = new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
    notation: compact ? 'compact' : 'standard',
    signDisplay: showSign ? 'exceptZero' : 'auto',
    // 小数通貨は小数点2桁、JPY/KRWは0桁(自動判定)
  });

  return formatter.format(amount);
}

// Usage example
formatCurrency(1234567, { currency: 'JPY' });          // "¥1,234,567"
formatCurrency(1234.56, { currency: 'USD' });           // "$1,234.56"
formatCurrency(1234.56, { currency: 'EUR', locale: 'de-DE' }); // "1.234,56 €"
formatCurrency(1234567, { currency: 'JPY', compact: true });    // "¥123万"

通貨計算の注意点

// src/lib/currency-math.ts

/**
 * 浮動小数点の誤差を避けるため、整数(最小通貨単位)で計算する
 * JPY: 1円 = 1 | USD: 1セント = 1 | EUR: 1セント = 1
 */
export class Money {
  constructor(
    private readonly amountInMinorUnit: number,
    private readonly currency: CurrencyCode
  ) {}

  // セント/円の小数桁数
  private static decimals(currency: CurrencyCode): number {
    return ['JPY', 'KRW'].includes(currency) ? 0 : 2;
  }

  static fromMajorUnit(amount: number, currency: CurrencyCode): Money {
    const decimals = Money.decimals(currency);
    const minor = Math.round(amount * Math.pow(10, decimals));
    return new Money(minor, currency);
  }

  toMajorUnit(): number {
    const decimals = Money.decimals(this.currency);
    return this.amountInMinorUnit / Math.pow(10, decimals);
  }

  add(other: Money): Money {
    this.assertSameCurrency(other);
    return new Money(this.amountInMinorUnit + other.amountInMinorUnit, this.currency);
  }

  subtract(other: Money): Money {
    this.assertSameCurrency(other);
    return new Money(this.amountInMinorUnit - other.amountInMinorUnit, this.currency);
  }

  multiply(factor: number): Money {
    return new Money(Math.round(this.amountInMinorUnit * factor), this.currency);
  }

  format(locale?: string): string {
    return formatCurrency(this.toMajorUnit(), { currency: this.currency, locale });
  }

  private assertSameCurrency(other: Money) {
    if (this.currency !== other.currency) {
      throw new Error(`通貨が異なります: ${this.currency} と ${other.currency}`);
    }
  }
}

// Usage example
const price = Money.fromMajorUnit(1980, 'JPY');
const tax = price.multiply(0.1);
const total = price.add(tax);
console.log(total.format()); // "¥2,178"

為替レート変換

// src/lib/exchange.ts
interface ExchangeRates {
  base: CurrencyCode;
  rates: Record<CurrencyCode, number>;
  updatedAt: string;
}

let cachedRates: ExchangeRates | null = null;

export async function getExchangeRates(base: CurrencyCode = 'JPY'): Promise<ExchangeRates> {
  // キャッシュが1時間以内なら再利用
  if (cachedRates && Date.now() - new Date(cachedRates.updatedAt).getTime() < 3600000) {
    return cachedRates;
  }

  const res = await fetch(`https://api.exchangerate-api.com/v4/latest/${base}`);
  const data = await res.json();

  cachedRates = {
    base,
    rates: data.rates,
    updatedAt: new Date().toISOString(),
  };

  return cachedRates;
}

export async function convertCurrency(
  amount: number,
  from: CurrencyCode,
  to: CurrencyCode
): Promise<{ converted: number; rate: number }> {
  const rates = await getExchangeRates(from);
  const rate = rates.rates[to];

  return {
    converted: Math.round(amount * rate * 100) / 100,
    rate,
  };
}

通貨入力コンポーネント

// src/components/CurrencyInput.tsx
'use client';
import { useState } from 'react';

interface Props {
  value: number;
  currency: CurrencyCode;
  onChange: (value: number) => void;
}

export function CurrencyInput({ value, currency, onChange }: Props) {
  const [displayValue, setDisplayValue] = useState(value.toLocaleString());

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const raw = e.target.value.replace(/[^0-9.-]/g, '');
    setDisplayValue(e.target.value);
    const num = parseFloat(raw);
    if (!isNaN(num)) onChange(num);
  };

  const handleBlur = () => {
    setDisplayValue(value.toLocaleString());
  };

  const symbol = new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency,
    currencyDisplay: 'narrowSymbol',
  }).formatToParts(0).find((p) => p.type === 'currency')?.value || '';

  return (
    <div className="relative">
      <span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">{symbol}</span>
      <input
        type="text"
        value={displayValue}
        onChange={handleChange}
        onBlur={handleBlur}
        className="w-full border rounded-lg pl-8 pr-4 py-2 text-right"
        inputMode="decimal"
      />
    </div>
  );
}

浮動小数点の落とし穴

JavaScriptで0.1 + 0.2 === 0.30000000000000004となる問題は有名です。通貨計算では必ず最小単位(セント/円)の整数で扱い、表示時にのみ小数に変換するのが鉄則です。

関連記事

日付のフォーマットは日付・時間の扱い、国際化全般はi18n実装ガイドを参照してください。

Intl.NumberFormatの詳細仕様はMDN(developer.mozilla.org/Intl/NumberFormat)で確認できます。

#Claude Code #currency #フォーマット #Intl #internationalization