Skip to content

Commit 37e2933

Browse files
authored
Add optional padding character to padLeft/padRight, new padBoth function, and optional chars parameter to trim (#10)
* Add third optional parameter to padLeft/padRight and implement padBoth function * Add optional second parameter to trim function for custom characters
1 parent 93e47f1 commit 37e2933

5 files changed

Lines changed: 202 additions & 13 deletions

File tree

docs/syntax.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ The parser includes comprehensive string manipulation capabilities.
112112

113113
| Function | Description |
114114
|:---------------- |:----------- |
115-
| trim(str) | Removes whitespace from both ends of a string. |
115+
| trim(str, chars?)| Removes whitespace (or specified characters) from both ends of a string. |
116116
| toUpper(str) | Converts a string to uppercase. |
117117
| toLower(str) | Converts a string to lowercase. |
118118
| toTitle(str) | Converts a string to title case (capitalizes first letter of each word). |
@@ -151,8 +151,9 @@ The parser includes comprehensive string manipulation capabilities.
151151

152152
| Function | Description |
153153
|:--------------------- |:----------- |
154-
| padLeft(str, len) | Pads a string on the left with spaces to reach the target length. |
155-
| padRight(str, len) | Pads a string on the right with spaces to reach the target length. |
154+
| padLeft(str, len, padChar?) | Pads a string on the left with spaces (or optional padding character) to reach the target length. |
155+
| padRight(str, len, padChar?) | Pads a string on the right with spaces (or optional padding character) to reach the target length. |
156+
| 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. |
156157

157158
### String Function Examples
158159

@@ -169,6 +170,7 @@ parser.evaluate('searchCount("hello hello", "hello")'); // 2
169170

170171
// String transformation
171172
parser.evaluate('trim(" hello ")'); // "hello"
173+
parser.evaluate('trim("**hello**", "*")'); // "hello"
172174
parser.evaluate('toUpper("hello")'); // "HELLO"
173175
parser.evaluate('toLower("HELLO")'); // "hello"
174176
parser.evaluate('toTitle("hello world")'); // "Hello World"
@@ -195,7 +197,11 @@ parser.evaluate('toBoolean("0")'); // false
195197

196198
// Padding
197199
parser.evaluate('padLeft("5", 3)'); // " 5"
200+
parser.evaluate('padLeft("5", 3, "0")'); // "005"
198201
parser.evaluate('padRight("5", 3)'); // "5 "
202+
parser.evaluate('padRight("5", 3, "0")'); // "500"
203+
parser.evaluate('padBoth("hi", 6)'); // " hi "
204+
parser.evaluate('padBoth("hi", 6, "-")'); // "--hi--"
199205

200206
// Complex string operations
201207
parser.evaluate('toUpper(trim(left(" hello world ", 10)))'); // "HELLO WOR"

src/functions/string/operations.ts

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,16 +104,36 @@ export function searchCount(text: string | undefined, substring: string | undefi
104104
}
105105

106106
/**
107-
* Removes whitespace from both ends of a string
107+
* Removes whitespace (or specified characters) from both ends of a string
108108
*/
109-
export function trim(str: string | undefined): string | undefined {
109+
export function trim(str: string | undefined, chars?: string): string | undefined {
110110
if (str === undefined) {
111111
return undefined;
112112
}
113113
if (typeof str !== 'string') {
114-
throw new Error('Argument to trim must be a string');
114+
throw new Error('First argument to trim must be a string');
115115
}
116-
return str.trim();
116+
if (chars !== undefined && typeof chars !== 'string') {
117+
throw new Error('Second argument to trim must be a string');
118+
}
119+
120+
if (chars === undefined) {
121+
return str.trim();
122+
}
123+
124+
// Trim custom characters from both ends
125+
let start = 0;
126+
let end = str.length;
127+
128+
while (start < end && chars.includes(str[start])) {
129+
start++;
130+
}
131+
132+
while (end > start && chars.includes(str[end - 1])) {
133+
end--;
134+
}
135+
136+
return str.slice(start, end);
117137
}
118138

119139
/**
@@ -377,6 +397,9 @@ export function padLeft(str: string | undefined, targetLength: number | undefine
377397
if (targetLength < 0 || !Number.isInteger(targetLength)) {
378398
throw new Error('Second argument to padLeft must be a non-negative integer');
379399
}
400+
if (padString !== undefined && typeof padString !== 'string') {
401+
throw new Error('Third argument to padLeft must be a string');
402+
}
380403
return str.padStart(targetLength, padString);
381404
}
382405

@@ -396,5 +419,44 @@ export function padRight(str: string | undefined, targetLength: number | undefin
396419
if (targetLength < 0 || !Number.isInteger(targetLength)) {
397420
throw new Error('Second argument to padRight must be a non-negative integer');
398421
}
422+
if (padString !== undefined && typeof padString !== 'string') {
423+
throw new Error('Third argument to padRight must be a string');
424+
}
399425
return str.padEnd(targetLength, padString);
400426
}
427+
428+
/**
429+
* Pads a string on both sides to reach the target length
430+
* If an odd number of padding characters is needed, the extra character is added on the right
431+
*/
432+
export function padBoth(str: string | undefined, targetLength: number | undefined, padString?: string): string | undefined {
433+
if (str === undefined || targetLength === undefined) {
434+
return undefined;
435+
}
436+
if (typeof str !== 'string') {
437+
throw new Error('First argument to padBoth must be a string');
438+
}
439+
if (typeof targetLength !== 'number') {
440+
throw new Error('Second argument to padBoth must be a number');
441+
}
442+
if (targetLength < 0 || !Number.isInteger(targetLength)) {
443+
throw new Error('Second argument to padBoth must be a non-negative integer');
444+
}
445+
if (padString !== undefined && typeof padString !== 'string') {
446+
throw new Error('Third argument to padBoth must be a string');
447+
}
448+
449+
const totalPadding = targetLength - str.length;
450+
if (totalPadding <= 0) {
451+
return str;
452+
}
453+
454+
const leftPadding = Math.floor(totalPadding / 2);
455+
const rightPadding = totalPadding - leftPadding;
456+
457+
const actualPadString = padString ?? ' ';
458+
const leftPad = actualPadString.repeat(Math.ceil(leftPadding / actualPadString.length)).slice(0, leftPadding);
459+
const rightPad = actualPadString.repeat(Math.ceil(rightPadding / actualPadString.length)).slice(0, rightPadding);
460+
461+
return leftPad + str + rightPad;
462+
}

src/language-service/language-service.documentation.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@ export const BUILTIN_FUNCTION_DOCS: Record<string, FunctionDoc> = {
191191
{ name: 'delimiter', description: 'Delimiter string.' }
192192
]
193193
},
194+
trim: {
195+
name: 'trim',
196+
description: 'Remove whitespace (or specified characters) from both ends of a string.',
197+
params: [
198+
{ name: 'str', description: 'Input string.' },
199+
{ name: 'chars', description: 'Characters to trim.', optional: true }
200+
]
201+
},
194202
padLeft: {
195203
name: 'padLeft',
196204
description: 'Pad string on the left to reach target length.',
@@ -208,6 +216,15 @@ export const BUILTIN_FUNCTION_DOCS: Record<string, FunctionDoc> = {
208216
{ name: 'length', description: 'Target length.' },
209217
{ name: 'padStr', description: 'Padding string.', optional: true }
210218
]
219+
},
220+
padBoth: {
221+
name: 'padBoth',
222+
description: 'Pad string on both sides to reach target length. Extra padding goes on the right.',
223+
params: [
224+
{ name: 'str', description: 'Input string.' },
225+
{ name: 'length', description: 'Target length.' },
226+
{ name: 'padStr', description: 'Padding string.', optional: true }
227+
]
211228
}
212229
};
213230

