UNPKG

khmer-money-formatter

Version:

Lightweight, dependency-free TypeScript library for formatting, parsing, and converting Khmer Riel and Khmer numbers.

461 lines (455 loc) • 11.1 kB
'use strict'; // src/number.ts var DEFAULT_OPTIONS = { decimals: 0, separator: ",", decimalSeparator: ".", khmerDigits: false, empty: "0" }; function toNumericValue(value) { if (value === null || value === void 0 || value === "") { return null; } if (typeof value === "number") { return Number.isFinite(value) ? value : null; } if (typeof value === "bigint") { return Number(value); } if (typeof value !== "string") { return null; } const normalized = value.trim().replace(/,/g, ""); if (!normalized) { return null; } const parsed = Number(normalized); return Number.isFinite(parsed) ? parsed : null; } function formatNumber(value, options = {}) { const config = { ...DEFAULT_OPTIONS, ...options }; const numericValue = toNumericValue(value); if (numericValue === null) { return config.empty; } const decimals = Math.max( 0, Math.floor(config.decimals) ); const fixed = numericValue.toFixed(decimals); const [integerPart = "", decimalPart] = fixed.split("."); const negative = integerPart.startsWith("-"); const absoluteInteger = negative ? integerPart.slice(1) : integerPart; const formattedInteger = absoluteInteger.replace( /\B(?=(\d{3})+(?!\d))/g, config.separator ); let result = `${negative ? "-" : ""}${formattedInteger}`; if (decimals > 0 && decimalPart !== void 0) { result += config.decimalSeparator + decimalPart; } return config.khmerDigits ? convertDigitsToKhmer(result) : result; } function parseNumber(value) { if (value === null || value === void 0 || value === "") { return 0; } if (typeof value === "number") { return Number.isFinite(value) ? value : 0; } if (typeof value !== "string") { return 0; } const normalized = convertDigitsFromKhmer( value ).trim().replace(/,/g, ""); if (!normalized) { return 0; } const parsed = Number(normalized); return Number.isFinite(parsed) ? parsed : 0; } function convertDigitsToKhmer(value) { const map = { "0": "\u17E0", "1": "\u17E1", "2": "\u17E2", "3": "\u17E3", "4": "\u17E4", "5": "\u17E5", "6": "\u17E6", "7": "\u17E7", "8": "\u17E8", "9": "\u17E9" }; return value.replace( /\d/g, (digit) => map[digit] ?? digit ); } function convertDigitsFromKhmer(value) { const map = { "\u17E0": "0", "\u17E1": "1", "\u17E2": "2", "\u17E3": "3", "\u17E4": "4", "\u17E5": "5", "\u17E6": "6", "\u17E7": "7", "\u17E8": "8", "\u17E9": "9" }; return value.replace( /[០-៩]/g, (digit) => map[digit] ?? digit ); } // src/formatter.ts var DEFAULT_MONEY_OPTIONS = { symbol: true, currency: "\u17DB", decimals: 0, separator: ",", decimalSeparator: ".", currencyPosition: "suffix", khmerDigits: false, empty: "0", roundTo: 100 }; function ceilMoney(value, unit = 100) { if (!Number.isFinite(value)) { return 0; } if (!Number.isFinite(unit) || unit <= 0) { return value; } return Math.ceil(value / unit) * unit; } function formatKHR(value, options = {}) { const config = { ...DEFAULT_MONEY_OPTIONS, ...options }; const numericValue = toNumericValue(value); if (numericValue === null) { const emptyValue = config.empty; if (!config.symbol) { return emptyValue; } if (!emptyValue) { return ""; } return config.currencyPosition === "prefix" ? `${config.currency}${emptyValue}` : `${emptyValue} ${config.currency}`; } const roundedValue = ceilMoney( numericValue, config.roundTo ); const formatted = formatNumber(roundedValue, { decimals: 0, separator: config.separator, decimalSeparator: config.decimalSeparator, khmerDigits: config.khmerDigits, empty: config.empty }); if (!config.symbol) { return formatted; } if (config.currencyPosition === "prefix") { return `${config.currency}${formatted}`; } return `${formatted} ${config.currency}`; } function formatKHRInput(value, options = {}) { const config = { ...DEFAULT_MONEY_OPTIONS, ...options }; const numericValue = toNumericValue(value); if (numericValue === null) { return config.empty; } const roundedValue = ceilMoney( numericValue, config.roundTo ); return formatNumber(roundedValue, { decimals: 0, separator: config.separator, decimalSeparator: config.decimalSeparator, khmerDigits: config.khmerDigits, empty: config.empty }); } function formatKHRLong(value, options = {}) { const { khmer = false, ...moneyOptions } = options; const formatted = formatKHR(value, { ...moneyOptions, symbol: false, khmerDigits: moneyOptions.khmerDigits ?? khmer }); if (!formatted) { return ""; } return moneyOptions.currencyPosition === "prefix" ? `\u179A\u17C0\u179B${formatted}` : `${formatted} \u179A\u17C0\u179B`; } // src/khmer.ts var KHMER_DIGITS = [ "\u17E0", "\u17E1", "\u17E2", "\u17E3", "\u17E4", "\u17E5", "\u17E6", "\u17E7", "\u17E8", "\u17E9" ]; var KHMER_TO_ARABIC = { "\u17E0": "0", "\u17E1": "1", "\u17E2": "2", "\u17E3": "3", "\u17E4": "4", "\u17E5": "5", "\u17E6": "6", "\u17E7": "7", "\u17E8": "8", "\u17E9": "9" }; function toKhmerNumber(value) { const input = String(value); return input.replace( /\d/g, (digit) => KHMER_DIGITS[Number(digit)] ?? digit ); } function fromKhmerNumber(value) { const normalized = value.replace( /[០-៩]/g, (digit) => KHMER_TO_ARABIC[digit] ?? digit ).replace(/,/g, "").trim(); if (!normalized) { return 0; } const result = Number(normalized); return Number.isFinite(result) ? result : 0; } // src/parser.ts function parseKHR(value) { if (value === null || value === void 0 || value === "") { return 0; } if (typeof value === "number") { return Number.isFinite(value) ? value : 0; } if (typeof value !== "string") { return 0; } const cleaned = value.replace(/៛/g, "").replace(/រៀល/g, "").replace(/\s+/g, "").trim(); if (!cleaned) { return 0; } if (/[០-៩]/.test(cleaned)) { return fromKhmerNumber(cleaned); } return parseNumber(cleaned); } function isValidMoney(value) { if (value === null || value === void 0 || value === "") { return false; } if (typeof value === "number") { return Number.isFinite(value); } if (typeof value !== "string") { return false; } const input = value.trim(); if (!input) { return false; } const cleaned = input.replace(/៛/g, "").replace(/រៀល/g, "").replace(/\s+/g, "").trim(); if (!cleaned) { return false; } const arabic = cleaned.replace( /[០-៩]/g, (digit) => { const map = { "\u17E0": "0", "\u17E1": "1", "\u17E2": "2", "\u17E3": "3", "\u17E4": "4", "\u17E5": "5", "\u17E6": "6", "\u17E7": "7", "\u17E8": "8", "\u17E9": "9" }; return map[digit] ?? digit; } ); const normalized = arabic.replace( /,/g, "" ); return /^[-+]?\d+(\.\d+)?$/.test( normalized ); } // src/khmer-text.ts var DIGITS = [ "\u179F\u17BC\u1793\u17D2\u1799", "\u1798\u17BD\u1799", "\u1796\u17B8\u179A", "\u1794\u17B8", "\u1794\u17BD\u1793", "\u1794\u17D2\u179A\u17B6\u17C6", "\u1794\u17D2\u179A\u17B6\u17C6\u1798\u17BD\u1799", "\u1794\u17D2\u179A\u17B6\u17C6\u1796\u17B8\u179A", "\u1794\u17D2\u179A\u17B6\u17C6\u1794\u17B8", "\u1794\u17D2\u179A\u17B6\u17C6\u1794\u17BD\u1793" ]; var TENS = { 2: "\u1798\u17D2\u1797\u17C3", 3: "\u179F\u17B6\u1798\u179F\u17B7\u1794", 4: "\u179F\u17C2\u179F\u17B7\u1794", 5: "\u17A0\u17B6\u179F\u17B7\u1794", 6: "\u17A0\u17BB\u1780\u179F\u17B7\u1794", 7: "\u1785\u17B7\u178F\u179F\u17B7\u1794", 8: "\u1794\u17C9\u17C2\u178F\u179F\u17B7\u1794", 9: "\u1780\u17C5\u179F\u17B7\u1794" }; var SCALES = [ "", "\u1796\u17B6\u1793\u17CB", "\u179B\u17B6\u1793", "\u1796\u17B6\u1793\u17CB\u179B\u17B6\u1793", "\u1791\u17D2\u179A\u17B8\u179B\u17B6\u1793", "\u1780\u17D2\u179C\u17B6\u1791\u17D2\u179A\u17B8\u179B\u17B6\u1793" ]; function under100(value) { if (value < 10) { return DIGITS[value] ?? ""; } if (value === 10) { return "\u178A\u1794\u17CB"; } if (value < 20) { return `\u178A\u1794\u17CB${DIGITS[value - 10]}`; } const tens = Math.floor(value / 10); var ones; ones = value % 10; let result = TENS[tens]; if (ones > 0) { result += DIGITS[ones] ?? ""; } return result ?? ""; } function under1000(value) { if (value === 0) { return ""; } const hundreds = Math.floor(value / 100); const remainder = value % 100; let result = ""; if (hundreds > 0) { result += `${DIGITS[hundreds]}\u179A\u1799`; } if (remainder > 0) { result += under100(remainder); } return result; } function integerToKhmerText(value) { if (value === 0) { return DIGITS[0]; } let number = value; let groupIndex = 0; let result = ""; while (number > 0) { const group = number % 1e3; if (group !== 0) { const text = under1000(group); const scale = SCALES[groupIndex] ?? ""; result = text + scale + result; } number = Math.floor(number / 1e3); groupIndex++; } return result; } function numberToKhmerText(value) { if (value === null || value === void 0 || value === "") { return DIGITS[0]; } const normalized = String(value).replace(/,/g, "").trim(); const number = Number(normalized); if (!Number.isFinite(number)) { return ""; } if (number === 0) { return DIGITS[0]; } if (number < 0) { return `\u178A\u1780${numberToKhmerText( Math.abs(number) )}`; } return integerToKhmerText( Math.floor(number) ); } function decimalToKhmerText(value) { if (value === null || value === void 0 || value === "") { return DIGITS[0]; } const normalized = String(value).replace(/,/g, "").trim(); const number = Number(normalized); if (!Number.isFinite(number)) { return ""; } if (!normalized.includes(".")) { return numberToKhmerText( normalized ); } const [ integerPart = "", decimalPart = "" ] = normalized.split("."); const integerText = numberToKhmerText(integerPart); const decimalText = numberToKhmerText(decimalPart); return `${integerText}\u1785\u17BB\u1785${decimalText}`; } function numberToKhmerMoneyText(value) { return `${numberToKhmerText(value)}\u179A\u17C0\u179B`; } exports.ceilMoney = ceilMoney; exports.decimalToKhmerText = decimalToKhmerText; exports.formatKHR = formatKHR; exports.formatKHRInput = formatKHRInput; exports.formatKHRLong = formatKHRLong; exports.formatNumber = formatNumber; exports.fromKhmerNumber = fromKhmerNumber; exports.isValidMoney = isValidMoney; exports.numberToKhmerMoneyText = numberToKhmerMoneyText; exports.numberToKhmerText = numberToKhmerText; exports.parseKHR = parseKHR; exports.parseNumber = parseNumber; exports.toKhmerNumber = toKhmerNumber; //# sourceMappingURL=index.cjs.map //# sourceMappingURL=index.cjs.map