Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |
Expand Down Expand Up @@ -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

Expand All @@ -169,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"
Expand All @@ -195,7 +197,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"
Expand Down
70 changes: 66 additions & 4 deletions src/functions/string/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -377,6 +397,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);
}

Expand All @@ -396,5 +419,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;
}
17 changes: 17 additions & 0 deletions src/language-service/language-service.documentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,14 @@ export const BUILTIN_FUNCTION_DOCS: Record<string, FunctionDoc> = {
{ 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.',
Expand All @@ -208,6 +216,15 @@ export const BUILTIN_FUNCTION_DOCS: Record<string, FunctionDoc> = {
{ 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 }
]
}
};

Expand Down
5 changes: 3 additions & 2 deletions src/parsing/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -218,7 +218,8 @@ export class Parser {
toNumber: toNumber,
toBoolean: toBoolean,
padLeft: padLeft,
padRight: padRight
padRight: padRight,
padBoth: padBoth
};

this.numericConstants = {
Expand Down
111 changes: 107 additions & 4 deletions test/functions/functions-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,22 +128,39 @@ 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');
assert.strictEqual(parser.evaluate('trim("\\n\\ttest\\n")'), 'test');
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);
});

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/);
});
});

Expand Down Expand Up @@ -467,13 +484,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');
Expand All @@ -497,15 +525,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');
Expand All @@ -529,5 +573,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/);
});
});
});