src/parsing/parser.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Expression } from '../core/expression.js';
66
import type { Value, VariableResolveResult, Values } from '../types/values.js';
77
import type { Instruction } from './instruction.js';
88
import type { OperatorFunction } from '../types/parser.js';
9-
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';
9+
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';
1010
import {
1111
add,
1212
sub,
@@ -218,7 +218,8 @@ export class Parser {
218218
toNumber: toNumber,
219219
toBoolean: toBoolean,
220220
padLeft: padLeft,
221-
padRight: padRight
221+
padRight: padRight,
222+
padBoth: padBoth
222223
};
223224

224225
this.numericConstants = {

test/functions/functions-string.ts

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -128,22 +128,39 @@ describe('String Functions TypeScript Test', function () {
128128
});
129129
});
130130

131-
describe('trim(str)', function () {
131+
describe('trim(str, chars?)', function () {
132132
it('should remove whitespace from both ends', function () {
133133
const parser = new Parser();
134134
assert.strictEqual(parser.evaluate('trim(" hello ")'), 'hello');
135135
assert.strictEqual(parser.evaluate('trim("\\n\\ttest\\n")'), 'test');
136136
assert.strictEqual(parser.evaluate('trim("test")'), 'test');
137137
});
138138

139+
it('should remove specified characters from both ends', function () {
140+
const parser = new Parser();
141+
assert.strictEqual(parser.evaluate('trim("**hello**", "*")'), 'hello');
142+
assert.strictEqual(parser.evaluate('trim("---test---", "-")'), 'test');
143+
assert.strictEqual(parser.evaluate('trim("abchelloabc", "abc")'), 'hello');
144+
});
145+
146+
it('should handle mixed characters to trim', function () {
147+
const parser = new Parser();
148+
assert.strictEqual(parser.evaluate('trim("*-hello-*", "*-")'), 'hello');
149+
});
150+
139151
it('should return undefined if argument is undefined', function () {
140152
const parser = new Parser();
141153
assert.strictEqual(parser.evaluate('trim(undefined)'), undefined);
142154
});
143155

144156
it('should throw error for non-string argument', function () {
145157
const parser = new Parser();
146-
assert.throws(() => parser.evaluate('trim(123)'), /must be a string/);
158+
assert.throws(() => parser.evaluate('trim(123)'), /First argument.*must be a string/);
159+
});
160+
161+
it('should throw error for non-string second argument', function () {
162+
const parser = new Parser();
163+
assert.throws(() => parser.evaluate('trim("test", 123)'), /Second argument.*must be a string/);
147164
});
148165
});
149166

@@ -467,13 +484,24 @@ describe('String Functions TypeScript Test', function () {
467484
});
468485
});
469486

