From ae32e322b63d6f36088ca2e1ddc3c56cc751492c Mon Sep 17 00:00:00 2001 From: Perryvw Date: Sun, 4 Oct 2020 20:52:39 +0200 Subject: [PATCH 1/3] Alias parseInt parseFloat to tonumber --- src/transformation/builtins/global.ts | 3 +++ test/unit/builtins/numbers.spec.ts | 33 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/transformation/builtins/global.ts b/src/transformation/builtins/global.ts index 2021951a5..585e77cbd 100644 --- a/src/transformation/builtins/global.ts +++ b/src/transformation/builtins/global.ts @@ -30,5 +30,8 @@ export function transformGlobalCall( node, ...numberParameters ); + case "parseInt": + case "parseFloat": + return lua.createCallExpression(lua.createIdentifier("tonumber"), parameters); } } diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index 2f52c1d7d..366a6bfb7 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -78,3 +78,36 @@ test("number intersected method", () => { test("numbers overflowing the float limit become math.huge", () => { util.testExpression`1e309`.expectToMatchJsResult(); }); + +describe.each(["parseInt", "parseFloat"])("parse numbers with %s", parseFunction => { + test.each(["3", "3.0", "9", "42", "239810241", "-20391"])("parses %s", numberString => { + util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); + }); + + test("empty string parses to undefined", () => { + // Note: JS returns NaN here + util.testExpression`${parseFunction}("")`.expectToEqual(undefined); + }); + + test("invalid string parses to undefined", () => { + // Note: JS returns NaN here + util.testExpression`${parseFunction}("bla")`.expectToEqual(undefined); + }); +}); + +test.each(["3.1415", "2.7182", "-34910.3"])("parseFloat floatingpoint numbers", numberString => { + util.testExpression`parseFloat("${numberString}")`.expectToMatchJsResult(); +}); + +test("parseInt does not round", () => { + // Note: Difference to the original JS implementation. + util.testExpression`parseInt("35.6")`.expectToEqual(35.6); +}); + +test.each([ + { numberString: "36", base: 8 }, + { numberString: "100010101101", base: 2 }, + { numberString: "3F", base: 16 }, +])("parseInt with base (%p)", ({ numberString, base }) => { + util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); +}); From dab4b648d1736aea544f0c3c9b0b011a50de3307 Mon Sep 17 00:00:00 2001 From: Perryvw Date: Tue, 6 Oct 2020 21:59:50 +0200 Subject: [PATCH 2/3] Implemented ParseFloat and ParseInt as lualib functions --- src/LuaLib.ts | 2 ++ src/lualib/ParseFloat.ts | 4 +++ src/lualib/ParseInt.ts | 31 ++++++++++++++++++++++++ src/lualib/declarations/math.d.ts | 3 +++ src/lualib/declarations/string.d.ts | 1 + src/transformation/builtins/global.ts | 5 ++-- test/unit/builtins/numbers.spec.ts | 35 +++++++++++++++------------ 7 files changed, 64 insertions(+), 17 deletions(-) create mode 100644 src/lualib/ParseFloat.ts create mode 100644 src/lualib/ParseInt.ts diff --git a/src/LuaLib.ts b/src/LuaLib.ts index c476882c7..1898be2cf 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -49,6 +49,8 @@ export enum LuaLibFeature { ObjectKeys = "ObjectKeys", ObjectRest = "ObjectRest", ObjectValues = "ObjectValues", + ParseFloat = "ParseFloat", + ParseInt = "ParseInt", Set = "Set", WeakMap = "WeakMap", WeakSet = "WeakSet", diff --git a/src/lualib/ParseFloat.ts b/src/lualib/ParseFloat.ts new file mode 100644 index 000000000..b4e4bf010 --- /dev/null +++ b/src/lualib/ParseFloat.ts @@ -0,0 +1,4 @@ +function __TS__ParseFloat(this: void, numberString: string): number { + const number = tonumber(string.match(numberString, "^-?%d+%.?%d*")); + return number ?? NaN; +} diff --git a/src/lualib/ParseInt.ts b/src/lualib/ParseInt.ts new file mode 100644 index 000000000..b8e72b323 --- /dev/null +++ b/src/lualib/ParseInt.ts @@ -0,0 +1,31 @@ +const __TS__parseInt_patterns = { + 2: "^-?[01]*", + 3: "^-?[012]*", + 4: "^-?[0123]*", + 5: "^-?[01234]*", + 6: "^-?[012345]*", + 7: "^-?[0123456]*", + 8: "^-?[01234567]*", + 9: "^-?[012345678]*", + 10: "^-?[0123456789]*", + 11: "^-?[0123456789aA]*", + 12: "^-?[0123456789aAbB]*", + 13: "^-?[0123456789aAbBcC]*", + 14: "^-?[0123456789aAbBcCdD]*", + 15: "^-?[0123456789aAbBcCdDeE]*", + 16: "^-?[0123456789aAbBcCdDeEfF]*", +}; + +function __TS__ParseInt(this: void, numberString: string, base?: number): number { + const number = tonumber(string.match(numberString, __TS__parseInt_patterns[base ?? 10]), base); + + if (number === undefined) { + return NaN; + } + + if (number >= 0) { + return math.floor(number); + } else { + return math.ceil(number); + } +} diff --git a/src/lualib/declarations/math.d.ts b/src/lualib/declarations/math.d.ts index 837f717f9..54568c031 100644 --- a/src/lualib/declarations/math.d.ts +++ b/src/lualib/declarations/math.d.ts @@ -8,4 +8,7 @@ declare namespace math { function atan(y: number, x?: number): number; function atan2(y: number, x: number): number; + + function ceil(x: number): number; + function floor(x: number): number; } diff --git a/src/lualib/declarations/string.d.ts b/src/lualib/declarations/string.d.ts index e8d657da6..2857690cb 100644 --- a/src/lualib/declarations/string.d.ts +++ b/src/lualib/declarations/string.d.ts @@ -13,4 +13,5 @@ declare namespace string { ): [string, number]; function sub(s: string, i: number, j?: number): string; function format(formatstring: string, ...args: any[]): string; + function match(string: string, pattern: string): string; } diff --git a/src/transformation/builtins/global.ts b/src/transformation/builtins/global.ts index 585e77cbd..79f359bbd 100644 --- a/src/transformation/builtins/global.ts +++ b/src/transformation/builtins/global.ts @@ -30,8 +30,9 @@ export function transformGlobalCall( node, ...numberParameters ); - case "parseInt": case "parseFloat": - return lua.createCallExpression(lua.createIdentifier("tonumber"), parameters); + return transformLuaLibFunction(context, LuaLibFeature.ParseFloat, node, ...parameters); + case "parseInt": + return transformLuaLibFunction(context, LuaLibFeature.ParseInt, node, ...parameters); } } diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index 366a6bfb7..513d9ed53 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -80,34 +80,39 @@ test("numbers overflowing the float limit become math.huge", () => { }); describe.each(["parseInt", "parseFloat"])("parse numbers with %s", parseFunction => { - test.each(["3", "3.0", "9", "42", "239810241", "-20391"])("parses %s", numberString => { + const numberStrings = ["3", "3.0", "9", "42", "239810241", "-20391", "3.1415", "2.7182", "-34910.3"]; + + test.each(numberStrings)("parses (%s)", numberString => { util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); }); - test("empty string parses to undefined", () => { - // Note: JS returns NaN here - util.testExpression`${parseFunction}("")`.expectToEqual(undefined); + test("empty string", () => { + util.testExpression`${parseFunction}("")`.expectToMatchJsResult(); }); - test("invalid string parses to undefined", () => { - // Note: JS returns NaN here - util.testExpression`${parseFunction}("bla")`.expectToEqual(undefined); + test("invalid string", () => { + util.testExpression`${parseFunction}("bla")`.expectToMatchJsResult(); }); -}); -test.each(["3.1415", "2.7182", "-34910.3"])("parseFloat floatingpoint numbers", numberString => { - util.testExpression`parseFloat("${numberString}")`.expectToMatchJsResult(); -}); - -test("parseInt does not round", () => { - // Note: Difference to the original JS implementation. - util.testExpression`parseInt("35.6")`.expectToEqual(35.6); + test.each(["1px", "2300m", "3,4", "452adkfl"])("trailing text (%s)", numberString => { + util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); + }); }); test.each([ { numberString: "36", base: 8 }, + { numberString: "-36", base: 8 }, { numberString: "100010101101", base: 2 }, + { numberString: "-100010101101", base: 2 }, { numberString: "3F", base: 16 }, ])("parseInt with base (%p)", ({ numberString, base }) => { util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); }); + +test.each([ + { numberString: "36px", base: 8 }, + { numberString: "10001010110231", base: 2 }, + { numberString: "3Fcolor", base: 16 }, +])("parseInt with base and trailing text (%p)", ({ numberString, base }) => { + util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); +}); From 6fb146166a9a9bb554cb18280aba1546c5f8b1fd Mon Sep 17 00:00:00 2001 From: Perryvw Date: Wed, 7 Oct 2020 22:59:45 +0200 Subject: [PATCH 3/3] PR comments --- src/lualib/ParseFloat.ts | 9 +++++- src/lualib/ParseInt.ts | 46 ++++++++++++++++++------------ test/unit/builtins/numbers.spec.ts | 16 +++++++++++ 3 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/lualib/ParseFloat.ts b/src/lualib/ParseFloat.ts index b4e4bf010..1cd069111 100644 --- a/src/lualib/ParseFloat.ts +++ b/src/lualib/ParseFloat.ts @@ -1,4 +1,11 @@ function __TS__ParseFloat(this: void, numberString: string): number { - const number = tonumber(string.match(numberString, "^-?%d+%.?%d*")); + // Check if string is infinity + const infinityMatch = string.match(numberString, "^%s*(-?Infinity)"); + if (infinityMatch) { + // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with + return infinityMatch[0] === "-" ? -Infinity : Infinity; + } + + const number = tonumber(string.match(numberString, "^%s*(-?%d+%.?%d*)")); return number ?? NaN; } diff --git a/src/lualib/ParseInt.ts b/src/lualib/ParseInt.ts index b8e72b323..b1d5710a0 100644 --- a/src/lualib/ParseInt.ts +++ b/src/lualib/ParseInt.ts @@ -1,28 +1,38 @@ -const __TS__parseInt_patterns = { - 2: "^-?[01]*", - 3: "^-?[012]*", - 4: "^-?[0123]*", - 5: "^-?[01234]*", - 6: "^-?[012345]*", - 7: "^-?[0123456]*", - 8: "^-?[01234567]*", - 9: "^-?[012345678]*", - 10: "^-?[0123456789]*", - 11: "^-?[0123456789aA]*", - 12: "^-?[0123456789aAbB]*", - 13: "^-?[0123456789aAbBcC]*", - 14: "^-?[0123456789aAbBcCdD]*", - 15: "^-?[0123456789aAbBcCdDeE]*", - 16: "^-?[0123456789aAbBcCdDeEfF]*", -}; +const __TS__parseInt_base_pattern = "0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTvVwWxXyYzZ"; function __TS__ParseInt(this: void, numberString: string, base?: number): number { - const number = tonumber(string.match(numberString, __TS__parseInt_patterns[base ?? 10]), base); + // Check which base to use if none specified + if (base === undefined) { + base = 10; + const hexMatch = string.match(numberString, "^%s*-?0[xX]"); + if (hexMatch) { + base = 16; + numberString = string.match(hexMatch, "-") + ? "-" + numberString.substr(hexMatch.length) + : numberString.substr(hexMatch.length); + } + } + + // Check if base is in bounds + if (base < 2 || base > 36) { + return NaN; + } + + // Calculate string match pattern to use + const allowedDigits = + base <= 10 + ? __TS__parseInt_base_pattern.substring(0, base) + : __TS__parseInt_base_pattern.substr(0, 10 + 2 * (base - 10)); + const pattern = `^%s*(-?[${allowedDigits}]*)`; + + // Try to parse with Lua tonumber + const number = tonumber(string.match(numberString, pattern), base); if (number === undefined) { return NaN; } + // Lua uses a different floor convention for negative numbers than JS if (number >= 0) { return math.floor(number); } else { diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index 513d9ed53..0ae65efb3 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -97,6 +97,14 @@ describe.each(["parseInt", "parseFloat"])("parse numbers with %s", parseFunction test.each(["1px", "2300m", "3,4", "452adkfl"])("trailing text (%s)", numberString => { util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); }); + + test.each([" 3", " 4", " -231", " 1px"])("leading whitespace (%s)", numberString => { + util.testExpression`${parseFunction}("${numberString}")`.expectToMatchJsResult(); + }); +}); + +test.each(["Infinity", "-Infinity", " -Infinity"])("parseFloat handles Infinity", numberString => { + util.testExpression`parseFloat("${numberString}")`.expectToMatchJsResult(); }); test.each([ @@ -109,6 +117,14 @@ test.each([ util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); }); +test.each(["0x4A", "-0x42", "0X42", " 0x391", " -0x8F"])("parseInt detects hexadecimal", numberString => { + util.testExpression`parseInt("${numberString}")`.expectToMatchJsResult(); +}); + +test.each([1, 37, -100])("parseInt with invalid base (%p)", base => { + util.testExpression`parseInt("11111", ${base})`.expectToMatchJsResult(); +}); + test.each([ { numberString: "36px", base: 8 }, { numberString: "10001010110231", base: 2 },