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..1cd069111 --- /dev/null +++ b/src/lualib/ParseFloat.ts @@ -0,0 +1,11 @@ +function __TS__ParseFloat(this: void, numberString: string): number { + // 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 new file mode 100644 index 000000000..b1d5710a0 --- /dev/null +++ b/src/lualib/ParseInt.ts @@ -0,0 +1,41 @@ +const __TS__parseInt_base_pattern = "0123456789aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTvVwWxXyYzZ"; + +function __TS__ParseInt(this: void, numberString: string, base?: number): number { + // 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 { + 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 2021951a5..79f359bbd 100644 --- a/src/transformation/builtins/global.ts +++ b/src/transformation/builtins/global.ts @@ -30,5 +30,9 @@ export function transformGlobalCall( node, ...numberParameters ); + case "parseFloat": + 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 2f52c1d7d..0ae65efb3 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -78,3 +78,57 @@ 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 => { + 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", () => { + util.testExpression`${parseFunction}("")`.expectToMatchJsResult(); + }); + + test("invalid string", () => { + util.testExpression`${parseFunction}("bla")`.expectToMatchJsResult(); + }); + + 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([ + { 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(["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 }, + { numberString: "3Fcolor", base: 16 }, +])("parseInt with base and trailing text (%p)", ({ numberString, base }) => { + util.testExpression`parseInt("${numberString}", ${base})`.expectToMatchJsResult(); +});