From 52429fe3314626064d85ac1e1cbfeae50a994c9c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:05:48 +0000 Subject: [PATCH 1/3] Initial plan From a1c9c9485a2b6131bf373975fd3b88db55f9fadd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:11:10 +0000 Subject: [PATCH 2/3] Add third optional parameter to padLeft/padRight and implement padBoth function Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --- docs/syntax.md | 9 +- src/functions/string/operations.ts | 42 +++++++++ .../language-service.documentation.ts | 9 ++ src/parsing/parser.ts | 5 +- test/functions/functions-string.ts | 90 ++++++++++++++++++- 5 files changed, 149 insertions(+), 6 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index f3c113b..6347130 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -151,8 +151,9 @@ The parser includes comprehensive string manipulation capabilities. | Function | Description | |:--------------------- |:----------- | -| padLeft(str, len) | Pads a string on the left with spaces to reach the target length. | -| padRight(str, len) | Pads a string on the right with spaces to reach the target length. | +| padLeft(str, len, padChar?) | Pads a string on the left with spaces (or optional padding character) to reach the target length. | +| padRight(str, len, padChar?) | Pads a string on the right with spaces (or optional padding character) to reach the target length. | +| padBoth(str, len, padChar?) | Pads a string on both sides with spaces (or optional padding character) to reach the target length. If an odd number of padding characters is needed, the extra character is added on the right. | ### String Function Examples @@ -195,7 +196,11 @@ parser.evaluate('toBoolean("0")'); // false // Padding parser.evaluate('padLeft("5", 3)'); // " 5" +parser.evaluate('padLeft("5", 3, "0")'); // "005" parser.evaluate('padRight("5", 3)'); // "5 " +parser.evaluate('padRight("5", 3, "0")'); // "500" +parser.evaluate('padBoth("hi", 6)'); // " hi " +parser.evaluate('padBoth("hi", 6, "-")'); // "--hi--" // Complex string operations parser.evaluate('toUpper(trim(left(" hello world ", 10)))'); // "HELLO WOR" diff --git a/src/functions/string/operations.ts b/src/functions/string/operations.ts index 22df36b..af07876 100644 --- a/src/functions/string/operations.ts +++ b/src/functions/string/operations.ts @@ -377,6 +377,9 @@ export function padLeft(str: string | undefined, targetLength: number | undefine if (targetLength < 0 || !Number.isInteger(targetLength)) { throw new Error('Second argument to padLeft must be a non-negative integer'); } + if (padString !== undefined && typeof padString !== 'string') { + throw new Error('Third argument to padLeft must be a string'); + } return str.padStart(targetLength, padString); } @@ -396,5 +399,44 @@ export function padRight(str: string | undefined, targetLength: number | undefin if (targetLength < 0 || !Number.isInteger(targetLength)) { throw new Error('Second argument to padRight must be a non-negative integer'); } + if (padString !== undefined && typeof padString !== 'string') { + throw new Error('Third argument to padRight must be a string'); + } return str.padEnd(targetLength, padString); } + +/** + * Pads a string on both sides to reach the target length + * If an odd number of padding characters is needed, the extra character is added on the right + */ +export function padBoth(str: string | undefined, targetLength: number | undefined, padString?: string): string | undefined { + if (str === undefined || targetLength === undefined) { + return undefined; + } + if (typeof str !== 'string') { + throw new Error('First argument to padBoth must be a string'); + } + if (typeof targetLength !== 'number') { + throw new Error('Second argument to padBoth must be a number'); + } + if (targetLength < 0 || !Number.isInteger(targetLength)) { + throw new Error('Second argument to padBoth must be a non-negative integer'); + } + if (padString !== undefined && typeof padString !== 'string') { + throw new Error('Third argument to padBoth must be a string'); + } + + const totalPadding = targetLength - str.length; + if (totalPadding <= 0) { + return str; + } + + const leftPadding = Math.floor(totalPadding / 2); + const rightPadding = totalPadding - leftPadding; + + const actualPadString = padString ?? ' '; + const leftPad = actualPadString.repeat(Math.ceil(leftPadding / actualPadString.length)).slice(0, leftPadding); + const rightPad = actualPadString.repeat(Math.ceil(rightPadding / actualPadString.length)).slice(0, rightPadding); + + return leftPad + str + rightPad; +} diff --git a/src/language-service/language-service.documentation.ts b/src/language-service/language-service.documentation.ts index 42a4509..e784daf 100644 --- a/src/language-service/language-service.documentation.ts +++ b/src/language-service/language-service.documentation.ts @@ -208,6 +208,15 @@ export const BUILTIN_FUNCTION_DOCS: Record = { { name: 'length', description: 'Target length.' }, { name: 'padStr', description: 'Padding string.', optional: true } ] + }, + padBoth: { + name: 'padBoth', + description: 'Pad string on both sides to reach target length. Extra padding goes on the right.', + params: [ + { name: 'str', description: 'Input string.' }, + { name: 'length', description: 'Target length.' }, + { name: 'padStr', description: 'Padding string.', optional: true } + ] } }; diff --git a/src/parsing/parser.ts b/src/parsing/parser.ts index bde3c0e..5728d93 100644 --- a/src/parsing/parser.ts +++ b/src/parsing/parser.ts @@ -6,7 +6,7 @@ import { Expression } from '../core/expression.js'; import type { Value, VariableResolveResult, Values } from '../types/values.js'; import type { Instruction } from './instruction.js'; import type { OperatorFunction } from '../types/parser.js'; -import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight } from '../functions/index.js'; +import { atan2, condition, fac, filter, fold, gamma, hypot, indexOf, join, map, max, min, random, roundTo, sum, json, stringLength, isEmpty, stringContains, startsWith, endsWith, searchCount, trim, toUpper, toLower, toTitle, split, repeat, reverse, left, right, replace, replaceFirst, naturalSort, toNumber, toBoolean, padLeft, padRight, padBoth } from '../functions/index.js'; import { add, sub, @@ -218,7 +218,8 @@ export class Parser { toNumber: toNumber, toBoolean: toBoolean, padLeft: padLeft, - padRight: padRight + padRight: padRight, + padBoth: padBoth }; this.numericConstants = { diff --git a/test/functions/functions-string.ts b/test/functions/functions-string.ts index 188032a..eac1714 100644 --- a/test/functions/functions-string.ts +++ b/test/functions/functions-string.ts @@ -467,13 +467,24 @@ describe('String Functions TypeScript Test', function () { }); }); - describe('padLeft(str, targetLength)', function () { + describe('padLeft(str, targetLength, padChar?)', function () { it('should pad string on the left with spaces by default', function () { const parser = new Parser(); assert.strictEqual(parser.evaluate('padLeft("5", 3)'), ' 5'); assert.strictEqual(parser.evaluate('padLeft("test", 10)'), ' test'); }); + it('should pad string on the left with custom padding character', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padLeft("5", 3, "0")'), '005'); + assert.strictEqual(parser.evaluate('padLeft("test", 10, "-")'), '------test'); + }); + + it('should handle multi-character padding string', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padLeft("5", 6, "ab")'), 'ababa5'); + }); + it('should not pad if string is already at target length', function () { const parser = new Parser(); assert.strictEqual(parser.evaluate('padLeft("hello", 5)'), 'hello'); @@ -497,15 +508,31 @@ describe('String Functions TypeScript Test', function () { assert.throws(() => parser.evaluate('padLeft(123, 5)'), /First argument.*must be a string/); assert.throws(() => parser.evaluate('padLeft("test", "5")'), /Second argument.*must be a number/); }); + + it('should throw error for non-string padding character', function () { + const parser = new Parser(); + assert.throws(() => parser.evaluate('padLeft("test", 5, 0)'), /Third argument.*must be a string/); + }); }); - describe('padRight(str, targetLength)', function () { + describe('padRight(str, targetLength, padChar?)', function () { it('should pad string on the right with spaces by default', function () { const parser = new Parser(); assert.strictEqual(parser.evaluate('padRight("5", 3)'), '5 '); assert.strictEqual(parser.evaluate('padRight("test", 10)'), 'test '); }); + it('should pad string on the right with custom padding character', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padRight("5", 3, "0")'), '500'); + assert.strictEqual(parser.evaluate('padRight("test", 10, "-")'), 'test------'); + }); + + it('should handle multi-character padding string', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padRight("5", 6, "ab")'), '5ababa'); + }); + it('should not pad if string is already at target length', function () { const parser = new Parser(); assert.strictEqual(parser.evaluate('padRight("hello", 5)'), 'hello'); @@ -529,5 +556,64 @@ describe('String Functions TypeScript Test', function () { assert.throws(() => parser.evaluate('padRight(123, 5)'), /First argument.*must be a string/); assert.throws(() => parser.evaluate('padRight("test", "5")'), /Second argument.*must be a number/); }); + + it('should throw error for non-string padding character', function () { + const parser = new Parser(); + assert.throws(() => parser.evaluate('padRight("test", 5, 0)'), /Third argument.*must be a string/); + }); + }); + + describe('padBoth(str, targetLength, padChar?)', function () { + it('should pad string on both sides with spaces by default', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padBoth("hi", 6)'), ' hi '); + assert.strictEqual(parser.evaluate('padBoth("test", 10)'), ' test '); + }); + + it('should pad string on both sides with custom padding character', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padBoth("hi", 6, "-")'), '--hi--'); + assert.strictEqual(parser.evaluate('padBoth("test", 10, "*")'), '***test***'); + }); + + it('should add extra padding on the right when odd number of padding characters needed', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padBoth("hi", 5)'), ' hi '); + assert.strictEqual(parser.evaluate('padBoth("x", 4)'), ' x '); + }); + + it('should handle multi-character padding string', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padBoth("x", 5, "ab")'), 'abxab'); + }); + + it('should not pad if string is already at target length', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padBoth("hello", 5)'), 'hello'); + assert.strictEqual(parser.evaluate('padBoth("hello", 3)'), 'hello'); + }); + + it('should return undefined if any argument is undefined', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('padBoth(undefined, 5)'), undefined); + assert.strictEqual(parser.evaluate('padBoth("test", undefined)'), undefined); + }); + + it('should throw error for negative or non-integer target length', function () { + const parser = new Parser(); + assert.throws(() => parser.evaluate('padBoth("test", -1)'), /non-negative integer/); + assert.throws(() => parser.evaluate('padBoth("test", 2.5)'), /non-negative integer/); + }); + + it('should throw error for non-string or non-number arguments', function () { + const parser = new Parser(); + assert.throws(() => parser.evaluate('padBoth(123, 5)'), /First argument.*must be a string/); + assert.throws(() => parser.evaluate('padBoth("test", "5")'), /Second argument.*must be a number/); + }); + + it('should throw error for non-string padding character', function () { + const parser = new Parser(); + assert.throws(() => parser.evaluate('padBoth("test", 5, 0)'), /Third argument.*must be a string/); + }); }); }); From a75bee0097708efbe943a377e082ee253fcd629c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 16 Jan 2026 12:29:40 +0000 Subject: [PATCH 3/3] Add optional second parameter to trim function for custom characters Co-authored-by: Sander-Toonen <5106372+Sander-Toonen@users.noreply.github.com> --- docs/syntax.md | 3 +- src/functions/string/operations.ts | 28 ++++++++++++++++--- .../language-service.documentation.ts | 8 ++++++ test/functions/functions-string.ts | 21 ++++++++++++-- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/docs/syntax.md b/docs/syntax.md index 6347130..4317043 100644 --- a/docs/syntax.md +++ b/docs/syntax.md @@ -112,7 +112,7 @@ The parser includes comprehensive string manipulation capabilities. | Function | Description | |:---------------- |:----------- | -| trim(str) | Removes whitespace from both ends of a string. | +| trim(str, chars?)| Removes whitespace (or specified characters) from both ends of a string. | | toUpper(str) | Converts a string to uppercase. | | toLower(str) | Converts a string to lowercase. | | toTitle(str) | Converts a string to title case (capitalizes first letter of each word). | @@ -170,6 +170,7 @@ parser.evaluate('searchCount("hello hello", "hello")'); // 2 // String transformation parser.evaluate('trim(" hello ")'); // "hello" +parser.evaluate('trim("**hello**", "*")'); // "hello" parser.evaluate('toUpper("hello")'); // "HELLO" parser.evaluate('toLower("HELLO")'); // "hello" parser.evaluate('toTitle("hello world")'); // "Hello World" diff --git a/src/functions/string/operations.ts b/src/functions/string/operations.ts index af07876..f0d86ae 100644 --- a/src/functions/string/operations.ts +++ b/src/functions/string/operations.ts @@ -104,16 +104,36 @@ export function searchCount(text: string | undefined, substring: string | undefi } /** - * Removes whitespace from both ends of a string + * Removes whitespace (or specified characters) from both ends of a string */ -export function trim(str: string | undefined): string | undefined { +export function trim(str: string | undefined, chars?: string): string | undefined { if (str === undefined) { return undefined; } if (typeof str !== 'string') { - throw new Error('Argument to trim must be a string'); + throw new Error('First argument to trim must be a string'); } - return str.trim(); + if (chars !== undefined && typeof chars !== 'string') { + throw new Error('Second argument to trim must be a string'); + } + + if (chars === undefined) { + return str.trim(); + } + + // Trim custom characters from both ends + let start = 0; + let end = str.length; + + while (start < end && chars.includes(str[start])) { + start++; + } + + while (end > start && chars.includes(str[end - 1])) { + end--; + } + + return str.slice(start, end); } /** diff --git a/src/language-service/language-service.documentation.ts b/src/language-service/language-service.documentation.ts index e784daf..7d7476e 100644 --- a/src/language-service/language-service.documentation.ts +++ b/src/language-service/language-service.documentation.ts @@ -191,6 +191,14 @@ export const BUILTIN_FUNCTION_DOCS: Record = { { name: 'delimiter', description: 'Delimiter string.' } ] }, + trim: { + name: 'trim', + description: 'Remove whitespace (or specified characters) from both ends of a string.', + params: [ + { name: 'str', description: 'Input string.' }, + { name: 'chars', description: 'Characters to trim.', optional: true } + ] + }, padLeft: { name: 'padLeft', description: 'Pad string on the left to reach target length.', diff --git a/test/functions/functions-string.ts b/test/functions/functions-string.ts index eac1714..d7b384c 100644 --- a/test/functions/functions-string.ts +++ b/test/functions/functions-string.ts @@ -128,7 +128,7 @@ describe('String Functions TypeScript Test', function () { }); }); - describe('trim(str)', function () { + describe('trim(str, chars?)', function () { it('should remove whitespace from both ends', function () { const parser = new Parser(); assert.strictEqual(parser.evaluate('trim(" hello ")'), 'hello'); @@ -136,6 +136,18 @@ describe('String Functions TypeScript Test', function () { assert.strictEqual(parser.evaluate('trim("test")'), 'test'); }); + it('should remove specified characters from both ends', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('trim("**hello**", "*")'), 'hello'); + assert.strictEqual(parser.evaluate('trim("---test---", "-")'), 'test'); + assert.strictEqual(parser.evaluate('trim("abchelloabc", "abc")'), 'hello'); + }); + + it('should handle mixed characters to trim', function () { + const parser = new Parser(); + assert.strictEqual(parser.evaluate('trim("*-hello-*", "*-")'), 'hello'); + }); + it('should return undefined if argument is undefined', function () { const parser = new Parser(); assert.strictEqual(parser.evaluate('trim(undefined)'), undefined); @@ -143,7 +155,12 @@ describe('String Functions TypeScript Test', function () { it('should throw error for non-string argument', function () { const parser = new Parser(); - assert.throws(() => parser.evaluate('trim(123)'), /must be a string/); + assert.throws(() => parser.evaluate('trim(123)'), /First argument.*must be a string/); + }); + + it('should throw error for non-string second argument', function () { + const parser = new Parser(); + assert.throws(() => parser.evaluate('trim("test", 123)'), /Second argument.*must be a string/); }); });