khmer-money-formatter
Version:
Lightweight, dependency-free TypeScript library for formatting, parsing, and converting Khmer Riel and Khmer numbers.
466 lines (458 loc) • 12.1 kB
JavaScript
var KhmerMoneyFormatter = (function (exports) {
'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;
return exports;
})({});
//# sourceMappingURL=index.iife.js.map
//# sourceMappingURL=index.iife.js.map