From 33bab1c97278d03c0d41f6592fd138099c0f6ed9 Mon Sep 17 00:00:00 2001 From: ark120202 Date: Sun, 5 May 2019 22:35:24 +0500 Subject: [PATCH 1/3] Add string.{repeat,padStart,padEnd} --- src/LuaLib.ts | 2 ++ src/LuaTransformer.ts | 9 +++++++ src/lualib/StringPadEnd.ts | 18 ++++++++++++++ src/lualib/StringPadStart.ts | 18 ++++++++++++++ test/unit/string.spec.ts | 48 ++++++++++++++++++++++++++++++------ 5 files changed, 88 insertions(+), 7 deletions(-) create mode 100644 src/lualib/StringPadEnd.ts create mode 100644 src/lualib/StringPadStart.ts 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..a2fe4cbea --- /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, maxLength); +} diff --git a/src/lualib/StringPadStart.ts b/src/lualib/StringPadStart.ts new file mode 100644 index 000000000..61ca3ee95 --- /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, maxLength) + this; +} diff --git a/test/unit/string.spec.ts b/test/unit/string.spec.ts index 6690a4111..5f620f10c 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: "" }, @@ -309,12 +320,35 @@ test.each<{ inp: string; args: Parameters }>([ }); 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 }, +])("string.repeat (%p)", ({ inp, count }) => { + const result = util.transpileAndExecute(`return "${inp}".repeat(${count})`); - expect(result).toBe(input[index]); + expect(result).toBe(inp.repeat(count)); +}); + +const padCases = [ + { inp: "foo", maxLength: 0 }, + { inp: "foo", maxLength: 3 }, + { inp: "foo", maxLength: 4 }, + { inp: "foo", maxLength: 10 }, + { inp: "foo", maxLength: 4, fillString: " " }, + { inp: "foo", maxLength: 10, fillString: " " }, + { inp: "foo", maxLength: 5, fillString: "1234" }, +]; + +test.each(padCases)("string.padStart (%p)", ({ inp, maxLength, fillString }) => { + const argsString = [maxLength, fillString].map(arg => JSON.stringify(arg)).join(", "); + 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 = [maxLength, fillString].map(arg => JSON.stringify(arg)).join(", "); + const result = util.transpileAndExecute(`return "${inp}".padEnd(${argsString})`); + + expect(result).toBe(inp.padEnd(maxLength, fillString)); }); From 223254a406fc2b72392464de041c5d4006e99e61 Mon Sep 17 00:00:00 2001 From: ark120202 Date: Tue, 14 May 2019 01:28:08 +0500 Subject: [PATCH 2/3] Add non-integer string.repeat test cases --- test/unit/string.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unit/string.spec.ts b/test/unit/string.spec.ts index 5f620f10c..ad0126fc7 100644 --- a/test/unit/string.spec.ts +++ b/test/unit/string.spec.ts @@ -323,6 +323,9 @@ test.each([ { 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})`); From 1f06807d25478a8bff9fe1ae1a33425f0c4c16e3 Mon Sep 17 00:00:00 2001 From: ark120202 Date: Tue, 14 May 2019 01:47:46 +0500 Subject: [PATCH 3/3] Fix non-integer length errors in string pad --- src/lualib/StringPadEnd.ts | 2 +- src/lualib/StringPadStart.ts | 2 +- test/unit/numbers.spec.ts | 16 +++++----------- test/unit/string.spec.ts | 13 +++++++------ test/util.ts | 7 +++++++ 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/lualib/StringPadEnd.ts b/src/lualib/StringPadEnd.ts index a2fe4cbea..6be8b8853 100644 --- a/src/lualib/StringPadEnd.ts +++ b/src/lualib/StringPadEnd.ts @@ -14,5 +14,5 @@ function __TS__StringPadEnd(this: string, maxLength: number, fillString = " "): fillString += fillString.repeat(maxLength / fillString.length); } - return this + fillString.slice(0, maxLength); + return this + fillString.slice(0, Math.floor(maxLength)); } diff --git a/src/lualib/StringPadStart.ts b/src/lualib/StringPadStart.ts index 61ca3ee95..1fee2e64d 100644 --- a/src/lualib/StringPadStart.ts +++ b/src/lualib/StringPadStart.ts @@ -14,5 +14,5 @@ function __TS__StringPadStart(this: string, maxLength: number, fillString = " ") fillString += fillString.repeat(maxLength / fillString.length); } - return fillString.slice(0, maxLength) + this; + 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 ad0126fc7..9bbf57ef6 100644 --- a/test/unit/string.spec.ts +++ b/test/unit/string.spec.ts @@ -301,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)); @@ -313,7 +313,7 @@ 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)); @@ -335,22 +335,23 @@ test.each([ const padCases = [ { inp: "foo", maxLength: 0 }, { inp: "foo", maxLength: 3 }, - { inp: "foo", maxLength: 4 }, - { inp: "foo", maxLength: 10 }, + { 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 = [maxLength, fillString].map(arg => JSON.stringify(arg)).join(", "); + 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 = [maxLength, fillString].map(arg => JSON.stringify(arg)).join(", "); + 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(", ");