Skip to Content
DocumentationAPICurrency

Currency

Comprehensive utilities for working with Indonesian Rupiah (IDR) currency.

Overview

The Currency module provides type-safe utilities for formatting, parsing, and converting Indonesian Rupiah. Built with zero dependencies and designed to handle common Indonesian business scenarios including invoicing, e-commerce, and financial reports.

Features

  • Formatting: Display Rupiah with proper separators and symbols
  • Compact Format: Indonesian-style abbreviations (ribu, juta, miliar, triliun)
  • Parsing: Convert formatted strings back to numbers
  • Terbilang: Convert numbers to Indonesian words (for checks, invoices, receipts)
  • Rounding: Clean currency amounts for display
  • Flexible: Customizable separators, precision, and display options

Why This Module?

Indonesian Rupiah formatting has unique requirements:

  • Uses . (dot) for thousands separator (not comma)
  • Uses , (comma) for decimal separator (not dot)
  • Has specific compact format units: ribu, juta, miliar, triliun
  • Requires “terbilang” (number-to-words) for legal documents
  • Follows Indonesian grammar rules for number pronunciation

Installation

npm install @indodev/toolkit

Quick Start

import { formatRupiah, formatCompact, toWords, parseRupiah } from '@indodev/toolkit/currency' // Format currency formatRupiah(1500000) // 'Rp 1.500.000' // Compact format formatCompact(1500000) // 'Rp 1,5 juta' // Convert to words (terbilang) toWords(1500000) // 'satu juta lima ratus ribu rupiah' // Parse back to number parseRupiah('Rp 1.500.000') // 1500000

API Reference

formatRupiah()

Formats a number as Indonesian Rupiah currency with customizable options.

Type Signature:

function formatRupiah(amount: number, options?: RupiahOptions): string

Parameters:

NameTypeDescription
amountnumberThe amount to format
optionsRupiahOptionsOptional formatting configuration

Returns:

string - Formatted Rupiah string

Options:

interface RupiahOptions { symbol?: boolean // Show 'Rp' symbol (default: true) decimal?: boolean // Show decimal places (default: false) separator?: string // Thousands separator (default: '.') decimalSeparator?: string // Decimal separator (default: ',') precision?: number // Decimal places (default: 0 or 2) spaceAfterSymbol?: boolean // Space after 'Rp' (default: true) }

Examples:

// Basic formatting formatRupiah(1500000) // 'Rp 1.500.000' formatRupiah(999) // 'Rp 999' formatRupiah(0) // 'Rp 0' // With decimals formatRupiah(1500000.50, { decimal: true }) // 'Rp 1.500.000,50' formatRupiah(1500000.5, { decimal: true, precision: 1 }) // 'Rp 1.500.000,5' formatRupiah(1500000, { decimal: true, precision: 2 }) // 'Rp 1.500.000,00' // Without symbol formatRupiah(1500000, { symbol: false }) // '1.500.000' formatRupiah(1500000.50, { symbol: false, decimal: true }) // '1.500.000,50' // Custom separators (international format) formatRupiah(1500000, { separator: ',', decimalSeparator: '.' }) // 'Rp 1,500,000' formatRupiah(1500000.50, { decimal: true, separator: ',', decimalSeparator: '.' }) // 'Rp 1,500,000.50' // No space after symbol formatRupiah(1500000, { spaceAfterSymbol: false }) // 'Rp1.500.000' // Negative numbers formatRupiah(-1500000) // 'Rp -1.500.000' formatRupiah(-1500000, { symbol: false }) // '-1.500.000' // Large numbers formatRupiah(1234567890) // 'Rp 1.234.567.890' formatRupiah(1234567890.12, { decimal: true }) // 'Rp 1.234.567.890,12'

Use Cases:

// E-commerce product display function ProductPrice({ price }: { price: number }) { return ( <div> <span className="currency"> {formatRupiah(price)} </span> </div> ) } // Invoice line items function InvoiceItem({ item }) { return ( <tr> <td>{item.name}</td> <td>{item.quantity}</td> <td>{formatRupiah(item.price, { symbol: false })}</td> <td>{formatRupiah(item.total)}</td> </tr> ) } // Payment summary with decimals function PaymentSummary({ amount }) { const tax = amount * 0.11 // 11% PPN const total = amount + tax return ( <div> <div>Subtotal: {formatRupiah(amount, { decimal: true })}</div> <div>PPN 11%: {formatRupiah(tax, { decimal: true })}</div> <div>Total: {formatRupiah(total, { decimal: true })}</div> </div> ) } // International format for exports function ExportData(amounts: number[]) { return amounts.map(amount => ({ amount: formatRupiah(amount, { separator: ',', decimalSeparator: '.', decimal: true }) })) }

formatCompact()

Formats numbers using Indonesian compact notation with proper grammar rules.

Type Signature:

function formatCompact(amount: number): string

Parameters:

NameTypeDescription
amountnumberThe amount to format

Returns:

string - Compact formatted string with Indonesian units

Compact Units:

UnitValueEnglish Equivalent
ribu1,000thousand
juta1,000,000million
miliar1,000,000,000billion
triliun1,000,000,000,000trillion

Grammar Rules:

The function follows proper Indonesian grammar:

