@indodev/toolkit 0.0.1

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/LICENCE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Adamm
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # @indodev/toolkit
2
+
3
+ **Production-ready utilities for Indonesian developers.** Stop rewriting the same validation logic - use battle-tested functions that just work.
4
+
5
+ [![npm version](https://badge.fury.io/js/@indodev%2Ftoolkit.svg)](https://www.npmjs.com/package/@indodev/toolkit)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](https://www.typescriptlang.org/)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
8
+
9
+ ## Why This Exists
10
+
11
+ Building apps for Indonesia? You need NIK validation, phone formatting, and Rupiah display. Instead of copy-pasting regex from StackOverflow, use this.
12
+
13
+ **What you get:**
14
+ - NIK validation that actually works (province codes, birth dates, gender)
15
+ - Phone numbers formatted correctly (Telkomsel, XL, Indosat, etc)
16
+ - Rupiah with proper grammar ("1,5 juta" not "1,0 juta")
17
+ - Terbilang converter (1500000 → "satu juta lima ratus ribu rupiah")
18
+ - TypeScript types that make sense
19
+ - 150+ tests so you can ship with confidence
20
+
21
+ ## Install
22
+ ```bash
23
+ npm install @indodev/toolkit
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ### NIK Validation & Parsing
29
+ ```typescript
30
+ import { validateNIK, parseNIK, maskNIK } from '@indodev/toolkit/nik';
31
+
32
+ // Validate
33
+ validateNIK('3201234567890123'); // true
34
+ validateNIK('1234'); // false
35
+
36
+ // Extract info
37
+ const info = parseNIK('3201234567890123');
38
+ console.log(info.province.name); // 'Jawa Barat'
39
+ console.log(info.birthDate); // Date object
40
+ console.log(info.gender); // 'male' or 'female'
41
+
42
+ // Mask for privacy
43
+ maskNIK('3201234567890123'); // '3201****0123'
44
+ ```
45
+
46
+ ### Phone Numbers
47
+ ```typescript
48
+ import { validatePhoneNumber, formatPhoneNumber, getOperator } from '@indodev/toolkit/phone';
49
+
50
+ // Validate (works with 08xx, +62, 62 formats)
51
+ validatePhoneNumber('081234567890'); // true
52
+ validatePhoneNumber('+6281234567890'); // true
53
+
54
+ // Format
55
+ formatPhoneNumber('081234567890', 'international'); // '+62 812-3456-7890'
56
+ formatPhoneNumber('081234567890', 'national'); // '0812-3456-7890'
57
+
58
+ // Detect operator
59
+ getOperator('081234567890'); // 'Telkomsel'
60
+ getOperator('085612345678'); // 'Indosat'
61
+ ```
62
+
63
+ ### Currency Formatting
64
+ ```typescript
65
+ import { formatRupiah, formatCompact, toWords } from '@indodev/toolkit/currency';
66
+
67
+ // Standard format
68
+ formatRupiah(1500000); // 'Rp 1.500.000'
69
+ formatRupiah(1500000.50, { decimal: true }); // 'Rp 1.500.000,50'
70
+
71
+ // Compact format (follows Indonesian grammar!)
72
+ formatCompact(1500000); // 'Rp 1,5 juta'
73
+ formatCompact(1000000); // 'Rp 1 juta' (not '1,0 juta')
74
+
75
+ // Terbilang
76
+ toWords(1500000);
77
+ // 'satu juta lima ratus ribu rupiah'
78
+
79
+ toWords(1500000, { uppercase: true, withCurrency: false });
80
+ // 'Satu juta lima ratus ribu'
81
+ ```
82
+
83
+ ### Parsing (Reverse Operations)
84
+ ```typescript
85
+ import { parseNIK } from '@indodev/toolkit/nik';
86
+ import { parsePhoneNumber } from '@indodev/toolkit/phone';
87
+ import { parseRupiah } from '@indodev/toolkit/currency';
88
+
89
+ // Parse formatted strings back
90
+ parseRupiah('Rp 1.500.000'); // 1500000
91
+ parseRupiah('Rp 1,5 juta'); // 1500000
92
+ parseRupiah('Rp 500 ribu'); // 500000
93
+ ```
94
+
95
+ ## Real-World Examples
96
+
97
+ ### E-commerce Checkout
98
+ ```typescript
99
+ import { formatRupiah, formatCompact } from '@indodev/toolkit/currency';
100
+
101
+ // Product card
102
+ <div className="price">
103
+ {formatCompact(product.price)} {/* "Rp 1,5 juta" */}
104
+ </div>
105
+
106
+ // Checkout total
107
+ <div className="total">
108
+ Total: {formatRupiah(cart.total, { decimal: true })}
109
+ {/* "Rp 1.500.000,50" */}
110
+ </div>
111
+ ```
112
+
113
+ ### User Registration Form
114
+ ```typescript
115
+ import { validateNIK } from '@indodev/toolkit/nik';
116
+ import { validatePhoneNumber } from '@indodev/toolkit/phone';
117
+
118
+ function validateForm(data) {
119
+ if (!validateNIK(data.nik)) {
120
+ return 'NIK tidak valid';
121
+ }
122
+
123
+ if (!validatePhoneNumber(data.phone)) {
124
+ return 'Nomor telepon tidak valid';
125
+ }
126
+
127
+ return null;
128
+ }
129
+ ```
130
+
131
+ ### Invoice Generator
132
+ ```typescript
133
+ import { formatRupiah, toWords } from '@indodev/toolkit/currency';
134
+
135
+ const total = 1500000;
136
+
137
+ console.log(`Total: ${formatRupiah(total)}`);
138
+ console.log(`Terbilang: ${toWords(total, { uppercase: true })}`);
139
+
140
+ // Output:
141
+ // Total: Rp 1.500.000
142
+ // Terbilang: Satu juta lima ratus ribu rupiah
143
+ ```
144
+
145
+ ## TypeScript Support
146
+
147
+ Full type inference out of the box:
148
+ ```typescript
149
+ import type { NIKInfo, PhoneInfo, RupiahOptions } from '@indodev/toolkit';
150
+
151
+ const nikInfo: NIKInfo = parseNIK('3201234567890123');
152
+ // nikInfo.province ✓ auto-complete works
153
+ // nikInfo.birthDate ✓ Date type
154
+ // nikInfo.gender ✓ 'male' | 'female' | null
155
+
156
+ const options: RupiahOptions = {
157
+ symbol: true,
158
+ decimal: true,
159
+ precision: 2,
160
+ separator: '.',
161
+ decimalSeparator: ',',
162
+ };
163
+ ```
164
+
165
+ ## Tree-Shaking
166
+
167
+ Import only what you need - unused code gets removed:
168
+ ```typescript
169
+ // ✅ Recommended: Import from submodules
170
+ import { formatRupiah } from '@indodev/toolkit/currency';
171
+ import { validateNIK } from '@indodev/toolkit/nik';
172
+
173
+ // ⚠️ Works but imports everything
174
+ import { formatRupiah, validateNIK } from '@indodev/toolkit';
175
+ ```
176
+
177
+ ## Framework Examples
178
+
179
+ Works with any framework:
180
+ ```typescript
181
+ // React
182
+ import { formatRupiah } from '@indodev/toolkit/currency';
183
+
184
+ function ProductCard({ price }) {
185
+ return <div>{formatRupiah(price)}</div>;
186
+ }
187
+
188
+ // Vue
189
+ import { formatPhoneNumber } from '@indodev/toolkit/phone';
190
+
191
+ export default {
192
+ computed: {
193
+ formattedPhone() {
194
+ return formatPhoneNumber(this.phone, 'international');
195
+ }
196
+ }
197
+ }
198
+
199
+ // Svelte
200
+ <script>
201
+ import { validateNIK } from '@indodev/toolkit/nik';
202
+
203
+ $: isValid = validateNIK(nik);
204
+ </script>
205
+ ```
206
+
207
+ ## API Reference
208
+
209
+ ### NIK Module
210
+
211
+ | Function | Description |
212
+ |----------|-------------|
213
+ | `validateNIK(nik)` | Check if NIK is valid |
214
+ | `parseNIK(nik)` | Extract province, birth date, gender |
215
+ | `formatNIK(nik, separator?)` | Format with separators |
216
+ | `maskNIK(nik, options?)` | Mask for privacy |
217
+
218
+ [Full NIK docs →](#)
219
+
220
+ ### Phone Module
221
+
222
+ | Function | Description |
223
+ |----------|-------------|
224
+ | `validatePhoneNumber(phone)` | Validate Indonesian phone numbers |
225
+ | `formatPhoneNumber(phone, format)` | Format to international/national/e164 |
226
+ | `getOperator(phone)` | Detect operator (Telkomsel, XL, etc) |
227
+ | `parsePhoneNumber(phone)` | Get all phone info |
228
+
229
+ [Full Phone docs →](#)
230
+
231
+ ### Currency Module
232
+
233
+ | Function | Description |
234
+ |----------|-------------|
235
+ | `formatRupiah(amount, options?)` | Standard Rupiah format |
236
+ | `formatCompact(amount)` | Compact format (1,5 juta) |
237
+ | `parseRupiah(formatted)` | Parse formatted string to number |
238
+ | `toWords(amount, options?)` | Convert to Indonesian words |
239
+ | `roundToClean(amount, unit?)` | Round to clean amounts |
240
+
241
+ [Full Currency docs →](#)
242
+
243
+ ## Bundle Size
244
+
245
+ | Module | Size (minified + gzipped) |
246
+ |--------|---------------------------|
247
+ | NIK | ~8 KB |
248
+ | Phone | ~12 KB |
249
+ | Currency | ~10 KB |
250
+ | **Total** | **~30 KB** |
251
+
252
+ Import only what you need to keep your bundle small.
253
+
254
+ ## Requirements
255
+
256
+ - Node.js >= 18
257
+ - TypeScript >= 5.0 (optional)
258
+
259
+ ## Contributing
260
+
261
+ Found a bug? Want to add more Indonesian utilities?
262
+
263
+ 1. Fork the repo
264
+ 2. Create a branch: `git checkout -b feat/my-feature`
265
+ 3. Make changes and add tests
266
+ 4. Submit a PR
267
+
268
+ We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
269
+
270
+ ## Roadmap
271
+
272
+ - [x] NIK validation & parsing
273
+ - [x] Phone number utilities
274
+ - [x] Currency formatting & terbilang
275
+ - [ ] NPWP validation
276
+ - [ ] Bank account validation
277
+ - [ ] Indonesian address parsing
278
+ - [ ] Date & holiday utilities
279
+ - [ ] Zod schema exports
280
+
281
+ ## License
282
+
283
+ MIT © [choiruladamm](https://github.com/choiruladamm)
284
+
285
+ ## Support
286
+
287
+ - 📖 [Documentation](#) (coming soon)
288
+ - 🐛 [Report Issues](https://github.com/yourusername/indo-dev-utils/issues)
289
+ - 💬 [Discussions](https://github.com/yourusername/indo-dev-utils/discussions)
290
+
291
+ ---
292
+
293
+ Made with ❤️ for Indonesian developers. Stop copy-pasting, start shipping.
@@ -0,0 +1,245 @@
1
+ 'use strict';
2
+
3
+ // src/currency/format.ts
4
+ function formatRupiah(amount, options) {
5
+ const {
6
+ symbol = true,
7
+ decimal = false,
8
+ separator = ".",
9
+ decimalSeparator = ",",
10
+ spaceAfterSymbol = true
11
+ } = options || {};
12
+ const precision = options?.precision !== void 0 ? options.precision : decimal ? 2 : 0;
13
+ const isNegative = amount < 0;
14
+ const absAmount = Math.abs(amount);
15
+ let result;
16
+ if (decimal) {
17
+ const factor = Math.pow(10, precision);
18
+ const rounded = Math.round(absAmount * factor) / factor;
19
+ if (precision > 0) {
20
+ const [intPart, decPart] = rounded.toFixed(precision).split(".");
21
+ const formattedInt = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
22
+ result = `${formattedInt}${decimalSeparator}${decPart}`;
23
+ } else {
24
+ const intPart = rounded.toString();
25
+ result = intPart.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
26
+ }
27
+ } else {
28
+ const intAmount = Math.floor(absAmount);
29
+ result = intAmount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, separator);
30
+ }
31
+ if (isNegative) {
32
+ result = `-${result}`;
33
+ }
34
+ if (symbol) {
35
+ const space = spaceAfterSymbol ? " " : "";
36
+ result = `Rp${space}${result}`;
37
+ }
38
+ return result;
39
+ }
40
+ function formatCompact(amount) {
41
+ const isNegative = amount < 0;
42
+ const abs = Math.abs(amount);
43
+ let result;
44
+ if (abs >= 1e12) {
45
+ result = formatCompactValue(abs / 1e12, "triliun");
46
+ } else if (abs >= 1e9) {
47
+ result = formatCompactValue(abs / 1e9, "miliar");
48
+ } else if (abs >= 1e6) {
49
+ result = formatCompactValue(abs / 1e6, "juta");
50
+ } else if (abs >= 1e5) {
51
+ result = formatCompactValue(abs / 1e3, "ribu");
52
+ } else if (abs >= 1e3) {
53
+ result = abs.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
54
+ } else {
55
+ result = abs.toString();
56
+ }
57
+ if (isNegative) {
58
+ result = `-${result}`;
59
+ }
60
+ return `Rp ${result}`;
61
+ }
62
+ function formatCompactValue(value, unit) {
63
+ const rounded = Math.round(value * 10) / 10;
64
+ if (rounded % 1 === 0) {
65
+ return `${rounded.toFixed(0)} ${unit}`;
66
+ }
67
+ return `${rounded.toString().replace(".", ",")} ${unit}`;
68
+ }
69
+
70
+ // src/currency/parse.ts
71
+ function parseRupiah(formatted) {
72
+ if (!formatted || typeof formatted !== "string") {
73
+ return null;
74
+ }
75
+ const cleaned = formatted.trim().toLowerCase();
76
+ const compactUnits = {
77
+ triliun: 1e12,
78
+ miliar: 1e9,
79
+ juta: 1e6,
80
+ ribu: 1e3
81
+ };
82
+ for (const [unit, multiplier] of Object.entries(compactUnits)) {
83
+ if (cleaned.includes(unit)) {
84
+ const match = cleaned.match(/(-?\d+[,.]?\d*)/);
85
+ if (match) {
86
+ const num = parseFloat(match[1].replace(",", "."));
87
+ return num * multiplier;
88
+ }
89
+ }
90
+ }
91
+ let numStr = cleaned.replace(/rp/gi, "").trim();
92
+ const hasDot = numStr.includes(".");
93
+ const hasComma = numStr.includes(",");
94
+ if (hasDot && hasComma) {
95
+ const lastDot = numStr.lastIndexOf(".");
96
+ const lastComma = numStr.lastIndexOf(",");
97
+ if (lastComma > lastDot) {
98
+ numStr = numStr.replace(/\./g, "").replace(",", ".");
99
+ } else {
100
+ numStr = numStr.replace(/,/g, "");
101
+ }
102
+ } else if (hasComma) {
103
+ const parts = numStr.split(",");
104
+ if (parts.length === 2 && parts[1].length <= 2) {
105
+ numStr = numStr.replace(",", ".");
106
+ } else {
107
+ numStr = numStr.replace(/,/g, "");
108
+ }
109
+ } else if (hasDot) {
110
+ const parts = numStr.split(".");
111
+ if (parts.length > 2 || parts.length === 2 && parts[1].length > 2) {
112
+ numStr = numStr.replace(/\./g, "");
113
+ }
114
+ }
115
+ const parsed = parseFloat(numStr);
116
+ return isNaN(parsed) ? null : parsed;
117
+ }
118
+
119
+ // src/currency/words.ts
120
+ var BASIC_NUMBERS = [
121
+ "",
122
+ "satu",
123
+ "dua",
124
+ "tiga",
125
+ "empat",
126
+ "lima",
127
+ "enam",
128
+ "tujuh",
129
+ "delapan",
130
+ "sembilan"
131
+ ];
132
+ var TEENS = [
133
+ "sepuluh",
134
+ "sebelas",
135
+ "dua belas",
136
+ "tiga belas",
137
+ "empat belas",
138
+ "lima belas",
139
+ "enam belas",
140
+ "tujuh belas",
141
+ "delapan belas",
142
+ "sembilan belas"
143
+ ];
144
+ var TENS = [
145
+ "",
146
+ "",
147
+ "dua puluh",
148
+ "tiga puluh",
149
+ "empat puluh",
150
+ "lima puluh",
151
+ "enam puluh",
152
+ "tujuh puluh",
153
+ "delapan puluh",
154
+ "sembilan puluh"
155
+ ];
156
+ function toWords(amount, options) {
157
+ const { uppercase = false, withCurrency = true } = options || {};
158
+ if (amount === 0) {
159
+ let result = "nol";
160
+ if (withCurrency) result += " rupiah";
161
+ return uppercase ? capitalize(result) : result;
162
+ }
163
+ const isNegative = amount < 0;
164
+ const absAmount = Math.floor(Math.abs(amount));
165
+ 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;
171
+ if (triliun > 0) {
172
+ words += convertGroup(triliun) + " triliun";
173
+ }
174
+ if (miliar > 0) {
175
+ if (words) words += " ";
176
+ words += convertGroup(miliar) + " miliar";
177
+ }
178
+ if (juta > 0) {
179
+ if (words) words += " ";
180
+ words += convertGroup(juta) + " juta";
181
+ }
182
+ if (ribu > 0) {
183
+ if (words) words += " ";
184
+ words += ribu === 1 ? "seribu" : convertGroup(ribu) + " ribu";
185
+ }
186
+ if (sisa > 0) {
187
+ if (words) words += " ";
188
+ words += convertGroup(sisa);
189
+ }
190
+ if (isNegative) {
191
+ words = "minus " + words;
192
+ }
193
+ if (withCurrency) {
194
+ words += " rupiah";
195
+ }
196
+ return uppercase ? capitalize(words) : words;
197
+ }
198
+ function convertGroup(num) {
199
+ if (num === 0) return "";
200
+ let result = "";
201
+ const hundreds = Math.floor(num / 100);
202
+ if (hundreds > 0) {
203
+ result = hundreds === 1 ? "seratus" : BASIC_NUMBERS[hundreds] + " ratus";
204
+ }
205
+ const remainder = num % 100;
206
+ if (remainder > 0) {
207
+ if (result) result += " ";
208
+ result += convertTwoDigits(remainder);
209
+ }
210
+ return result;
211
+ }
212
+ function convertTwoDigits(num) {
213
+ if (num === 0) return "";
214
+ if (num < 10) return BASIC_NUMBERS[num];
215
+ if (num >= 10 && num < 20) return TEENS[num - 10];
216
+ const tens = Math.floor(num / 10);
217
+ const ones = num % 10;
218
+ let result = TENS[tens];
219
+ if (ones > 0) {
220
+ result += " " + BASIC_NUMBERS[ones];
221
+ }
222
+ return result;
223
+ }
224
+ function capitalize(str) {
225
+ return str.charAt(0).toUpperCase() + str.slice(1);
226
+ }
227
+
228
+ // src/currency/utils.ts
229
+ function roundToClean(amount, unit = "ribu") {
230
+ const divisors = {
231
+ ribu: 1e3,
232
+ "ratus-ribu": 1e5,
233
+ juta: 1e6
234
+ };
235
+ const divisor = divisors[unit];
236
+ return Math.round(amount / divisor) * divisor;
237
+ }
238
+
239
+ exports.formatCompact = formatCompact;
240
+ exports.formatRupiah = formatRupiah;
241
+ exports.parseRupiah = parseRupiah;
242
+ exports.roundToClean = roundToClean;
243
+ exports.toWords = toWords;
244
+ //# sourceMappingURL=index.cjs.map
245
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +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 type { RoundUnit } 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"]}
@@ -0,0 +1,43 @@
1
+ import { b as RoundUnit } from '../words-Dy8iYkbv.cjs';
2
+ export { R as RupiahOptions, W as WordOptions, a as formatCompact, f as formatRupiah, p as parseRupiah, t as toWords } from '../words-Dy8iYkbv.cjs';
3
+
4
+ /**
5
+ * Currency utility functions.
6
+ *
7
+ * @module currency/utils
8
+ * @packageDocumentation
9
+ */
10
+
11
+ /**
12
+ * Rounds a number to a clean currency amount.
13
+ *
14
+ * Common use case: displaying approximate prices or budgets
15
+ * in clean, rounded numbers.
16
+ *
17
+ * @param amount - The amount to round
18
+ * @param unit - The unit to round to (default: 'ribu')
19
+ * @returns Rounded amount
20
+ *
21
+ * @example
22
+ * Round to thousands:
23
+ * ```typescript
24
+ * roundToClean(1234567, 'ribu'); // 1235000
25
+ * ```
26
+ *
27
+ * @example
28
+ * Round to hundred thousands:
29
+ * ```typescript
30
+ * roundToClean(1234567, 'ratus-ribu'); // 1200000
31
+ * ```
32
+ *
33
+ * @example
34
+ * Round to millions:
35
+ * ```typescript
36
+ * roundToClean(1234567, 'juta'); // 1000000
37
+ * ```
38
+ *
39
+ * @public
40
+ */
41
+ declare function roundToClean(amount: number, unit?: RoundUnit): number;
42
+
43
+ export { RoundUnit, roundToClean };