UNPKG

legal-markdown-js

Version:

Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version

480 lines 16 kB
/** * @fileoverview Number Formatting and Conversion Helpers * * This module provides comprehensive number formatting utilities specifically * designed for legal and financial documents. It includes functions for currency * formatting, percentage display, number-to-words conversion, and various * numerical representations commonly used in legal contexts. * * Features: * - Currency formatting with multiple currency support (EUR, USD, GBP) * - Percentage formatting with customizable precision * - Number-to-words conversion for legal document amounts * - Integer formatting with thousand separators * - Flexible number formatting with custom separators * - Rounding utilities for precise calculations * * @example * ```typescript * import { formatCurrency, formatPercent, numberToWords } from './number-helpers.js'; * * // Currency formatting * const amount = formatCurrency(1234.56, 'USD'); // "$1,234.56" * * // Percentage formatting * const rate = formatPercent(0.155, 2); // "15.50%" * * // Number to words for legal documents * const words = numberToWords(1500); // "one thousand five hundred" * ``` */ import { getConfig } from '../../config/index.js'; function parseNumber(value) { if (typeof value === 'number') return value; const cleaned = String(value).replace(/[,\s]/g, ''); return parseFloat(cleaned); } function clampDecimals(decimals, fallback = 2) { const numericDecimals = Number(decimals); const resolved = Number.isFinite(numericDecimals) ? numericDecimals : fallback; return Math.max(0, Math.min(20, Math.trunc(resolved))); } /** * Formats a number as an integer with thousand separators * * Converts a number to an integer representation with customizable thousand * separators. Useful for displaying whole numbers in legal documents with * proper formatting for readability. * * @param {number | string} value - The number to format * @param {string} separator - The thousand separator character (default: ',') * @returns {string} The formatted integer string * * @example * ```typescript * // Standard formatting * formatInteger(1234567); // "1,234,567" * formatInteger(1234567.89); // "1,234,567" (decimal part removed) * * // Custom separator * formatInteger(1234567, '.'); // "1.234.567" * formatInteger(1234567, ' '); // "1 234 567" * * // String input * formatInteger('1234567'); // "1,234,567" * * // Invalid input handling * formatInteger('not a number'); // "not a number" * ``` */ export function formatInteger(value, separator = ',') { const num = parseNumber(value); if (isNaN(num)) return String(value); return Math.trunc(num) .toString() .replace(/\B(?=(\d{3})+(?!\d))/g, separator); } /** * Formats a number as a percentage with customizable precision * * Converts a number to percentage format with configurable decimal places * and optional percentage symbol. Useful for displaying rates, discounts, * and other percentage values in legal documents. * * @param {number | string} value - The number to format as percentage * @param {number} decimals - Number of decimal places (default: 2) * @param {boolean} symbol - Whether to include the % symbol (default: true) * @returns {string} The formatted percentage string * * @example * ```typescript * // Standard percentage formatting * formatPercent(0.1556); // "15.56%" * formatPercent(0.1556, 1); // "15.6%" * formatPercent(0.1556, 0); // "16%" * * // Without percentage symbol * formatPercent(0.1556, 2, false); // "15.56" * * // String input * formatPercent('0.25'); // "25.00%" * * // Values greater than 1 are treated as decimals too * formatPercent(1.5); // "150.00%" * formatPercent(2.5); // "250.00%" * ``` */ export function formatPercent(value, decimals = 2, symbol = true) { const num = parseNumber(value); if (isNaN(num)) return String(value); const safeDecimals = clampDecimals(decimals, 2); // Always convert to percentage (multiply by 100) // Input should always be in decimal format: 0.21 = 21%, 1.5 = 150% const percentage = num * 100; const formatted = percentage.toFixed(safeDecimals); return symbol ? `${formatted}%` : formatted; } /** * Formats a number as currency with support for multiple currencies * * Converts a number to currency format with proper symbol placement, * thousand separators, and decimal precision. Supports EUR, USD, and GBP * with correct formatting conventions for each currency. * * @param {number | string} value - The number to format as currency * @param {'EUR' | 'USD' | 'GBP'} currency - The currency type (default: 'EUR') * @param {number} decimals - Number of decimal places (default: 2) * @returns {string} The formatted currency string * * @example * ```typescript * // Euro formatting (symbol after amount) * formatCurrency(1234.56, 'EUR'); // "1,234.56 €" * formatCurrency(1234.56, 'EUR', 0); // "1,235 €" * * // US Dollar formatting (symbol before amount) * formatCurrency(1234.56, 'USD'); // "$1,234.56" * * // British Pound formatting (symbol before amount) * formatCurrency(1234.56, 'GBP'); // "£1,234.56" * * // String input * formatCurrency('1234.56', 'USD'); // "$1,234.56" * ``` */ export function formatCurrency(value, currency = 'EUR', decimals = 2, locale) { const num = parseNumber(value); if (isNaN(num)) return String(value); const lang = locale || getConfig().processing.locale || 'en'; const safeDecimals = clampDecimals(decimals, 2); if (lang.toLowerCase().startsWith('en')) { return manualCurrencyFormat(num, currency, safeDecimals); } try { return new Intl.NumberFormat(lang, { style: 'currency', currency, minimumFractionDigits: safeDecimals, maximumFractionDigits: safeDecimals, }).format(num); } catch { return manualCurrencyFormat(num, currency, safeDecimals); } } function manualCurrencyFormat(amount, currency, decimals) { const symbols = { EUR: '€', USD: '$', GBP: '£', }; const formatted = amount.toFixed(decimals).replace(/\B(?=(\d{3})+(?!\d))/g, ','); const symbol = symbols[currency] || currency; // Different positioning for different currencies if (currency === 'EUR') { return `${formatted} ${symbol}`; } else { return `${symbol}${formatted}`; } } /** * Formats a number as Euro currency * * Convenience function for formatting numbers as Euro currency with * proper Euro symbol placement and formatting conventions. * * @param {number | string} value - The number to format as Euro * @param {number} decimals - Number of decimal places (default: 2) * @returns {string} The formatted Euro currency string * * @example * ```typescript * formatEuro(1234.56); // "1,234.56 €" * formatEuro(1234.56, 0); // "1,235 €" * formatEuro('500'); // "500.00 €" * ``` */ export function formatEuro(value, decimals = 2) { // Handle Handlebars options object const decimalCount = typeof decimals === 'object' ? 2 : decimals; return formatCurrency(value, 'EUR', decimalCount); } /** * Formats a number as US Dollar currency * * Convenience function for formatting numbers as US Dollar currency with * proper dollar symbol placement and formatting conventions. * * @param {number | string} value - The number to format as US Dollar * @param {number} decimals - Number of decimal places (default: 2) * @returns {string} The formatted US Dollar currency string * * @example * ```typescript * formatDollar(1234.56); // "$1,234.56" * formatDollar(1234.56, 0); // "$1,235" * formatDollar('500'); // "$500.00" * ``` */ export function formatDollar(value, decimals = 2) { // Handle Handlebars options object const decimalCount = typeof decimals === 'object' ? 2 : decimals; return formatCurrency(value, 'USD', decimalCount); } /** * Formats a number as British Pound currency * * Convenience function for formatting numbers as British Pound currency with * proper pound symbol placement and formatting conventions. * * @param {number | string} value - The number to format as British Pound * @param {number} decimals - Number of decimal places (default: 2) * @returns {string} The formatted British Pound currency string * * @example * ```typescript * formatPound(1234.56); // "£1,234.56" * formatPound(1234.56, 0); // "£1,235" * formatPound('500'); // "£500.00" * ``` */ export function formatPound(value, decimals = 2) { // Handle Handlebars options object const decimalCount = typeof decimals === 'object' ? 2 : decimals; return formatCurrency(value, 'GBP', decimalCount); } /** * Converts a number to its written word representation * * Transforms numeric values into their English word equivalents, which is * particularly useful for legal documents where amounts are often written * in both numeric and word form for clarity and legal precision. * * @param {number | string} num - The number to convert to words * @returns {string} The number expressed in words * * Supported range: Supports numbers from 0 to 999,999,999 with decimal support * Decimal handling: Decimal parts are expressed as "and X cents" * * @example * ```typescript * // Whole numbers * numberToWords(0); // "zero" * numberToWords(42); // "forty two" * numberToWords(1500); // "one thousand five hundred" * numberToWords(1000000); // "one million" * * // Decimal numbers * numberToWords(123.45); // "one hundred twenty three and forty five cents" * numberToWords(1000.50); // "one thousand and fifty cents" * * // String input * numberToWords('456'); // "four hundred fifty six" * * // Negative numbers * numberToWords(-100); // "negative one hundred" * * // Invalid input * numberToWords('invalid'); // "invalid" * ``` */ export function numberToWords(num) { const n = parseNumber(num); if (isNaN(n)) return String(num); if (Math.abs(n) > 999_999_999_999) { return n.toLocaleString('en-US'); } if (n === 0) return 'zero'; const ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']; const tens = [ '', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety', ]; const teens = [ 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', ]; const convertMillions = (num) => { if (num >= 1000000) { return (convertMillions(Math.floor(num / 1000000)) + ' million ' + convertThousands(num % 1000000)); } else { return convertThousands(num); } }; const convertThousands = (num) => { if (num >= 1000) { return convertHundreds(Math.floor(num / 1000)) + ' thousand ' + convertHundreds(num % 1000); } else { return convertHundreds(num); } }; const convertHundreds = (num) => { let str = ''; if (num > 99) { str += ones[Math.floor(num / 100)] + ' hundred '; num %= 100; } if (num > 19) { str += tens[Math.floor(num / 10)] + ' '; num %= 10; } else if (num > 9) { str += teens[num - 10] + ' '; return str.trim(); } if (num > 0) { str += ones[num] + ' '; } return str.trim(); }; const isNegative = n < 0; const absoluteValue = Math.abs(n); let integerPart = Math.floor(absoluteValue); let decimalPart = Math.round((absoluteValue - integerPart) * 100); if (decimalPart === 100) { integerPart += 1; decimalPart = 0; } let result = convertMillions(integerPart); if (decimalPart > 0) { result += ' and ' + convertHundreds(decimalPart) + ' cents'; } result = result.trim(); return isNegative ? `negative ${result}` : result; } /** * Formats a number with custom decimal and thousand separators * * Provides flexible number formatting with customizable separators for * different locales and formatting requirements. Useful for international * legal documents that require specific number formatting conventions. * * @param {number | string} value - The number to format * @param {number} decimals - Number of decimal places (default: 2) * @param {string} decimalSeparator - Character for decimal separation (default: '.') * @param {string} thousandSeparator - Character for thousand separation (default: ',') * @returns {string} The formatted number string * * @example * ```typescript * // Standard US formatting * formatNumber(1234.56); // "1,234.56" * formatNumber(1234.56, 3); // "1,234.560" * * // European formatting (comma as decimal separator) * formatNumber(1234.56, 2, ',', ' '); // "1 234,56" * formatNumber(1234.56, 2, ',', '.'); // "1.234,56" * * // Custom formatting * formatNumber(1234.56, 1, ':', '|'); // "1|234:6" * * // String input * formatNumber('1234.56'); // "1,234.56" * ``` */ export function formatNumber(value, decimals = 2, decimalSeparator = '.', thousandSeparator = ',') { const num = parseNumber(value); if (isNaN(num)) return String(value); const safeDecimals = clampDecimals(decimals, 2); const parts = num.toFixed(safeDecimals).split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, thousandSeparator); return parts.join(decimalSeparator); } /** * Rounds a number to a specified number of decimal places * * Provides precise rounding functionality for numerical calculations in * legal documents. Uses standard mathematical rounding rules (0.5 rounds up). * * @param {number | string} value - The number to round * @param {number} decimals - Number of decimal places to round to (default: 0) * @returns {number} The rounded number * * @example * ```typescript * // Round to whole numbers * round(1234.56); // 1235 * round(1234.44); // 1234 * * // Round to decimal places * round(1234.5678, 2); // 1234.57 * round(1234.5678, 1); // 1234.6 * round(1234.5678, 3); // 1234.568 * * // String input * round('1234.56', 1); // 1234.6 * * // Invalid input handling * round('invalid'); // 0 * ``` */ export function round(value, decimals = 0) { const num = parseNumber(value); if (isNaN(num)) return 0; const factor = Math.pow(10, decimals); return Math.round(num * factor) / factor; } const ORDINAL_RULES = { en: n => { const j = n % 10; const k = n % 100; if (j === 1 && k !== 11) return n + 'st'; if (j === 2 && k !== 12) return n + 'nd'; if (j === 3 && k !== 13) return n + 'rd'; return n + 'th'; }, es: n => n + '.º', fr: n => (n === 1 ? n + 'er' : n + 'e'), de: n => n + '.', pt: n => n + '.º', it: n => n + '°', }; export function ordinal(n, locale) { const num = parseNumber(n); if (isNaN(num)) return String(n); const i = Math.trunc(num); const lang = (locale || getConfig().processing.locale || 'en').split('-')[0]; const rule = ORDINAL_RULES[lang] || ORDINAL_RULES['en']; return rule(i); } export function abs(n) { return Math.abs(parseNumber(n)); } export function max(a, b) { return Math.max(parseNumber(a), parseNumber(b)); } export function min(a, b) { return Math.min(parseNumber(a), parseNumber(b)); } //# sourceMappingURL=number-helpers.js.map