Skip to content
VersionSize

currency

Formats a monetary amount as a currency string with proper locale and symbol. Handles decimal places automatically based on currency.

Implementation

View Source Code
ts
import type { Money } from './types';

/**
 * Options for currency formatting.
 */
export type CurrencyFormatOptions = {
  locale?: string; // BCP 47 language tag (e.g., 'en-US', 'de-DE')
  style?: 'symbol' | 'code' | 'name'; // Display style
  minimumFractionDigits?: number; // Minimum decimal places
  maximumFractionDigits?: number; // Maximum decimal places
};

/**
 * Formats a monetary amount as a currency string with proper locale and symbol.
 * Handles decimal places automatically based on currency.
 *
 * @example
 * ```ts
 * const money = { amount: 123456n, currency: 'USD' };
 *
 * currency(money); // '$1,234.56' (default en-US)
 * currency(money, { locale: 'de-DE' }); // '1.234,56 $'
 * currency(money, { style: 'code' }); // 'USD 1,234.56'
 * currency(money, { style: 'name' }); // '1,234.56 US dollars'
 * ```
 *
 * @param money - Money object to format
 * @param options - Formatting options
 * @returns Formatted currency string
 */
export function currency(money: Money, options: CurrencyFormatOptions = {}): string {
  const { locale = 'en-US', style = 'symbol', minimumFractionDigits, maximumFractionDigits } = options;

  // Get decimal places for currency (default to 2 for most currencies)
  const decimalPlaces = getCurrencyDecimals(money.currency);

  // Convert bigint amount to decimal (divide by 10^decimalPlaces)
  const divisor = 10 ** decimalPlaces;
  const amount = Number(money.amount) / divisor;

  // Determine Intl.NumberFormat style
  let currencyDisplay: 'symbol' | 'code' | 'name';
  switch (style) {
    case 'symbol':
      currencyDisplay = 'symbol';
      break;
    case 'code':
      currencyDisplay = 'code';
      break;
    case 'name':
      currencyDisplay = 'name';
      break;
    default:
      currencyDisplay = 'symbol';
  }

  const formatter = new Intl.NumberFormat(locale, {
    currency: money.currency,
    currencyDisplay,
    maximumFractionDigits: maximumFractionDigits ?? decimalPlaces,
    minimumFractionDigits: minimumFractionDigits ?? decimalPlaces,
    style: 'currency',
  });

  return formatter.format(amount);
}

/**
 * Gets the number of decimal places for a currency.
 * Most currencies use 2 decimal places, but some use 0 or 3.
 */
function getCurrencyDecimals(currencyCode: string): number {
  const zeroDecimalCurrencies = [
    'BIF',
    'CLP',
    'DJF',
    'GNF',
    'JPY',
    'KMF',
    'KRW',
    'MGA',
    'PYG',
    'RWF',
    'UGX',
    'VND',
    'VUV',
    'XAF',
    'XOF',
    'XPF',
  ];
  const threeDecimalCurrencies = ['BHD', 'IQD', 'JOD', 'KWD', 'LYD', 'OMR', 'TND'];

  if (zeroDecimalCurrencies.includes(currencyCode.toUpperCase())) {
    return 0;
  }
  if (threeDecimalCurrencies.includes(currencyCode.toUpperCase())) {
    return 3;
  }
  return 2; // Default for most currencies
}

Features

  • Locale-Aware: Formats according to user's locale preferences
  • Currency Symbols: Displays proper currency symbols ($, €, ¥, etc.)
  • Multiple Styles: Symbol, code, or name display
  • Auto-Decimals: Handles 0, 2, or 3 decimal currencies automatically
  • Type-Safe: Uses Money type for precision
  • Isomorphic: Works in both Browser and Node.js

API

Type Definitions
ts
/**
 * Represents a monetary amount with currency.
 * Amount is stored as bigint (minor units/cents) for precision.
 */
export type Money = {
  readonly amount: bigint; // Amount in minor units (e.g., cents for USD)
  readonly currency: string; // ISO 4217 currency code (e.g., 'USD', 'EUR')
};
ts
function currency(money: Money, options?: CurrencyFormatOptions): string;

Parameters

  • money: Money object { amount: bigint, currency: string }
    • amount: Amount in minor units (cents) as bigint
    • currency: ISO 4217 currency code (e.g., 'USD', 'EUR', 'JPY')
  • options: Optional formatting options
    • locale: BCP 47 language tag (default: 'en-US')
    • style: Display style - 'symbol', 'code', or 'name' (default: 'symbol')
    • minimumFractionDigits: Minimum decimal places
    • maximumFractionDigits: Maximum decimal places