  • 1 juta not 1,0 juta (removes trailing .0)
  • 500 ribu not 0,5 juta (uses appropriate unit)
  • Numbers below 100,000 use standard formatting

Examples:

// Millions (juta) formatCompact(1000000) // 'Rp 1 juta' formatCompact(1500000) // 'Rp 1,5 juta' formatCompact(2300000) // 'Rp 2,3 juta' // Thousands (ribu) formatCompact(500000) // 'Rp 500 ribu' formatCompact(750000) // 'Rp 750 ribu' // Below 100k: standard formatting formatCompact(99000) // 'Rp 99.000' formatCompact(1500) // 'Rp 1.500' // Billions (miliar) formatCompact(1000000000) // 'Rp 1 miliar' formatCompact(2500000000) // 'Rp 2,5 miliar' // Trillions (triliun) formatCompact(1000000000000) // 'Rp 1 triliun' formatCompact(1500000000000) // 'Rp 1,5 triliun' // Negative numbers formatCompact(-1500000) // 'Rp -1,5 juta' // Grammar: proper rounding formatCompact(1000000) // 'Rp 1 juta' (not '1,0 juta') formatCompact(1100000) // 'Rp 1,1 juta'

Use Cases:

// Dashboard statistics function StatCard({ label, value }: { label: string, value: number }) { return ( <div className="stat-card"> <div className="label">{label}</div> <div className="value">{formatCompact(value)}</div> </div> ) } // Budget display function BudgetOverview({ budget }) { return ( <div> <h3>Total Budget</h3> <p className="large">{formatCompact(budget.total)}</p> <p>Allocated: {formatCompact(budget.allocated)}</p> <p>Remaining: {formatCompact(budget.remaining)}</p> </div> ) } // Social media friendly numbers function ShareButton({ views, shares }: { views: number, shares: number }) { return ( <button> Share this post <span>{formatCompact(views)} views</span> <span>{formatCompact(shares)} shares</span> </button> ) } // Chart labels function ChartLabel({ value }: { value: number }) { // More readable than full numbers return <text>{formatCompact(value)}</text> }

Grammar Note: The function automatically handles Indonesian grammar rules. Numbers are rounded to one decimal place, and trailing zeros are removed (e.g., “1 juta” instead of “1,0 juta”).


parseRupiah()

Parses formatted Rupiah strings back to numeric values. Handles multiple formats including standard, compact, and international notation.

Type Signature:

function parseRupiah(formatted: string): number | null

Parameters:

NameTypeDescription
formattedstringThe formatted Rupiah string to parse

Returns:

number | null - Parsed number, or null if invalid

Supported Formats:

  • Standard: "Rp 1.500.000"
  • No symbol: "1.500.000"
  • With decimals: "Rp 1.500.000,50"
  • Compact: "Rp 1,5 juta", "Rp 500 ribu"
  • International: "Rp 1,500,000.50"
  • Negative: "Rp -1.500.000"

Examples:

// Standard format parseRupiah('Rp 1.500.000') // 1500000 parseRupiah('1.500.000') // 1500000 // With decimals parseRupiah('Rp 1.500.000,50') // 1500000.5 parseRupiah('Rp 1.500.000,99') // 1500000.99 // Compact formats parseRupiah('Rp 1,5 juta') // 1500000 parseRupiah('Rp 500 ribu') // 500000 parseRupiah('Rp 2,5 miliar') // 2500000000 parseRupiah('Rp 1 triliun') // 1000000000000 // International format parseRupiah('Rp 1,500,000') // 1500000 parseRupiah('Rp 1,500,000.50') // 1500000.5 // Negative numbers parseRupiah('Rp -1.500.000') // -1500000 parseRupiah('-1.500.000') // -1500000 // Without 'Rp' symbol parseRupiah('1500000') // 1500000 parseRupiah('1.500.000') // 1500000 // Invalid input parseRupiah('invalid') // null parseRupiah('') // null parseRupiah('abc') // null // Edge cases parseRupiah('Rp 0') // 0 parseRupiah('0') // 0

Use Cases:

// Parse user input from forms function handlePriceInput(input: string) { const amount = parseRupiah(input) if (amount === null) { return { error: 'Invalid price format' } } return { amount } } // Import data from CSV/Excel function importPrices(csvData: string[][]) { return csvData.map(row => ({ name: row[0], price: parseRupiah(row[1]) ?? 0 })) } // Calculate totals from formatted strings function calculateTotal(items: { price: string }[]) { return items.reduce((sum, item) => { const amount = parseRupiah(item.price) ?? 0 return sum + amount }, 0) } // Validate and convert user input function PriceInput({ value, onChange }) { const [displayValue, setDisplayValue] = useState(value) const handleChange = (input: string) => { // Allow user to type freely setDisplayValue(input) // Parse and notify parent if valid const parsed = parseRupiah(input) if (parsed !== null) { onChange(parsed) } } const handleBlur = () => { // Format on blur const parsed = parseRupiah(displayValue) if (parsed !== null) { setDisplayValue(formatRupiah(parsed)) } } return ( <input value={displayValue} onChange={e => handleChange(e.target.value)} onBlur={handleBlur} /> ) } // Parse mixed format data function normalizeAmounts(amounts: string[]) { return amounts .map(parseRupiah) .filter((amount): amount is number => amount !== null) }

Smart Parsing: The function automatically detects the format (Indonesian vs International) based on separator positions and handles both correctly.


toWords()

Converts numbers to Indonesian words (terbilang) following proper Indonesian grammar rules. Essential for checks, invoices, and legal documents.

Type Signature:

function toWords(amount: number, options?: WordOptions): string

Parameters:

NameTypeDescription
amountnumberThe number to convert (supports up to trillions)
optionsWordOptionsOptional conversion settings

Returns:

string - Indonesian words representation

Options:

interface WordOptions { uppercase?: boolean // Capitalize first letter (default: false) withCurrency?: boolean // Add 'rupiah' suffix (default: true) }

Indonesian Grammar Rules:

The function implements proper Indonesian number pronunciation:

  • 100 = “seratus” (not “satu ratus”)
  • 1000 = “seribu” (not “satu ribu”)
  • 11 = “sebelas” (not “satu belas”)
  • Proper spacing and word order

Examples:

// Basic numbers toWords(0) // 'nol rupiah' toWords(1) // 'satu rupiah' toWords(10) // 'sepuluh rupiah' toWords(11) // 'sebelas rupiah' toWords(100) // 'seratus rupiah' toWords(1000) // 'seribu rupiah' // Complex numbers toWords(123) // 'seratus dua puluh tiga rupiah' toWords(1500) // 'seribu lima ratus rupiah' toWords(12345) // 'dua belas ribu tiga ratus empat puluh lima rupiah' // Large numbers toWords(1000000) // 'satu juta rupiah' toWords(1500000) // 'satu juta lima ratus ribu rupiah' toWords(2345678) // 'dua juta tiga ratus empat puluh lima ribu enam ratus tujuh puluh delapan rupiah' // Billions toWords(1000000000) // 'satu miliar rupiah' toWords(1500000000) // 'satu miliar lima ratus juta rupiah' // Trillions toWords(1000000000000) // 'satu triliun rupiah' // With options: uppercase toWords(1500000, { uppercase: true }) // 'Satu juta lima ratus ribu rupiah' // With options: no currency toWords(1500000, { withCurrency: false }) // 'satu juta lima ratus ribu' toWords(1500000, { uppercase: true, withCurrency: false }) // 'Satu juta lima ratus ribu' // Negative numbers toWords(-1500000) // 'minus satu juta lima ratus ribu rupiah' // Edge cases toWords(99999999) // 'sembilan puluh sembilan juta sembilan ratus sembilan puluh sembilan ribu sembilan ratus sembilan puluh sembilan rupiah'

Use Cases:

// Invoice terbilang function InvoiceTotal({ total }: { total: number }) { return ( <div className="invoice-total"> <div className="amount"> Total: {formatRupiah(total)} </div> <div className="words"> Terbilang: {toWords(total, { uppercase: true })} </div> </div> ) } // Check printing function PrintCheck({ amount, recipient }: { amount: number, recipient: string }) { const words = toWords(amount, { uppercase: true }) return ( <div className="check"> <div>Pay to: {recipient}</div> <div>Amount: {formatRupiah(amount)}</div> <div>({words})</div> </div> ) } // Receipt generator function Receipt({ items, total }) { return ( <div className="receipt"> {items.map(item => ( <div key={item.id}> <span>{item.name}</span> <span>{formatRupiah(item.price)}</span> </div> ))} <div className="total"> <div>Total: {formatRupiah(total)}</div> <div className="terbilang"> {toWords(total, { uppercase: true })} </div> </div> </div> ) } // Legal document amount function ContractAmount({ amount }: { amount: number }) { return ( <p> Jumlah kontrak sebesar {formatRupiah(amount)} ({toWords(amount)}). </p> ) } // Quantity descriptions (without currency) function QuantityInWords({ quantity }: { quantity: number }) { return ( <span> {quantity} ({toWords(quantity, { withCurrency: false })}) unit </span> ) }

Note: The function converts the integer part only. Decimal values are floored before conversion. For financial documents, format decimals separately or round to whole numbers first.


roundToClean()

Rounds currency amounts to clean, readable values. Useful for budgets, estimates, and approximate displays.

Type Signature:

function roundToClean(amount: number, unit?: RoundUnit): number

Parameters:

NameTypeDefaultDescription
amountnumber-The amount to round
unitRoundUnit'ribu'The unit to round to

Round Units:

type RoundUnit = 'ribu' | 'ratus-ribu' | 'juta' // Values: // 'ribu' = 1,000 // 'ratus-ribu' = 100,000 // 'juta' = 1,000,000

Examples:

// Round to thousands (ribu) roundToClean(1234567) // 1235000 roundToClean(1234567, 'ribu') // 1235000 roundToClean(1234, 'ribu') // 1000 roundToClean(1567, 'ribu') // 2000 // Round to hundred thousands (ratus-ribu) roundToClean(1234567, 'ratus-ribu') // 1200000 roundToClean(1567890, 'ratus-ribu') // 1600000 roundToClean(45000, 'ratus-ribu') // 0 roundToClean(87654, 'ratus-ribu') // 100000 // Round to millions (juta) roundToClean(1234567, 'juta') // 1000000 roundToClean(1567890, 'juta') // 2000000 roundToClean(456789, 'juta') // 0 // Negative numbers roundToClean(-1234567, 'ribu') // -1235000 roundToClean(-1234567, 'juta') // -1000000 // Edge cases roundToClean(0) // 0 roundToClean(500, 'ribu') // 1000 (rounds up at midpoint)

Use Cases:

// Budget estimates function BudgetEstimate({ rawAmount }: { rawAmount: number }) { const rounded = roundToClean(rawAmount, 'juta') return ( <div> <div>Estimated Budget</div> <div>{formatCompact(rounded)}</div> <small>Rounded from {formatRupiah(rawAmount)}</small> </div> ) } // Price ranges function PriceRange({ min, max }: { min: number, max: number }) { const roundedMin = roundToClean(min, 'ratus-ribu') const roundedMax = roundToClean(max, 'ratus-ribu') return ( <div> {formatCompact(roundedMin)} - {formatCompact(roundedMax)} </div> ) } // Approximate totals function ApproximateTotal({ items }) { const exact = items.reduce((sum, item) => sum + item.price, 0) const approx = roundToClean(exact, 'ribu') return ( <div> <div>Approximately {formatCompact(approx)}</div> <small>Exact: {formatRupiah(exact)}</small> </div> ) } // Salary range display function SalaryRange({ min, max }: { min: number, max: number }) { return ( <div className="salary-range"> {formatCompact(roundToClean(min, 'juta'))} - {formatCompact(roundToClean(max, 'juta'))} </div> ) } // Budget allocation with rounding function allocateBudget(total: number, percentages: number[]) { return percentages.map(pct => { const amount = total * (pct / 100) return roundToClean(amount, 'ratus-ribu') }) }

Rounding Behavior: Uses standard rounding (0.5 rounds up). For financial calculations, always round after calculations, not before.


Type Reference

RupiahOptions

Configuration options for formatting Rupiah currency.

interface RupiahOptions { symbol?: boolean // Show 'Rp' symbol (default: true) decimal?: boolean // Show decimal places (default: false) separator?: string // Thousands separator (default: '.') decimalSeparator?: string // Decimal separator (default: ',') precision?: number // Decimal places (default: 0 or 2) spaceAfterSymbol?: boolean // Space after 'Rp' (default: true) }

Default Values:

  • symbol: true - Shows “Rp” prefix
  • decimal: false - No decimal places
  • separator: '.' - Indonesian standard
  • decimalSeparator: ',' - Indonesian standard
  • precision: 0 (or 2 if decimal=true)
  • spaceAfterSymbol: true - “Rp 1.000” not “Rp1.000”

WordOptions

Options for converting numbers to Indonesian words.

interface WordOptions { uppercase?: boolean // Capitalize first letter (default: false) withCurrency?: boolean // Add 'rupiah' suffix (default: true) }

Usage:

  • uppercase: For formal documents (invoices, contracts)
  • withCurrency: Omit when describing quantities, not money

RoundUnit

Units for rounding currency amounts.

type RoundUnit = 'ribu' | 'ratus-ribu' | 'juta'

Values:

  • 'ribu': 1,000 (thousands)
  • 'ratus-ribu': 100,000 (hundred thousands)
  • 'juta': 1,000,000 (millions)

Common Use Cases

E-commerce Product Display

import { formatRupiah, formatCompact } from '@indodev/toolkit/currency' function ProductCard({ product }) { const hasDiscount = product.discount > 0 const finalPrice = product.price * (1 - product.discount) return ( <div className="product-card"> <h3>{product.name}</h3> {hasDiscount ? ( <div className="price"> <span className="original">{formatRupiah(product.price)}</span> <span className="final">{formatRupiah(finalPrice)}</span> <span className="discount">{product.discount * 100}% OFF</span> </div> ) : ( <div className="price">{formatRupiah(product.price)}</div> )} {product.installment && ( <div className="installment"> or {formatCompact(product.price / 12)}/month </div> )} </div> ) }

Invoice Generator

import { formatRupiah, toWords } from '@indodev/toolkit/currency' interface InvoiceData { items: Array<{ name: string; qty: number; price: number }> tax: number discount: number } function generateInvoice(data: InvoiceData) { const subtotal = data.items.reduce((sum, item) => { return sum + item.qty * item.price }, 0) const discountAmount = subtotal * data.discount const afterDiscount = subtotal - discountAmount const taxAmount = afterDiscount * data.tax const total = afterDiscount + taxAmount return { items: data.items.map(item => ({ ...item, amount: formatRupiah(item.qty * item.price) })), subtotal: formatRupiah(subtotal), discount: formatRupiah(discountAmount), tax: formatRupiah(taxAmount), total: formatRupiah(total), totalWords: toWords(total, { uppercase: true }) } } // Usage in component function Invoice({ data }: { data: InvoiceData }) { const invoice = generateInvoice(data) return ( <div className="invoice"> <table> <thead> <tr> <th>Item</th> <th>Qty</th> <th>Price</th> <th>Amount</th> </tr> </thead> <tbody> {invoice.items.map((item, i) => ( <tr key={i}> <td>{item.name}</td> <td>{item.qty}</td> <td>{formatRupiah(item.price)}</td> <td>{item.amount}</td> </tr> ))} </tbody> </table> <div className="summary"> <div>Subtotal: {invoice.subtotal}</div> <div>Discount: -{invoice.discount}</div> <div>PPN {data.tax * 100}%: {invoice.tax}</div> <div className="total">Total: {invoice.total}</div> <div className="terbilang">Terbilang: {invoice.totalWords}</div> </div> </div> ) }

Financial Dashboard

import { formatCompact, roundToClean } from '@indodev/toolkit/currency' interface DashboardStats { revenue: number expenses: number profit: number growth: number } function FinancialDashboard({ stats }: { stats: DashboardStats }) { return ( <div className="dashboard"> <div className="stat-grid"> <StatCard label="Revenue" value={formatCompact(stats.revenue)} trend={stats.growth > 0 ? 'up' : 'down'} change={`${(stats.growth * 100).toFixed(1)}%`} /> <StatCard label="Expenses" value={formatCompact(stats.expenses)} /> <StatCard label="Profit" value={formatCompact(stats.profit)} highlight={stats.profit > 0} /> <StatCard label="Target" value={formatCompact(roundToClean(stats.revenue * 1.2, 'juta'))} subtitle="Next quarter goal" /> </div> </div> ) }

Payment Form with Validation

import { formatRupiah, parseRupiah } from '@indodev/toolkit/currency' import { useState } from 'react' function PaymentForm() { const [displayAmount, setDisplayAmount] = useState('') const [actualAmount, setActualAmount] = useState(0) const [error, setError] = useState('') const handleAmountChange = (input: string) => { setDisplayAmount(input) const parsed = parseRupiah(input) if (parsed === null) { setError('Invalid amount format') setActualAmount(0) } else if (parsed < 10000) { setError('Minimum amount is Rp 10.000') setActualAmount(parsed) } else { setError('') setActualAmount(parsed) } } const handleBlur = () => { if (actualAmount > 0) { setDisplayAmount(formatRupiah(actualAmount)) } } const handleSubmit = () => { if (actualAmount < 10000) { alert('Amount too low') return } // Process payment console.log('Processing payment:', actualAmount) } return ( <form> <div className="form-group"> <label>Amount</label> <input type="text" value={displayAmount} onChange={e => handleAmountChange(e.target.value)} onBlur={handleBlur} placeholder="Rp 0" /> {error && <span className="error">{error}</span>} </div> <button type="button" onClick={handleSubmit} disabled={actualAmount < 10000} > Pay {formatRupiah(actualAmount)} </button> </form> ) }

Budget Calculator

import { formatRupiah, roundToClean } from '@indodev/toolkit/currency' interface BudgetCategory { name: string percentage: number } function BudgetCalculator({ income }: { income: number }) { const categories: BudgetCategory[] = [ { name: 'Kebutuhan Pokok', percentage: 50 }, { name: 'Tabungan & Investasi', percentage: 20 }, { name: 'Gaya Hidup', percentage: 30 }, ] const allocations = categories.map(cat => ({ ...cat, amount: income * (cat.percentage / 100), rounded: roundToClean(income * (cat.percentage / 100), 'ribu') })) return ( <div className="budget-calculator"> <div className="income"> <h3>Penghasilan Bulanan</h3> <div className="amount">{formatRupiah(income)}</div> </div> <div className="allocations"> {allocations.map(allocation => ( <div key={allocation.name} className="allocation"> <div className="category"> <span>{allocation.name}</span> <span>{allocation.percentage}%</span> </div> <div className="amount"> {formatRupiah(allocation.rounded)} </div> </div> ))} </div> </div> ) }

Best Practices

1. Choose the Right Format

// ❌ BAD: Using full format in constrained spaces <div className="small-card"> {formatRupiah(1500000)} // Too long </div> // ✅ GOOD: Use compact format for small spaces <div className="small-card"> {formatCompact(1500000)} // Rp 1,5 juta </div> // ❌ BAD: Compact format for exact amounts <div className="invoice-total"> {formatCompact(1234567)} // Loses precision </div> // ✅ GOOD: Full format for exact amounts <div className="invoice-total"> {formatRupiah(1234567)} // Rp 1.234.567 </div>

2. Handle Decimals Correctly

// ❌ BAD: Not showing decimals for prices with cents formatRupiah(1500000.50) // 'Rp 1.500.000' (loses .50) // ✅ GOOD: Enable decimals when needed formatRupiah(1500000.50, { decimal: true }) // 'Rp 1.500.000,50' // ✅ GOOD: Set precision explicitly formatRupiah(1500000.5, { decimal: true, precision: 2 }) // 'Rp 1.500.000,50'

3. Validate User Input

// ❌ BAD: Directly using user input function processPayment(input: string) { const amount = parseFloat(input) // Can fail // ... } // ✅ GOOD: Parse and validate function processPayment(input: string) { const amount = parseRupiah(input) if (amount === null) { throw new Error('Invalid amount format') } if (amount < 1000) { throw new Error('Amount too low') } // Process valid amount return amount }

4. Use Terbilang for Documents

// âś… GOOD: Always include terbilang in invoices function InvoiceTotal({ amount }) { return ( <div> <div>Total: {formatRupiah(amount, { decimal: true })}</div> <div>Terbilang: {toWords(amount, { uppercase: true })}</div> </div> ) } // âś… GOOD: Format checks properly function Check({ amount, payee }) { return ( <div className="check"> <div>Pay to the order of: {payee}</div> <div>Sum of: {formatRupiah(amount)}</div> <div className="words">({toWords(amount, { uppercase: true })})</div> </div> ) }

5. Performance Optimization

// ❌ BAD: Formatting in loops without memoization function ProductList({ products }) { return products.map(product => ( <div key={product.id}> {formatRupiah(product.price)} // Formatted on every render </div> )) } // ✅ GOOD: Pre-format or memoize function ProductList({ products }) { const formatted = useMemo( () => products.map(p => ({ ...p, formattedPrice: formatRupiah(p.price) })), [products] ) return formatted.map(product => ( <div key={product.id}> {product.formattedPrice} </div> )) } // ✅ GOOD: Format once in data transformation const products = rawProducts.map(p => ({ ...p, formattedPrice: formatRupiah(p.price) }))

6. Consistent Separator Usage

// ❌ BAD: Mixing separators in same application formatRupiah(1000, { separator: '.' }) formatRupiah(2000, { separator: ',' }) // Inconsistent // ✅ GOOD: Create wrapper with default options const formatIDR = (amount: number, options?: RupiahOptions) => { return formatRupiah(amount, { separator: '.', decimalSeparator: ',', ...options }) } // Use everywhere formatIDR(1000) formatIDR(2000)

Troubleshooting

Q: Why does parseRupiah return null?

A: Common reasons:

  • Input contains non-numeric characters (except separators)
  • Empty or invalid string
  • Malformed format
// Check input first const input = 'Rp abc' const result = parseRupiah(input) if (result === null) { console.log('Invalid format') // Will log this }

Q: How to handle decimal precision?

A: Set precision explicitly:

// Default: 2 decimals when decimal=true formatRupiah(1500.5, { decimal: true }) // 'Rp 1.500,50' // Custom precision formatRupiah(1500.5, { decimal: true, precision: 1 }) // 'Rp 1.500,5' formatRupiah(1500, { decimal: true, precision: 0 }) // 'Rp 1.500'

Q: Can I format for international audience?

A: Yes, customize separators:

formatRupiah(1500000.50, { separator: ',', decimalSeparator: '.', decimal: true }) // 'Rp 1,500,000.50'

Q: Why is terbilang not showing decimals?

A: toWords() only converts integers. Format decimals separately:

const amount = 1500000.50 const integer = Math.floor(amount) const decimal = Math.round((amount - integer) * 100) const words = `${toWords(integer)} koma ${toWords(decimal, { withCurrency: false })}` // 'satu juta lima ratus ribu rupiah koma lima puluh'

Q: How to handle very large numbers?

A: The library supports up to trillions:

formatRupiah(1234567890123) // 'Rp 1.234.567.890.123' formatCompact(1234567890123) // 'Rp 1,2 triliun' toWords(1000000000000) // 'satu triliun rupiah'

Data Standards

All formatting follows official Indonesian standards:

  • Separator: . (dot) for thousands
  • Decimal: , (comma) for decimals
  • Symbol: Rp with space
  • Units: ribu, juta, miliar, triliun
  • Grammar: Proper Indonesian number pronunciation rules

The library is based on Bahasa Indonesia formal writing standards and common business practices in Indonesia.

Last updated on