@indodev/toolkit 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,26 +2,14 @@
2
2
 
3
3
  # @indodev/toolkit
4
4
 
5
- TypeScript utilities for Indonesian data validation and formatting.
6
-
7
- [![CI](https://github.com/choiruladamm/indo-dev-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/choiruladamm/indo-dev-utils/actions) [![npm version](https://img.shields.io/npm/v/@indodev/toolkit.svg)](https://npmjs.com/package/@indodev/toolkit) [![bundle size](https://img.shields.io/bundlephobia/minzip/@indodev/toolkit)](https://bundlephobia.com/package/@indodev/toolkit) [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://typescriptlang.org/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![CI](https://github.com/choiruladamm/indo-dev-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/choiruladamm/indo-dev-utils/actions)
6
+ [![npm version](https://img.shields.io/npm/v/@indodev/toolkit.svg)](https://npmjs.com/package/@indodev/toolkit)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-blue)](https://typescriptlang.org/)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
9
 
9
10
  </div>
10
11
 
11
- ## Why?
12
-
13
- Building apps for Indonesia means dealing with NIK validation, phone number formatting, Rupiah display, and proper text handling. Instead of rewriting the same logic across projects, use battle-tested utilities that just work.
14
-
15
- ## Features
16
-
17
- - **NIK validation** - Verify Indonesian National Identity Numbers with province, date, and gender checks
18
- - **Phone formatting** - Support for all major operators (Telkomsel, XL, Indosat, Smartfren, Axis) and 200+ area codes
19
- - **Rupiah formatting** - Display currency with proper grammar rules (1,5 juta, not 1,0 juta)
20
- - **Text utilities** - Smart capitalization, slug generation, abbreviation expansion, and string comparison with Indonesian language support
21
- - **Terbilang converter** - Numbers to Indonesian words (1500000 → "satu juta lima ratus ribu rupiah")
22
- - **Type-safe** - Full TypeScript support with proper type inference
23
- - **Well-tested** - 1060+ test cases with 95%+ coverage
24
- - **Zero dependencies** - Lightweight and tree-shakeable
12
+ TypeScript utilities for Indonesian data. Handles Rupiah formatting, terbilang, NIK validation, phone normalization, and text rules that generic libraries don't cover.
25
13
 
26
14
  ## Install
27
15
 
@@ -29,178 +17,66 @@ Building apps for Indonesia means dealing with NIK validation, phone number form
29
17
  npm install @indodev/toolkit
30
18
  ```
31
19
 
32
- ## Quick Start
20
+ ## Usage
33
21
 
34
- ### NIK Validation & Parsing
22
+ Generate an invoice with proper Rupiah formatting and terbilang:
35
23
 
36
24
  ```typescript
37
- import { validateNIK, parseNIK, maskNIK } from '@indodev/toolkit/nik';
38
-
39
- // Validate
40
- validateNIK('3201234567890123'); // true
25
+ import { formatRupiah, toWords, calculateTax } from '@indodev/toolkit/currency';
41
26
 
42
- // Extract info
43
- const info = parseNIK('3201234567890123');
44
- console.log(info.province.name); // 'Jawa Barat'
45
- console.log(info.gender); // 'male' or 'female'
46
-
47
- // Mask for privacy
48
- maskNIK('3201234567890123'); // '3201****0123'
49
- ```
27
+ const items = [
28
+ { name: 'Jasa Desain Website', qty: 1, price: 5000000 },
29
+ { name: 'Hosting 1 Tahun', qty: 1, price: 1200000 },
30
+ ];
50
31
 
51
- ### Phone Numbers
52
-
53
- ```typescript
54
- import {
55
- validatePhoneNumber,
56
- formatPhoneNumber,
57
- getOperator,
58
- } from '@indodev/toolkit/phone';
59
-
60
- // Validate and format
61
- validatePhoneNumber('081234567890'); // true
62
- formatPhoneNumber('081234567890', 'international'); // '+62 812-3456-7890'
32
+ const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
33
+ const tax = calculateTax(subtotal, 0.11);
34
+ const total = subtotal + tax;
63
35
 
64
- // Detect operator
65
- getOperator('081234567890'); // 'Telkomsel'
36
+ console.log(formatRupiah(subtotal)); // 'Rp 6.200.000'
37
+ console.log(formatRupiah(tax)); // 'Rp 682.000'
38
+ console.log(formatRupiah(total)); // 'Rp 6.882.000'
39
+ console.log(toWords(total)); // 'enam juta delapan ratus delapan puluh dua ribu rupiah'
66
40
  ```
67
41
 
68
- ### Currency Formatting
42
+ Validate and parse an Indonesian NIK:
69
43
 
70
44
  ```typescript
71
- import {
72
- formatRupiah,
73
- formatCompact,
74
- toWords,
75
- } from '@indodev/toolkit/currency';
76
-
77
- // Standard format
78
- formatRupiah(1500000); // 'Rp 1.500.000'
79
-
80
- // Compact format (follows Indonesian grammar!)
81
- formatCompact(1500000); // 'Rp 1,5 juta'
82
- formatCompact(1000000); // 'Rp 1 juta' (not '1,0 juta')
83
-
84
- // Terbilang
85
- toWords(1500000); // 'satu juta lima ratus ribu rupiah'
86
- ```
87
-
88
- ### Text Utilities
89
-
90
- ```typescript
91
- import {
92
- toTitleCase,
93
- slugify,
94
- expandAbbreviation,
95
- truncate,
96
- } from '@indodev/toolkit/text';
97
-
98
- // Smart title case (respects Indonesian particles)
99
- toTitleCase('buku panduan belajar di rumah');
100
- // 'Buku Panduan Belajar di Rumah'
101
-
102
- // Indonesian-aware slugs
103
- slugify('Pria & Wanita'); // 'pria-dan-wanita'
104
- slugify('Hitam/Putih'); // 'hitam-atau-putih'
105
-
106
- // Expand abbreviations
107
- expandAbbreviation('Jl. Sudirman No. 45');
108
- // 'Jalan Sudirman Nomor 45'
109
-
110
- // Smart truncation
111
- truncate('Ini adalah text yang sangat panjang', 20);
112
- // 'Ini adalah text...'
113
- ```
45
+ import { validateNIK, parseNIK } from '@indodev/toolkit/nik';
114
46
 
115
- ## API Reference
116
-
117
- ### NIK Module
118
-
119
- | Function | Description |
120
- | ---------------------------- | ------------------------------------ |
121
- | `validateNIK(nik)` | Check if NIK is valid |
122
- | `parseNIK(nik)` | Extract province, birth date, gender |
123
- | `formatNIK(nik, separator?)` | Format with separators |
124
- | `maskNIK(nik, options?)` | Mask for privacy |
125
-
126
- ### Phone Module
127
-
128
- | Function | Description |
129
- | ---------------------------------- | ------------------------------------- |
130
- | `validatePhoneNumber(phone)` | Validate Indonesian phone numbers |
131
- | `formatPhoneNumber(phone, format)` | Format to international/national/e164 |
132
- | `getOperator(phone)` | Detect operator (Telkomsel, XL, etc) |
133
- | `parsePhoneNumber(phone)` | Get all phone info |
134
-
135
- ### Currency Module
136
-
137
- | Function | Description |
138
- | -------------------------------- | -------------------------------- |
139
- | `formatRupiah(amount, options?)` | Standard Rupiah format |
140
- | `formatCompact(amount)` | Compact format (1,5 juta) |
141
- | `parseRupiah(formatted)` | Parse formatted string to number |
142
- | `toWords(amount, options?)` | Convert to Indonesian words |
143
-
144
- ### Text Module
145
-
146
- | Function | Description |
147
- | -------------------------------------- | ----------------------------------------------- |
148
- | `toTitleCase(text, options?)` | Smart capitalization with Indonesian rules |
149
- | `slugify(text, options?)` | URL-friendly slugs with Indonesian conjunctions |
150
- | `expandAbbreviation(text, options?)` | Expand Indonesian abbreviations (Jl., Bpk.) |
151
- | `truncate(text, maxLength, options?)` | Smart text truncation at word boundaries |
152
- | `compareStrings(str1, str2, options?)` | Robust string comparison |
153
- | `sanitize(text, options?)` | Clean and normalize text |
154
-
155
- ## TypeScript Support
156
-
157
- Full type inference out of the box:
158
-
159
- ```typescript
160
- import type { NIKInfo, PhoneInfo, RupiahOptions } from '@indodev/toolkit';
47
+ validateNIK('3201234567890123'); // true
161
48
 
162
- const nikInfo: NIKInfo = parseNIK('3201234567890123');
163
- // Auto-complete for province, birthDate, gender ✓
49
+ const info = parseNIK('3201234567890123');
50
+ // info.province.name → 'Jawa Barat'
51
+ // info.gender → 'male'
52
+ // info.birthDate → Date(1990-01-01)
164
53
  ```
165
54
 
166
- ## Tree-Shaking
167
-
168
- Import only what you need - unused code gets removed:
55
+ Format phone numbers and mask sensitive data:
169
56
 
170
57
  ```typescript
171
- // Recommended: Import from submodules
172
- import { formatRupiah } from '@indodev/toolkit/currency';
173
- import { validateNIK } from '@indodev/toolkit/nik';
174
- import { slugify } from '@indodev/toolkit/text';
58
+ import { formatPhoneNumber } from '@indodev/toolkit/phone';
59
+ import { maskText, toTitleCase, slugify } from '@indodev/toolkit/text';
175
60
 
176
- // ⚠️ Works but imports everything
177
- import { formatRupiah, validateNIK, slugify } from '@indodev/toolkit';
61
+ formatPhoneNumber('081234567890', 'international'); // '+62 812-3456-7890'
62
+ maskText('08123456789', { pattern: 'middle', visibleStart: 4, visibleEnd: 3 }); // '0812****789'
63
+ toTitleCase('pt bank central asia tbk'); // 'PT Bank Central Asia Tbk'
64
+ slugify('Pria & Wanita'); // 'pria-dan-wanita'
178
65
  ```
179
66
 
180
- ## Bundle Size
181
-
182
- | Module | Size (minified + gzipped) |
183
- | --------- | ------------------------- |
184
- | NIK | ~5 KB |
185
- | Phone | ~12 KB |
186
- | Currency | ~6 KB |
187
- | Text | ~8 KB |
188
- | **Total** | **~31 KB** |
189
-
190
- ## Requirements
191
-
192
- - Node.js >= 18
193
- - TypeScript >= 5.0 (optional)
194
-
195
- ## Documentation
196
-
197
- - 📖 [Full Documentation](https://toolkit.adamm.cloud/docs)
198
- - 🐛 [Report Issues](https://github.com/choiruladamm/indo-dev-utils/issues)
199
-
200
- ## License
67
+ ## Modules
201
68
 
202
- MIT © [choiruladamm](https://github.com/choiruladamm)
69
+ | Module | Description |
70
+ | --------------------------------------------------------------- | -------------------------------------------------------------- |
71
+ | [Currency](https://toolkit.adamm.cloud/docs/financial/currency) | Format Rupiah, terbilang, split amounts, percentages |
72
+ | [Text](https://toolkit.adamm.cloud/docs/text-utils/text) | Title case, slugs, abbreviations, case conversion, masking |
73
+ | [NIK](https://toolkit.adamm.cloud/docs/identity/nik) | Validate, parse, and mask Indonesian National Identity Numbers |
74
+ | [NPWP](https://toolkit.adamm.cloud/docs/identity/npwp) | Validate and format Tax Identification Numbers |
75
+ | [Phone](https://toolkit.adamm.cloud/docs/contact/phone) | Format, validate, and detect mobile operators |
76
+ | [Email](https://toolkit.adamm.cloud/docs/contact/email) | Validate emails with disposable domain detection |
77
+ | [Plate](https://toolkit.adamm.cloud/docs/vehicles/plate) | Validate license plates with region detection |
78
+ | [VIN](https://toolkit.adamm.cloud/docs/vehicles/vin) | Validate Vehicle Identification Numbers (ISO 3779) |
203
79
 
204
- ---
80
+ Full docs, examples, and API reference at [toolkit.adamm.cloud](https://toolkit.adamm.cloud/docs)
205
81
 
206
- Made with ❤️ for Indonesian developers. Stop copy-pasting, start shipping.
82
+ MIT
@@ -10,7 +10,7 @@ function formatRupiah(amount, options) {
10
10
  spaceAfterSymbol = true
11
11
  } = options || {};
12
12
  const precision = options?.precision !== void 0 ? options.precision : decimal ? 2 : 0;
13
- const isNegative = amount < 0;
13
+ const isNegative = amount < 0 && amount !== 0;
14
14
  const absAmount = Math.abs(amount);
15
15
  let result;
16
16
  if (decimal) {
@@ -28,17 +28,21 @@ function formatRupiah(amount, options) {
28
28
  const intAmount = Math.floor(absAmount);
29
29
  result = intAmount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
30
30
  }
31
- if (isNegative) {
32
- result = `-${result}`;
33
- }
34
31
  if (symbol) {
35
32
  const space = spaceAfterSymbol ? " " : "";
36
- result = `Rp${space}${result}`;
33
+ if (isNegative) {
34
+ result = `-Rp${space}${result}`;
35
+ } else {
36
+ result = `Rp${space}${result}`;
37
+ }
38
+ } else if (isNegative) {
39
+ result = `-${result}`;
37
40
  }
38
41
  return result;
39
42
  }
40
- function formatCompact(amount) {
41
- const isNegative = amount < 0;
43
+ function formatCompact(amount, options) {
44
+ const { symbol = true, spaceAfterSymbol = true } = options || {};
45
+ const isNegative = amount < 0 && amount !== 0;
42
46
  const abs = Math.abs(amount);
43
47
  let result;
44
48
  if (abs >= 1e12) {
@@ -54,10 +58,17 @@ function formatCompact(amount) {
54
58
  } else {
55
59
  result = abs.toString();
56
60
  }
57
- if (isNegative) {
61
+ if (symbol) {
62
+ const space = spaceAfterSymbol ? " " : "";
63
+ if (isNegative) {
64
+ result = `-Rp${space}${result}`;
65
+ } else {
66
+ result = `Rp${space}${result}`;
67
+ }
68
+ } else if (isNegative) {
58
69
  result = `-${result}`;
59
70
  }
60
- return `Rp ${result}`;
71
+ return result;
61
72
  }
62
73
  function formatCompactValue(value, unit) {
63
74
  const rounded = Math.round(value * 10) / 10;
@@ -154,20 +165,42 @@ var TENS = [
154
165
  "sembilan puluh"
155
166
  ];
156
167
  function toWords(amount, options) {
157
- const { uppercase = false, withCurrency = true } = options || {};
168
+ const {
169
+ uppercase = false,
170
+ withCurrency = true,
171
+ withDecimals = false
172
+ } = options || {};
158
173
  if (amount === 0) {
159
174
  let result = "nol";
160
175
  if (withCurrency) result += " rupiah";
161
176
  return uppercase ? capitalize(result) : result;
162
177
  }
163
178
  const isNegative = amount < 0;
164
- const absAmount = Math.floor(Math.abs(amount));
179
+ const absAmount = Math.abs(amount);
180
+ const intPart = Math.floor(absAmount);
181
+ let words = convertInteger(intPart);
182
+ if (isNegative) {
183
+ words = "minus " + words;
184
+ }
185
+ if (withCurrency) {
186
+ words += " rupiah";
187
+ }
188
+ if (withDecimals) {
189
+ const decimalPart = Math.round((absAmount - intPart) * 100);
190
+ if (decimalPart > 0) {
191
+ words += " koma " + convertDecimal(decimalPart);
192
+ }
193
+ }
194
+ return uppercase ? capitalize(words) : words;
195
+ }
196
+ function convertInteger(num) {
197
+ if (num === 0) return "nol";
165
198
  let words = "";
166
- const triliun = Math.floor(absAmount / 1e12);
167
- const miliar = Math.floor(absAmount % 1e12 / 1e9);
168
- const juta = Math.floor(absAmount % 1e9 / 1e6);
169
- const ribu = Math.floor(absAmount % 1e6 / 1e3);
170
- const sisa = absAmount % 1e3;
199
+ const triliun = Math.floor(num / 1e12);
200
+ const miliar = Math.floor(num % 1e12 / 1e9);
201
+ const juta = Math.floor(num % 1e9 / 1e6);
202
+ const ribu = Math.floor(num % 1e6 / 1e3);
203
+ const sisa = num % 1e3;
171
204
  if (triliun > 0) {
172
205
  words += convertGroup(triliun) + " triliun";
173
206
  }
@@ -187,13 +220,19 @@ function toWords(amount, options) {
187
220
  if (words) words += " ";
188
221
  words += convertGroup(sisa);
189
222
  }
190
- if (isNegative) {
191
- words = "minus " + words;
192
- }
193
- if (withCurrency) {
194
- words += " rupiah";
223
+ return words;
224
+ }
225
+ function convertDecimal(num) {
226
+ if (num === 0) return "";
227
+ if (num < 10) return BASIC_NUMBERS[num];
228
+ if (num < 20) return TEENS[num - 10];
229
+ const tens = Math.floor(num / 10);
230
+ const ones = num % 10;
231
+ let result = TENS[tens];
232
+ if (ones > 0) {
233
+ result += " " + BASIC_NUMBERS[ones];
195
234
  }
196
- return uppercase ? capitalize(words) : words;
235
+ return result;
197
236
  }
198
237
  function convertGroup(num) {
199
238
  if (num === 0) return "";
@@ -243,26 +282,120 @@ function formatAccounting(amount, options) {
243
282
  }
244
283
  return formatted;
245
284
  }
246
- function calculateTax(amount, rate = 0.11) {
285
+ function calculateTax(amount, rate) {
247
286
  return amount * rate;
248
287
  }
249
288
  function addRupiahSymbol(amount) {
250
289
  if (typeof amount === "number") {
251
- return `Rp ${amount.toLocaleString("id-ID")}`;
290
+ const formatted = amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
291
+ return `Rp ${formatted}`;
252
292
  }
253
293
  if (amount.trim().startsWith("Rp")) {
254
- return amount;
294
+ return amount.trim();
255
295
  }
256
296
  return `Rp ${amount.trim()}`;
257
297
  }
258
298
 
299
+ // src/currency/calc.ts
300
+ var InvalidSplitError = class extends Error {
301
+ constructor(message) {
302
+ super(message);
303
+ this.name = "InvalidSplitError";
304
+ }
305
+ };
306
+ function splitAmount(amount, parts, options) {
307
+ if (parts < 1) {
308
+ throw new InvalidSplitError("Parts must be at least 1");
309
+ }
310
+ if (parts === 1) {
311
+ return [amount];
312
+ }
313
+ const { ratios, roundTo } = options || {};
314
+ if (ratios) {
315
+ if (ratios.length !== parts) {
316
+ throw new InvalidSplitError(
317
+ `Ratios length (${ratios.length}) must match parts count (${parts})`
318
+ );
319
+ }
320
+ const sum = ratios.reduce((a, b) => a + b, 0);
321
+ if (Math.abs(sum - 100) > 0.01) {
322
+ throw new InvalidSplitError(`Ratios must sum to 100 (got ${sum})`);
323
+ }
324
+ let result2 = ratios.map((r) => amount * (r / 100));
325
+ if (roundTo) {
326
+ result2 = result2.map((v) => roundToClean2(v, roundTo));
327
+ }
328
+ return result2;
329
+ }
330
+ const base = Math.floor(amount / parts);
331
+ const remainder = amount - base * parts;
332
+ const result = [];
333
+ for (let i = 0; i < parts; i++) {
334
+ result.push(base + (i < remainder ? 1 : 0));
335
+ }
336
+ if (roundTo) {
337
+ return result.map((v) => roundToClean2(v, roundTo));
338
+ }
339
+ return result;
340
+ }
341
+ function percentageOf(part, total) {
342
+ if (total === 0) return 0;
343
+ return part / total * 100;
344
+ }
345
+ function difference(amount1, amount2) {
346
+ const absolute = amount1 - amount2;
347
+ let percentage;
348
+ if (amount2 === 0) {
349
+ percentage = amount1 === 0 ? 0 : null;
350
+ } else {
351
+ percentage = absolute / amount2 * 100;
352
+ }
353
+ const direction = absolute > 0 ? "increase" : absolute < 0 ? "decrease" : "same";
354
+ return { absolute, percentage, direction };
355
+ }
356
+ function roundToClean2(amount, unit) {
357
+ const divisors = {
358
+ ribu: 1e3,
359
+ "ratus-ribu": 1e5,
360
+ juta: 1e6
361
+ };
362
+ return Math.round(amount / divisors[unit]) * divisors[unit];
363
+ }
364
+
365
+ // src/currency/validate.ts
366
+ function validateRupiah(formatted) {
367
+ if (!formatted || typeof formatted !== "string") {
368
+ return false;
369
+ }
370
+ const trimmed = formatted.trim();
371
+ if (!trimmed) return false;
372
+ const compactUnits = ["triliun", "miliar", "juta", "ribu"];
373
+ for (const unit of compactUnits) {
374
+ if (trimmed.toLowerCase().includes(unit)) {
375
+ return /-?\d+[,.]?\d*\s*(ribu|juta|miliar|triliun)/i.test(trimmed);
376
+ }
377
+ }
378
+ let cleaned = trimmed.replace(/^(-?\s*)?Rp\s*/i, "");
379
+ cleaned = cleaned.replace(/^\s*-/, "");
380
+ cleaned = cleaned.trim();
381
+ if (!cleaned) return false;
382
+ if (!/^[0-9.,]+$/.test(cleaned)) return false;
383
+ if (!/\d/.test(cleaned)) return false;
384
+ return true;
385
+ }
386
+
387
+ exports.InvalidSplitError = InvalidSplitError;
259
388
  exports.addRupiahSymbol = addRupiahSymbol;
260
389
  exports.calculateTax = calculateTax;
390
+ exports.difference = difference;
261
391
  exports.formatAccounting = formatAccounting;
262
392
  exports.formatCompact = formatCompact;
263
393
  exports.formatRupiah = formatRupiah;
264
394
  exports.parseRupiah = parseRupiah;
395
+ exports.percentageOf = percentageOf;
265
396
  exports.roundToClean = roundToClean;
397
+ exports.splitAmount = splitAmount;
266
398
  exports.toWords = toWords;
399
+ exports.validateRupiah = validateRupiah;
267
400
  //# sourceMappingURL=index.cjs.map
268
401
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/currency/format.ts","../../src/currency/parse.ts","../../src/currency/words.ts","../../src/currency/utils.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Currency formatting utilities for Indonesian Rupiah.\n *\n * @module currency/format\n * @packageDocumentation\n */\n\nimport type { RupiahOptions } from './types';\n\n/**\n * Formats a number as Indonesian Rupiah currency.\n *\n * Provides flexible formatting options including symbol display,\n * decimal places, and custom separators.\n *\n * @param amount - The amount to format\n * @param options - Formatting options\n * @returns Formatted Rupiah string\n *\n * @example\n * Basic formatting:\n * ```typescript\n * formatRupiah(1500000); // 'Rp 1.500.000'\n * ```\n *\n * @example\n * With decimals:\n * ```typescript\n * formatRupiah(1500000.50, { decimal: true }); // 'Rp 1.500.000,50'\n * ```\n *\n * @example\n * Without symbol:\n * ```typescript\n * formatRupiah(1500000, { symbol: false }); // '1.500.000'\n * ```\n *\n * @example\n * Custom separators:\n * ```typescript\n * formatRupiah(1500000, { separator: ',' }); // 'Rp 1,500,000'\n * ```\n *\n * @public\n */\nexport function formatRupiah(amount: number, options?: RupiahOptions): string {\n const {\n symbol = true,\n decimal = false,\n separator = '.',\n decimalSeparator = ',',\n spaceAfterSymbol = true,\n } = options || {};\n\n // Default precision: 2 for decimals, 0 otherwise\n const precision =\n options?.precision !== undefined ? options.precision : decimal ? 2 : 0;\n\n const isNegative = amount < 0;\n const absAmount = Math.abs(amount);\n\n let result: string;\n\n if (decimal) {\n const factor = Math.pow(10, precision);\n const rounded = Math.round(absAmount * factor) / factor;\n\n if (precision > 0) {\n const [intPart, decPart] = rounded.toFixed(precision).split('.');\n const formattedInt = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator);\n result = `${formattedInt}${decimalSeparator}${decPart}`;\n } else {\n // Precision 0: no decimal separator needed\n const intPart = rounded.toString();\n result = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator);\n }\n } else {\n const intAmount = Math.floor(absAmount);\n result = intAmount.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator);\n }\n\n if (isNegative) {\n result = `-${result}`;\n }\n\n if (symbol) {\n const space = spaceAfterSymbol ? ' ' : '';\n result = `Rp${space}${result}`;\n }\n\n return result;\n}\n\n/**\n * Formats a number in compact Indonesian format.\n *\n * Uses Indonesian units: ribu, juta, miliar, triliun.\n * Follows Indonesian grammar rules (e.g., \"1 juta\" not \"1,0 juta\").\n *\n * @param amount - The amount to format\n * @returns Compact formatted string\n *\n * @example\n * Millions:\n * ```typescript\n * formatCompact(1500000); // 'Rp 1,5 juta'\n * formatCompact(1000000); // 'Rp 1 juta'\n * ```\n *\n * @example\n * Thousands:\n * ```typescript\n * formatCompact(500000); // 'Rp 500 ribu'\n * ```\n *\n * @example\n * Small numbers:\n * ```typescript\n * formatCompact(1500); // 'Rp 1.500'\n * ```\n *\n * @public\n */\nexport function formatCompact(amount: number): string {\n const isNegative = amount < 0;\n const abs = Math.abs(amount);\n\n let result: string;\n\n if (abs >= 1_000_000_000_000) {\n result = formatCompactValue(abs / 1_000_000_000_000, 'triliun');\n } else if (abs >= 1_000_000_000) {\n result = formatCompactValue(abs / 1_000_000_000, 'miliar');\n } else if (abs >= 1_000_000) {\n result = formatCompactValue(abs / 1_000_000, 'juta');\n } else if (abs >= 100_000) {\n result = formatCompactValue(abs / 1000, 'ribu');\n } else if (abs >= 1_000) {\n // Below 100k: use standard formatting instead of \"ribu\"\n result = abs.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, '.');\n } else {\n result = abs.toString();\n }\n\n if (isNegative) {\n result = `-${result}`;\n }\n\n return `Rp ${result}`;\n}\n\n/**\n * Formats a value with Indonesian unit, applying grammar rules.\n *\n * Automatically removes trailing \".0\" to follow proper Indonesian grammar.\n * For example: \"1 juta\" instead of \"1,0 juta\".\n *\n * @param value - The numeric value to format\n * @param unit - The Indonesian unit (ribu, juta, miliar, triliun)\n * @returns Formatted string with unit\n * @internal\n */\nfunction formatCompactValue(value: number, unit: string): string {\n const rounded = Math.round(value * 10) / 10;\n\n if (rounded % 1 === 0) {\n return `${rounded.toFixed(0)} ${unit}`;\n }\n\n return `${rounded.toString().replace('.', ',')} ${unit}`;\n}\n","/**\n * Currency parsing utilities for Indonesian Rupiah.\n *\n * @module currency/parse\n * @packageDocumentation\n */\n\n/**\n * Parses a formatted Rupiah string back to a number.\n *\n * Handles multiple formats:\n * - Standard: \"Rp 1.500.000\"\n * - No symbol: \"1.500.000\"\n * - With decimals: \"Rp 1.500.000,50\"\n * - Compact: \"Rp 1,5 juta\", \"Rp 500 ribu\"\n *\n * @param formatted - The formatted Rupiah string to parse\n * @returns Parsed number, or null if invalid\n *\n * @example\n * Standard format:\n * ```typescript\n * parseRupiah('Rp 1.500.000'); // 1500000\n * ```\n *\n * @example\n * With decimals:\n * ```typescript\n * parseRupiah('Rp 1.500.000,50'); // 1500000.50\n * ```\n *\n * @example\n * Compact format:\n * ```typescript\n * parseRupiah('Rp 1,5 juta'); // 1500000\n * parseRupiah('Rp 500 ribu'); // 500000\n * ```\n *\n * @example\n * Invalid input:\n * ```typescript\n * parseRupiah('invalid'); // null\n * ```\n *\n * @public\n */\nexport function parseRupiah(formatted: string): number | null {\n if (!formatted || typeof formatted !== 'string') {\n return null;\n }\n\n const cleaned = formatted.trim().toLowerCase();\n\n // Check for compact units (juta, ribu, miliar, triliun)\n const compactUnits = {\n triliun: 1_000_000_000_000,\n miliar: 1_000_000_000,\n juta: 1_000_000,\n ribu: 1_000,\n };\n\n for (const [unit, multiplier] of Object.entries(compactUnits)) {\n if (cleaned.includes(unit)) {\n const match = cleaned.match(/(-?\\d+[,.]?\\d*)/);\n if (match) {\n const num = parseFloat(match[1].replace(',', '.'));\n return num * multiplier;\n }\n }\n }\n\n // Standard format: remove 'Rp' and spaces\n let numStr = cleaned.replace(/rp/gi, '').trim();\n\n const hasDot = numStr.includes('.');\n const hasComma = numStr.includes(',');\n\n if (hasDot && hasComma) {\n // Determine format based on last separator position\n // Indonesian: 1.500.000,50 vs International: 1,500,000.50\n const lastDot = numStr.lastIndexOf('.');\n const lastComma = numStr.lastIndexOf(',');\n\n if (lastComma > lastDot) {\n numStr = numStr.replace(/\\./g, '').replace(',', '.');\n } else {\n numStr = numStr.replace(/,/g, '');\n }\n } else if (hasComma) {\n const parts = numStr.split(',');\n // Decimal if only 1-2 digits after comma\n if (parts.length === 2 && parts[1].length <= 2) {\n numStr = numStr.replace(',', '.');\n } else {\n numStr = numStr.replace(/,/g, '');\n }\n } else if (hasDot) {\n const parts = numStr.split('.');\n // If not decimal format, remove dots (thousands separator)\n if (parts.length > 2 || (parts.length === 2 && parts[1].length > 2)) {\n numStr = numStr.replace(/\\./g, '');\n }\n }\n\n const parsed = parseFloat(numStr);\n return isNaN(parsed) ? null : parsed;\n}\n","/**\n * Convert numbers to Indonesian words (terbilang).\n *\n * @module currency/words\n * @packageDocumentation\n */\n\nimport type { WordOptions } from './types';\n\n/**\n * Basic Indonesian number words (0-9).\n * @internal\n */\nconst BASIC_NUMBERS = [\n '',\n 'satu',\n 'dua',\n 'tiga',\n 'empat',\n 'lima',\n 'enam',\n 'tujuh',\n 'delapan',\n 'sembilan',\n];\n\n/**\n * Indonesian words for 10-19.\n * @internal\n */\nconst TEENS = [\n 'sepuluh',\n 'sebelas',\n 'dua belas',\n 'tiga belas',\n 'empat belas',\n 'lima belas',\n 'enam belas',\n 'tujuh belas',\n 'delapan belas',\n 'sembilan belas',\n];\n\n/**\n * Indonesian words for tens (20, 30, 40, etc).\n * @internal\n */\nconst TENS = [\n '',\n '',\n 'dua puluh',\n 'tiga puluh',\n 'empat puluh',\n 'lima puluh',\n 'enam puluh',\n 'tujuh puluh',\n 'delapan puluh',\n 'sembilan puluh',\n];\n\n/**\n * Converts a number to Indonesian words (terbilang).\n *\n * Supports numbers up to trillions (triliun).\n * Follows Indonesian language rules for number pronunciation.\n *\n * Special rules:\n * - 1 = \"satu\" in most cases, but \"se-\" for 100, 1000\n * - 11 = \"sebelas\" (not \"satu belas\")\n * - 100 = \"seratus\" (not \"satu ratus\")\n * - 1000 = \"seribu\" (not \"satu ribu\")\n *\n * @param amount - The number to convert\n * @param options - Conversion options\n * @returns Indonesian words representation\n *\n * @example\n * Basic numbers:\n * ```typescript\n * toWords(123); // 'seratus dua puluh tiga rupiah'\n * ```\n *\n * @example\n * Large numbers:\n * ```typescript\n * toWords(1500000); // 'satu juta lima ratus ribu rupiah'\n * ```\n *\n * @example\n * With options:\n * ```typescript\n * toWords(1500000, { uppercase: true });\n * // 'Satu juta lima ratus ribu rupiah'\n *\n * toWords(1500000, { withCurrency: false });\n * // 'satu juta lima ratus ribu'\n * ```\n *\n * @public\n */\nexport function toWords(amount: number, options?: WordOptions): string {\n const { uppercase = false, withCurrency = true } = options || {};\n\n if (amount === 0) {\n let result = 'nol';\n if (withCurrency) result += ' rupiah';\n return uppercase ? capitalize(result) : result;\n }\n\n const isNegative = amount < 0;\n const absAmount = Math.floor(Math.abs(amount));\n\n let words = '';\n\n // Break into groups: triliun, miliar, juta, ribu, sisa\n const triliun = Math.floor(absAmount / 1_000_000_000_000);\n const miliar = Math.floor((absAmount % 1_000_000_000_000) / 1_000_000_000);\n const juta = Math.floor((absAmount % 1_000_000_000) / 1_000_000);\n const ribu = Math.floor((absAmount % 1_000_000) / 1_000);\n const sisa = absAmount % 1_000;\n\n if (triliun > 0) {\n words += convertGroup(triliun) + ' triliun';\n }\n\n if (miliar > 0) {\n if (words) words += ' ';\n words += convertGroup(miliar) + ' miliar';\n }\n\n if (juta > 0) {\n if (words) words += ' ';\n words += convertGroup(juta) + ' juta';\n }\n\n if (ribu > 0) {\n if (words) words += ' ';\n // Special rule: 1000 = \"seribu\" not \"satu ribu\"\n words += ribu === 1 ? 'seribu' : convertGroup(ribu) + ' ribu';\n }\n\n if (sisa > 0) {\n if (words) words += ' ';\n words += convertGroup(sisa);\n }\n\n if (isNegative) {\n words = 'minus ' + words;\n }\n\n if (withCurrency) {\n words += ' rupiah';\n }\n\n return uppercase ? capitalize(words) : words;\n}\n\n/**\n * Converts a group of 1-3 digits (0-999) to Indonesian words.\n *\n * @param num - Number to convert (0-999)\n * @returns Indonesian words for the number\n * @internal\n */\nfunction convertGroup(num: number): string {\n if (num === 0) return '';\n\n let result = '';\n\n const hundreds = Math.floor(num / 100);\n if (hundreds > 0) {\n // Special rule: 100 = \"seratus\" not \"satu ratus\"\n result = hundreds === 1 ? 'seratus' : BASIC_NUMBERS[hundreds] + ' ratus';\n }\n\n const remainder = num % 100;\n if (remainder > 0) {\n if (result) result += ' ';\n result += convertTwoDigits(remainder);\n }\n\n return result;\n}\n\n/**\n * Converts numbers 1-99 to Indonesian words.\n *\n * @param num - Number to convert (1-99)\n * @returns Indonesian words for the number\n * @internal\n */\nfunction convertTwoDigits(num: number): string {\n if (num === 0) return '';\n if (num < 10) return BASIC_NUMBERS[num];\n if (num >= 10 && num < 20) return TEENS[num - 10];\n\n const tens = Math.floor(num / 10);\n const ones = num % 10;\n\n let result = TENS[tens];\n if (ones > 0) {\n result += ' ' + BASIC_NUMBERS[ones];\n }\n\n return result;\n}\n\n/**\n * Capitalizes the first letter of a string.\n *\n * @param str - String to capitalize\n * @returns String with first letter capitalized\n * @internal\n */\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n","/**\n * Currency utility functions.\n *\n * @module currency/utils\n * @packageDocumentation\n */\n\nimport { formatRupiah } from './format';\nimport type { RoundUnit, RupiahOptions } from './types';\n\n/**\n * Rounds a number to a clean currency amount.\n *\n * Common use case: displaying approximate prices or budgets\n * in clean, rounded numbers.\n *\n * @param amount - The amount to round\n * @param unit - The unit to round to (default: 'ribu')\n * @returns Rounded amount\n *\n * @example\n * Round to thousands:\n * ```typescript\n * roundToClean(1234567, 'ribu'); // 1235000\n * ```\n *\n * @example\n * Round to hundred thousands:\n * ```typescript\n * roundToClean(1234567, 'ratus-ribu'); // 1200000\n * ```\n *\n * @example\n * Round to millions:\n * ```typescript\n * roundToClean(1234567, 'juta'); // 1000000\n * ```\n *\n * @public\n */\nexport function roundToClean(amount: number, unit: RoundUnit = 'ribu'): number {\n const divisors: Record<RoundUnit, number> = {\n ribu: 1000,\n 'ratus-ribu': 100000,\n juta: 1000000,\n };\n\n const divisor = divisors[unit];\n\n // Math.round handles both positive and negative numbers\n return Math.round(amount / divisor) * divisor;\n}\n\n/**\n * Formats a number as Indonesian Rupiah in accounting style.\n * Negative numbers are wrapped in parentheses.\n *\n * @param amount - The amount to format\n * @param options - Formatting options\n * @returns Formatted accounting string\n *\n * @example\n * ```typescript\n * formatAccounting(-1500000); // '(Rp 1.500.000)'\n * ```\n */\nexport function formatAccounting(\n amount: number,\n options?: RupiahOptions\n): string {\n const isNegative = amount < 0;\n const formatted = formatRupiah(Math.abs(amount), options);\n\n if (isNegative) {\n return `(${formatted})`;\n }\n\n return formatted;\n}\n\n/**\n * Calculates tax (PPN) for a given amount.\n *\n * @param amount - The base amount\n * @param rate - The tax rate (default: 0.11 for 11%)\n * @returns The calculated tax amount\n *\n * @example\n * ```typescript\n * calculateTax(1000000); // 110000\n * ```\n */\nexport function calculateTax(amount: number, rate: number = 0.11): number {\n return amount * rate;\n}\n\n/**\n * Helper to ensure a string or number has the 'Rp ' prefix.\n * If already prefixed, it returns the input as is.\n *\n * @param amount - The amount or formatted string\n * @returns String with Rupiah prefix\n */\nexport function addRupiahSymbol(amount: string | number): string {\n if (typeof amount === 'number') {\n return `Rp ${amount.toLocaleString('id-ID')}`;\n }\n\n if (amount.trim().startsWith('Rp')) {\n return amount;\n }\n\n return `Rp ${amount.trim()}`;\n}\n"]}
1
+ {"version":3,"sources":["../../src/currency/format.ts","../../src/currency/parse.ts","../../src/currency/words.ts","../../src/currency/utils.ts","../../src/currency/calc.ts","../../src/currency/validate.ts"],"names":["result","roundToClean"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Currency formatting utilities for Indonesian Rupiah.\n *\n * @module currency/format\n * @packageDocumentation\n */\n\nimport type { CompactOptions, RupiahOptions } from './types';\n\n/**\n * Formats a number as Indonesian Rupiah currency.\n *\n * Provides flexible formatting options including symbol display,\n * decimal places, and custom separators.\n *\n * @param amount - The amount to format\n * @param options - Formatting options\n * @returns Formatted Rupiah string\n *\n * @example\n * Basic formatting:\n * ```typescript\n * formatRupiah(1500000); // 'Rp 1.500.000'\n * ```\n *\n * @example\n * With decimals:\n * ```typescript\n * formatRupiah(1500000.50, { decimal: true }); // 'Rp 1.500.000,50'\n * ```\n *\n * @example\n * Without symbol:\n * ```typescript\n * formatRupiah(1500000, { symbol: false }); // '1.500.000'\n * ```\n *\n * @example\n * Custom separators:\n * ```typescript\n * formatRupiah(1500000, { separator: ',' }); // 'Rp 1,500,000'\n * ```\n *\n * @public\n */\nexport function formatRupiah(amount: number, options?: RupiahOptions): string {\n const {\n symbol = true,\n decimal = false,\n separator = '.',\n decimalSeparator = ',',\n spaceAfterSymbol = true,\n } = options || {};\n\n // Default precision: 2 for decimals, 0 otherwise\n const precision =\n options?.precision !== undefined ? options.precision : decimal ? 2 : 0;\n\n const isNegative = amount < 0 && amount !== 0;\n const absAmount = Math.abs(amount);\n\n let result: string;\n\n if (decimal) {\n const factor = Math.pow(10, precision);\n const rounded = Math.round(absAmount * factor) / factor;\n\n if (precision > 0) {\n const [intPart, decPart] = rounded.toFixed(precision).split('.');\n const formattedInt = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator);\n result = `${formattedInt}${decimalSeparator}${decPart}`;\n } else {\n const intPart = rounded.toString();\n result = intPart.replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator);\n }\n } else {\n const intAmount = Math.floor(absAmount);\n result = intAmount.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, separator);\n }\n\n if (symbol) {\n const space = spaceAfterSymbol ? ' ' : '';\n if (isNegative) {\n result = `-Rp${space}${result}`;\n } else {\n result = `Rp${space}${result}`;\n }\n } else if (isNegative) {\n result = `-${result}`;\n }\n\n return result;\n}\n\n/**\n * Formats a number in compact Indonesian format.\n *\n * Uses Indonesian units: ribu, juta, miliar, triliun.\n * Follows Indonesian grammar rules (e.g., \"1 juta\" not \"1,0 juta\").\n *\n * @param amount - The amount to format\n * @param options - Compact formatting options\n * @returns Compact formatted string\n *\n * @example\n * Millions:\n * ```typescript\n * formatCompact(1500000); // 'Rp 1,5 juta'\n * formatCompact(1000000); // 'Rp 1 juta'\n * ```\n *\n * @example\n * Thousands:\n * ```typescript\n * formatCompact(500000); // 'Rp 500 ribu'\n * ```\n *\n * @example\n * Small numbers:\n * ```typescript\n * formatCompact(1500); // 'Rp 1.500'\n * ```\n *\n * @example\n * Without symbol:\n * ```typescript\n * formatCompact(1500000, { symbol: false }); // '1,5 juta'\n * ```\n *\n * @public\n */\nexport function formatCompact(\n amount: number,\n options?: CompactOptions\n): string {\n const { symbol = true, spaceAfterSymbol = true } = options || {};\n\n const isNegative = amount < 0 && amount !== 0;\n const abs = Math.abs(amount);\n\n let result: string;\n\n if (abs >= 1_000_000_000_000) {\n result = formatCompactValue(abs / 1_000_000_000_000, 'triliun');\n } else if (abs >= 1_000_000_000) {\n result = formatCompactValue(abs / 1_000_000_000, 'miliar');\n } else if (abs >= 1_000_000) {\n result = formatCompactValue(abs / 1_000_000, 'juta');\n } else if (abs >= 100_000) {\n result = formatCompactValue(abs / 1000, 'ribu');\n } else if (abs >= 1_000) {\n result = abs.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, '.');\n } else {\n result = abs.toString();\n }\n\n if (symbol) {\n const space = spaceAfterSymbol ? ' ' : '';\n if (isNegative) {\n result = `-Rp${space}${result}`;\n } else {\n result = `Rp${space}${result}`;\n }\n } else if (isNegative) {\n result = `-${result}`;\n }\n\n return result;\n}\n\n/**\n * Formats a value with Indonesian unit, applying grammar rules.\n *\n * Automatically removes trailing \".0\" to follow proper Indonesian grammar.\n * For example: \"1 juta\" instead of \"1,0 juta\".\n *\n * @param value - The numeric value to format\n * @param unit - The Indonesian unit (ribu, juta, miliar, triliun)\n * @returns Formatted string with unit\n * @internal\n */\nfunction formatCompactValue(value: number, unit: string): string {\n const rounded = Math.round(value * 10) / 10;\n\n if (rounded % 1 === 0) {\n return `${rounded.toFixed(0)} ${unit}`;\n }\n\n return `${rounded.toString().replace('.', ',')} ${unit}`;\n}\n","/**\n * Currency parsing utilities for Indonesian Rupiah.\n *\n * @module currency/parse\n * @packageDocumentation\n */\n\n/**\n * Parses a formatted Rupiah string back to a number.\n *\n * Handles multiple formats:\n * - Standard: \"Rp 1.500.000\"\n * - No symbol: \"1.500.000\"\n * - With decimals: \"Rp 1.500.000,50\"\n * - Compact: \"Rp 1,5 juta\", \"Rp 500 ribu\"\n *\n * @param formatted - The formatted Rupiah string to parse\n * @returns Parsed number, or null if invalid\n *\n * @example\n * Standard format:\n * ```typescript\n * parseRupiah('Rp 1.500.000'); // 1500000\n * ```\n *\n * @example\n * With decimals:\n * ```typescript\n * parseRupiah('Rp 1.500.000,50'); // 1500000.50\n * ```\n *\n * @example\n * Compact format:\n * ```typescript\n * parseRupiah('Rp 1,5 juta'); // 1500000\n * parseRupiah('Rp 500 ribu'); // 500000\n * ```\n *\n * @example\n * Invalid input:\n * ```typescript\n * parseRupiah('invalid'); // null\n * ```\n *\n * @public\n */\nexport function parseRupiah(formatted: string): number | null {\n if (!formatted || typeof formatted !== 'string') {\n return null;\n }\n\n const cleaned = formatted.trim().toLowerCase();\n\n // Check for compact units (juta, ribu, miliar, triliun)\n const compactUnits = {\n triliun: 1_000_000_000_000,\n miliar: 1_000_000_000,\n juta: 1_000_000,\n ribu: 1_000,\n };\n\n for (const [unit, multiplier] of Object.entries(compactUnits)) {\n if (cleaned.includes(unit)) {\n const match = cleaned.match(/(-?\\d+[,.]?\\d*)/);\n if (match) {\n const num = parseFloat(match[1].replace(',', '.'));\n return num * multiplier;\n }\n }\n }\n\n // Standard format: remove 'Rp' and spaces\n let numStr = cleaned.replace(/rp/gi, '').trim();\n\n const hasDot = numStr.includes('.');\n const hasComma = numStr.includes(',');\n\n if (hasDot && hasComma) {\n // Determine format based on last separator position\n // Indonesian: 1.500.000,50 vs International: 1,500,000.50\n const lastDot = numStr.lastIndexOf('.');\n const lastComma = numStr.lastIndexOf(',');\n\n if (lastComma > lastDot) {\n numStr = numStr.replace(/\\./g, '').replace(',', '.');\n } else {\n numStr = numStr.replace(/,/g, '');\n }\n } else if (hasComma) {\n const parts = numStr.split(',');\n // Decimal if only 1-2 digits after comma\n if (parts.length === 2 && parts[1].length <= 2) {\n numStr = numStr.replace(',', '.');\n } else {\n numStr = numStr.replace(/,/g, '');\n }\n } else if (hasDot) {\n const parts = numStr.split('.');\n // If not decimal format, remove dots (thousands separator)\n if (parts.length > 2 || (parts.length === 2 && parts[1].length > 2)) {\n numStr = numStr.replace(/\\./g, '');\n }\n }\n\n const parsed = parseFloat(numStr);\n return isNaN(parsed) ? null : parsed;\n}\n","/**\n * Convert numbers to Indonesian words (terbilang).\n *\n * @module currency/words\n * @packageDocumentation\n */\n\nimport type { WordOptions } from './types';\n\n/**\n * Basic Indonesian number words (0-9).\n * @internal\n */\nconst BASIC_NUMBERS = [\n '',\n 'satu',\n 'dua',\n 'tiga',\n 'empat',\n 'lima',\n 'enam',\n 'tujuh',\n 'delapan',\n 'sembilan',\n];\n\n/**\n * Indonesian words for 10-19.\n * @internal\n */\nconst TEENS = [\n 'sepuluh',\n 'sebelas',\n 'dua belas',\n 'tiga belas',\n 'empat belas',\n 'lima belas',\n 'enam belas',\n 'tujuh belas',\n 'delapan belas',\n 'sembilan belas',\n];\n\n/**\n * Indonesian words for tens (20, 30, 40, etc).\n * @internal\n */\nconst TENS = [\n '',\n '',\n 'dua puluh',\n 'tiga puluh',\n 'empat puluh',\n 'lima puluh',\n 'enam puluh',\n 'tujuh puluh',\n 'delapan puluh',\n 'sembilan puluh',\n];\n\n/**\n * Converts a number to Indonesian words (terbilang).\n *\n * Supports numbers up to trillions (triliun).\n * Follows Indonesian language rules for number pronunciation.\n *\n * Special rules:\n * - 1 = \"satu\" in most cases, but \"se-\" for 100, 1000\n * - 11 = \"sebelas\" (not \"satu belas\")\n * - 100 = \"seratus\" (not \"satu ratus\")\n * - 1000 = \"seribu\" (not \"satu ribu\")\n *\n * @param amount - The number to convert\n * @param options - Conversion options\n * @returns Indonesian words representation\n *\n * @example\n * Basic numbers:\n * ```typescript\n * toWords(123); // 'seratus dua puluh tiga rupiah'\n * ```\n *\n * @example\n * Large numbers:\n * ```typescript\n * toWords(1500000); // 'satu juta lima ratus ribu rupiah'\n * ```\n *\n * @example\n * With options:\n * ```typescript\n * toWords(1500000, { uppercase: true });\n * // 'Satu juta lima ratus ribu rupiah'\n *\n * toWords(1500000, { withCurrency: false });\n * // 'satu juta lima ratus ribu'\n * ```\n *\n * @public\n */\nexport function toWords(amount: number, options?: WordOptions): string {\n const {\n uppercase = false,\n withCurrency = true,\n withDecimals = false,\n } = options || {};\n\n if (amount === 0) {\n let result = 'nol';\n if (withCurrency) result += ' rupiah';\n return uppercase ? capitalize(result) : result;\n }\n\n const isNegative = amount < 0;\n const absAmount = Math.abs(amount);\n const intPart = Math.floor(absAmount);\n\n let words = convertInteger(intPart);\n\n if (isNegative) {\n words = 'minus ' + words;\n }\n\n if (withCurrency) {\n words += ' rupiah';\n }\n\n if (withDecimals) {\n const decimalPart = Math.round((absAmount - intPart) * 100);\n if (decimalPart > 0) {\n words += ' koma ' + convertDecimal(decimalPart);\n }\n }\n\n return uppercase ? capitalize(words) : words;\n}\n\n/**\n * Converts the integer part to Indonesian words.\n */\nfunction convertInteger(num: number): string {\n if (num === 0) return 'nol';\n\n let words = '';\n\n const triliun = Math.floor(num / 1_000_000_000_000);\n const miliar = Math.floor((num % 1_000_000_000_000) / 1_000_000_000);\n const juta = Math.floor((num % 1_000_000_000) / 1_000_000);\n const ribu = Math.floor((num % 1_000_000) / 1_000);\n const sisa = num % 1_000;\n\n if (triliun > 0) {\n words += convertGroup(triliun) + ' triliun';\n }\n\n if (miliar > 0) {\n if (words) words += ' ';\n words += convertGroup(miliar) + ' miliar';\n }\n\n if (juta > 0) {\n if (words) words += ' ';\n words += convertGroup(juta) + ' juta';\n }\n\n if (ribu > 0) {\n if (words) words += ' ';\n words += ribu === 1 ? 'seribu' : convertGroup(ribu) + ' ribu';\n }\n\n if (sisa > 0) {\n if (words) words += ' ';\n words += convertGroup(sisa);\n }\n\n return words;\n}\n\n/**\n * Converts decimal part (0-99) to Indonesian words.\n */\nfunction convertDecimal(num: number): string {\n if (num === 0) return '';\n if (num < 10) return BASIC_NUMBERS[num];\n if (num < 20) return TEENS[num - 10];\n\n const tens = Math.floor(num / 10);\n const ones = num % 10;\n\n let result = TENS[tens];\n if (ones > 0) {\n result += ' ' + BASIC_NUMBERS[ones];\n }\n\n return result;\n}\n\n/**\n * Converts a group of 1-3 digits (0-999) to Indonesian words.\n *\n * @param num - Number to convert (0-999)\n * @returns Indonesian words for the number\n * @internal\n */\nfunction convertGroup(num: number): string {\n if (num === 0) return '';\n\n let result = '';\n\n const hundreds = Math.floor(num / 100);\n if (hundreds > 0) {\n // Special rule: 100 = \"seratus\" not \"satu ratus\"\n result = hundreds === 1 ? 'seratus' : BASIC_NUMBERS[hundreds] + ' ratus';\n }\n\n const remainder = num % 100;\n if (remainder > 0) {\n if (result) result += ' ';\n result += convertTwoDigits(remainder);\n }\n\n return result;\n}\n\n/**\n * Converts numbers 1-99 to Indonesian words.\n *\n * @param num - Number to convert (1-99)\n * @returns Indonesian words for the number\n * @internal\n */\nfunction convertTwoDigits(num: number): string {\n if (num === 0) return '';\n if (num < 10) return BASIC_NUMBERS[num];\n if (num >= 10 && num < 20) return TEENS[num - 10];\n\n const tens = Math.floor(num / 10);\n const ones = num % 10;\n\n let result = TENS[tens];\n if (ones > 0) {\n result += ' ' + BASIC_NUMBERS[ones];\n }\n\n return result;\n}\n\n/**\n * Capitalizes the first letter of a string.\n *\n * @param str - String to capitalize\n * @returns String with first letter capitalized\n * @internal\n */\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n","/**\n * Currency utility functions.\n *\n * @module currency/utils\n * @packageDocumentation\n */\n\nimport { formatRupiah } from './format';\nimport type { RoundUnit, RupiahOptions } from './types';\n\n/**\n * Rounds a number to a clean currency amount.\n *\n * Common use case: displaying approximate prices or budgets\n * in clean, rounded numbers.\n *\n * @param amount - The amount to round\n * @param unit - The unit to round to (default: 'ribu')\n * @returns Rounded amount\n *\n * @example\n * Round to thousands:\n * ```typescript\n * roundToClean(1234567, 'ribu'); // 1235000\n * ```\n *\n * @example\n * Round to hundred thousands:\n * ```typescript\n * roundToClean(1234567, 'ratus-ribu'); // 1200000\n * ```\n *\n * @example\n * Round to millions:\n * ```typescript\n * roundToClean(1234567, 'juta'); // 1000000\n * ```\n *\n * @public\n */\nexport function roundToClean(amount: number, unit: RoundUnit = 'ribu'): number {\n const divisors: Record<RoundUnit, number> = {\n ribu: 1000,\n 'ratus-ribu': 100000,\n juta: 1000000,\n };\n\n const divisor = divisors[unit];\n\n // Math.round handles both positive and negative numbers\n return Math.round(amount / divisor) * divisor;\n}\n\n/**\n * Formats a number as Indonesian Rupiah in accounting style.\n * Negative numbers are wrapped in parentheses.\n *\n * @param amount - The amount to format\n * @param options - Formatting options\n * @returns Formatted accounting string\n *\n * @example\n * ```typescript\n * formatAccounting(-1500000); // '(Rp 1.500.000)'\n * ```\n */\nexport function formatAccounting(\n amount: number,\n options?: RupiahOptions\n): string {\n const isNegative = amount < 0;\n const formatted = formatRupiah(Math.abs(amount), options);\n\n if (isNegative) {\n return `(${formatted})`;\n }\n\n return formatted;\n}\n\n/**\n * Calculates tax (PPN) for a given amount.\n *\n * @param amount - The base amount\n * @param rate - The tax rate (e.g., 0.11 for 11% PPN)\n * @returns The calculated tax amount\n *\n * @example\n * ```typescript\n * calculateTax(1000000, 0.11); // 110000\n * ```\n */\nexport function calculateTax(amount: number, rate: number): number {\n return amount * rate;\n}\n\n/**\n * Helper to ensure a string or number has the 'Rp ' prefix.\n * If already prefixed, it returns the input as is.\n *\n * @param amount - The amount or formatted string\n * @returns String with Rupiah prefix\n */\nexport function addRupiahSymbol(amount: string | number): string {\n if (typeof amount === 'number') {\n const formatted = amount.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, '.');\n return `Rp ${formatted}`;\n }\n\n if (amount.trim().startsWith('Rp')) {\n return amount.trim();\n }\n\n return `Rp ${amount.trim()}`;\n}\n","import type { RoundUnit, SplitOptions } from './types';\n\n/**\n * Invalid split error thrown when split parameters are invalid.\n *\n * @public\n */\nexport class InvalidSplitError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'InvalidSplitError';\n }\n}\n\n/**\n * Splits an amount into equal or custom-ratio parts.\n *\n * @param amount - The amount to split\n * @param parts - Number of parts to split into\n * @param options - Split options (ratios, rounding)\n * @returns Array of split amounts\n *\n * @example\n * Equal split:\n * ```typescript\n * splitAmount(1500000, 3); // [500000, 500000, 500000]\n * ```\n *\n * @example\n * Custom ratios:\n * ```typescript\n * splitAmount(1000000, 2, { ratios: [70, 30] }); // [700000, 300000]\n * ```\n *\n * @example\n * With rounding:\n * ```typescript\n * splitAmount(1234567, 3, { roundTo: 'ribu' }); // [412000, 411000, 411000]\n * ```\n *\n * @public\n */\nexport function splitAmount(\n amount: number,\n parts: number,\n options?: SplitOptions\n): number[] {\n if (parts < 1) {\n throw new InvalidSplitError('Parts must be at least 1');\n }\n\n if (parts === 1) {\n return [amount];\n }\n\n const { ratios, roundTo } = options || {};\n\n if (ratios) {\n if (ratios.length !== parts) {\n throw new InvalidSplitError(\n `Ratios length (${ratios.length}) must match parts count (${parts})`\n );\n }\n\n const sum = ratios.reduce((a, b) => a + b, 0);\n if (Math.abs(sum - 100) > 0.01) {\n throw new InvalidSplitError(`Ratios must sum to 100 (got ${sum})`);\n }\n\n let result = ratios.map((r) => amount * (r / 100));\n\n if (roundTo) {\n result = result.map((v) => roundToClean(v, roundTo));\n }\n\n return result;\n }\n\n const base = Math.floor(amount / parts);\n const remainder = amount - base * parts;\n\n const result: number[] = [];\n for (let i = 0; i < parts; i++) {\n result.push(base + (i < remainder ? 1 : 0));\n }\n\n if (roundTo) {\n return result.map((v) => roundToClean(v, roundTo));\n }\n\n return result;\n}\n\n/**\n * Calculates what percentage a part is of a total.\n *\n * @param part - The part value\n * @param total - The total value\n * @returns Percentage as number (e.g., 15 for 15%)\n *\n * @example\n * ```typescript\n * percentageOf(150000, 1000000); // 15\n * percentageOf(0, 1000000); // 0\n * percentageOf(100, 0); // 0 (not NaN)\n * ```\n *\n * @public\n */\nexport function percentageOf(part: number, total: number): number {\n if (total === 0) return 0;\n return (part / total) * 100;\n}\n\n/**\n * Calculates absolute and percentage difference between two amounts.\n *\n * @param amount1 - The new/current amount\n * @param amount2 - The original/reference amount\n * @returns Object with absolute difference, percentage, and direction\n *\n * @example\n * ```typescript\n * difference(1200000, 1000000);\n * // { absolute: 200000, percentage: 20, direction: 'increase' }\n *\n * difference(0, 1000000);\n * // { absolute: -1000000, percentage: null, direction: 'decrease' }\n * ```\n *\n * @public\n */\nexport function difference(\n amount1: number,\n amount2: number\n): {\n absolute: number;\n percentage: number | null;\n direction: 'increase' | 'decrease' | 'same';\n} {\n const absolute = amount1 - amount2;\n\n let percentage: number | null;\n if (amount2 === 0) {\n percentage = amount1 === 0 ? 0 : null;\n } else {\n percentage = (absolute / amount2) * 100;\n }\n\n const direction: 'increase' | 'decrease' | 'same' =\n absolute > 0 ? 'increase' : absolute < 0 ? 'decrease' : 'same';\n\n return { absolute, percentage, direction };\n}\n\n/**\n * Rounds a number to a clean currency amount.\n * Internal helper — also exported from utils.ts for public API.\n */\nfunction roundToClean(amount: number, unit: RoundUnit): number {\n const divisors: Record<RoundUnit, number> = {\n ribu: 1000,\n 'ratus-ribu': 100000,\n juta: 1000000,\n };\n\n return Math.round(amount / divisors[unit]) * divisors[unit];\n}\n","/**\n * Validates whether a string is a valid Rupiah format.\n *\n * Accepts standard, compact, and negative formats.\n *\n * @param formatted - The string to validate\n * @returns `true` if valid Rupiah format, `false` otherwise\n *\n * @example\n * ```typescript\n * validateRupiah('Rp 1.500.000'); // true\n * validateRupiah('1.500.000'); // true\n * validateRupiah('Rp 1,5 juta'); // true\n * validateRupiah('abc'); // false\n * validateRupiah(''); // false\n * ```\n *\n * @public\n */\nexport function validateRupiah(formatted: string): boolean {\n if (!formatted || typeof formatted !== 'string') {\n return false;\n }\n\n const trimmed = formatted.trim();\n\n if (!trimmed) return false;\n\n const compactUnits = ['triliun', 'miliar', 'juta', 'ribu'];\n\n // Check for compact format\n for (const unit of compactUnits) {\n if (trimmed.toLowerCase().includes(unit)) {\n return /-?\\d+[,.]?\\d*\\s*(ribu|juta|miliar|triliun)/i.test(trimmed);\n }\n }\n\n // Standard format: remove optional Rp prefix and optional negative sign\n let cleaned = trimmed.replace(/^(-?\\s*)?Rp\\s*/i, '');\n\n // Remove negative sign if still present\n cleaned = cleaned.replace(/^\\s*-/, '');\n\n cleaned = cleaned.trim();\n\n if (!cleaned) return false;\n\n // Must be digits with dots and/or commas\n if (!/^[0-9.,]+$/.test(cleaned)) return false;\n\n // Must have at least one digit\n if (!/\\d/.test(cleaned)) return false;\n\n return true;\n}\n"]}