470-
describe('padLeft(str, targetLength)', function () {
487+
describe('padLeft(str, targetLength, padChar?)', function () {
471488
it('should pad string on the left with spaces by default', function () {
472489
const parser = new Parser();
473490
assert.strictEqual(parser.evaluate('padLeft("5", 3)'), ' 5');
474491
assert.strictEqual(parser.evaluate('padLeft("test", 10)'), ' test');
475492
});
476493

494+
it('should pad string on the left with custom padding character', function () {
495+
const parser = new Parser();
496+
assert.strictEqual(parser.evaluate('padLeft("5", 3, "0")'), '005');
497+
assert.strictEqual(parser.evaluate('padLeft("test", 10, "-")'), '------test');
498+
});
499+
500+
it('should handle multi-character padding string', function () {
501+
const parser = new Parser();
502+
assert.strictEqual(parser.evaluate('padLeft("5", 6, "ab")'), 'ababa5');
503+
});
504+
477505
it('should not pad if string is already at target length', function () {
478506
const parser = new Parser();
479507
assert.strictEqual(parser.evaluate('padLeft("hello", 5)'), 'hello');
@@ -497,15 +525,31 @@ describe('String Functions TypeScript Test', function () {
497525
assert.throws(() => parser.evaluate('padLeft(123, 5)'), /First argument.*must be a string/);
498526
assert.throws(() => parser.evaluate('padLeft("test", "5")'), /Second argument.*must be a number/);
499527
});
528+
529+
it('should throw error for non-string padding character', function () {
530+
const parser = new Parser();
531+
assert.throws(() => parser.evaluate('padLeft("test", 5, 0)'), /Third argument.*must be a string/);
532+
});
500533
});
501534

502-
describe('padRight(str, targetLength)', function () {
535+
describe('padRight(str, targetLength, padChar?)', function () {
503536
it('should pad string on the right with spaces by default', function () {
504537
const parser = new Parser();
505538
assert.strictEqual(parser.evaluate('padRight("5", 3)'), '5 ');
506539
assert.strictEqual(parser.evaluate('padRight("test", 10)'), 'test ');
507540
});
508541

542+
it('should pad string on the right with custom padding character', function () {
543+
const parser = new Parser();
544+
assert.strictEqual(parser.evaluate('padRight("5", 3, "0")'), '500');
545+
assert.strictEqual(parser.evaluate('padRight("test", 10, "-")'), 'test------');
546+
});
547+
548+
it('should handle multi-character padding string', function () {
549+
const parser = new Parser();
550+
assert.strictEqual(parser.evaluate('padRight("5", 6, "ab")'), '5ababa');
551+
});
552+
509553
it('should not pad if string is already at target length', function () {
510554
const parser = new Parser();
511555
assert.strictEqual(parser.evaluate('padRight("hello", 5)'), 'hello');
@@ -529,5 +573,64 @@ describe('String Functions TypeScript Test', function () {
529573
assert.throws(() => parser.evaluate('padRight(123, 5)'), /First argument.*must be a string/);
530574
assert.throws(() => parser.evaluate('padRight("test", "5")'), /Second argument.*must be a number/);
531575
});
576+
577+
it('should throw error for non-string padding character', function () {
578+
const parser = new Parser();
579+
assert.throws(() => parser.evaluate('padRight("test", 5, 0)'), /Third argument.*must be a string/);
580+
});
581+
});
582+
583+
describe('padBoth(str, targetLength, padChar?)', function () {
584+
it('should pad string on both sides with spaces by default', function () {
585+
const parser = new Parser();
586+
assert.strictEqual(parser.evaluate('padBoth("hi", 6)'), ' hi ');
587+
assert.strictEqual(parser.evaluate('padBoth("test", 10)'), ' test ');
588+
});
589+
590+
it('should pad string on both sides with custom padding character', function () {
591+
const parser = new Parser();
592+
assert.strictEqual(parser.evaluate('padBoth("hi", 6, "-")'), '--hi--');
593+
assert.strictEqual(parser.evaluate('padBoth("test", 10, "*")'), '***test***');
594+
});
595+
596+
it('should add extra padding on the right when odd number of padding characters needed', function () {
597+
const parser = new Parser();
598+
assert.strictEqual(parser.evaluate('padBoth("hi", 5)'), ' hi ');
599+
assert.strictEqual(parser.evaluate('padBoth("x", 4)'), ' x ');
600+
});
601+
602+
it('should handle multi-character padding string', function () {
603+
const parser = new Parser();
604+
assert.strictEqual(parser.evaluate('padBoth("x", 5, "ab")'), 'abxab');
605+
});
606+
607+
it('should not pad if string is already at target length', function () {
608+
const parser = new Parser();
609+
assert.strictEqual(parser.evaluate('padBoth("hello", 5)'), 'hello');
610+
assert.strictEqual(parser.evaluate('padBoth("hello", 3)'), 'hello');
611+
});
612+
613+
it('should return undefined if any argument is undefined', function () {
614+
const parser = new Parser();
615+
assert.strictEqual(parser.evaluate('padBoth(undefined, 5)'), undefined);
616+
assert.strictEqual(parser.evaluate('padBoth("test", undefined)'), undefined);
617+
});
618+
619+
it('should throw error for negative or non-integer target length', function () {
620+
const parser = new Parser();
621+
assert.throws(() => parser.evaluate('padBoth("test", -1)'), /non-negative integer/);
622+
assert.throws(() => parser.evaluate('padBoth("test", 2.5)'), /non-negative integer/);
623+
});
624+
625+
it('should throw error for non-string or non-number arguments', function () {
626+
const parser = new Parser();
627+
assert.throws(() => parser.evaluate('padBoth(123, 5)'), /First argument.*must be a string/);
628+
assert.throws(() => parser.evaluate('padBoth("test", "5")'), /Second argument.*must be a number/);
629+
});
630+
631+
it('should throw error for non-string padding character', function () {
632+
const parser = new Parser();
633+
assert.throws(() => parser.evaluate('padBoth("test", 5, 0)'), /Third argument.*must be a string/);
634+
});
532635
});
533636
});

0 commit comments

Comments
 (0)