Returns

  • Formatted currency string

Examples

Basic Formatting

ts
import { currency } from '@vielzeug/toolkit';

const money = { amount: 123456n, currency: 'USD' };

currency(money);
// '$1,234.56' (US format with symbol)

Different Locales

ts
import { currency } from '@vielzeug/toolkit';

const money = { amount: 123456n, currency: 'EUR' };

// US English
currency(money, { locale: 'en-US' });
// '€1,234.56'

// German
currency(money, { locale: 'de-DE' });
// '1.234,56 €'

// French
currency(money, { locale: 'fr-FR' });
// '1 234,56 €'

Display Styles

ts
import { currency } from '@vielzeug/toolkit';

const money = { amount: 100000n, currency: 'USD' };

// Symbol (default)
currency(money, { style: 'symbol' });
// '$1,000.00'

// Currency code
currency(money, { style: 'code' });
// 'USD 1,000.00'

// Full name
currency(money, { style: 'name' });
// '1,000.00 US dollars'

Zero-Decimal Currencies

ts
import { currency } from '@vielzeug/toolkit';

// Japanese Yen (no decimals)
const yen = { amount: 1234n, currency: 'JPY' };
currency(yen);
// '¥1,234'

// Korean Won (no decimals)
const won = { amount: 5000n, currency: 'KRW' };
currency(won);
// '₩5,000'

Three-Decimal Currencies

ts
import { currency } from '@vielzeug/toolkit';

// Kuwaiti Dinar (3 decimals)
const kwd = { amount: 123456n, currency: 'KWD' };
currency(kwd);
// 'KD 123.456'

// Bahraini Dinar (3 decimals)
const bhd = { amount: 10000n, currency: 'BHD' };
currency(bhd);
// 'BD 10.000'

Real-World Example: E-commerce Display

ts
import { currency } from '@vielzeug/toolkit';

const products = [
  { name: 'Laptop', price: { amount: 99999n, currency: 'USD' } },
  { name: 'Mouse', price: { amount: 2499n, currency: 'USD' } },
  { name: 'Keyboard', price: { amount: 7999n, currency: 'USD' } },
];

products.forEach((product) => {
  console.log(`${product.name}: ${currency(product.price)}`);
});
// Laptop: $999.99
// Mouse: $24.99
// Keyboard: $79.99

Multi-Currency Support

ts
import { currency } from '@vielzeug/toolkit';

const prices = {
  usd: { amount: 100000n, currency: 'USD' },
  eur: { amount: 85000n, currency: 'EUR' },
  gbp: { amount: 73000n, currency: 'GBP' },
  jpy: { amount: 11000n, currency: 'JPY' },
};

Object.entries(prices).forEach(([code, money]) => {
  console.log(`${code.toUpperCase()}: ${currency(money)}`);
});
// USD: $1,000.00
// EUR: €850.00
// GBP: £730.00
// JPY: ¥11,000

Negative Amounts

ts
import { currency } from '@vielzeug/toolkit';

const refund = { amount: -15000n, currency: 'USD' };

currency(refund);
// '-$150.00' or '($150.00)' depending on locale

Custom Fraction Digits

ts
import { currency } from '@vielzeug/toolkit';

const money = { amount: 100000n, currency: 'USD' };

// Force specific decimal places
currency(money, {
  minimumFractionDigits: 2,
  maximumFractionDigits: 3,
});
// '$1,000.00'

Implementation Notes

  • Amount Storage: Store amounts as bigint in minor units (cents) to avoid floating-point errors
  • Currency Codes: Use ISO 4217 currency codes (3-letter codes like 'USD', 'EUR')
  • Decimal Detection: Automatically determines decimal places based on currency
    • 0 decimals: JPY, KRW, VND, and others
    • 2 decimals: Most currencies (USD, EUR, GBP, etc.)
    • 3 decimals: BHD, KWD, OMR, JOD, and others
  • Intl.NumberFormat: Uses native browser/Node.js API for formatting
  • Locale Format: Follows BCP 47 standard (e.g., 'en-US', 'de-DE', 'fr-FR')
  • Symbol Position: Varies by locale ($ before in US, € after in some European locales)

See Also