UNPKG

numeric-quantity

Version:

Number parser with support for mixed numbers, vulgar fractions, and Roman numerals

1 lines 29.4 kB
{"version":3,"file":"numeric-quantity.mjs","names":[],"sources":["../src/constants.ts","../src/parseRomanNumerals.ts","../src/numericQuantity.ts","../src/isNumericQuantity.ts"],"sourcesContent":["import type {\n NumericQuantityOptions,\n RomanNumeralAscii,\n RomanNumeralUnicode,\n SubscriptDigit,\n SuperscriptDigit,\n VulgarFraction,\n} from './types';\n\n// #region Decimal_Number Unicode category\n\n/**\n * Unicode decimal digit block start code points.\n * Each block contains 10 contiguous digits (0-9).\n * This list covers all \\p{Nd} (Decimal_Number) blocks through Unicode 17.0.\n * The drift test in index.test.ts validates completeness against the JS engine.\n */\nconst decimalDigitBlockStarts = [\n 0x0030, // ASCII (0-9)\n 0x0660, // Arabic-Indic\n 0x06f0, // Extended Arabic-Indic (Persian/Urdu)\n 0x07c0, // NKo\n 0x0966, // Devanagari\n 0x09e6, // Bengali\n 0x0a66, // Gurmukhi\n 0x0ae6, // Gujarati\n 0x0b66, // Oriya\n 0x0be6, // Tamil\n 0x0c66, // Telugu\n 0x0ce6, // Kannada\n 0x0d66, // Malayalam\n 0x0de6, // Sinhala Lith\n 0x0e50, // Thai\n 0x0ed0, // Lao\n 0x0f20, // Tibetan\n 0x1040, // Myanmar\n 0x1090, // Myanmar Shan\n 0x17e0, // Khmer\n 0x1810, // Mongolian\n 0x1946, // Limbu\n 0x19d0, // New Tai Lue\n 0x1a80, // Tai Tham Hora\n 0x1a90, // Tai Tham Tham\n 0x1b50, // Balinese\n 0x1bb0, // Sundanese\n 0x1c40, // Lepcha\n 0x1c50, // Ol Chiki\n 0xa620, // Vai\n 0xa8d0, // Saurashtra\n 0xa900, // Kayah Li\n 0xa9d0, // Javanese\n 0xa9f0, // Myanmar Tai Laing\n 0xaa50, // Cham\n 0xabf0, // Meetei Mayek\n 0xff10, // Fullwidth\n 0x104a0, // Osmanya\n 0x10d30, // Hanifi Rohingya\n 0x10d40, // Garay\n 0x11066, // Brahmi\n 0x110f0, // Sora Sompeng\n 0x11136, // Chakma\n 0x111d0, // Sharada\n 0x112f0, // Khudawadi\n 0x11450, // Newa\n 0x114d0, // Tirhuta\n 0x11650, // Modi\n 0x116c0, // Takri\n 0x116d0, // Myanmar Pao\n 0x116da, // Myanmar Eastern Pwo Karen\n 0x11730, // Ahom\n 0x118e0, // Warang Citi\n 0x11950, // Dives Akuru\n 0x11bf0, // Sunuwar\n 0x11c50, // Bhaiksuki\n 0x11d50, // Masaram Gondi\n 0x11da0, // Gunjala Gondi\n 0x11de0, // Tolong Siki\n 0x11f50, // Kawi\n 0x16130, // Gurung Khema\n 0x16a60, // Mro\n 0x16ac0, // Tangsa\n 0x16b50, // Pahawh Hmong\n 0x16d70, // Kirat Rai\n 0x1ccf0, // Outlined Digits\n 0x1d7ce, // Mathematical Bold\n 0x1d7d8, // Mathematical Double-Struck\n 0x1d7e2, // Mathematical Sans-Serif\n 0x1d7ec, // Mathematical Sans-Serif Bold\n 0x1d7f6, // Mathematical Monospace\n 0x1e140, // Nyiakeng Puachue Hmong\n 0x1e2f0, // Wancho\n 0x1e4f0, // Nag Mundari\n 0x1e5f1, // Ol Onal\n 0x1e950, // Adlam\n 0x1fbf0, // Segmented Digits\n] as const;\n\n/**\n * Normalizes non-ASCII decimal digits to ASCII digits.\n * Converts characters from Unicode decimal digit blocks (e.g., Arabic-Indic,\n * Devanagari, Bengali) to their ASCII equivalents (0-9).\n *\n * All current Unicode \\p{Nd} blocks are included in decimalDigitBlockStarts.\n */\nexport const normalizeDigits = (str: string): string =>\n str.replace(/\\p{Nd}/gu, ch => {\n const cp = ch.codePointAt(0)!;\n // ASCII digits (0x0030-0x0039) don't need conversion\n if (cp <= 0x39) return ch;\n // Binary search for the largest block start ≤ cp\n let lo = 0;\n let hi = decimalDigitBlockStarts.length - 1;\n while (lo < hi) {\n const mid = (lo + hi + 1) >>> 1;\n if (decimalDigitBlockStarts[mid] <= cp) {\n lo = mid;\n } else {\n hi = mid - 1;\n }\n }\n return String(cp - decimalDigitBlockStarts[lo]!);\n });\n\n/**\n * Map of Unicode superscript and subscript digit code points to ASCII digits.\n */\nexport const superSubDigitToAsciiMap: Record<SuperscriptDigit | SubscriptDigit, string> = {\n '⁰': '0',\n '¹': '1',\n '²': '2',\n '³': '3',\n '⁴': '4',\n '⁵': '5',\n '⁶': '6',\n '⁷': '7',\n '⁸': '8',\n '⁹': '9',\n '₀': '0',\n '₁': '1',\n '₂': '2',\n '₃': '3',\n '₄': '4',\n '₅': '5',\n '₆': '6',\n '₇': '7',\n '₈': '8',\n '₉': '9',\n} as const;\n\n/**\n * Captures Unicode superscript and subscript digits.\n */\nexport const superSubDigitsRegex: RegExp = /[⁰¹²³⁴⁵⁶⁷⁸⁹₀₁₂₃₄₅₆₇₈₉]/g;\n\n/**\n * Map of Unicode fraction code points to their ASCII equivalents.\n */\nexport const vulgarFractionToAsciiMap: Record<VulgarFraction, `${number}/${number | ''}`> = {\n '¼': '1/4',\n '½': '1/2',\n '¾': '3/4',\n '⅐': '1/7',\n '⅑': '1/9',\n '⅒': '1/10',\n '⅓': '1/3',\n '⅔': '2/3',\n '⅕': '1/5',\n '⅖': '2/5',\n '⅗': '3/5',\n '⅘': '4/5',\n '⅙': '1/6',\n '⅚': '5/6',\n '⅛': '1/8',\n '⅜': '3/8',\n '⅝': '5/8',\n '⅞': '7/8',\n '⅟': '1/',\n} as const;\n\n/**\n * Captures the individual elements of a numeric string. Commas and underscores are allowed\n * as separators, as long as they appear between digits and are not consecutive.\n *\n * Capture groups:\n *\n * | # | Description | Example(s) |\n * | --- | ------------------------------------------------ | ------------------------------------------------------------------- |\n * | `0` | entire string | `\"2 1/3\"` from `\"2 1/3\"` |\n * | `1` | sign (`-` or `+`) | `\"-\"` from `\"-2 1/3\"` |\n * | `2` | whole number or numerator | `\"2\"` from `\"2 1/3\"`; `\"1\"` from `\"1/3\"` |\n * | `3` | entire fraction, decimal portion, or denominator | `\" 1/3\"` from `\"2 1/3\"`; `\".33\"` from `\"2.33\"`; `\"/3\"` from `\"1/3\"` |\n *\n * _Capture group 2 may include comma/underscore separators._\n *\n * @example\n *\n * ```ts\n * numericRegex.exec(\"1\") // [ \"1\", \"1\", null, null ]\n * numericRegex.exec(\"1.23\") // [ \"1.23\", \"1\", \".23\", null ]\n * numericRegex.exec(\"1 2/3\") // [ \"1 2/3\", \"1\", \" 2/3\", \" 2\" ]\n * numericRegex.exec(\"2/3\") // [ \"2/3\", \"2\", \"/3\", null ]\n * numericRegex.exec(\"2 / 3\") // [ \"2 / 3\", \"2\", \"/ 3\", null ]\n * ```\n */\nexport const numericRegex: RegExp =\n /^(?=[-+]?\\s*\\.\\d|[-+]?\\s*\\d)([-+])?\\s*((?:\\d(?:[,_]\\d|\\d)*)*)(([eE][+-]?\\d(?:[,_]\\d|\\d)*)?|\\.\\d(?:[,_]\\d|\\d)*([eE][+-]?\\d(?:[,_]\\d|\\d)*)?|(\\s+\\d(?:[,_]\\d|\\d)*\\s*)?\\s*\\/\\s*\\d(?:[,_]\\d|\\d)*)?$/;\n/**\n * Same as {@link numericRegex}, but allows (and ignores) trailing invalid characters.\n * Capture group 7 contains the trailing invalid portion.\n */\nexport const numericRegexWithTrailingInvalid: RegExp =\n /^(?=[-+]?\\s*\\.\\d|[-+]?\\s*\\d)([-+])?\\s*((?:\\d(?:[,_]\\d|\\d)*)*)(([eE][+-]?\\d(?:[,_]\\d|\\d)*)?|\\.\\d(?:[,_]\\d|\\d)*([eE][+-]?\\d(?:[,_]\\d|\\d)*)?|(\\s+\\d(?:[,_]\\d|\\d)*\\s*)?\\s*\\/\\s*\\d(?:[,_]\\d|\\d)*)?(\\s*[^.\\d/].*)?/;\n\n/**\n * Captures any Unicode vulgar fractions.\n */\nexport const vulgarFractionsRegex: RegExp = /([¼½¾⅐⅑⅒⅓⅔⅕⅖⅗⅘⅙⅚⅛⅜⅝⅞⅟}])/g;\n\n// #endregion\n\n// #region Roman numerals\n\ntype RomanNumeralSequenceFragment =\n | `${RomanNumeralAscii}`\n | `${RomanNumeralAscii}${RomanNumeralAscii}`\n | `${RomanNumeralAscii}${RomanNumeralAscii}${RomanNumeralAscii}`\n | `${RomanNumeralAscii}${RomanNumeralAscii}${RomanNumeralAscii}${RomanNumeralAscii}`;\n\n/**\n * Map of Roman numeral sequences to their decimal equivalents.\n */\nexport const romanNumeralValues: {\n [k in RomanNumeralSequenceFragment]?: number;\n} = {\n MMM: 3000,\n MM: 2000,\n M: 1000,\n CM: 900,\n DCCC: 800,\n DCC: 700,\n DC: 600,\n D: 500,\n CD: 400,\n CCC: 300,\n CC: 200,\n C: 100,\n XC: 90,\n LXXX: 80,\n LXX: 70,\n LX: 60,\n L: 50,\n XL: 40,\n XXX: 30,\n XX: 20,\n XII: 12, // only here for tests; not used in practice\n XI: 11, // only here for tests; not used in practice\n X: 10,\n IX: 9,\n VIII: 8,\n VII: 7,\n VI: 6,\n V: 5,\n IV: 4,\n III: 3,\n II: 2,\n I: 1,\n} as const;\n\n/**\n * Map of Unicode Roman numeral code points to their ASCII equivalents.\n */\nexport const romanNumeralUnicodeToAsciiMap: Record<\n RomanNumeralUnicode,\n keyof typeof romanNumeralValues\n> = {\n // Roman Numeral One (U+2160)\n Ⅰ: 'I',\n // Roman Numeral Two (U+2161)\n Ⅱ: 'II',\n // Roman Numeral Three (U+2162)\n Ⅲ: 'III',\n // Roman Numeral Four (U+2163)\n Ⅳ: 'IV',\n // Roman Numeral Five (U+2164)\n Ⅴ: 'V',\n // Roman Numeral Six (U+2165)\n Ⅵ: 'VI',\n // Roman Numeral Seven (U+2166)\n Ⅶ: 'VII',\n // Roman Numeral Eight (U+2167)\n Ⅷ: 'VIII',\n // Roman Numeral Nine (U+2168)\n Ⅸ: 'IX',\n // Roman Numeral Ten (U+2169)\n Ⅹ: 'X',\n // Roman Numeral Eleven (U+216A)\n Ⅺ: 'XI',\n // Roman Numeral Twelve (U+216B)\n Ⅻ: 'XII',\n // Roman Numeral Fifty (U+216C)\n Ⅼ: 'L',\n // Roman Numeral One Hundred (U+216D)\n Ⅽ: 'C',\n // Roman Numeral Five Hundred (U+216E)\n Ⅾ: 'D',\n // Roman Numeral One Thousand (U+216F)\n Ⅿ: 'M',\n // Small Roman Numeral One (U+2170)\n ⅰ: 'I',\n // Small Roman Numeral Two (U+2171)\n ⅱ: 'II',\n // Small Roman Numeral Three (U+2172)\n ⅲ: 'III',\n // Small Roman Numeral Four (U+2173)\n ⅳ: 'IV',\n // Small Roman Numeral Five (U+2174)\n ⅴ: 'V',\n // Small Roman Numeral Six (U+2175)\n ⅵ: 'VI',\n // Small Roman Numeral Seven (U+2176)\n ⅶ: 'VII',\n // Small Roman Numeral Eight (U+2177)\n ⅷ: 'VIII',\n // Small Roman Numeral Nine (U+2178)\n ⅸ: 'IX',\n // Small Roman Numeral Ten (U+2179)\n ⅹ: 'X',\n // Small Roman Numeral Eleven (U+217A)\n ⅺ: 'XI',\n // Small Roman Numeral Twelve (U+217B)\n ⅻ: 'XII',\n // Small Roman Numeral Fifty (U+217C)\n ⅼ: 'L',\n // Small Roman Numeral One Hundred (U+217D)\n ⅽ: 'C',\n // Small Roman Numeral Five Hundred (U+217E)\n ⅾ: 'D',\n // Small Roman Numeral One Thousand (U+217F)\n ⅿ: 'M',\n} as const;\n\n/**\n * Captures all Unicode Roman numeral code points.\n */\nexport const romanNumeralUnicodeRegex: RegExp = /([ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫⅬⅭⅮⅯⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻⅼⅽⅾⅿ])/gi;\n\n/**\n * Captures a valid Roman numeral sequence.\n *\n * Capture groups:\n *\n * | # | Description | Example |\n * | --- | --------------- | ------------------------ |\n * | `0` | Entire string | \"MCCXIV\" from \"MCCXIV\" |\n * | `1` | Thousands | \"M\" from \"MCCXIV\" |\n * | `2` | Hundreds | \"CC\" from \"MCCXIV\" |\n * | `3` | Tens | \"X\" from \"MCCXIV\" |\n * | `4` | Ones | \"IV\" from \"MCCXIV\" |\n *\n * @example\n *\n * ```ts\n * romanNumeralRegex.exec(\"M\") // [ \"M\", \"M\", \"\", \"\", \"\" ]\n * romanNumeralRegex.exec(\"XII\") // [ \"XII\", \"\", \"\", \"X\", \"II\" ]\n * romanNumeralRegex.exec(\"MCCXIV\") // [ \"MCCXIV\", \"M\", \"CC\", \"X\", \"IV\" ]\n * ```\n */\nexport const romanNumeralRegex: RegExp =\n /^(?=[MDCLXVI])(M{0,3})(C[MD]|D?C{0,3})(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$/i;\n\n// #endregion\n\n/**\n * Default options for {@link numericQuantity}.\n */\nexport const defaultOptions: Required<NumericQuantityOptions> = {\n round: 3,\n allowTrailingInvalid: false,\n romanNumerals: false,\n bigIntOnOverflow: false,\n decimalSeparator: '.',\n allowCurrency: false,\n percentage: false,\n verbose: false,\n} as const;\n","import {\n romanNumeralRegex,\n romanNumeralUnicodeRegex,\n romanNumeralUnicodeToAsciiMap,\n romanNumeralValues,\n} from './constants';\n\n// Just a shorthand type alias\ntype RNV = keyof typeof romanNumeralValues;\n\n/**\n * Converts a string of Roman numerals to a number, like `parseInt`\n * for Roman numerals. Uses modern, strict rules (only 1 to 3999).\n *\n * The string can include ASCII representations of Roman numerals\n * or Unicode Roman numeral code points (`U+2160` through `U+217F`).\n */\nexport const parseRomanNumerals = (romanNumerals: string): number => {\n const normalized = `${romanNumerals}`\n // Convert Unicode Roman numerals to ASCII\n .replace(\n romanNumeralUnicodeRegex,\n (_m, rn: keyof typeof romanNumeralUnicodeToAsciiMap) => romanNumeralUnicodeToAsciiMap[rn]\n )\n // Normalize to uppercase (more common for Roman numerals)\n .toUpperCase();\n\n const regexResult = romanNumeralRegex.exec(normalized);\n\n if (!regexResult) {\n return NaN;\n }\n\n const [, thousands, hundreds, tens, ones] = regexResult;\n\n return (\n (romanNumeralValues[thousands as RNV] ?? 0) +\n (romanNumeralValues[hundreds as RNV] ?? 0) +\n (romanNumeralValues[tens as RNV] ?? 0) +\n (romanNumeralValues[ones as RNV] ?? 0)\n );\n};\n","import {\n defaultOptions,\n normalizeDigits,\n numericRegexWithTrailingInvalid,\n superSubDigitToAsciiMap,\n superSubDigitsRegex,\n vulgarFractionToAsciiMap,\n vulgarFractionsRegex,\n} from './constants';\nimport { parseRomanNumerals } from './parseRomanNumerals';\nimport type {\n NumericQuantityOptions,\n NumericQuantityReturnType,\n NumericQuantityVerboseResult,\n} from './types';\n\nconst spaceThenSlashRegex = /^\\s*\\//;\nconst currencyPrefixRegex = /^([-+]?)\\s*(\\p{Sc}+)\\s*/u;\nconst currencySuffixRegex = /\\s*(\\p{Sc}+)$/u;\nconst percentageSuffixRegex = /%$/;\n\n/**\n * Converts a string to a number, like an enhanced version of `parseFloat`.\n *\n * The string can include mixed numbers, vulgar fractions, or Roman numerals.\n * Input is expected to be a `string`, but will be coerced to `string` if necessary.\n *\n * @param quantity - The value to parse as a numeric quantity.\n * @param options - Optional settings to control parsing behavior.\n */\nfunction numericQuantity(quantity: unknown): number;\nfunction numericQuantity<T extends NumericQuantityOptions>(\n quantity: unknown,\n options: T\n): NumericQuantityReturnType<T>;\nfunction numericQuantity(quantity: unknown, options?: NumericQuantityOptions): number;\nfunction numericQuantity(\n quantity: unknown,\n options: NumericQuantityOptions = defaultOptions\n): number | bigint | NumericQuantityVerboseResult {\n const opts: Required<NumericQuantityOptions> = {\n ...defaultOptions,\n ...options,\n };\n\n // oxlint-disable-next-line typescript/restrict-template-expressions\n const originalInput = typeof quantity === 'string' ? quantity : `${quantity}`;\n\n // Metadata for verbose output\n let currencyPrefix: string | undefined;\n let currencySuffix: string | undefined;\n let percentageSuffix: boolean | undefined;\n let trailingInvalid: string | undefined;\n let parsedSign: '-' | '+' | undefined;\n let parsedWhole: number | undefined;\n let parsedNumerator: number | undefined;\n let parsedDenominator: number | undefined;\n\n const buildVerboseResult = (value: number | bigint): NumericQuantityVerboseResult => {\n const result: NumericQuantityVerboseResult = {\n value,\n input: originalInput,\n };\n if (currencyPrefix) result.currencyPrefix = currencyPrefix;\n if (currencySuffix) result.currencySuffix = currencySuffix;\n if (percentageSuffix) result.percentageSuffix = percentageSuffix;\n if (trailingInvalid) result.trailingInvalid = trailingInvalid;\n if (parsedSign) result.sign = parsedSign;\n if (parsedWhole !== undefined) result.whole = parsedWhole;\n if (parsedNumerator !== undefined) result.numerator = parsedNumerator;\n if (parsedDenominator !== undefined) result.denominator = parsedDenominator;\n return result;\n };\n\n const returnValue = (value: number | bigint) =>\n opts.verbose ? buildVerboseResult(value) : value;\n\n if (typeof quantity === 'number' || typeof quantity === 'bigint') {\n return returnValue(quantity);\n }\n\n let finalResult = NaN;\n let workingString = originalInput;\n\n // Strip currency prefix if allowed (preserving leading dash for negatives)\n if (opts.allowCurrency) {\n const prefixMatch = currencyPrefixRegex.exec(workingString);\n if (prefixMatch && prefixMatch[2]) {\n currencyPrefix = prefixMatch[2];\n // Keep the dash if present, remove currency symbol\n workingString = (prefixMatch[1] || '') + workingString.slice(prefixMatch[0].length);\n }\n }\n\n // Strip currency suffix if allowed (before percentage check)\n if (opts.allowCurrency) {\n const suffixMatch = currencySuffixRegex.exec(workingString);\n if (suffixMatch) {\n currencySuffix = suffixMatch[1];\n workingString = workingString.slice(0, -suffixMatch[0].length);\n }\n }\n\n // Strip percentage suffix if option is set\n if (opts.percentage && percentageSuffixRegex.test(workingString)) {\n percentageSuffix = true;\n workingString = workingString.slice(0, -1);\n }\n\n // Coerce to string and normalize\n const quantityAsString = normalizeDigits(\n workingString\n // Convert vulgar fractions to ASCII, with a leading space\n // to keep the whole number and the fraction separate\n .replace(\n vulgarFractionsRegex,\n (_m, vf: keyof typeof vulgarFractionToAsciiMap) => ` ${vulgarFractionToAsciiMap[vf]}`\n )\n // Convert superscript/subscript digits to ASCII\n .replace(\n superSubDigitsRegex,\n ch => superSubDigitToAsciiMap[ch as keyof typeof superSubDigitToAsciiMap]\n )\n // Convert fraction slash to standard slash\n .replace('⁄', '/')\n .trim()\n );\n\n // Bail out if the string was only white space\n if (quantityAsString.length === 0) {\n return returnValue(NaN);\n }\n\n let normalizedString = quantityAsString;\n\n if (opts.decimalSeparator === ',') {\n const commaCount = (quantityAsString.match(/,/g) || []).length;\n if (commaCount === 1) {\n // Treat lone comma as decimal separator; remove all \".\" since they represent\n // thousands/whatever separators\n normalizedString = quantityAsString.replaceAll('.', '_').replace(',', '.');\n } else if (commaCount > 1) {\n // The second comma and everything after is \"trailing invalid\"\n if (!opts.allowTrailingInvalid) {\n // Bail out if trailing invalid is not allowed\n return returnValue(NaN);\n }\n\n const firstCommaIndex = quantityAsString.indexOf(',');\n const secondCommaIndex = quantityAsString.indexOf(',', firstCommaIndex + 1);\n const beforeSecondComma = quantityAsString\n .substring(0, secondCommaIndex)\n .replaceAll('.', '_')\n .replace(',', '.');\n const afterSecondComma = quantityAsString.substring(secondCommaIndex + 1);\n normalizedString = opts.allowTrailingInvalid\n ? beforeSecondComma + '&' + afterSecondComma\n : beforeSecondComma;\n } else {\n // No comma as decimal separator, so remove all \".\" since they represent\n // thousands/whatever separators\n normalizedString = quantityAsString.replaceAll('.', '_');\n }\n }\n\n const regexResult = numericRegexWithTrailingInvalid.exec(normalizedString);\n\n // If the Arabic numeral regex fails, try Roman numerals\n if (!regexResult) {\n return returnValue(opts.romanNumerals ? parseRomanNumerals(quantityAsString) : NaN);\n }\n\n // Capture trailing invalid characters: group 7 catches chars starting with\n // [^.\\d/], but the regex (which lacks a $ anchor) may also leave unconsumed\n // input starting with \".\", \"/\", or digits (e.g. \"0.1.2\" or \"1/\").\n const rawTrailing = (regexResult[7] || normalizedString.slice(regexResult[0].length)).trim();\n if (rawTrailing) {\n trailingInvalid = rawTrailing;\n if (!opts.allowTrailingInvalid) {\n return returnValue(NaN);\n }\n }\n\n const [, sign, ng1temp, ng2temp] = regexResult;\n if (sign === '-' || sign === '+') parsedSign = sign;\n const numberGroup1 = ng1temp.replaceAll(',', '').replaceAll('_', '');\n const numberGroup2 = ng2temp?.replaceAll(',', '').replaceAll('_', '');\n\n // Numerify capture group 1\n if (!numberGroup1 && numberGroup2 && numberGroup2.startsWith('.')) {\n finalResult = 0;\n } else {\n if (opts.bigIntOnOverflow) {\n const asBigInt = sign === '-' ? BigInt(`-${numberGroup1}`) : BigInt(numberGroup1);\n if (\n asBigInt > BigInt(Number.MAX_SAFE_INTEGER) ||\n asBigInt < BigInt(Number.MIN_SAFE_INTEGER)\n ) {\n // Note: percentage division not applied to bigint overflow\n return returnValue(asBigInt);\n }\n }\n\n finalResult = parseInt(numberGroup1);\n }\n\n // If capture group 2 is null, then we're dealing with an integer\n // and there is nothing left to process\n if (!numberGroup2) {\n finalResult = sign === '-' ? finalResult * -1 : finalResult;\n if (percentageSuffix && opts.percentage !== 'number') {\n finalResult = finalResult / 100;\n }\n return returnValue(finalResult);\n }\n\n const roundingFactor =\n opts.round === false ? NaN : parseFloat(`1e${Math.floor(Math.max(0, opts.round))}`);\n\n if (\n numberGroup2.startsWith('.') ||\n numberGroup2.startsWith('e') ||\n numberGroup2.startsWith('E')\n ) {\n // If first char of `numberGroup2` is \".\" or \"e\"/\"E\", it's a decimal\n const decimalValue = parseFloat(`${finalResult}${numberGroup2}`);\n finalResult = isNaN(roundingFactor)\n ? decimalValue\n : Math.round(decimalValue * roundingFactor) / roundingFactor;\n } else if (spaceThenSlashRegex.test(numberGroup2)) {\n // If the first non-space char is \"/\" it's a pure fraction (e.g. \"1/2\")\n const numerator = parseInt(numberGroup1);\n const denominator = parseInt(numberGroup2.replace('/', ''));\n parsedNumerator = numerator;\n parsedDenominator = denominator;\n finalResult = isNaN(roundingFactor)\n ? numerator / denominator\n : Math.round((numerator * roundingFactor) / denominator) / roundingFactor;\n } else {\n // Otherwise it's a mixed fraction (e.g. \"1 2/3\")\n const fractionArray = numberGroup2.split('/');\n const [numerator, denominator] = fractionArray.map(v => parseInt(v));\n parsedWhole = finalResult;\n parsedNumerator = numerator;\n parsedDenominator = denominator;\n finalResult += isNaN(roundingFactor)\n ? numerator / denominator\n : Math.round((numerator * roundingFactor) / denominator) / roundingFactor;\n }\n\n finalResult = sign === '-' ? finalResult * -1 : finalResult;\n\n // Apply percentage division if needed\n if (percentageSuffix && opts.percentage !== 'number') {\n finalResult = isNaN(roundingFactor)\n ? finalResult / 100\n : Math.round((finalResult / 100) * roundingFactor) / roundingFactor;\n }\n\n return returnValue(finalResult);\n}\n\nexport { numericQuantity };\n","import { numericQuantity } from './numericQuantity';\nimport type { NumericQuantityOptions } from './types';\n\n/**\n * Checks if a string represents a valid numeric quantity.\n *\n * Returns `true` if the string can be parsed as a number, `false` otherwise.\n * Accepts the same options as `numericQuantity`.\n */\nexport const isNumericQuantity = (\n quantity: string | number,\n options?: NumericQuantityOptions\n): boolean => {\n const result = numericQuantity(quantity, { ...options, verbose: false });\n return typeof result === 'bigint' || !isNaN(result);\n};\n"],"mappings":";;;;;;;AAiBA,MAAM,0BAA0B;CAC9B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;AASA,MAAa,mBAAmB,QAC9B,IAAI,QAAQ,aAAY,OAAM;CAC5B,MAAM,KAAK,GAAG,YAAY,CAAC;CAE3B,IAAI,MAAM,IAAM,OAAO;CAEvB,IAAI,KAAK;CACT,IAAI,KAAK,wBAAwB,SAAS;CAC1C,OAAO,KAAK,IAAI;EACd,MAAM,MAAO,KAAK,KAAK,MAAO;EAC9B,IAAI,wBAAwB,QAAQ,IAClC,KAAK;OAEL,KAAK,MAAM;CAEf;CACA,OAAO,OAAO,KAAK,wBAAwB,GAAI;AACjD,CAAC;;;;AAKH,MAAa,0BAA6E;CACxF,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP;;;;AAKA,MAAa,sBAA8B;;;;AAK3C,MAAa,2BAA+E;CAC1F,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;CACL,KAAK;AACP;;;;;;;;;;;;;;;;;;;;;;;;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"}