diff --git a/src/LuaLib.ts b/src/LuaLib.ts index c877dd9cc..b50ab1e75 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -40,6 +40,7 @@ export enum LuaLibFeature { Number = "Number", NumberIsFinite = "NumberIsFinite", NumberIsNaN = "NumberIsNaN", + NumberToString = "NumberToString", ObjectAssign = "ObjectAssign", ObjectEntries = "ObjectEntries", ObjectFromEntries = "ObjectFromEntries", diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index e460993b7..5479fc889 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -4295,7 +4295,7 @@ export class LuaTransformer { } if (tsHelper.isStandardLibraryType(ownerType, "NumberConstructor", this.program)) { - return this.transformNumberCallExpression(node); + return this.transformNumberExpression(node); } const classDecorators = tsHelper.getCustomDecorators(ownerType, this.checker); @@ -4308,6 +4308,10 @@ export class LuaTransformer { return this.transformStringCallExpression(node); } + if (tsHelper.isNumberType(ownerType, this.checker, this.program)) { + return this.transformNumberCallExpression(node); + } + // if ownerType is a array, use only supported functions if (tsHelper.isExplicitArrayType(ownerType, this.checker, this.program)) { return this.transformArrayCallExpression(node); @@ -4883,6 +4887,23 @@ export class LuaTransformer { ); } + protected transformNumberCallExpression(node: ts.CallExpression): tstl.Expression { + const expression = node.expression as ts.PropertyAccessExpression; + const signature = this.checker.getResolvedSignature(node); + const params = this.transformArguments(node.arguments, signature); + const caller = this.transformExpression(expression.expression); + + const expressionName = expression.name.text; + switch (expressionName) { + case "toString": + return params.length === 0 + ? tstl.createCallExpression(tstl.createIdentifier("tostring"), [caller], node) + : this.transformLuaLibFunction(LuaLibFeature.NumberToString, node, caller, ...params); + default: + throw TSTLErrors.UnsupportedProperty("number", expressionName, node); + } + } + // Transpile a String._ property protected transformStringExpression(identifier: ts.Identifier): ExpressionVisitResult { const identifierString = identifier.text; @@ -5015,7 +5036,7 @@ export class LuaTransformer { } // Transpile a Number._ property - protected transformNumberCallExpression(expression: ts.CallExpression): tstl.CallExpression { + protected transformNumberExpression(expression: ts.CallExpression): tstl.CallExpression { const method = expression.expression as ts.PropertyAccessExpression; const parameters = this.transformArguments(expression.arguments); const methodName = method.name.text; diff --git a/src/lualib/NumberToString.ts b/src/lualib/NumberToString.ts new file mode 100644 index 000000000..3ad9a3d5e --- /dev/null +++ b/src/lualib/NumberToString.ts @@ -0,0 +1,49 @@ +// tslint:disable-next-line: variable-name +const ____radixChars = "0123456789abcdefghijklmnopqrstuvwxyz"; + +// https://www.ecma-international.org/ecma-262/10.0/index.html#sec-number.prototype.tostring +function __TS__NumberToString(this: number, radix?: number): string { + if (radix === undefined || radix === 10 || this === Infinity || this === -Infinity || this !== this) { + return this.toString(); + } + + radix = Math.floor(radix); + if (radix < 2 || radix > 36) { + // tslint:disable-next-line: no-string-throw + throw "toString() radix argument must be between 2 and 36"; + } + + let [integer, fraction] = math.modf(Math.abs(this)); + + let result = ""; + if (radix === 8) { + result = string.format("%o", integer); + } else if (radix === 16) { + result = string.format("%x", integer); + } else { + do { + result = ____radixChars[integer % radix] + result; + integer = Math.floor(integer / radix); + } while (integer !== 0); + } + + // https://github.com/v8/v8/blob/f78e8d43c224847fa56b3220a90be250fc0f0d6e/src/numbers/conversions.cc#L1221 + if (fraction !== 0) { + result += "."; + let delta = 1e-16; + do { + fraction *= radix; + delta *= radix; + const digit = Math.floor(fraction); + result += ____radixChars[digit]; + fraction -= digit; + // TODO: Round to even + } while (fraction >= delta); + } + + if (this < 0) { + result = "-" + result; + } + + return result; +} diff --git a/src/lualib/declarations/math.d.ts b/src/lualib/declarations/math.d.ts new file mode 100644 index 000000000..a0fb6c0e6 --- /dev/null +++ b/src/lualib/declarations/math.d.ts @@ -0,0 +1,5 @@ +/** @noSelf */ +declare namespace math { + /** @tupleReturn */ + function modf(x: number): [number, number]; +} diff --git a/src/lualib/declarations/string.d.ts b/src/lualib/declarations/string.d.ts index 1003f5450..a21500d28 100644 --- a/src/lualib/declarations/string.d.ts +++ b/src/lualib/declarations/string.d.ts @@ -8,4 +8,5 @@ declare namespace string { n?: number ): [string, number]; function sub(s: string, i: number, j?: number): string; + function format(formatstring: string, ...args: any[]): string; } diff --git a/src/utils.ts b/src/utils.ts index 0fe3f68b6..204a1d16c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1 +1,16 @@ export const normalizeSlashes = (filePath: string) => filePath.replace(/\\/g, "/"); + +export function flatMap(array: readonly T[], callback: (value: T, index: number) => U | readonly U[]): U[] { + const result: U[] = []; + + for (const [index, value] of array.entries()) { + const mappedValue = callback(value, index); + if (Array.isArray(mappedValue)) { + result.push(...mappedValue); + } else { + result[result.length] = mappedValue as U; + } + } + + return result; +} diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts index 4369f22c6..7a5d5c731 100644 --- a/test/unit/builtins/numbers.spec.ts +++ b/test/unit/builtins/numbers.spec.ts @@ -1,3 +1,4 @@ +import { flatMap } from "../../../src/utils"; import * as util from "../../util"; test.each([ @@ -48,6 +49,18 @@ describe("Number", () => { }); }); +const toStringRadixes = [undefined, 10, 2, 8, 9, 16, 17, 36, 36.9]; +const toStringValues = [-1, 0, 1, 1.5, 1024, 1.2]; +const toStringPairs = flatMap(toStringValues, value => toStringRadixes.map(radix => [value, radix] as const)); + +test.each(toStringPairs)("(%p).toString(%p)", (value, radix) => { + util.testExpressionTemplate`(${value}).toString(${radix})`.expectToMatchJsResult(); +}); + +test.each([NaN, Infinity, -Infinity])("%p.toString(2)", value => { + util.testExpressionTemplate`(${value}).toString(2)`.expectToMatchJsResult(); +}); + test.each(cases)("isNaN(%p)", value => { util.testExpressionTemplate`isNaN(${value} as any)`.expectToMatchJsResult(); });