From 028e3a1e3efe8c5035a3bf34e62ac8a18753b37e Mon Sep 17 00:00:00 2001 From: Cold Fry Date: Sun, 5 Apr 2026 21:52:01 +0000 Subject: [PATCH] fix Number(), parseFloat, parseInt not handling some string formats --- src/lualib/Number.ts | 12 ++++++++++++ src/lualib/ParseFloat.ts | 4 +++- src/lualib/ParseInt.ts | 8 ++++++++ test/unit/builtins/numbers.spec.ts | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/lualib/Number.ts b/src/lualib/Number.ts index b258b48f6..21840a188 100644 --- a/src/lualib/Number.ts +++ b/src/lualib/Number.ts @@ -1,3 +1,5 @@ +import { __TS__Match } from "./Match"; + export function __TS__Number(this: void, value: unknown): number { const valueType = type(value); if (valueType === "number") { @@ -11,6 +13,16 @@ export function __TS__Number(this: void, value: unknown): number { const [stringWithoutSpaces] = string.gsub(value as string, "%s", ""); if (stringWithoutSpaces === "") return 0; + // Handle 0b/0B (binary) and 0o/0O (octal) literal prefixes + const [sign, prefix, digits] = __TS__Match(stringWithoutSpaces, "^(-?)0([bBoO])(.+)"); + if (prefix !== undefined) { + const base = prefix === "b" || prefix === "B" ? 2 : 8; + const result = tonumber(digits, base); + if (result !== undefined) { + return sign === "-" ? -result : result; + } + } + return NaN; } else if (valueType === "boolean") { return value ? 1 : 0; diff --git a/src/lualib/ParseFloat.ts b/src/lualib/ParseFloat.ts index 7720acb6b..76c175146 100644 --- a/src/lualib/ParseFloat.ts +++ b/src/lualib/ParseFloat.ts @@ -8,6 +8,8 @@ export function __TS__ParseFloat(this: void, numberString: string): number { return infinityMatch[0] === "-" ? -Infinity : Infinity; } - const number = tonumber(__TS__Match(numberString, "^%s*(-?%d+%.?%d*)")[0]); + // Try with scientific notation first, fall back to basic decimal + const [numberMatch] = __TS__Match(numberString, "^%s*(-?%d+%.?%d*[eE][+-]?%d+)"); + const number = tonumber(numberMatch ?? __TS__Match(numberString, "^%s*(-?%d+%.?%d*)")[0]); return number ?? NaN; } diff --git a/src/lualib/ParseInt.ts b/src/lualib/ParseInt.ts index c27d62662..0cbef23ad 100644 --- a/src/lualib/ParseInt.ts +++ b/src/lualib/ParseInt.ts @@ -13,6 +13,14 @@ export function __TS__ParseInt(this: void, numberString: string, base?: number): ? "-" + numberString.substring(hexMatch.length) : numberString.substring(hexMatch.length); } + } else if (base === 16) { + // Strip 0x/0X prefix when radix is explicitly 16 (per ECMA-262 21.1.2.13 step 11) + const [hexMatch] = __TS__Match(numberString, "^%s*-?0[xX]"); + if (hexMatch !== undefined) { + numberString = __TS__Match(hexMatch, "-")[0] + ? "-" + numberString.substring(hexMatch.length) + : numberString.substring(hexMatch.length); + } } // Check if base is in bounds diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index 9570e4af2..e6d471ac2 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -34,6 +34,11 @@ const stringCases = ["-1", "0", "1", "1.5", "Infinity", "-Infinity"]; const restCases = [true, false, "", " ", "\t", "\n", "foo", {}]; const cases = [...numberCases, ...stringCases, ...restCases]; +// Number() with ES6 numeric literal strings +test.each(["0o17", "0O17", "0b101", "0B101", "0x1A", "0X1a"])("Number(%p) parses literal prefix", value => { + util.testExpressionTemplate`Number(${value})`.expectToMatchJsResult(); +}); + describe("Number", () => { test.each(cases)("constructor(%p)", value => { util.testExpressionTemplate`Number(${value})`.expectToMatchJsResult(); @@ -143,6 +148,13 @@ test.each(["Infinity", "-Infinity", " -Infinity"])("parseFloat handles Infinit util.testExpression`parseFloat("${numberString}")`.expectToMatchJsResult(); }); +test.each(["1.5e2", "1e3", "-2.5E4", "1.5e-2", "1e+3", "1.5e2px"])( + "parseFloat handles scientific notation (%s)", + numberString => { + util.testExpression`parseFloat("${numberString}")`.expectToMatchJsResult(); + } +); + test.each([ { numberString: "36", base: 8 }, { numberString: "-36", base: 8 }, @@ -157,6 +169,10 @@ test.each(["0x4A", "-0x42", "0X42", " 0x391", " -0x8F"])("parseInt detects h util.testExpression`parseInt("${numberString}")`.expectToMatchJsResult(); }); +test.each(["0xFF", "-0xFF", "0X1A", " 0xff"])("parseInt with 0x prefix and explicit base 16 (%s)", numberString => { + util.testExpression`parseInt("${numberString}", 16)`.expectToMatchJsResult(); +}); + test.each([1, 37, -100])("parseInt with invalid base (%p)", base => { util.testExpression`parseInt("11111", ${base})`.expectToMatchJsResult(); });