diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 6ab22d97c..d339fc945 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -47,6 +47,8 @@ export enum LuaLibFeature { Spread = "Spread", StringConcat = "StringConcat", StringEndsWith = "StringEndsWith", + StringPadEnd = "StringPadEnd", + StringPadStart = "StringPadStart", StringReplace = "StringReplace", StringSplit = "StringSplit", StringStartsWith = "StringStartsWith", diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index cb9c6cb25..5cc7068a2 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -4106,6 +4106,15 @@ export class LuaTransformer { return this.transformLuaLibFunction(LuaLibFeature.StringStartsWith, node, caller, ...params); case "endsWith": return this.transformLuaLibFunction(LuaLibFeature.StringEndsWith, node, caller, ...params); + case "repeat": + const math = tstl.createIdentifier("math"); + const floor = tstl.createStringLiteral("floor"); + const parameter = tstl.createCallExpression(tstl.createTableIndexExpression(math, floor), [params[0]]); + return this.createStringCall("rep", node, caller, parameter); + case "padStart": + return this.transformLuaLibFunction(LuaLibFeature.StringPadStart, node, caller, ...params); + case "padEnd": + return this.transformLuaLibFunction(LuaLibFeature.StringPadEnd, node, caller, ...params); case "byte": case "char": case "dump": diff --git a/src/lualib/StringPadEnd.ts b/src/lualib/StringPadEnd.ts new file mode 100644 index 000000000..6be8b8853 --- /dev/null +++ b/src/lualib/StringPadEnd.ts @@ -0,0 +1,18 @@ +function __TS__StringPadEnd(this: string, maxLength: number, fillString = " "): string { + if (maxLength !== maxLength) maxLength = 0; + if (maxLength === -Infinity || maxLength === Infinity) { + // tslint:disable-next-line: no-string-throw + throw "Invalid string length"; + } + + if (this.length >= maxLength || fillString.length === 0) { + return this; + } + + maxLength = maxLength - this.length; + if (maxLength > fillString.length) { + fillString += fillString.repeat(maxLength / fillString.length); + } + + return this + fillString.slice(0, Math.floor(maxLength)); +} diff --git a/src/lualib/StringPadStart.ts b/src/lualib/StringPadStart.ts new file mode 100644 index 000000000..1fee2e64d --- /dev/null +++ b/src/lualib/StringPadStart.ts @@ -0,0 +1,18 @@ +function __TS__StringPadStart(this: string, maxLength: number, fillString = " "): string { + if (maxLength !== maxLength) maxLength = 0; + if (maxLength === -Infinity || maxLength === Infinity) { + // tslint:disable-next-line: no-string-throw + throw "Invalid string length"; + } + + if (this.length >= maxLength || fillString.length === 0) { + return this; + } + + maxLength = maxLength - this.length; + if (maxLength > fillString.length) { + fillString += fillString.repeat(maxLength / fillString.length); + } + + return fillString.slice(0, Math.floor(maxLength)) + this; +} diff --git a/test/unit/numbers.spec.ts b/test/unit/numbers.spec.ts index 623335683..e3f78ff10 100644 --- a/test/unit/numbers.spec.ts +++ b/test/unit/numbers.spec.ts @@ -39,21 +39,15 @@ const stringCases = ["-1", "0", "1", "1.5", "Infinity", "-Infinity"]; const restCases: any[] = [true, false, "", " ", "\t", "\n", "foo", {}]; const cases: any[] = [...numberCases, ...stringCases, ...restCases]; -// TODO: Add more general utils to serialize values -const valueToString = (value: unknown) => - value === Infinity || value === -Infinity || (typeof value === "number" && Number.isNaN(value)) - ? String(value) - : JSON.stringify(value); - describe("Number", () => { test.each(cases)("constructor(%p)", value => { - const result = util.transpileAndExecute(`return Number(${valueToString(value)})`); + const result = util.transpileAndExecute(`return Number(${util.valueToString(value)})`); expect(result).toBe(Number(value)); }); test.each(cases)("isNaN(%p)", value => { const result = util.transpileAndExecute(` - return Number.isNaN(${valueToString(value)} as any) + return Number.isNaN(${util.valueToString(value)} as any) `); expect(result).toBe(Number.isNaN(value)); @@ -61,7 +55,7 @@ describe("Number", () => { test.each(cases)("isFinite(%p)", value => { const result = util.transpileAndExecute(` - return Number.isFinite(${valueToString(value)} as any) + return Number.isFinite(${util.valueToString(value)} as any) `); expect(result).toBe(Number.isFinite(value)); @@ -69,11 +63,11 @@ describe("Number", () => { }); test.each(cases)("isNaN(%p)", value => { - const result = util.transpileAndExecute(`return isNaN(${valueToString(value)} as any)`); + const result = util.transpileAndExecute(`return isNaN(${util.valueToString(value)} as any)`); expect(result).toBe(isNaN(value)); }); test.each(cases)("isFinite(%p)", value => { - const result = util.transpileAndExecute(`return isFinite(${valueToString(value)} as any)`); + const result = util.transpileAndExecute(`return isFinite(${util.valueToString(value)} as any)`); expect(result).toBe(isFinite(value)); }); diff --git a/test/unit/string.spec.ts b/test/unit/string.spec.ts index 6690a4111..9bbf57ef6 100644 --- a/test/unit/string.spec.ts +++ b/test/unit/string.spec.ts @@ -73,6 +73,17 @@ test.each([ expect(result).toBe(`${a} ${b} test ${c}`); }); +test.each([ + { input: "abcd", index: 3 }, + { input: "abcde", index: 3 }, + { input: "abcde", index: 0 }, + { input: "a", index: 0 }, +])("string index (%p)", ({ input, index }) => { + const result = util.transpileAndExecute(`return "${input}"[${index}];`); + + expect(result).toBe(input[index]); +}); + test.each([ { inp: "hello test", searchValue: "", replaceValue: "" }, { inp: "hello test", searchValue: " ", replaceValue: "" }, @@ -290,7 +301,7 @@ test.each<{ inp: string; args: Parameters }>([ { inp: "hello test", args: ["test"] }, { inp: "hello test", args: ["test", 6] }, ])("string.startsWith (%p)", ({ inp, args }) => { - const argsString = args.map(arg => JSON.stringify(arg)).join(", "); + const argsString = util.valuesToString(args); const result = util.transpileAndExecute(`return "${inp}".startsWith(${argsString})`); expect(result).toBe(inp.startsWith(...args)); @@ -302,19 +313,46 @@ test.each<{ inp: string; args: Parameters }>([ { inp: "hello test", args: ["hello"] }, { inp: "hello test", args: ["hello", 5] }, ])("string.endsWith (%p)", ({ inp, args }) => { - const argsString = args.map(arg => JSON.stringify(arg)).join(", "); + const argsString = util.valuesToString(args); const result = util.transpileAndExecute(`return "${inp}".endsWith(${argsString})`); expect(result).toBe(inp.endsWith(...args)); }); test.each([ - { input: "abcd", index: 3 }, - { input: "abcde", index: 3 }, - { input: "abcde", index: 0 }, - { input: "a", index: 0 }, -])("string index (%p)", ({ input, index }) => { - const result = util.transpileAndExecute(`return "${input}"[${index}];`); + { inp: "hello test", count: 0 }, + { inp: "hello test", count: 1 }, + { inp: "hello test", count: 2 }, + { inp: "hello test", count: 1.1 }, + { inp: "hello test", count: 1.5 }, + { inp: "hello test", count: 1.9 }, +])("string.repeat (%p)", ({ inp, count }) => { + const result = util.transpileAndExecute(`return "${inp}".repeat(${count})`); + + expect(result).toBe(inp.repeat(count)); +}); - expect(result).toBe(input[index]); +const padCases = [ + { inp: "foo", maxLength: 0 }, + { inp: "foo", maxLength: 3 }, + { inp: "foo", maxLength: 5 }, + { inp: "foo", maxLength: 4, fillString: " " }, + { inp: "foo", maxLength: 10, fillString: " " }, + { inp: "foo", maxLength: 5, fillString: "1234" }, + { inp: "foo", maxLength: 5.9, fillString: "1234" }, + { inp: "foo", maxLength: NaN }, +]; + +test.each(padCases)("string.padStart (%p)", ({ inp, maxLength, fillString }) => { + const argsString = util.valuesToString([maxLength, fillString]); + const result = util.transpileAndExecute(`return "${inp}".padStart(${argsString})`); + + expect(result).toBe(inp.padStart(maxLength, fillString)); +}); + +test.each(padCases)("string.padEnd (%p)", ({ inp, maxLength, fillString }) => { + const argsString = util.valuesToString([maxLength, fillString]); + const result = util.transpileAndExecute(`return "${inp}".padEnd(${argsString})`); + + expect(result).toBe(inp.padEnd(maxLength, fillString)); }); diff --git a/test/util.ts b/test/util.ts index 092e6afda..84b77d0e9 100644 --- a/test/util.ts +++ b/test/util.ts @@ -167,3 +167,10 @@ export function expectToBeDefined(subject: T | null | undefined): subject is expect(subject).toBeDefined(); return true; // If this was false the expect would have thrown an error } + +export const valueToString = (value: unknown) => + value === Infinity || value === -Infinity || (typeof value === "number" && Number.isNaN(value)) + ? String(value) + : JSON.stringify(value); + +export const valuesToString = (values: Array) => values.map(valueToString).join(", ");