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
npm install @indodev/toolkitQuick 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')
// 1500000API Reference
formatRupiah()
Formats a number as Indonesian Rupiah currency with customizable options.
Type Signature:
function formatRupiah(amount: number, options?: RupiahOptions): stringParameters:
| Name | Type | Description |
|---|---|---|
amount | number | The amount to format |
options | RupiahOptions | Optional 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): stringParameters:
| Name | Type | Description |
|---|---|---|
amount | number | The amount to format |
Returns:
string - Compact formatted string with Indonesian units
Compact Units:
| Unit | Value | English Equivalent |
|---|---|---|
| ribu | 1,000 | thousand |
| juta | 1,000,000 | million |
| miliar | 1,000,000,000 | billion |
| triliun | 1,000,000,000,000 | trillion |
Grammar Rules:
The function follows proper Indonesian grammar:
1 jutanot1,0 juta(removes trailing .0)500 ribunot0,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 | nullParameters:
| Name | Type | Description |
|---|---|---|
formatted | string | The 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')
// 0Use 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): stringParameters:
| Name | Type | Description |
|---|---|---|
amount | number | The number to convert (supports up to trillions) |
options | WordOptions | Optional 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): numberParameters:
| Name | Type | Default | Description |
|---|---|---|---|
amount | number | - | The amount to round |
unit | RoundUnit | '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,000Examples:
// 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” prefixdecimal:false- No decimal placesseparator:'.'- Indonesian standarddecimalSeparator:','- Indonesian standardprecision:0(or2if 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:
Rpwith 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.