v0.3.4 released — Currency module enhancements with splitAmount, percentageOf, and more. Read changelog
Skip to Content
DocumentationFinancialRupiah Utilities

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
  • Split Amounts: Divide amounts into equal or custom-ratio parts
  • Validation: Check if a string is a valid Rupiah format
  • Calculations: Percentage, difference, and tax calculations
  • 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, splitAmount, percentageOf, difference, validateRupiah } from '@indodev/toolkit/currency'; // Format currency formatRupiah(1500000); // 'Rp 1.500.000' // Negative format (standard accounting) formatRupiah(-1500000); // '-Rp 1.500.000' // Compact format formatCompact(1500000); // 'Rp 1,5 juta' // Compact without symbol formatCompact(1500000, { symbol: false }); // '1,5 juta' // Convert to words (terbilang) toWords(1500000); // 'satu juta lima ratus ribu rupiah' // Terbilang with decimals toWords(1500000.5, { withDecimals: true }); // 'satu juta lima ratus ribu rupiah koma lima puluh' // Parse back to number parseRupiah('Rp 1.500.000'); // 1500000 // Accounting format (parentheses for negatives) formatAccounting(-1500000); // '(Rp 1.500.000)' // Calculate tax (rate required) calculateTax(1000000, 0.11); // 110000 // Split amount (equal) splitAmount(1500000, 3); // [500000, 500000, 500000] // Split amount (custom ratios) splitAmount(1000000, 2, { ratios: [70, 30] }); // [700000, 300000] // Percentage calculation percentageOf(150000, 1000000); // 15 // Difference calculation difference(1200000, 1000000); // { absolute: 200000, percentage: 20, direction: 'increase' } // Validate Rupiah format validateRupiah('Rp 1.500.000'); // true

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.5, { 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.5, { symbol: false, decimal: true }); // '1.500.000,50' // Custom separators (international format) formatRupiah(1500000, { separator: ',', decimalSeparator: '.' }); // 'Rp 1,500,000' formatRupiah(1500000.5, { 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 }) })) }

formatAccounting()

Formats a number using accounting notation, where negative numbers are enclosed in parentheses.

Type Signature:

function formatAccounting(amount: number, options?: RupiahOptions): string;

Parameters:

NameTypeDescription
amountnumberThe amount to format
optionsRupiahOptionsOptional formatting configuration

Returns:

string - Formatted accounting string.

Examples:

formatAccounting(1500000); // 'Rp 1.500.000' formatAccounting(-1500000); // '(Rp 1.500.000)' formatAccounting(-1500, { symbol: false }); // '(1.500)'

addRupiahSymbol()

Adds the Rupiah symbol (‘Rp ’) to a string if it’s not already present.

Type Signature:

function addRupiahSymbol(amount: string | number): string;

Parameters:

| Name | Type | Description | | -------- | ------- | ----------- | ------------------------------ | | amount | string | number | The value to add the symbol to |

Returns:

string - String with ‘Rp ’ prefix.

Examples:

addRupiahSymbol('1.500.000'); // 'Rp 1.500.000' addRupiahSymbol(1500000); // 'Rp 1500000'

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.



calculateTax()

Calculates tax (PPN) from a given amount and rate.

Type Signature:

function calculateTax(amount: number, rate: number): number;

Parameters:

NameTypeDescription
amountnumberThe base amount
ratenumberThe tax rate (e.g., 0.11 for 11% PPN)

Returns:

number - The calculated tax amount.

Examples:

calculateTax(1000000, 0.11); // 110000 calculateTax(500000, 0.10); // 50000 calculateTax(0, 0.11); // 0 calculateTax(-1000000, 0.11);// -110000 (tax credit)

Breaking Change (v0.3.5): The rate parameter is now required. The previous default of 0.11 was removed because tax rates can change over time and violate the Data Independence mandate.


splitAmount()

Splits an amount into equal or custom-ratio parts. Useful for split bills, installments, and budget allocation.

Type Signature:

function splitAmount(amount: number, parts: number, options?: SplitOptions): number[];

Parameters:

NameTypeDescription
amountnumberThe amount to split
partsnumberNumber of parts to split into
optionsSplitOptionsOptional split configuration

Returns:

number[] - Array of split amounts

SplitOptions:

interface SplitOptions { ratios?: number[]; // Custom percentage ratios (must sum to 100) roundTo?: RoundUnit; // Round each part to clean amount }

Examples:

// Equal split splitAmount(1500000, 3); // [500000, 500000, 500000] // Remainder distributed to first parts splitAmount(10000, 3); // [3334, 3333, 3333] // Custom ratios splitAmount(1000000, 2, { ratios: [70, 30] }); // [700000, 300000] // With rounding splitAmount(1234567, 3, { roundTo: 'ribu' }); // [412000, 412000, 412000] // Negative amounts splitAmount(-1500000, 3); // [-500000, -500000, -500000] // Single part splitAmount(1000000, 1); // [1000000]

Errors:

  • Throws InvalidSplitError if parts < 1
  • Throws InvalidSplitError if ratios length doesn’t match parts
  • Throws InvalidSplitError if ratios don’t sum to 100

percentageOf()

Calculates what percentage a part is of a total.

Type Signature:

function percentageOf(part: number, total: number): number;

Parameters:

NameTypeDescription
partnumberThe part value
totalnumberThe total value

Returns:

number - Percentage (e.g., 15 for 15%)

Examples:

percentageOf(150000, 1000000); // 15 percentageOf(500000, 1000000); // 50 percentageOf(0, 1000000); // 0 percentageOf(1000000, 1000000); // 100 percentageOf(1500000, 1000000); // 150 (over 100%) percentageOf(100, 0); // 0 (not NaN) percentageOf(-200000, 1000000); // -20 percentageOf(-200000, -1000000);// 20 (signs cancel)

difference()

Calculates absolute and percentage difference between two amounts with direction tracking.

Type Signature:

function difference(amount1: number, amount2: number): { absolute: number; percentage: number | null; direction: 'increase' | 'decrease' | 'same'; };

Parameters:

NameTypeDescription
amount1numberThe new/current amount
amount2numberThe original/reference amount

Returns:

Object with absolute, percentage, and direction

Examples:

difference(1200000, 1000000); // { absolute: 200000, percentage: 20, direction: 'increase' } difference(800000, 1000000); // { absolute: -200000, percentage: -20, direction: 'decrease' } difference(1000000, 1000000); // { absolute: 0, percentage: 0, direction: 'same' } difference(0, 1000000); // { absolute: -1000000, percentage: null, direction: 'decrease' } difference(0, 0); // { absolute: 0, percentage: 0, direction: 'same' }

validateRupiah()

Checks if a string is a valid Rupiah format.

Type Signature:

function validateRupiah(formatted: string): boolean;

Parameters:

NameTypeDescription
formattedstringThe string to validate

Returns:

boolean - true if valid Rupiah format, false otherwise

Examples:

// Valid formats validateRupiah('Rp 1.500.000'); // true validateRupiah('1.500.000'); // true validateRupiah('Rp 1,5 juta'); // true validateRupiah('-Rp 1.500.000'); // true validateRupiah('Rp -1.500.000'); // true (legacy format) validateRupiah(' Rp 1.500.000 ');// true (trimmed) // Invalid formats validateRupiah('abc'); // false validateRupiah(''); // false validateRupiah('Rp abc'); // false validateRupiah('1.500.000abc'); // false validateRupiah(' '); // false validateRupiah('Rp'); // false

toWords() — Decimal Support (Enhanced)

The toWords() function now supports decimal terbilang via the withDecimals option.

Updated Type Signature:

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

New Option:

interface WordOptions { uppercase?: boolean; withCurrency?: boolean; withDecimals?: boolean; // NEW (default: false) }

Examples:

// Default (no decimals) toWords(1500000.5); // 'satu juta lima ratus ribu rupiah' // With decimals toWords(1500000.5, { withDecimals: true }); // 'satu juta lima ratus ribu rupiah koma lima puluh' toWords(1234.56, { withDecimals: true }); // 'seribu dua ratus tiga puluh empat rupiah koma lima enam' toWords(1500000.5, { withDecimals: true, withCurrency: false }); // 'satu juta lima ratus ribu koma lima puluh' // Negative with decimals toWords(-1500000.5, { withDecimals: true }); // 'minus satu juta lima ratus ribu rupiah koma lima puluh' // Zero decimal part (no decimal words) toWords(1500000.0, { withDecimals: true }); // 'satu juta lima ratus ribu rupiah'

formatCompact() — Enhanced with Options

The formatCompact() function now accepts CompactOptions for customization.

Updated Type Signature:

function formatCompact(amount: number, options?: CompactOptions): string;

CompactOptions:

interface CompactOptions { symbol?: boolean; // Show 'Rp' (default: true) spaceAfterSymbol?: boolean; // Space after 'Rp' (default: true) }

Examples:

// Default formatCompact(1500000); // 'Rp 1,5 juta' // Without symbol formatCompact(1500000, { symbol: false }); // '1,5 juta' // Without space formatCompact(1500000, { spaceAfterSymbol: false }); // 'Rp1,5 juta' // Negative (new format) formatCompact(-1500000); // '-Rp 1,5 juta' formatCompact(-1500000, { symbol: false }); // '-1,5 juta'

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) withDecimals?: boolean; // Include decimal words (default: false) }

Usage:

  • uppercase: For formal documents (invoices, contracts)
  • withCurrency: Omit when describing quantities, not money
  • withDecimals: Include decimal terbilang with “koma” separator

CompactOptions

Options for compact currency formatting.

interface CompactOptions { symbol?: boolean; // Show 'Rp' (default: true) spaceAfterSymbol?: boolean;// Space after 'Rp' (default: true) }

SplitOptions

Options for splitting amounts.

interface SplitOptions { ratios?: number[]; // Custom percentage ratios (must sum to 100) roundTo?: RoundUnit; // Round each part to clean amount }

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.5); // 'Rp 1.500.000' (loses .50) // ✅ GOOD: Enable decimals when needed formatRupiah(1500000.5, { 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.5, { 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.5; 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