Merge branch 'main' of github.com:silverbulletmd/silverbullet into to-node
This commit is contained in:
@@ -48,7 +48,6 @@ test("[Lua] String to number tests", async () => {
|
||||
|
||||
test("[Lua] String tests", async () => {
|
||||
await runLuaTest("./stdlib/string_test.lua");
|
||||
// await runLuaTest("./stdlib/string_test2.lua");
|
||||
});
|
||||
|
||||
test("[Lua] Space Lua tests", async () => {
|
||||
|
||||
@@ -365,4 +365,9 @@ export const osApi = new LuaTable({
|
||||
return luaFormatTime(fmt, d, utc);
|
||||
},
|
||||
),
|
||||
|
||||
// Returns an approximation of CPU time used by the program in seconds.
|
||||
clock: new LuaBuiltinFunction((_sf): number => {
|
||||
return performance.now() / 1000.0;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -168,3 +168,15 @@ assert(type(err) == "string")
|
||||
|
||||
-- Repeated specifiers
|
||||
assertEquals(os.date("!%Y%Y", ts), "20062006")
|
||||
|
||||
-- os.clock
|
||||
-- Returns a number (seconds)
|
||||
assert(type(os.clock()) == "number")
|
||||
|
||||
-- Must be non-negative
|
||||
assert(os.clock() >= 0)
|
||||
|
||||
-- Monotonically non-decreasing
|
||||
local c1 = os.clock()
|
||||
local c2 = os.clock()
|
||||
assert(c2 >= c1)
|
||||
|
||||
@@ -0,0 +1,703 @@
|
||||
// Lua Optimized Pattern Matching Engine Implementation
|
||||
|
||||
const CH_ESC = 37; // '%'
|
||||
const CH_OPEN = 40; // '('
|
||||
const CH_CLOSE = 41; // ')'
|
||||
const CH_DOT = 46; // '.'
|
||||
const CH_DOLLAR = 36; // '$'
|
||||
const CH_LBRACKET = 91; // '['
|
||||
const CH_RBRACKET = 93; // ']'
|
||||
const CH_CARET = 94; // '^'
|
||||
const CH_DASH = 45; // '-'
|
||||
const CH_STAR = 42; // '*'
|
||||
const CH_PLUS = 43; // '+'
|
||||
const CH_QUESTION = 63; // '?'
|
||||
const CH_0 = 48; // '0'
|
||||
const CH_9 = 57; // '9'
|
||||
|
||||
const SPECIALS_SET = new Set<number>([
|
||||
CH_CARET,
|
||||
CH_DOLLAR,
|
||||
CH_STAR,
|
||||
CH_PLUS,
|
||||
CH_QUESTION,
|
||||
CH_DOT,
|
||||
CH_OPEN,
|
||||
CH_LBRACKET,
|
||||
CH_ESC,
|
||||
CH_DASH,
|
||||
]);
|
||||
|
||||
const MAX_CAPTURES = 32;
|
||||
const MAX_MATCH_DEPTH = 200;
|
||||
|
||||
const CAP_UNFINISHED = -1;
|
||||
const CAP_POSITION = -2;
|
||||
|
||||
interface Capture {
|
||||
init: number;
|
||||
len: number;
|
||||
}
|
||||
|
||||
export interface MatchState {
|
||||
src: string; // original source string (for substring extraction)
|
||||
s: Uint8Array; // source bytes
|
||||
slen: number;
|
||||
p: Uint8Array; // pattern bytes
|
||||
plen: number;
|
||||
level: number;
|
||||
capture: Capture[]; // pre-allocated, length `MAX_CAPTURES`
|
||||
matchdepth: number;
|
||||
}
|
||||
|
||||
function toBytes(s: string): Uint8Array {
|
||||
const len = s.length;
|
||||
const arr = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr[i] = s.charCodeAt(i) & 0xFF;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function matchClass(c: number, cl: number): boolean {
|
||||
const lcl = cl | 32;
|
||||
let res: boolean;
|
||||
switch (lcl) {
|
||||
case 97: // 'a'
|
||||
res = (c >= 65 && c <= 90) || (c >= 97 && c <= 122);
|
||||
break;
|
||||
case 99: // 'c'
|
||||
res = c < 32 || c === 127;
|
||||
break;
|
||||
case 100: // 'd'
|
||||
res = c >= 48 && c <= 57;
|
||||
break;
|
||||
case 103: // 'g'
|
||||
res = c > 32 && c < 127;
|
||||
break;
|
||||
case 108: // 'l'
|
||||
res = c >= 97 && c <= 122;
|
||||
break;
|
||||
case 112: // 'p'
|
||||
res = (c >= 33 && c <= 47) || (c >= 58 && c <= 64) ||
|
||||
(c >= 91 && c <= 96) || (c >= 123 && c <= 126);
|
||||
break;
|
||||
case 115: // 's'
|
||||
res = c === 32 || (c >= 9 && c <= 13);
|
||||
break;
|
||||
case 117: // 'u'
|
||||
res = c >= 65 && c <= 90;
|
||||
break;
|
||||
case 119: // 'w'
|
||||
res = (c >= 65 && c <= 90) || (c >= 97 && c <= 122) ||
|
||||
(c >= 48 && c <= 57);
|
||||
break;
|
||||
case 120: // 'x'
|
||||
res = (c >= 48 && c <= 57) || (c >= 65 && c <= 70) ||
|
||||
(c >= 97 && c <= 102);
|
||||
break;
|
||||
case 122: // 'z'
|
||||
res = c === 0;
|
||||
break;
|
||||
default:
|
||||
return cl === c;
|
||||
}
|
||||
return (cl >= 97 && cl <= 122) ? res : !res;
|
||||
}
|
||||
|
||||
function classEnd(p: Uint8Array, plen: number, pi: number): number {
|
||||
const ch = p[pi];
|
||||
pi++;
|
||||
if (ch === CH_ESC) {
|
||||
if (pi >= plen) {
|
||||
throw new Error("malformed pattern (ends with '%')");
|
||||
}
|
||||
return pi + 1;
|
||||
}
|
||||
if (ch === CH_LBRACKET) {
|
||||
if (pi < plen && p[pi] === CH_CARET) pi++;
|
||||
do {
|
||||
if (pi >= plen) {
|
||||
throw new Error("malformed pattern (missing ']')");
|
||||
}
|
||||
if (p[pi] === CH_ESC && pi + 1 < plen) {
|
||||
pi++;
|
||||
}
|
||||
pi++;
|
||||
} while (pi < plen && p[pi] !== CH_RBRACKET);
|
||||
if (pi >= plen) {
|
||||
throw new Error("malformed pattern (missing ']')");
|
||||
}
|
||||
return pi + 1;
|
||||
}
|
||||
return pi;
|
||||
}
|
||||
|
||||
function matchBracketClass(
|
||||
c: number,
|
||||
p: Uint8Array,
|
||||
pi: number,
|
||||
ec: number,
|
||||
): boolean {
|
||||
let sig = true;
|
||||
if (p[pi + 1] === CH_CARET) {
|
||||
sig = false;
|
||||
pi++;
|
||||
}
|
||||
pi++;
|
||||
while (pi < ec) {
|
||||
const pch = p[pi];
|
||||
if (pch === CH_ESC) {
|
||||
pi++;
|
||||
if (matchClass(c, p[pi])) return sig;
|
||||
} else if (
|
||||
pi + 2 < ec && p[pi + 1] === CH_DASH
|
||||
) {
|
||||
pi += 2;
|
||||
if (pch <= c && c <= p[pi]) return sig;
|
||||
} else if (pch === c) {
|
||||
return sig;
|
||||
}
|
||||
pi++;
|
||||
}
|
||||
return !sig;
|
||||
}
|
||||
|
||||
function singleMatch(
|
||||
ms: MatchState,
|
||||
si: number,
|
||||
pi: number,
|
||||
ep: number,
|
||||
): boolean {
|
||||
if (si >= ms.slen) return false;
|
||||
const c = ms.s[si];
|
||||
const pch = ms.p[pi];
|
||||
if (pch === CH_DOT) return true;
|
||||
if (pch === CH_ESC) {
|
||||
return matchClass(c, ms.p[pi + 1]);
|
||||
}
|
||||
if (pch === CH_LBRACKET) {
|
||||
return matchBracketClass(c, ms.p, pi, ep - 1);
|
||||
}
|
||||
return pch === c;
|
||||
}
|
||||
|
||||
function matchBalance(
|
||||
ms: MatchState,
|
||||
si: number,
|
||||
pi: number,
|
||||
): number {
|
||||
if (pi >= ms.plen - 1) {
|
||||
throw new Error("malformed pattern (missing arguments to '%b')");
|
||||
}
|
||||
if (si >= ms.slen || ms.s[si] !== ms.p[pi]) {
|
||||
return -1;
|
||||
}
|
||||
const b = ms.p[pi];
|
||||
const e = ms.p[pi + 1];
|
||||
let cont = 1;
|
||||
si++;
|
||||
while (si < ms.slen) {
|
||||
const sc = ms.s[si];
|
||||
if (sc === e) {
|
||||
if (--cont === 0) return si + 1;
|
||||
} else if (sc === b) {
|
||||
cont++;
|
||||
}
|
||||
si++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function maxExpand(
|
||||
ms: MatchState,
|
||||
si: number,
|
||||
pi: number,
|
||||
ep: number,
|
||||
): number {
|
||||
let i = 0;
|
||||
while (singleMatch(ms, si + i, pi, ep)) i++;
|
||||
while (i >= 0) {
|
||||
const res = match(ms, si + i, ep + 1);
|
||||
if (res >= 0) return res;
|
||||
i--;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function minExpand(
|
||||
ms: MatchState,
|
||||
si: number,
|
||||
pi: number,
|
||||
ep: number,
|
||||
): number {
|
||||
for (;;) {
|
||||
const res = match(ms, si, ep + 1);
|
||||
if (res >= 0) return res;
|
||||
if (singleMatch(ms, si, pi, ep)) {
|
||||
si++;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkCapture(ms: MatchState, l: number): number {
|
||||
l -= CH_0 + 1;
|
||||
if (l < 0 || l >= ms.level || ms.capture[l].len === CAP_UNFINISHED) {
|
||||
throw new Error(`invalid capture index %${l + 1}`);
|
||||
}
|
||||
return l;
|
||||
}
|
||||
|
||||
function captureToClose(ms: MatchState): number {
|
||||
for (let level = ms.level - 1; level >= 0; level--) {
|
||||
if (ms.capture[level].len === CAP_UNFINISHED) return level;
|
||||
}
|
||||
throw new Error("invalid pattern capture");
|
||||
}
|
||||
|
||||
function startCapture(
|
||||
ms: MatchState,
|
||||
si: number,
|
||||
pi: number,
|
||||
what: number,
|
||||
): number {
|
||||
const level = ms.level;
|
||||
if (level >= MAX_CAPTURES) throw new Error("too many captures");
|
||||
ms.capture[level].init = si;
|
||||
ms.capture[level].len = what;
|
||||
ms.level = level + 1;
|
||||
const res = match(ms, si, pi);
|
||||
if (res < 0) ms.level--;
|
||||
return res;
|
||||
}
|
||||
|
||||
function endCapture(ms: MatchState, si: number, pi: number): number {
|
||||
const l = captureToClose(ms);
|
||||
const savedLen = ms.capture[l].len;
|
||||
ms.capture[l].len = si - ms.capture[l].init;
|
||||
const res = match(ms, si, pi);
|
||||
if (res < 0) ms.capture[l].len = savedLen;
|
||||
return res;
|
||||
}
|
||||
|
||||
function matchCapture(ms: MatchState, si: number, l: number): number {
|
||||
const idx = checkCapture(ms, l);
|
||||
const cap = ms.capture[idx];
|
||||
if (cap.len === CAP_POSITION) {
|
||||
throw new Error(`invalid capture index %${idx + 1}`);
|
||||
}
|
||||
const len = cap.len;
|
||||
if (ms.slen - si < len) return -1;
|
||||
for (let k = 0; k < len; k++) {
|
||||
if (ms.s[cap.init + k] !== ms.s[si + k]) return -1;
|
||||
}
|
||||
return si + len;
|
||||
}
|
||||
|
||||
function match(ms: MatchState, si: number, pi: number): number {
|
||||
if (ms.matchdepth-- <= 0) {
|
||||
throw new Error("pattern too complex");
|
||||
}
|
||||
while (true) {
|
||||
if (pi >= ms.plen) {
|
||||
ms.matchdepth++;
|
||||
return si;
|
||||
}
|
||||
const pch = ms.p[pi];
|
||||
if (pch === CH_OPEN) {
|
||||
if (pi + 1 < ms.plen && ms.p[pi + 1] === CH_CLOSE) {
|
||||
si = startCapture(ms, si, pi + 2, CAP_POSITION);
|
||||
} else {
|
||||
si = startCapture(ms, si, pi + 1, CAP_UNFINISHED);
|
||||
}
|
||||
ms.matchdepth++;
|
||||
return si;
|
||||
}
|
||||
if (pch === CH_CLOSE) {
|
||||
si = endCapture(ms, si, pi + 1);
|
||||
ms.matchdepth++;
|
||||
return si;
|
||||
}
|
||||
if (pch === CH_DOLLAR && pi + 1 === ms.plen) {
|
||||
ms.matchdepth++;
|
||||
return si === ms.slen ? si : -1;
|
||||
}
|
||||
if (pch === CH_ESC && pi + 1 < ms.plen) {
|
||||
const next = ms.p[pi + 1];
|
||||
if (next === 98) { // 'b'
|
||||
si = matchBalance(ms, si, pi + 2);
|
||||
if (si >= 0) {
|
||||
pi += 4;
|
||||
continue;
|
||||
}
|
||||
ms.matchdepth++;
|
||||
return -1;
|
||||
}
|
||||
if (next === 102) { // 'f'
|
||||
pi += 2;
|
||||
if (pi >= ms.plen || ms.p[pi] !== CH_LBRACKET) {
|
||||
throw new Error("missing '[' after '%f' in pattern");
|
||||
}
|
||||
const ep = classEnd(ms.p, ms.plen, pi);
|
||||
const previous = si === 0 ? 0 : ms.s[si - 1];
|
||||
const current = si < ms.slen ? ms.s[si] : 0;
|
||||
if (
|
||||
!matchBracketClass(previous, ms.p, pi, ep - 1) &&
|
||||
matchBracketClass(current, ms.p, pi, ep - 1)
|
||||
) {
|
||||
pi = ep;
|
||||
continue;
|
||||
}
|
||||
ms.matchdepth++;
|
||||
return -1;
|
||||
}
|
||||
if (next >= CH_0 && next <= CH_9) {
|
||||
si = matchCapture(ms, si, next);
|
||||
if (si >= 0) {
|
||||
pi += 2;
|
||||
continue;
|
||||
}
|
||||
ms.matchdepth++;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
// default: class[*+?-]?
|
||||
const ep = classEnd(ms.p, ms.plen, pi);
|
||||
const matched = singleMatch(ms, si, pi, ep);
|
||||
if (!matched) {
|
||||
if (ep < ms.plen) {
|
||||
const suffix = ms.p[ep];
|
||||
if (
|
||||
suffix === CH_STAR || suffix === CH_QUESTION || suffix === CH_DASH
|
||||
) {
|
||||
pi = ep + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ms.matchdepth++;
|
||||
return -1;
|
||||
}
|
||||
if (ep < ms.plen) {
|
||||
const suffix = ms.p[ep];
|
||||
if (suffix === CH_QUESTION) {
|
||||
const res = match(ms, si + 1, ep + 1);
|
||||
if (res >= 0) {
|
||||
ms.matchdepth++;
|
||||
return res;
|
||||
}
|
||||
pi = ep + 1;
|
||||
continue;
|
||||
}
|
||||
if (suffix === CH_PLUS) {
|
||||
si++;
|
||||
const res = maxExpand(ms, si, pi, ep);
|
||||
ms.matchdepth++;
|
||||
return res;
|
||||
}
|
||||
if (suffix === CH_STAR) {
|
||||
const res = maxExpand(ms, si, pi, ep);
|
||||
ms.matchdepth++;
|
||||
return res;
|
||||
}
|
||||
if (suffix === CH_DASH) {
|
||||
const res = minExpand(ms, si, pi, ep);
|
||||
ms.matchdepth++;
|
||||
return res;
|
||||
}
|
||||
}
|
||||
si++;
|
||||
pi = ep;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
function createMatchState(s: string, p: string): MatchState {
|
||||
const sb = toBytes(s);
|
||||
const pb = toBytes(p);
|
||||
const capture: Capture[] = new Array(MAX_CAPTURES);
|
||||
for (let i = 0; i < MAX_CAPTURES; i++) {
|
||||
capture[i] = { init: 0, len: 0 };
|
||||
}
|
||||
return {
|
||||
src: s,
|
||||
s: sb,
|
||||
slen: sb.length,
|
||||
p: pb,
|
||||
plen: pb.length,
|
||||
level: 0,
|
||||
capture,
|
||||
matchdepth: MAX_MATCH_DEPTH,
|
||||
};
|
||||
}
|
||||
|
||||
function resetMatchState(ms: MatchState): void {
|
||||
ms.level = 0;
|
||||
ms.matchdepth = MAX_MATCH_DEPTH;
|
||||
}
|
||||
|
||||
function noSpecials(p: string): boolean {
|
||||
for (let i = 0; i < p.length; i++) {
|
||||
if (SPECIALS_SET.has(p.charCodeAt(i))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
export type CaptureResult = { s: string } | { position: number };
|
||||
|
||||
interface RawCapture {
|
||||
kind: 0 | 1; // 0 = string slice, 1 = position
|
||||
start: number;
|
||||
len: number; // kind = 0: substring length; kind = 1: 1-based position
|
||||
}
|
||||
|
||||
function getOneRawCapture(
|
||||
ms: MatchState,
|
||||
i: number,
|
||||
matchStart: number,
|
||||
matchEnd: number,
|
||||
): RawCapture {
|
||||
if (i >= ms.level) {
|
||||
if (i !== 0) {
|
||||
throw new Error(`invalid capture index %${i + 1}`);
|
||||
}
|
||||
return { kind: 0, start: matchStart, len: matchEnd - matchStart };
|
||||
}
|
||||
const cap = ms.capture[i];
|
||||
if (cap.len === CAP_UNFINISHED) {
|
||||
throw new Error("unfinished capture");
|
||||
}
|
||||
if (cap.len === CAP_POSITION) {
|
||||
return { kind: 1, start: cap.init + 1, len: 0 };
|
||||
}
|
||||
return { kind: 0, start: cap.init, len: cap.len };
|
||||
}
|
||||
|
||||
function rawToResult(ms: MatchState, raw: RawCapture): CaptureResult {
|
||||
if (raw.kind === 1) {
|
||||
return { position: raw.start };
|
||||
}
|
||||
return { s: ms.src.substring(raw.start, raw.start + raw.len) };
|
||||
}
|
||||
|
||||
function getCaptures(
|
||||
ms: MatchState,
|
||||
matchStart: number,
|
||||
matchEnd: number,
|
||||
): CaptureResult[] {
|
||||
const nlevels = ms.level === 0 ? 1 : ms.level;
|
||||
const result: CaptureResult[] = [];
|
||||
for (let i = 0; i < nlevels; i++) {
|
||||
result.push(rawToResult(ms, getOneRawCapture(ms, i, matchStart, matchEnd)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getRawCaptureString(ms: MatchState, raw: RawCapture): string {
|
||||
if (raw.kind === 1) {
|
||||
return raw.start.toString();
|
||||
}
|
||||
return ms.src.substring(raw.start, raw.start + raw.len);
|
||||
}
|
||||
|
||||
export function patternFind(
|
||||
s: string,
|
||||
pattern: string,
|
||||
init: number = 1,
|
||||
plain: boolean = false,
|
||||
): { start: number; end: number; captures: CaptureResult[] } | null {
|
||||
if (init < 1) init = 1;
|
||||
if (init > s.length + 1) return null;
|
||||
const si0 = init - 1;
|
||||
if (plain || noSpecials(pattern)) {
|
||||
const idx = s.indexOf(pattern, si0);
|
||||
if (idx < 0) return null;
|
||||
return { start: idx + 1, end: idx + pattern.length, captures: [] };
|
||||
}
|
||||
let p = pattern;
|
||||
let anchor = false;
|
||||
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
|
||||
anchor = true;
|
||||
p = p.substring(1);
|
||||
}
|
||||
const ms = createMatchState(s, p);
|
||||
for (let si = si0; si <= ms.slen; si++) {
|
||||
resetMatchState(ms);
|
||||
const res = match(ms, si, 0);
|
||||
if (res >= 0) {
|
||||
const caps = ms.level === 0 ? [] : getCaptures(ms, si, res);
|
||||
return { start: si + 1, end: res, captures: caps };
|
||||
}
|
||||
if (anchor) break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function patternMatch(
|
||||
s: string,
|
||||
pattern: string,
|
||||
init: number = 1,
|
||||
): CaptureResult[] | null {
|
||||
if (init < 1) init = 1;
|
||||
if (init > s.length + 1) return null;
|
||||
const si0 = init - 1;
|
||||
let p = pattern;
|
||||
let anchor = false;
|
||||
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
|
||||
anchor = true;
|
||||
p = p.substring(1);
|
||||
}
|
||||
const ms = createMatchState(s, p);
|
||||
for (let si = si0; si <= ms.slen; si++) {
|
||||
resetMatchState(ms);
|
||||
const res = match(ms, si, 0);
|
||||
if (res >= 0) {
|
||||
return getCaptures(ms, si, res);
|
||||
}
|
||||
if (anchor) break;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function patternGmatch(
|
||||
s: string,
|
||||
pattern: string,
|
||||
init: number = 1,
|
||||
): () => CaptureResult[] | null {
|
||||
if (init < 1) init = 1;
|
||||
let p = pattern;
|
||||
let anchor = false;
|
||||
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
|
||||
anchor = true;
|
||||
p = p.substring(1);
|
||||
}
|
||||
const ms = createMatchState(s, p);
|
||||
let src = init - 1;
|
||||
let lastMatch: number | null = null;
|
||||
return () => {
|
||||
while (src <= ms.slen) {
|
||||
resetMatchState(ms);
|
||||
const e = match(ms, src, 0);
|
||||
if (e >= 0 && e !== lastMatch) {
|
||||
const captures = getCaptures(ms, src, e);
|
||||
src = e;
|
||||
lastMatch = e;
|
||||
return captures;
|
||||
}
|
||||
src++;
|
||||
if (anchor) break;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
function expandReplacementString(
|
||||
repl: string,
|
||||
ms: MatchState,
|
||||
matchStart: number,
|
||||
matchEnd: number,
|
||||
): string {
|
||||
const parts: string[] = [];
|
||||
let i = 0;
|
||||
while (i < repl.length) {
|
||||
const ch = repl.charCodeAt(i);
|
||||
if (ch === CH_ESC) {
|
||||
i++;
|
||||
if (i >= repl.length) {
|
||||
throw new Error("invalid use of '%' in replacement string");
|
||||
}
|
||||
const rc = repl.charCodeAt(i);
|
||||
if (rc === CH_ESC) {
|
||||
parts.push("%");
|
||||
} else if (rc === CH_0) {
|
||||
parts.push(ms.src.substring(matchStart, matchEnd));
|
||||
} else if (rc >= 49 && rc <= CH_9) {
|
||||
parts.push(getRawCaptureString(
|
||||
ms,
|
||||
getOneRawCapture(ms, rc - 49, matchStart, matchEnd),
|
||||
));
|
||||
} else {
|
||||
throw new Error("invalid use of '%' in replacement string");
|
||||
}
|
||||
} else {
|
||||
// Collect consecutive literal characters
|
||||
let j = i + 1;
|
||||
while (j < repl.length && repl.charCodeAt(j) !== CH_ESC) j++;
|
||||
parts.push(repl.substring(i, j));
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
export interface GsubCallbacks {
|
||||
replString?: string;
|
||||
replFunction?: (
|
||||
...captures: CaptureResult[]
|
||||
) => Promise<string | null | undefined> | string | null | undefined;
|
||||
replTable?: (key: string) => string | null | undefined;
|
||||
}
|
||||
|
||||
export async function patternGsub(
|
||||
s: string,
|
||||
pattern: string,
|
||||
callbacks: GsubCallbacks,
|
||||
maxN?: number,
|
||||
): Promise<[string, number]> {
|
||||
const max_s = maxN !== undefined ? maxN : s.length + 1;
|
||||
let p = pattern;
|
||||
let anchor = false;
|
||||
if (p.length > 0 && p.charCodeAt(0) === CH_CARET) {
|
||||
anchor = true;
|
||||
p = p.substring(1);
|
||||
}
|
||||
const ms = createMatchState(s, p);
|
||||
let src = 0;
|
||||
let lastMatch: number | null = null;
|
||||
let n = 0;
|
||||
const resultParts: string[] = [];
|
||||
|
||||
while (n < max_s) {
|
||||
resetMatchState(ms);
|
||||
const e = match(ms, src, 0);
|
||||
if (e >= 0 && e !== lastMatch) {
|
||||
n++;
|
||||
let replStr: string | null | undefined;
|
||||
if (callbacks.replString !== undefined) {
|
||||
replStr = expandReplacementString(callbacks.replString, ms, src, e);
|
||||
} else if (callbacks.replFunction) {
|
||||
const caps = getCaptures(ms, src, e);
|
||||
replStr = await callbacks.replFunction(...caps);
|
||||
if (replStr === null || replStr === undefined) {
|
||||
replStr = ms.src.substring(src, e);
|
||||
}
|
||||
} else if (callbacks.replTable) {
|
||||
const raw = getOneRawCapture(ms, 0, src, e);
|
||||
const key = getRawCaptureString(ms, raw);
|
||||
replStr = callbacks.replTable(key);
|
||||
if (replStr === null || replStr === undefined) {
|
||||
replStr = ms.src.substring(src, e);
|
||||
}
|
||||
}
|
||||
resultParts.push(replStr!);
|
||||
src = e;
|
||||
lastMatch = e;
|
||||
} else if (src < ms.slen) {
|
||||
resultParts.push(s[src]);
|
||||
src++;
|
||||
lastMatch = null;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if (anchor) break;
|
||||
}
|
||||
if (src < s.length) {
|
||||
resultParts.push(s.substring(src));
|
||||
}
|
||||
return [resultParts.join(""), n];
|
||||
}
|
||||
@@ -0,0 +1,557 @@
|
||||
local function assertEqual(a, b)
|
||||
if a ~= b then
|
||||
error("Assertion failed: " .. tostring(a) .. " ~= " .. tostring(b))
|
||||
end
|
||||
end
|
||||
|
||||
local function assertError(f, expectedMsg)
|
||||
local ok, err = pcall(f)
|
||||
assert(not ok, "Expected error but call succeeded")
|
||||
local msg = tostring(err)
|
||||
-- Strip stack trace prefix if present (e.g. "LuaRuntimeError: ...")
|
||||
local clean = msg:match(":%s*(.+)$") or msg
|
||||
assert(clean:find(expectedMsg, 1, true),
|
||||
"Expected error containing '" .. expectedMsg .. "' but got: " .. msg)
|
||||
end
|
||||
|
||||
-- 1. Character classes
|
||||
|
||||
-- %a / %A
|
||||
assertEqual(string.match("hello123", "%a+"), "hello")
|
||||
assertEqual(string.match("123hello", "%A+"), "123")
|
||||
|
||||
-- %d / %D
|
||||
assertEqual(string.match("abc42xyz", "%d+"), "42")
|
||||
assertEqual(string.match("42abc", "%D+"), "abc")
|
||||
|
||||
-- %l / %L (lowercase)
|
||||
assertEqual(string.match("Hello", "%l+"), "ello")
|
||||
assertEqual(string.match("hello", "%L+"), nil)
|
||||
|
||||
-- %u / %U (uppercase)
|
||||
assertEqual(string.match("helloWORLD", "%u+"), "WORLD")
|
||||
assertEqual(string.match("HELLO", "%U+"), nil)
|
||||
|
||||
-- %w / %W (alphanumeric)
|
||||
assertEqual(string.match("hello world", "%w+"), "hello")
|
||||
assertEqual(string.match("hello world", "%W+"), " ")
|
||||
|
||||
-- %s / %S (whitespace)
|
||||
assertEqual(string.match("hello world", "%s+"), " ")
|
||||
assertEqual(string.match(" hello", "%S+"), "hello")
|
||||
|
||||
-- %p (punctuation)
|
||||
assertEqual(string.match("hello, world!", "%p+"), ",")
|
||||
|
||||
-- %g (printable, non-space)
|
||||
assertEqual(string.match(" abc ", "%g+"), "abc")
|
||||
|
||||
-- %x (hex digits)
|
||||
assertEqual(string.match("ghABCDij", "%x+"), "ABCD")
|
||||
|
||||
-- %. (dot matches any)
|
||||
assertEqual(string.match("abc", "."), "a")
|
||||
assertEqual(string.match("abc", ".."), "ab")
|
||||
|
||||
-- 2. Anchors
|
||||
|
||||
assertEqual(string.match("hello", "^hello$"), "hello")
|
||||
assertEqual(string.match("hello world", "^hello$"), nil)
|
||||
assertEqual(string.match("hello", "^h"), "h")
|
||||
assertEqual(string.match("hello", "o$"), "o")
|
||||
assertEqual(string.match("hello", "x$"), nil)
|
||||
|
||||
-- 3. Repetition: *, +, -, ?
|
||||
|
||||
-- * (greedy, 0 or more)
|
||||
assertEqual(string.match("aaa", "a*"), "aaa")
|
||||
assertEqual(string.match("bbb", "a*"), "")
|
||||
|
||||
-- + (greedy, 1 or more)
|
||||
assertEqual(string.match("aaa", "a+"), "aaa")
|
||||
assertEqual(string.match("bbb", "a+"), nil)
|
||||
|
||||
-- - (lazy, 0 or more)
|
||||
assertEqual(string.match("aaa", "a-b"), nil)
|
||||
assertEqual(string.match("aab", "a-b"), "aab")
|
||||
-- lazy match takes shortest prefix
|
||||
local cap = string.match("<tag>content</tag>", "<(.-)>")
|
||||
assertEqual(cap, "tag")
|
||||
|
||||
-- ? (optional)
|
||||
assertEqual(string.match("colour", "colou?r"), "colour")
|
||||
assertEqual(string.match("color", "colou?r"), "color")
|
||||
|
||||
-- 4. Escaping magic characters with %
|
||||
|
||||
assertEqual(string.match("100%", "(%d+)%%"), "100")
|
||||
assertEqual(string.match("hello.world", "(%a+)%.(%a+)"), "hello")
|
||||
assertEqual(string.match("a+b", "a%+b"), "a+b")
|
||||
assertEqual(string.match("a*b", "a%*b"), "a*b")
|
||||
assertEqual(string.match("(x)", "%(x%)"), "(x)")
|
||||
|
||||
-- Non-magic chars after % are literal
|
||||
assertEqual(string.match("*", "%*"), "*")
|
||||
assertEqual(string.match("?", "%?"), "?")
|
||||
|
||||
-- 5. Character sets [...]
|
||||
|
||||
assertEqual(string.match("cat", "[abc]"), "c")
|
||||
assertEqual(string.match("dog", "[abc]"), nil)
|
||||
|
||||
-- Ranges
|
||||
assertEqual(string.match("m", "[a-z]"), "m")
|
||||
assertEqual(string.match("M", "[a-z]"), nil)
|
||||
assertEqual(string.match("5", "[0-9]"), "5")
|
||||
|
||||
-- Negated set
|
||||
assertEqual(string.match("x", "[^abc]"), "x")
|
||||
assertEqual(string.match("a", "[^abc]"), nil)
|
||||
|
||||
-- Classes inside sets
|
||||
assertEqual(string.match("3", "[%d]"), "3")
|
||||
assertEqual(string.match("a", "[%d]"), nil)
|
||||
|
||||
-- 6. Captures
|
||||
|
||||
-- Single capture
|
||||
local y, m, d = string.match("2024-03-14", "(%d+)-(%d+)-(%d+)")
|
||||
assertEqual(y, "2024")
|
||||
assertEqual(m, "03")
|
||||
assertEqual(d, "14")
|
||||
|
||||
-- No captures returns whole match
|
||||
assertEqual(string.match("hello", "%a+"), "hello")
|
||||
|
||||
-- Nested captures
|
||||
local outer, inner = string.match("hello world", "(h(ello))")
|
||||
assertEqual(outer, "hello")
|
||||
assertEqual(inner, "ello")
|
||||
|
||||
-- Captures with literal parens
|
||||
local content = string.match("((test))", "%((%(%a+%))%)")
|
||||
assertEqual(content, "(test)")
|
||||
|
||||
-- Multiple captures
|
||||
local a, b = string.match("hello world", "(%a+) (%a+)")
|
||||
assertEqual(a, "hello")
|
||||
assertEqual(b, "world")
|
||||
|
||||
-- 7. Position captures ()
|
||||
|
||||
local p1, p2 = string.match("hello", "()()")
|
||||
assertEqual(p1, 1)
|
||||
assertEqual(p2, 1)
|
||||
|
||||
local pos = string.match("hello world", "()world")
|
||||
assertEqual(pos, 7)
|
||||
|
||||
-- 8. Back-references %1-%9
|
||||
|
||||
-- Match repeated word
|
||||
local word = string.match("hello hello", "(%a+) %1")
|
||||
assertEqual(word, "hello")
|
||||
|
||||
-- No match if different
|
||||
assertEqual(string.match("hello world", "(%a+) %1"), nil)
|
||||
|
||||
-- Match repeated char
|
||||
local ch = string.match("aabcc", "(.)%1")
|
||||
assertEqual(ch, "a")
|
||||
|
||||
-- 9. Balanced match %bxy
|
||||
|
||||
assertEqual(string.match("(hello (world))", "%b()"), "(hello (world))")
|
||||
assertEqual(string.match("{a{b}c}", "%b{}"), "{a{b}c}")
|
||||
assertEqual(string.match("(unbalanced", "%b()"), nil)
|
||||
|
||||
-- 10. Frontier pattern %f[set]
|
||||
|
||||
-- Transition from non-alpha to alpha
|
||||
local fw = string.match("hello world", "%f[%a]%a+", 2)
|
||||
assertEqual(fw, "world")
|
||||
|
||||
-- Transition at string start
|
||||
local fs = string.match("hello", "%f[%a]%a+")
|
||||
assertEqual(fs, "hello")
|
||||
|
||||
-- Word boundaries
|
||||
local words = {}
|
||||
for w in string.gmatch("one two three", "%f[%a]%a+") do
|
||||
table.insert(words, w)
|
||||
end
|
||||
assertEqual(#words, 3)
|
||||
assertEqual(words[1], "one")
|
||||
assertEqual(words[2], "two")
|
||||
assertEqual(words[3], "three")
|
||||
|
||||
-- 11. string.find
|
||||
|
||||
-- Basic find
|
||||
local s, e = string.find("hello world", "world")
|
||||
assertEqual(s, 7)
|
||||
assertEqual(e, 11)
|
||||
|
||||
-- Plain find
|
||||
s, e = string.find("hello.world", ".", 1, true)
|
||||
assertEqual(s, 6)
|
||||
assertEqual(e, 6)
|
||||
|
||||
-- Find with captures
|
||||
local s2, e2, c1 = string.find("hello world", "(%a+)")
|
||||
assertEqual(s2, 1)
|
||||
assertEqual(e2, 5)
|
||||
assertEqual(c1, "hello")
|
||||
|
||||
-- Not found
|
||||
assertEqual(string.find("hello", "xyz"), nil)
|
||||
|
||||
-- Find with init
|
||||
s, e = string.find("abcabc", "abc", 2)
|
||||
assertEqual(s, 4)
|
||||
assertEqual(e, 6)
|
||||
|
||||
-- Find returns nil (not false)
|
||||
assert(string.find("a", "b") == nil)
|
||||
assert(not (string.find("a", "b") ~= nil))
|
||||
|
||||
-- Find with special chars
|
||||
local bf = string.find("[", "[_%w]")
|
||||
assert(bf == nil)
|
||||
|
||||
-- 12. string.match
|
||||
|
||||
-- Match from init position
|
||||
assertEqual(string.match("abcdef", "%a+", 4), "def")
|
||||
|
||||
-- Match empty pattern
|
||||
assertEqual(string.match("abc", ""), "")
|
||||
|
||||
-- Match returns nil (not false)
|
||||
assert(string.match("a", "b") == nil)
|
||||
assert(not (string.match("a", "b") ~= nil))
|
||||
|
||||
-- Match with init
|
||||
local initMatch = string.match("hello world", "(world)", 7)
|
||||
assertEqual(initMatch, "world")
|
||||
|
||||
-- 13. string.gmatch
|
||||
|
||||
-- Basic iteration
|
||||
local t = {}
|
||||
for w in string.gmatch("hello world lua", "%a+") do
|
||||
table.insert(t, w)
|
||||
end
|
||||
assertEqual(#t, 3)
|
||||
assertEqual(t[1], "hello")
|
||||
assertEqual(t[2], "world")
|
||||
assertEqual(t[3], "lua")
|
||||
|
||||
-- With captures
|
||||
local kv = {}
|
||||
for k, v in string.gmatch("from=world, to=Lua", "(%w+)=(%w+)") do
|
||||
kv[k] = v
|
||||
end
|
||||
assertEqual(kv.from, "world")
|
||||
assertEqual(kv.to, "Lua")
|
||||
|
||||
-- gmatch with empty matches (like Lua reference: ";a;" with "a*")
|
||||
local r = {}
|
||||
for mm in string.gmatch(";a;", "a*") do
|
||||
table.insert(r, mm)
|
||||
end
|
||||
assertEqual(r[1], "")
|
||||
assertEqual(r[2], "a")
|
||||
assertEqual(r[3], "")
|
||||
|
||||
-- gmatch without captures returns whole match
|
||||
local t2 = {}
|
||||
for mm in string.gmatch("from=world, to=Lua", "%w+=%w+") do
|
||||
table.insert(t2, mm)
|
||||
end
|
||||
assertEqual(t2[1], "from=world")
|
||||
assertEqual(t2[2], "to=Lua")
|
||||
|
||||
-- 14. string.gsub
|
||||
|
||||
-- Simple string replacement
|
||||
local res, count = string.gsub("hello world", "(%w+)", "%1-%1")
|
||||
assertEqual(res, "hello-hello world-world")
|
||||
assertEqual(count, 2)
|
||||
|
||||
-- Limited replacements
|
||||
res, count = string.gsub("aaa", "a", "b", 2)
|
||||
assertEqual(res, "bba")
|
||||
assertEqual(count, 2)
|
||||
|
||||
-- Replacement with %0
|
||||
res = string.gsub("hello", "%w+", "[%0]")
|
||||
assertEqual(res, "[hello]")
|
||||
|
||||
-- Function replacement
|
||||
res, count = string.gsub("hello world", "%w+", function(w)
|
||||
return w:upper()
|
||||
end)
|
||||
assertEqual(res, "HELLO WORLD")
|
||||
assertEqual(count, 2)
|
||||
|
||||
-- Function returning nil keeps original
|
||||
res = string.gsub("hello world", "%w+", function(w)
|
||||
if w == "hello" then return "HI" end
|
||||
end)
|
||||
assertEqual(res, "HI world")
|
||||
|
||||
-- Table replacement
|
||||
local tbl = {hello = "HI", world = "EARTH"}
|
||||
res = string.gsub("hello world", "(%w+)", tbl)
|
||||
assertEqual(res, "HI EARTH")
|
||||
|
||||
-- Table with missing key keeps original
|
||||
tbl = {hello = "HI"}
|
||||
res = string.gsub("hello world", "(%w+)", tbl)
|
||||
assertEqual(res, "HI world")
|
||||
|
||||
-- gsub with magic chars in pattern
|
||||
res = string.gsub("hello.world", "%.", "-")
|
||||
assertEqual(res, "hello-world")
|
||||
|
||||
-- gsub %% in replacement
|
||||
res = string.gsub("hello", "hello", "100%%")
|
||||
assertEqual(res, "100%")
|
||||
|
||||
-- Empty pattern match (inserts between every char)
|
||||
res = string.gsub("abc", "", "-")
|
||||
assertEqual(res, "-a-b-c-")
|
||||
|
||||
-- Anchored gsub
|
||||
res, count = string.gsub("abc", "^a", "x")
|
||||
assertEqual(res, "xbc")
|
||||
assertEqual(count, 1)
|
||||
|
||||
-- gsub with XML pattern
|
||||
local xmlpat = '<%?xml version="1.0" encoding="UTF%-8"%?>'
|
||||
local xmlstr = '<?xml version="1.0" encoding="UTF-8"?><my-xml></my-xml>'
|
||||
res = string.gsub(xmlstr, xmlpat, "moo")
|
||||
assertEqual(res, "moo<my-xml></my-xml>")
|
||||
|
||||
-- gsub with %%1
|
||||
res = string.gsub("Hello %1", "%%1", "world")
|
||||
assertEqual(res, "Hello world")
|
||||
|
||||
-- gsub counting digits
|
||||
res, count = string.gsub("ab5kfd8scf4lll", "%d", "")
|
||||
assertEqual(res, "abkfdscflll")
|
||||
assertEqual(count, 3)
|
||||
|
||||
-- 15. The dash (lazy) vs literal dash
|
||||
|
||||
assertEqual(string.match("2024-03-14", "%d+-(%d+)-%d+"), "03")
|
||||
assertEqual(string.match("2024-03-14", "(%d+)-(%d+)-(%d+)"), "2024")
|
||||
|
||||
local y2, m2, d2 = string.match("2024-03-14", "(%d+)-(%d+)-(%d+)")
|
||||
assertEqual(y2, "2024")
|
||||
assertEqual(m2, "03")
|
||||
assertEqual(d2, "14")
|
||||
|
||||
-- 16. Edge cases
|
||||
|
||||
-- Empty string
|
||||
assertEqual(string.match("", ".*"), "")
|
||||
local fs2, fe2 = string.find("", "")
|
||||
assertEqual(fs2, 1)
|
||||
assertEqual(fe2, 0)
|
||||
|
||||
-- Pattern matching entire string
|
||||
assertEqual(string.match("abc", "^(.-)$"), "abc")
|
||||
|
||||
-- Pattern at end of string
|
||||
assertEqual(string.match("test!", "!$"), "!")
|
||||
|
||||
-- Multiple position captures
|
||||
local p3, p4 = string.match("abcd", "()ab()cd")
|
||||
assertEqual(p3, 1)
|
||||
assertEqual(p4, 3)
|
||||
|
||||
-- string.rep with separator
|
||||
assertEqual(string.rep("ab", 3, ","), "ab,ab,ab")
|
||||
assertEqual(string.rep("x", 1, ","), "x")
|
||||
assertEqual(string.rep("x", 0), "")
|
||||
|
||||
-- 17. More complex patterns
|
||||
|
||||
-- CSV-like parsing
|
||||
local fields = {}
|
||||
for f in string.gmatch("one,two,,four", "([^,]*)") do
|
||||
table.insert(fields, f)
|
||||
end
|
||||
assertEqual(fields[1], "one")
|
||||
assertEqual(fields[2], "two")
|
||||
assertEqual(fields[3], "")
|
||||
assertEqual(fields[4], "four")
|
||||
|
||||
-- Trim whitespace
|
||||
local function trim(s2)
|
||||
return string.match(s2, "^%s*(.-)%s*$")
|
||||
end
|
||||
assertEqual(trim(" hello "), "hello")
|
||||
assertEqual(trim("hello"), "hello")
|
||||
assertEqual(trim(" "), "")
|
||||
|
||||
-- Match identifier
|
||||
assertEqual(string.match("my_var123", "^[%a_][%w_]*$"), "my_var123")
|
||||
assertEqual(string.match("123bad", "^[%a_][%w_]*$"), nil)
|
||||
|
||||
-- Hex color
|
||||
local hex = string.match("#FF00AA", "^#(%x%x)(%x%x)(%x%x)$")
|
||||
assertEqual(hex, "FF")
|
||||
|
||||
local r2, g, b2 = string.match("#FF00AA", "^#(%x%x)(%x%x)(%x%x)$")
|
||||
assertEqual(r2, "FF")
|
||||
assertEqual(g, "00")
|
||||
assertEqual(b2, "AA")
|
||||
|
||||
-- Email-like pattern
|
||||
local user, domain = string.match("user@example.com", "([%w_.]+)@([%w_.]+)")
|
||||
assertEqual(user, "user")
|
||||
assertEqual(domain, "example.com")
|
||||
|
||||
-- 18. Error cases — malformed patterns
|
||||
|
||||
-- Pattern ending with lone %
|
||||
assertError(function()
|
||||
string.find("abc", "abc%")
|
||||
end, "malformed pattern (ends with '%')")
|
||||
|
||||
assertError(function()
|
||||
string.match("abc", "%")
|
||||
end, "malformed pattern (ends with '%')")
|
||||
|
||||
assertError(function()
|
||||
local iter = string.gmatch("abc", "a%")
|
||||
iter()
|
||||
end, "malformed pattern (ends with '%')")
|
||||
|
||||
-- %b with missing arguments
|
||||
assertError(function()
|
||||
string.match("abc", "%b")
|
||||
end, "malformed pattern (missing arguments to '%b')")
|
||||
|
||||
assertError(function()
|
||||
string.match("abc", "%b(")
|
||||
end, "malformed pattern (missing arguments to '%b')")
|
||||
|
||||
-- %f not followed by [
|
||||
assertError(function()
|
||||
string.match("abc", "%f")
|
||||
end, "missing '[' after '%f' in pattern")
|
||||
|
||||
assertError(function()
|
||||
string.match("abc", "%fa")
|
||||
end, "missing '[' after '%f' in pattern")
|
||||
|
||||
-- Lone % at end of replacement
|
||||
assertError(function()
|
||||
string.gsub("abc", "a", "x%")
|
||||
end, "invalid use of '%' in replacement string")
|
||||
|
||||
-- Invalid escape in replacement
|
||||
assertError(function()
|
||||
string.gsub("abc", "a", "%z")
|
||||
end, "invalid use of '%' in replacement string")
|
||||
|
||||
-- 19. Error cases — invalid captures
|
||||
|
||||
-- Unmatched close paren
|
||||
assertError(function()
|
||||
string.match("abc", ")")
|
||||
end, "invalid pattern capture")
|
||||
|
||||
-- Back-reference to non-existent capture
|
||||
assertError(function()
|
||||
string.match("abc", "%1")
|
||||
end, "invalid capture index")
|
||||
|
||||
-- Back-reference to capture not yet closed
|
||||
assertError(function()
|
||||
string.match("abab", "(%a+%1)")
|
||||
end, "invalid capture index")
|
||||
|
||||
-- Too high capture index in replacement
|
||||
assertError(function()
|
||||
string.gsub("abc", "(%a+)", "%2")
|
||||
end, "invalid capture index")
|
||||
|
||||
-- Unfinished capture used in match (tries to return it)
|
||||
assertError(function()
|
||||
string.match("abc", "(abc")
|
||||
end, "unfinished capture")
|
||||
|
||||
-- Unfinished capture used in find (tries to return it)
|
||||
assertError(function()
|
||||
string.find("abc", "(abc")
|
||||
end, "unfinished capture")
|
||||
|
||||
-- Nested unfinished capture
|
||||
assertError(function()
|
||||
string.match("abc", "(ab(c)")
|
||||
end, "unfinished capture")
|
||||
|
||||
-- 20. Error cases — %b and %f
|
||||
|
||||
-- %b with missing arguments
|
||||
assertError(function()
|
||||
string.match("abc", "%b")
|
||||
end, "malformed pattern (missing arguments to '%b')")
|
||||
|
||||
assertError(function()
|
||||
string.match("abc", "%b(")
|
||||
end, "malformed pattern (missing arguments to '%b')")
|
||||
|
||||
-- %f not followed by [
|
||||
assertError(function()
|
||||
string.match("abc", "%f")
|
||||
end, "missing '[' after '%f' in pattern")
|
||||
|
||||
assertError(function()
|
||||
string.match("abc", "%fa")
|
||||
end, "missing '[' after '%f' in pattern")
|
||||
|
||||
-- 21. Error cases — gsub replacement string
|
||||
|
||||
-- Lone % at end of replacement
|
||||
assertError(function()
|
||||
string.gsub("abc", "a", "x%")
|
||||
end, "invalid use of '%' in replacement string")
|
||||
|
||||
-- Invalid escape in replacement (not a digit or %)
|
||||
assertError(function()
|
||||
string.gsub("abc", "a", "%z")
|
||||
end, "invalid use of '%' in replacement string")
|
||||
|
||||
-- %2 when only one capture exists
|
||||
assertError(function()
|
||||
string.gsub("abc", "(a)", "%2")
|
||||
end, "invalid capture index")
|
||||
|
||||
-- %9 with no captures at all
|
||||
assertError(function()
|
||||
string.gsub("abc", "a", "%9")
|
||||
end, "invalid capture index")
|
||||
|
||||
-- 22. Error cases — too many captures
|
||||
|
||||
assertError(function()
|
||||
local pat = string.rep("(", 33) .. "a" .. string.rep(")", 33)
|
||||
string.match("a", pat)
|
||||
end, "too many captures")
|
||||
|
||||
-- 23. Error cases — unfinished capture
|
||||
|
||||
-- Open paren never closed
|
||||
assertError(function()
|
||||
string.find("abc", "(abc")
|
||||
end, "unfinished capture")
|
||||
|
||||
assertError(function()
|
||||
string.match("abc", "(ab(c)")
|
||||
end, "unfinished capture")
|
||||
@@ -8,77 +8,32 @@ import {
|
||||
} from "../runtime.ts";
|
||||
import { untagNumber } from "../numeric.ts";
|
||||
import { luaFormat } from "./format.ts";
|
||||
import {
|
||||
type CaptureResult,
|
||||
type GsubCallbacks,
|
||||
patternFind,
|
||||
patternGmatch,
|
||||
patternGsub,
|
||||
patternMatch,
|
||||
} from "./pattern.ts";
|
||||
|
||||
// Bits and pieces borrowed from https://github.com/paulcuth/starlight/blob/master/src/runtime/lib/string.js
|
||||
|
||||
const ROSETTA_STONE = {
|
||||
"([^a-zA-Z0-9%(])-": "$1*?",
|
||||
"([^%])-([^a-zA-Z0-9?])": "$1*?$2",
|
||||
"([^%])-$": "$1*?",
|
||||
"%a": "[a-zA-Z]",
|
||||
"%A": "[^a-zA-Z]",
|
||||
"%c": "[\x00-\x1f]",
|
||||
"%C": "[^\x00-\x1f]",
|
||||
"%d": "\\d",
|
||||
"%D": "[^\d]",
|
||||
"%l": "[a-z]",
|
||||
"%L": "[^a-z]",
|
||||
"%p": "[\.\,\"'\?\!\;\:\#\$\%\&\(\)\*\+\-\/\<\>\=\@\\[\\]\\\\^\_\{\}\|\~]",
|
||||
"%P": "[^\.\,\"'\?\!\;\:\#\$\%\&\(\)\*\+\-\/\<\>\=\@\\[\\]\\\\^\_\{\}\|\~]",
|
||||
"%s": "[ \\t\\n\\f\\v\\r]",
|
||||
"%S": "[^ \t\n\f\v\r]",
|
||||
"%u": "[A-Z]",
|
||||
"%U": "[^A-Z]",
|
||||
"%w": "[a-zA-Z0-9]",
|
||||
"%W": "[^a-zA-Z0-9]",
|
||||
"%x": "[a-fA-F0-9]",
|
||||
"%X": "[^a-fA-F0-9]",
|
||||
"%([^a-zA-Z])": "\\$1",
|
||||
};
|
||||
|
||||
function translatePattern(pattern: string): string {
|
||||
pattern = "" + pattern;
|
||||
|
||||
// Replace single backslash with double backslashes
|
||||
pattern = pattern.replace(new RegExp("\\\\", "g"), "\\\\");
|
||||
pattern = pattern.replace(new RegExp("\\|", "g"), "\\|");
|
||||
|
||||
for (const [key, value] of Object.entries(ROSETTA_STONE)) {
|
||||
pattern = pattern.replace(new RegExp(key, "g"), value);
|
||||
function capturesToLua(caps: CaptureResult[]): any {
|
||||
if (caps.length === 0) {return null;}
|
||||
if (caps.length === 1) {
|
||||
const c = caps[0];
|
||||
return "s" in c ? c.s : c.position;
|
||||
}
|
||||
|
||||
let l = pattern.length;
|
||||
let n = 0;
|
||||
|
||||
for (let i = 0; i < l; i++) {
|
||||
const character = pattern.slice(i, 1);
|
||||
if (i && pattern.slice(i - 1, 1) == "\\") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let addSlash = false;
|
||||
|
||||
if (character == "[") {
|
||||
if (n) addSlash = true;
|
||||
n++;
|
||||
} else if (character == "]" && pattern.slice(i - 1, 1) !== "\\") {
|
||||
n--;
|
||||
if (n) addSlash = true;
|
||||
}
|
||||
|
||||
if (addSlash) {
|
||||
pattern = pattern.slice(0, i) + pattern.slice(i++ + 1);
|
||||
l++;
|
||||
}
|
||||
}
|
||||
|
||||
return pattern;
|
||||
return new LuaMultiRes(
|
||||
caps.map((c) => ("s" in c ? c.s : c.position)),
|
||||
);
|
||||
}
|
||||
|
||||
export const stringApi = new LuaTable({
|
||||
byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => {
|
||||
i = i ?? 1;
|
||||
j = j ?? i;
|
||||
if (j > s.length) {j = s.length;}
|
||||
if (i < 1) {i = 1;}
|
||||
const result = [];
|
||||
for (let k = i; k <= j; k++) {
|
||||
result.push(s.charCodeAt(k - 1));
|
||||
@@ -90,119 +45,68 @@ export const stringApi = new LuaTable({
|
||||
}),
|
||||
find: new LuaBuiltinFunction(
|
||||
(_sf, s: string, pattern: string, init = 1, plain = false) => {
|
||||
// Regex
|
||||
if (!plain) {
|
||||
pattern = translatePattern(pattern);
|
||||
const reg = new RegExp(pattern);
|
||||
const index = s.slice(init - 1).search(reg);
|
||||
|
||||
if (index < 0) return null;
|
||||
|
||||
const match = s.slice(init - 1).match(reg);
|
||||
const result = [index + init, index + init + match![0].length - 1];
|
||||
|
||||
match!.shift();
|
||||
return new LuaMultiRes(result.concat(match));
|
||||
const r = patternFind(s, pattern, init, plain);
|
||||
if (!r) {return null;}
|
||||
const result: any[] = [r.start, r.end];
|
||||
for (const c of r.captures) {
|
||||
result.push("s" in c ? c.s : c.position);
|
||||
}
|
||||
|
||||
// Plain
|
||||
const index = s.indexOf(pattern, init - 1);
|
||||
return (index === -1)
|
||||
? null
|
||||
: new LuaMultiRes([index + 1, index + pattern.length]);
|
||||
return new LuaMultiRes(result);
|
||||
},
|
||||
),
|
||||
format: new LuaBuiltinFunction((_sf, format: string, ...args: any[]) => {
|
||||
// Unwrap tagged floats so luaFormat sees plain numbers
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
args[i] = untagNumber(args[i]);
|
||||
}
|
||||
return luaFormat(format, ...args);
|
||||
}),
|
||||
gmatch: new LuaBuiltinFunction((_sf, s: string, pattern: string) => {
|
||||
pattern = translatePattern(pattern);
|
||||
const reg = new RegExp(pattern, "g"),
|
||||
matches = s.match(reg);
|
||||
return () => {
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
const match = matches.shift();
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
const groups = new RegExp(pattern).exec(match) || [];
|
||||
|
||||
groups.shift();
|
||||
return groups.length ? new LuaMultiRes(groups) : match;
|
||||
};
|
||||
}),
|
||||
gmatch: new LuaBuiltinFunction(
|
||||
(_sf, s: string, pattern: string, init = 1) => {
|
||||
const iter = patternGmatch(s, pattern, init);
|
||||
return () => {
|
||||
const caps = iter();
|
||||
if (!caps) {return;}
|
||||
return capturesToLua(caps);
|
||||
};
|
||||
},
|
||||
),
|
||||
gsub: new LuaBuiltinFunction(
|
||||
async (
|
||||
sf,
|
||||
s: string,
|
||||
pattern: string,
|
||||
repl: any, // string or LuaFunction
|
||||
n = Infinity,
|
||||
repl: any,
|
||||
n?: number,
|
||||
) => {
|
||||
pattern = translatePattern(pattern);
|
||||
const replIsFunction = repl.call;
|
||||
|
||||
let count = 0,
|
||||
result = "",
|
||||
str,
|
||||
prefix,
|
||||
match: any,
|
||||
lastMatch;
|
||||
|
||||
while (
|
||||
count < n &&
|
||||
s &&
|
||||
(match = s.match(pattern))
|
||||
) {
|
||||
if (replIsFunction) {
|
||||
// If no captures, pass in the whole match
|
||||
if (match[1] === undefined) {
|
||||
str = await repl.call(sf, match[0]);
|
||||
} else {
|
||||
// Else pass in the captures
|
||||
str = await repl.call(sf, ...match.slice(1));
|
||||
const callbacks: GsubCallbacks = {};
|
||||
if (typeof repl === "string") {
|
||||
callbacks.replString = repl;
|
||||
} else if (repl instanceof LuaTable) {
|
||||
callbacks.replTable = (key: string) => {
|
||||
const v = repl.get(key);
|
||||
if (v === null || v === undefined || v === false) {return null;}
|
||||
return String(v);
|
||||
};
|
||||
} else if (repl.call) {
|
||||
callbacks.replFunction = async (...caps: CaptureResult[]) => {
|
||||
const args = caps.map((c) => ("s" in c ? c.s : c.position));
|
||||
let result = await repl.call(sf, ...args);
|
||||
if (result instanceof LuaMultiRes) {
|
||||
result = result.values[0];
|
||||
}
|
||||
if (str instanceof LuaMultiRes) {
|
||||
str = str.values[0];
|
||||
if (result === null || result === undefined || result === false) {
|
||||
return null;
|
||||
}
|
||||
if (str === undefined || str === null) {
|
||||
str = match[0];
|
||||
}
|
||||
} else if (repl instanceof LuaTable) {
|
||||
str = repl.get(match[0]);
|
||||
} else if (typeof repl === "string") {
|
||||
str = repl.replaceAll(/%([0-9]+)/g, (_, i) => match[i]);
|
||||
} else {
|
||||
throw new LuaRuntimeError(
|
||||
"string.gsub replacement argument should be a function, table or string",
|
||||
sf,
|
||||
);
|
||||
}
|
||||
|
||||
if (match[0].length === 0) {
|
||||
if (lastMatch === void 0) {
|
||||
prefix = "";
|
||||
} else {
|
||||
prefix = s.slice(0, 1);
|
||||
}
|
||||
} else {
|
||||
prefix = s.slice(0, match.index);
|
||||
}
|
||||
|
||||
lastMatch = match[0];
|
||||
result += `${prefix}${str}`;
|
||||
s = s.slice(`${prefix}${lastMatch}`.length);
|
||||
|
||||
count++;
|
||||
return luaToString(result);
|
||||
};
|
||||
} else {
|
||||
throw new LuaRuntimeError(
|
||||
"string.gsub replacement argument should be a function, table or string",
|
||||
sf,
|
||||
);
|
||||
}
|
||||
|
||||
return new LuaMultiRes([`${result}${s}`, count]);
|
||||
const [result, count] = await patternGsub(s, pattern, callbacks, n);
|
||||
return new LuaMultiRes([result, count]);
|
||||
},
|
||||
),
|
||||
len: new LuaBuiltinFunction((_sf, s: string) => {
|
||||
@@ -216,43 +120,53 @@ export const stringApi = new LuaTable({
|
||||
}),
|
||||
match: new LuaBuiltinFunction(
|
||||
(_sf, s: string, pattern: string, init = 1) => {
|
||||
s = s.slice(init - 1);
|
||||
const matches = s.match(new RegExp(translatePattern(pattern)));
|
||||
|
||||
if (!matches) {
|
||||
return null;
|
||||
}
|
||||
if (matches[1] === undefined) {
|
||||
// No captures
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
matches.shift();
|
||||
return new LuaMultiRes(matches);
|
||||
const caps = patternMatch(s, pattern, init);
|
||||
if (!caps) {return null;}
|
||||
return capturesToLua(caps);
|
||||
},
|
||||
),
|
||||
rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => {
|
||||
if (n <= 0) {return "";}
|
||||
sep = sep ?? "";
|
||||
return s.repeat(n) + sep;
|
||||
const parts: string[] = [];
|
||||
for (let i = 0; i < n; i++) {
|
||||
parts.push(s);
|
||||
}
|
||||
return parts.join(sep);
|
||||
}),
|
||||
reverse: new LuaBuiltinFunction((_sf, s: string) => {
|
||||
return s.split("").reverse().join("");
|
||||
}),
|
||||
sub: new LuaBuiltinFunction((_sf, s: string, i: number, j?: number) => {
|
||||
j = j ?? s.length;
|
||||
if (i < 0) {
|
||||
i = s.length + i + 1;
|
||||
const len = s.length;
|
||||
let start: number;
|
||||
if (i > 0) {
|
||||
start = i;
|
||||
} else if (i < -len) {
|
||||
start = 1;
|
||||
} else {
|
||||
start = i === 0 ? 1 : len + i + 1;
|
||||
}
|
||||
if (j < 0) {
|
||||
j = s.length + j + 1;
|
||||
let end: number;
|
||||
if (j === undefined || j === null || j > len) {
|
||||
end = len;
|
||||
} else if (j >= 0) {
|
||||
end = j;
|
||||
} else if (j < -len) {
|
||||
end = 0;
|
||||
} else {
|
||||
end = len + j + 1;
|
||||
}
|
||||
return s.slice(i - 1, j);
|
||||
if (start <= end) {
|
||||
return s.substring(start - 1, end);
|
||||
}
|
||||
return "";
|
||||
}),
|
||||
split: new LuaBuiltinFunction((_sf, s: string, sep: string) => {
|
||||
return s.split(sep);
|
||||
}),
|
||||
|
||||
// Non-standard
|
||||
// Non-standard extensions
|
||||
startsWith: new LuaBuiltinFunction((_sf, s: string, prefix: string) => {
|
||||
return s.startsWith(prefix);
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
local function assertEqual(a, b)
|
||||
if a ~= b then
|
||||
error("Assertion failed: " .. a .. " is not equal to " .. b)
|
||||
error("Assertion failed: " .. tostring(a) .. " is not equal to " .. tostring(b))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -8,124 +8,80 @@ end
|
||||
assert(string.len("Hello") == 5)
|
||||
assert(string.byte("Hello", 1) == 72)
|
||||
assert(string.char(72) == "H")
|
||||
assert(string.find("Hello", "l") == 3)
|
||||
assert(string.rep("Hello", 3) == "HelloHelloHello")
|
||||
assert(string.sub("Hello", 2, 4) == "ell")
|
||||
assert(string.upper("Hello") == "HELLO")
|
||||
assert(string.lower("Hello") == "hello")
|
||||
|
||||
-- string.byte tests
|
||||
local a, b = string.byte('Mo0')
|
||||
assert(a == 77, 'string.byte() should return the code for the first char')
|
||||
assert(b == nil, 'string.byte() should return only one item when no length is given')
|
||||
|
||||
a, b = string.byte('Mo0', 2)
|
||||
assert(a == 111, 'string.byte() should return the code for the nth character')
|
||||
assert(b == nil, 'string.byte() should return only one item when no length is given')
|
||||
|
||||
local a2, b2, c2 = string.byte('Mo0', 2, 3)
|
||||
assert(a2 == 111, 'string.byte() multi-return [1]')
|
||||
assert(b2 == 48, 'string.byte() multi-return [2]')
|
||||
assert(c2 == nil, 'string.byte() should stop at end of string')
|
||||
|
||||
a2, b2 = string.byte('Mo0', 3, 20)
|
||||
assert(a2 == 48, 'string.byte() should clamp to string length')
|
||||
assert(b2 == nil, 'string.byte() should not return past end of string')
|
||||
|
||||
-- string.char tests
|
||||
assertEqual(string.char(), '')
|
||||
assertEqual(string.char(116, 101, 115, 116), 'test')
|
||||
|
||||
-- string.len tests
|
||||
assertEqual(string.len(''), 0)
|
||||
assertEqual(string.len('McLaren Mercedes'), 16)
|
||||
|
||||
-- string.lower tests
|
||||
assertEqual(string.lower(''), '')
|
||||
assertEqual(string.lower('McLaren Mercedes'), 'mclaren mercedes')
|
||||
|
||||
-- string.upper tests
|
||||
assertEqual(string.upper(''), '')
|
||||
assertEqual(string.upper('JavaScript'), 'JAVASCRIPT')
|
||||
|
||||
-- string.rep tests
|
||||
assertEqual(string.rep('Ho', 0), '')
|
||||
assertEqual(string.rep('Ho', 1), 'Ho')
|
||||
assertEqual(string.rep('Ho', 3), 'HoHoHo')
|
||||
assertEqual(string.rep("ab", 3, ","), "ab,ab,ab")
|
||||
assertEqual(string.rep("x", 1, ","), "x")
|
||||
|
||||
-- string.reverse tests
|
||||
assertEqual(string.reverse(''), '')
|
||||
assertEqual(string.reverse('x'), 'x')
|
||||
assertEqual(string.reverse('tpircSavaJ'), 'JavaScript')
|
||||
|
||||
-- string.sub tests
|
||||
assertEqual(string.sub("hello", 1), "hello")
|
||||
assertEqual(string.sub("hello", 1, 1), "h")
|
||||
-- with negative indeses
|
||||
-- with negative indices
|
||||
assertEqual(string.sub("hello", -1), "o")
|
||||
assertEqual(string.sub("hello", -2), "lo")
|
||||
assertEqual(string.sub("hello", -2, -1), "lo")
|
||||
|
||||
local s = 'Pub Standards'
|
||||
assertEqual(string.sub(s, 1), 'Pub Standards')
|
||||
assertEqual(string.sub(s, 5), 'Standards')
|
||||
assertEqual(string.sub(s, -4), 'ards')
|
||||
assertEqual(string.sub(s, 1, 3), 'Pub')
|
||||
assertEqual(string.sub(s, 7, 9), 'and')
|
||||
assertEqual(string.sub(s, 5, -2), 'Standard')
|
||||
assertEqual(string.sub(s, 0), 'Pub Standards')
|
||||
|
||||
-- Invoke string metatable methods
|
||||
assertEqual(("hello"):len(), 5)
|
||||
assertEqual(("hello"):upper(), "HELLO")
|
||||
assertEqual(('Hey'):lower(), 'hey')
|
||||
|
||||
-- Test string.gsub with various replacement types
|
||||
-- Simple string replacement
|
||||
local result, count = string.gsub("hello world", "hello", "hi")
|
||||
assert(result == "hi world", "Basic string replacement failed")
|
||||
assert(count == 1, "Basic replacement count failed")
|
||||
|
||||
-- https://github.com/silverbulletmd/silverbullet/issues/1326
|
||||
result, count = ("abc|de"):gsub("|", "-")
|
||||
assert(result == "abc-de", "replacements failed [got: " .. result .. ", expected: abc-de]")
|
||||
assert(count == 1, "replacement count failed")
|
||||
|
||||
result, count = ("abc\\|de|"):gsub("|", "-")
|
||||
assert(result == "abc\\-de-", "replacements failed [got: " .. result .. ", expected: abc\\-de-]")
|
||||
assert(count == 2, "replacement count failed")
|
||||
|
||||
-- Multiple replacements
|
||||
result, count = string.gsub("hello hello hello", "hello", "hi")
|
||||
assert(result == "hi hi hi", "Multiple replacements failed")
|
||||
assert(count == 3, "Multiple replacement count failed")
|
||||
|
||||
-- Limited replacements with n parameter
|
||||
result, count = string.gsub("hello hello hello", "hello", "hi", 2)
|
||||
assert(result == "hi hi hello", "Limited replacements failed")
|
||||
assert(count == 2, "Limited replacement count failed")
|
||||
|
||||
-- Function replacement without captures
|
||||
result = string.gsub("hello world", "hello", function(match)
|
||||
assert(match == "hello", "Function received incorrect match")
|
||||
return string.upper(match)
|
||||
end)
|
||||
assertEqual(result, "HELLO world", "Function replacement without captures failed")
|
||||
|
||||
-- Function replacement with single capture
|
||||
result = string.gsub("hello world", "(h)ello", function(h)
|
||||
assertEqual(h, "h", "Function received incorrect capture")
|
||||
return string.upper(h) .. "i"
|
||||
end)
|
||||
assert(result == "Hi world", "Function replacement with single capture failed")
|
||||
|
||||
-- Function replacement with multiple captures
|
||||
result = string.gsub("hello world", "(h)(e)(l)(l)o", function(h, e, l1, l2)
|
||||
assert(h == "h" and e == "e" and l1 == "l" and l2 == "l",
|
||||
"Function received incorrect captures: " .. h .. ", " .. e .. ", " .. l1 .. ", " .. l2)
|
||||
return string.upper(h) .. string.upper(e) .. l1 .. l2 .. "o"
|
||||
end)
|
||||
assert(result == "HEllo world", "Function replacement with multiple captures failed")
|
||||
|
||||
-- Function returning nil (should keep original match)
|
||||
result = string.gsub("hello world", "hello", function() return nil end)
|
||||
assert(result == "hello world", "Function returning nil failed")
|
||||
|
||||
-- Pattern with multiple matches on same position
|
||||
result = string.gsub("hello world", "h?e", "X")
|
||||
assert(result == "Xllo world", "Overlapping matches failed")
|
||||
|
||||
-- Empty captures
|
||||
result = string.gsub("hello", "(h()e)", function(full, empty)
|
||||
assert(full == "he" and empty == "", "Empty capture handling failed")
|
||||
return "XX"
|
||||
end)
|
||||
assert(result == "XXllo", "Empty capture replacement failed")
|
||||
|
||||
-- Patterns with magic characters
|
||||
result = string.gsub("hello.world", "%.", "-")
|
||||
assert(result == "hello-world", "Magic character replacement failed")
|
||||
|
||||
-- Test string.match
|
||||
local m1, m2 = string.match("hello world", "(h)(ello)")
|
||||
assertEqual(m1, "h")
|
||||
assertEqual(m2, "ello")
|
||||
|
||||
-- Test with pattern with character class
|
||||
assertEqual(string.match("c", "[abc]"), "c")
|
||||
|
||||
-- Test match with init position - need to capture the group
|
||||
local initMatch = string.match("hello world", "(world)", 7)
|
||||
assertEqual(initMatch, "world")
|
||||
|
||||
-- Test string.gmatch
|
||||
local words = {}
|
||||
for word in string.gmatch("hello world lua", "%w+") do
|
||||
table.insert(words, word)
|
||||
end
|
||||
assertEqual(words[1], "hello")
|
||||
assertEqual(words[2], "world")
|
||||
assertEqual(words[3], "lua")
|
||||
|
||||
-- with capture
|
||||
local captures = {}
|
||||
for k, v in string.gmatch("from=world, to=Lua", "(%w+)=(%w+)") do
|
||||
captures[k] = v
|
||||
end
|
||||
assertEqual(captures.from, "world")
|
||||
assertEqual(captures.to, "Lua")
|
||||
|
||||
-- Test string.reverse
|
||||
assertEqual(string.reverse("hello"), "olleh")
|
||||
assertEqual(string.reverse(""), "")
|
||||
|
||||
-- Test string.split
|
||||
-- Test string.split (non-standard)
|
||||
local parts = string.split("a,b,c", ",")
|
||||
assertEqual(parts[1], "a")
|
||||
assertEqual(parts[2], "b")
|
||||
@@ -134,49 +90,10 @@ assertEqual(parts[3], "c")
|
||||
-- Test non-standard string extensions
|
||||
assertEqual(string.startsWith("hello world", "hello"), true)
|
||||
assertEqual(string.startsWith("hello world", "world"), false)
|
||||
|
||||
assertEqual(string.endsWith("hello world", "world"), true)
|
||||
assertEqual(string.endsWith("hello world", "hello"), false)
|
||||
|
||||
-- Extended string.match tests
|
||||
-- Basic pattern matching
|
||||
assertEqual(string.match("hello", "h"), "h")
|
||||
assertEqual(string.match("hello", "hello"), "hello")
|
||||
|
||||
-- Test with no matches
|
||||
assertEqual(string.match("hello", "x"), nil)
|
||||
|
||||
-- Test with captures
|
||||
local m1, m2 = string.match("hello", "(h)(ello)")
|
||||
assertEqual(m1, "h")
|
||||
assertEqual(m2, "ello")
|
||||
|
||||
-- Test with init position
|
||||
local initMatch = string.match("hello world", "(world)", 7)
|
||||
assertEqual(initMatch, "world")
|
||||
|
||||
-- Test init position with no match
|
||||
assertEqual(string.match("hello world", "hello", 7), nil)
|
||||
|
||||
-- Test pattern characters
|
||||
assertEqual(string.match("123", "%d+"), "123")
|
||||
assertEqual(string.match("abc123", "%a+"), "abc")
|
||||
assertEqual(string.match(" abc", "%s+"), " ")
|
||||
|
||||
-- Test multiple captures
|
||||
local year, month, day = string.match("2024-03-14", "(%d+)%-(%d+)%-(%d+)")
|
||||
assertEqual(year, "2024")
|
||||
assertEqual(month, "03")
|
||||
assertEqual(day, "14")
|
||||
|
||||
-- Test escaped hyphen at the end of a pattern
|
||||
assertEqual(string.match("4-", "%d%-"), "4-")
|
||||
|
||||
-- Test optional captures
|
||||
local word = string.match("The quick brown fox", "%s*(%w+)%s*")
|
||||
assertEqual(word, "The")
|
||||
|
||||
-- Test matchRegexAll
|
||||
-- Test matchRegexAll (non-standard, regex-based)
|
||||
local matches = {}
|
||||
for match in string.matchRegexAll("hellolllbl", "(l+)") do
|
||||
table.insert(matches, match)
|
||||
@@ -185,9 +102,3 @@ assertEqual(#matches, 3)
|
||||
assertEqual(matches[1][1], "ll")
|
||||
assertEqual(matches[2][1], "lll")
|
||||
assertEqual(matches[3][1], "l")
|
||||
|
||||
-- https://community.silverbullet.md/t/test-if-string-find-outputs-nil-not-working-as-expected/2342
|
||||
assert(string.find("a", "b") == nil)
|
||||
assert(not (string.find("a", "b") ~= nil))
|
||||
assert(string.match("a", "b") == nil)
|
||||
assert(not (string.match("a", "b") ~= nil))
|
||||
|
||||
@@ -1,624 +0,0 @@
|
||||
local function assertEqual(a, b, message)
|
||||
if a ~= b then
|
||||
if a == nil then
|
||||
a = "nil"
|
||||
end
|
||||
if b == nil then
|
||||
b = "nil"
|
||||
end
|
||||
error("Assertion failed: " .. a .. " is not equal to " .. b .. ". " .. message)
|
||||
end
|
||||
end
|
||||
|
||||
local function assertTrue(a, message)
|
||||
if not a then
|
||||
error("Assertion failed: " .. message)
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------
|
||||
-- Moonshine - a Lua virtual machine.
|
||||
--
|
||||
-- Email: moonshine@gamesys.co.uk
|
||||
-- http://moonshinejs.org
|
||||
--
|
||||
-- Copyright (c) 2013-2015 Gamesys Limited. All rights reserved.
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining
|
||||
-- a copy of this software and associated documentation files (the
|
||||
-- "Software"), to deal in the Software without restriction, including
|
||||
-- without limitation the rights to use, copy, modify, merge, publish,
|
||||
-- distribute, sublicense, and/or sell copies of the Software, and to
|
||||
-- permit persons to whom the Software is furnished to do so, subject to
|
||||
-- the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be
|
||||
-- included in all copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
-- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
-- CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
-- SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
--
|
||||
|
||||
-- STRING FUNCTIONS
|
||||
|
||||
-- byte
|
||||
|
||||
local a, b = string.byte('Mo0')
|
||||
|
||||
assertTrue(a == 77, 'string.byte() should return the numerical code for the first character in the first returned item')
|
||||
assertTrue(b == nil, 'string.byte() should return only one item when only no length is given [1]')
|
||||
|
||||
local a, b = string.byte('Mo0', 2)
|
||||
|
||||
assertTrue(a == 111,
|
||||
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [1]')
|
||||
assertTrue(b == nil, 'string.byte() should return only one item when only no length is given [2]')
|
||||
|
||||
local a, b, c = string.byte('Mo0', 2, 3)
|
||||
|
||||
assertTrue(a == 111,
|
||||
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [2]')
|
||||
assertTrue(b == 48,
|
||||
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [3]')
|
||||
assertTrue(c == nil,
|
||||
'string.byte() should return only the number of items specified in the length argument or the up to the end of the string, whichever is encountered first [1]')
|
||||
|
||||
local a, b, c = string.byte('Mo0', 3, 20)
|
||||
|
||||
assertTrue(a == 48,
|
||||
'string.byte() should return the numerical code for the nth character in the first returned item, when n is specified in the second argument [4]')
|
||||
assertTrue(b == nil,
|
||||
'string.byte() should return only the number of items specified in the length argument or the up to the end of the string, whichever is encountered first [2]')
|
||||
|
||||
-- char
|
||||
|
||||
local a = string.char()
|
||||
local b = string.char(116, 101, 115, 116, 105, 99, 108, 101, 115)
|
||||
|
||||
assertTrue(a == '', 'string.byte() should return an empty string when called with no arguments')
|
||||
assertTrue(b == 'testicles',
|
||||
'string.byte() should return a string comprising of characters representing by the value each of the arguments passed')
|
||||
|
||||
-- dump
|
||||
|
||||
-- local f = function () end
|
||||
-- local a = string.dump(f)
|
||||
-- assertTrue (type(a) == 'string', 'string.dump() should return a string when called with a function')
|
||||
|
||||
-- local s = string.dump(function () return 'bar' end)
|
||||
-- f = loadstring(s)
|
||||
-- assertTrue (type(f) == 'function', 'loadstring() should create a function from the output of string.dump() [1]')
|
||||
|
||||
-- result = f()
|
||||
-- assertTrue (result == 'bar', 'The result of loadstring(string.dump(f)) should behave the same as f() [1]')
|
||||
|
||||
-- function namedFuncWithParams (a, b)
|
||||
-- return a..b
|
||||
-- end
|
||||
|
||||
-- s = string.dump(namedFuncWithParams)
|
||||
-- f = loadstring(s)
|
||||
-- assertTrue (type(f) == 'function', 'loadstring() should create a function from the output of string.dump() [2]')
|
||||
|
||||
-- result = f('hel','lo')
|
||||
-- assertTrue (result == 'hello', 'The result of loadstring(string.dump(f)) should behave the same as f() [2]')
|
||||
|
||||
-- find
|
||||
|
||||
local a = 'The quick brown fox'
|
||||
|
||||
local b = string.find(a, 'quick');
|
||||
local c = string.find(a, 'fox');
|
||||
local d = string.find(a, 'kipper');
|
||||
local e = string.find(a, '');
|
||||
|
||||
local f = string.find(a, 'quick', 8);
|
||||
local g = string.find(a, 'fox', 8);
|
||||
|
||||
assertTrue(b == 5,
|
||||
'string.find() should return the location of the first occurrence of the second argument within the first, if it is present [1]')
|
||||
assertTrue(c == 17,
|
||||
'string.find() should return the location of the first occurrence of the second argument within the first, if it is present [2]')
|
||||
assertTrue(d == nil, 'string.find() should return nil if the second argument is not contained within the first [1]')
|
||||
assertTrue(e == 1, 'string.find() should return return 1 if the second argument is an empty string')
|
||||
assertTrue(f == nil,
|
||||
'string.find() should return nil if the second argument is not contained within the first after the index specified by the third argument')
|
||||
assertTrue(g == 17,
|
||||
'string.find() should return the location of the second argument if it is contained within the first after the index specified by the third argument')
|
||||
|
||||
local b, c, d, e = string.find(a, 'q(.)(.)');
|
||||
assertEqual(b, 5,
|
||||
'string.find() should return the location of the first occurrence of the second argument within the first, if it is present [3]')
|
||||
assertEqual(c, 7,
|
||||
'string.find() should return the location of the last character of the first occurrence of the second argument within the first, if it is present')
|
||||
assertEqual(d, 'u', 'string.find() should return the groups that are specified in the regex. [1]')
|
||||
assertEqual(e, 'i', 'string.find() should return the groups that are specified in the regex. [2]')
|
||||
|
||||
b = string.find('[', '[_%w]')
|
||||
assertTrue(b == nil, 'string.find() should not return the location of special syntax [ and ].')
|
||||
|
||||
-- -- format
|
||||
|
||||
-- do
|
||||
-- local a = string.format("%s %q", "Hello", "Lua user!")
|
||||
-- local b = string.format("%c%c%c", 76,117,97) -- char
|
||||
-- local c = string.format("%e, %E", math.pi,math.pi) -- exponent
|
||||
-- local d1 = string.format("%f", math.pi) -- float
|
||||
-- local d2 = string.format("%g", math.pi) -- compact float
|
||||
|
||||
-- -- issues:
|
||||
-- local e = string.format("%d, %i, %u", -100,-100,-100) -- signed, signed, unsigned integer
|
||||
-- local f = string.format("%o, %x, %X", -100,-100,-100) -- octal, hex, hex
|
||||
|
||||
-- local g = string.format("%%s", 100)
|
||||
|
||||
-- assertTrue (a == 'Hello "Lua user!"', 'string.format() should format %s and %q correctly')
|
||||
-- assertTrue (b == 'Lua', 'string.format() should format %c correctly')
|
||||
-- assertTrue (d1 == '3.141593', 'string.format() should format %f correctly')
|
||||
-- -- assertTrue (e == '-100, -100, 4294967196', 'string.format() should format %d, %i and %u correctly')
|
||||
-- -- assertTrue (f == '37777777634, ffffff9c, FFFFFF9C', 'string.format() should format %o, %x and %X correctly')
|
||||
-- -- assertTrue (e == '-100, -100, 18446744073709551516', 'string.format() should format %d, %i and %u correctly')
|
||||
-- -- assertTrue (f == '1777777777777777777634, ffffffffffffff9c, FFFFFFFFFFFFFF9C', 'string.format() should format %o, %x and %X correctly')
|
||||
-- assertTrue (g == '%s', 'string.format() should format %% correctly')
|
||||
|
||||
-- -- TODO!!!
|
||||
-- -- assertTrue (c == '3.141593e+00, 3.141593E+00', 'string.format() should format %e and %E correctly')
|
||||
-- -- assertTrue (d2 == '3.14159', 'string.format() should format %g correctly')
|
||||
|
||||
-- a = function () string.format("%*", 100) end
|
||||
-- b = function () string.format("%l", 100) end
|
||||
-- c = function () string.format("%L", 100) end
|
||||
-- d = function () string.format("%n", 100) end
|
||||
-- e = function () string.format("%p", 100) end
|
||||
-- f = function () string.format("%h", 100) end
|
||||
|
||||
-- assertTrue (not pcall(a), 'string.format() should error when passed %*')
|
||||
-- assertTrue (not pcall(b), 'string.format() should error when passed %l')
|
||||
-- assertTrue (not pcall(c), 'string.format() should error when passed %L')
|
||||
-- assertTrue (not pcall(d), 'string.format() should error when passed %n')
|
||||
-- assertTrue (not pcall(e), 'string.format() should error when passed %p')
|
||||
-- assertTrue (not pcall(f), 'string.format() should error when passed %h')
|
||||
|
||||
-- a = string.format("%.3f", 5.1)
|
||||
-- b = "Lua version " .. string.format("%.1f", 5.1)
|
||||
-- c = string.format("pi = %.4f", math.pi)
|
||||
-- f = string.format("%.3f", 5)
|
||||
|
||||
-- local d, m, y = 5, 11, 1990
|
||||
-- e = string.format("%02d/%02d/%04d", d, m, y)
|
||||
|
||||
-- assertTrue (a == '5.100', 'string.format() should format floating point numbers correctly[1]')
|
||||
-- assertTrue (b == 'Lua version 5.1', 'string.format() should format floating point numbers correctly[2]')
|
||||
-- assertTrue (c == 'pi = 3.1416', 'string.format() should format floating point numbers correctly[3]')
|
||||
-- assertTrue (e == '05/11/1990', 'string.format() should format decimals correctly [0]')
|
||||
-- assertTrue (f == '5.000', 'string.format() should format floating point numbers correctly[4]')
|
||||
|
||||
-- a = function () string.format('%#####s', 'x') end
|
||||
-- b = function () string.format('%######s', 'x') end
|
||||
|
||||
-- assertTrue (pcall(a), 'string.format() should handle five flags')
|
||||
-- assertTrue (not pcall(b), 'string.format() should not handle six flags')
|
||||
|
||||
-- local tag, title = "h1", "a title"
|
||||
-- a = string.format("<%s>%s</%s>", tag, title, tag)
|
||||
-- b = string.format("%8s", "Lua")
|
||||
-- c = string.format("%.8s", "Lua")
|
||||
-- d = string.format("%.2s", "Lua")
|
||||
-- e = string.format("%8.2s", "Lua")
|
||||
-- f = string.format("%+8.2s", "Lua")
|
||||
-- g = string.format("%-8.2s", "Lua")
|
||||
-- local h = string.format("%08.2s", "Lua")
|
||||
-- local i = string.format("%#8.2s", "Lua")
|
||||
-- local j = string.format("% 8.2s", "Lua")
|
||||
-- local k = string.format("%+-0# 8.2s", "Lua")
|
||||
-- local l = string.format("%0.2s", "Lua")
|
||||
|
||||
-- assertTrue (a == '<h1>a title</h1>', 'string.format() should format strings correctly[1]')
|
||||
-- assertTrue (b == ' Lua', 'string.format() should format strings correctly[2]')
|
||||
-- assertTrue (c == 'Lua', 'string.format() should format strings correctly[3]')
|
||||
-- assertTrue (d == 'Lu', 'string.format() should format strings correctly[4]')
|
||||
-- assertTrue (e == ' Lu', 'string.format() should format strings correctly[5]')
|
||||
-- assertTrue (f == ' Lu', 'string.format() should format strings correctly[6]')
|
||||
-- assertTrue (g == 'Lu ', 'string.format() should format strings correctly[7]')
|
||||
-- assertTrue (h == '000000Lu', 'string.format() should format strings correctly[8]')
|
||||
-- assertTrue (i == ' Lu', 'string.format() should format strings correctly[9]')
|
||||
-- assertTrue (j == ' Lu', 'string.format() should format strings correctly[10]')
|
||||
-- assertTrue (k == 'Lu ', 'string.format() should format strings correctly[11]')
|
||||
-- assertTrue (l == 'Lu', 'string.format() should format strings correctly[12]')
|
||||
|
||||
-- a = string.format("%8d", 123.45)
|
||||
-- b = string.format("%.8d", 123.45)
|
||||
-- c = string.format("%.2d", 123.45)
|
||||
-- d = string.format("%8.2d", 123.45)
|
||||
-- e = string.format("%+8.2d", 123.45)
|
||||
-- f = string.format("%-8.2d", 123.45)
|
||||
-- g = string.format("%08.2d", 123.45)
|
||||
-- h = string.format("%#8.2d", 123.45)
|
||||
-- i = string.format("% 8.2d", 123.45)
|
||||
-- j = string.format("%+-0# 8.2d", 123.45)
|
||||
-- k = string.format("%0.2d", 123.45)
|
||||
-- l = string.format("%+.8d", 123.45)
|
||||
-- local m = string.format("%-.8d", 123.45)
|
||||
-- local n = string.format("%#.8d", 123.45)
|
||||
-- local o = string.format("%0.8d", 123.45)
|
||||
-- local p = string.format("% .8d", 123.45)
|
||||
-- local q = string.format("%+-#0 .8d", 123.45)
|
||||
-- local r = string.format("%8.5d", 123.45)
|
||||
-- local s = string.format("%+8.5d", 123.45)
|
||||
-- local t = string.format("%-8.5d", 123.45)
|
||||
-- local u = string.format("%-+8.5d", 123.45)
|
||||
-- local v = string.format("%5d", 12.3e10)
|
||||
-- local w = string.format("%.d", 123.45)
|
||||
|
||||
-- assertTrue (a == ' 123', 'string.format() should format decimals correctly[1]')
|
||||
-- assertTrue (b == '00000123', 'string.format() should format decimals correctly[2]')
|
||||
-- assertTrue (c == '123', 'string.format() should format decimals correctly[3]')
|
||||
-- assertTrue (d == ' 123', 'string.format() should format decimals correctly[4]')
|
||||
-- assertTrue (e == ' +123', 'string.format() should format decimals correctly[5]')
|
||||
-- assertTrue (f == '123 ', 'string.format() should format decimals correctly[6]')
|
||||
-- assertTrue (g == ' 123', 'string.format() should format decimals correctly[7]')
|
||||
-- assertTrue (h == ' 123', 'string.format() should format decimals correctly[8]')
|
||||
-- assertTrue (i == ' 123', 'string.format() should format decimals correctly[9]')
|
||||
-- assertTrue (j == '+123 ', 'string.format() should format decimals correctly[10]')
|
||||
-- assertTrue (k == '123', 'string.format() should format decimals correctly[11]')
|
||||
-- assertTrue (l == '+00000123', 'string.format() should format decimals correctly[12]')
|
||||
-- assertTrue (m == '00000123', 'string.format() should format decimals correctly[13]')
|
||||
-- assertTrue (n == '00000123', 'string.format() should format decimals correctly[14]')
|
||||
-- assertTrue (o == '00000123', 'string.format() should format decimals correctly[15]')
|
||||
-- assertTrue (p == ' 00000123', 'string.format() should format decimals correctly[16]')
|
||||
-- assertTrue (q == '+00000123', 'string.format() should format decimals correctly[17]')
|
||||
-- assertTrue (r == ' 00123', 'string.format() should format decimals correctly[18]')
|
||||
-- assertTrue (s == ' +00123', 'string.format() should format decimals correctly[19]')
|
||||
-- assertTrue (t == '00123 ', 'string.format() should format decimals correctly[20]')
|
||||
-- assertTrue (u == '+00123 ', 'string.format() should format decimals correctly[21]')
|
||||
-- assertTrue (v == '123000000000', 'string.format() should format decimals correctly[22]')
|
||||
-- assertTrue (w == '123', 'string.format() should format decimals correctly[23]')
|
||||
|
||||
-- a = string.format("%8d", -123.45)
|
||||
-- b = string.format("%.8d", -123.45)
|
||||
-- c = string.format("%.2d", -123.45)
|
||||
-- d = string.format("%8.2d", -123.45)
|
||||
-- e = string.format("%+8.2d", -123.45)
|
||||
-- f = string.format("%-8.2d", -123.45)
|
||||
-- g = string.format("%08.2d", -123.45)
|
||||
-- h = string.format("%#8.2d", -123.45)
|
||||
-- i = string.format("% 8.2d", -123.45)
|
||||
-- j = string.format("%+-0# 8.2d", -123.45)
|
||||
-- k = string.format("%0.2d", -123.45)
|
||||
-- l = string.format("%+.8d", -123.45)
|
||||
-- m = string.format("%-.8d", -123.45)
|
||||
-- n = string.format("%#.8d", -123.45)
|
||||
-- o = string.format("%0.8d", -123.45)
|
||||
-- p = string.format("% .8d", -123.45)
|
||||
-- q = string.format("%+-#0 .8d", -123.45)
|
||||
-- r = string.format("%8.5d", -123.45)
|
||||
-- s = string.format("%+8.5d", -123.45)
|
||||
-- t = string.format("%-8.5d", -123.45)
|
||||
-- u = string.format("%-+8.5d", -123.45)
|
||||
-- v = string.format("%5d", -12.3e10)
|
||||
-- w = string.format("%.d", -123.45)
|
||||
|
||||
-- assertTrue (a == ' -123', 'string.format() should format decimals correctly[31]')
|
||||
-- assertTrue (b == '-00000123', 'string.format() should format decimals correctly[32]')
|
||||
-- assertTrue (c == '-123', 'string.format() should format decimals correctly[33]')
|
||||
-- assertTrue (d == ' -123', 'string.format() should format decimals correctly[34]')
|
||||
-- assertTrue (e == ' -123', 'string.format() should format decimals correctly[35]')
|
||||
-- assertTrue (f == '-123 ', 'string.format() should format decimals correctly[36]')
|
||||
-- assertTrue (g == ' -123', 'string.format() should format decimals correctly[37]')
|
||||
-- assertTrue (h == ' -123', 'string.format() should format decimals correctly[38]')
|
||||
-- assertTrue (i == ' -123', 'string.format() should format decimals correctly[39]')
|
||||
-- assertTrue (j == '-123 ', 'string.format() should format decimals correctly[40]')
|
||||
-- assertTrue (k == '-123', 'string.format() should format decimals correctly[41]')
|
||||
-- assertTrue (l == '-00000123', 'string.format() should format decimals correctly[42]')
|
||||
-- assertTrue (m == '-00000123', 'string.format() should format decimals correctly[43]')
|
||||
-- assertTrue (n == '-00000123', 'string.format() should format decimals correctly[44]')
|
||||
-- assertTrue (o == '-00000123', 'string.format() should format decimals correctly[45]')
|
||||
-- assertTrue (p == '-00000123', 'string.format() should format decimals correctly[46]')
|
||||
-- assertTrue (q == '-00000123', 'string.format() should format decimals correctly[47]')
|
||||
-- assertTrue (r == ' -00123', 'string.format() should format decimals correctly[48]')
|
||||
-- assertTrue (s == ' -00123', 'string.format() should format decimals correctly[49]')
|
||||
-- assertTrue (t == '-00123 ', 'string.format() should format decimals correctly[50]')
|
||||
-- assertTrue (u == '-00123 ', 'string.format() should format decimals correctly[51]')
|
||||
-- assertTrue (v == '-123000000000', 'string.format() should format decimals correctly[52]')
|
||||
-- assertTrue (w == '-123', 'string.format() should format decimals correctly[53]')
|
||||
|
||||
-- a = string.format("%+05.d", 123.45)
|
||||
-- b = string.format("%05d", 123.45)
|
||||
-- c = string.format("%05d", -123.45)
|
||||
-- d = string.format("%+05d", 123.45)
|
||||
|
||||
-- assertTrue (a == ' +123', 'string.format() should format decimals correctly[60]')
|
||||
-- assertTrue (b == '00123', 'string.format() should format decimals correctly[61]')
|
||||
-- assertTrue (c == '-0123', 'string.format() should format decimals correctly[62]')
|
||||
-- assertTrue (d == '+0123', 'string.format() should format decimals correctly[63]')
|
||||
|
||||
-- a = string.format("%8f", 123.45)
|
||||
-- b = string.format("%.8f", 123.45)
|
||||
-- c = string.format("%.1f", 123.45)
|
||||
-- d = string.format("%8.2f", 123.45)
|
||||
-- e = string.format("%+8.2f", 123.45)
|
||||
-- f = string.format("%-8.3f", 123.45)
|
||||
-- g = string.format("%08.3f", 123.45)
|
||||
-- h = string.format("%#8.3f", 123.45)
|
||||
-- i = string.format("% 8.3f", 123.45)
|
||||
-- j = string.format("%+-0# 8.2f", 123.45)
|
||||
-- k = string.format("%0.2f", 123.45)
|
||||
-- l = string.format("%+.8f", 123.45)
|
||||
-- m = string.format("%-.8f", 123.45)
|
||||
-- n = string.format("%#.8f", 123.45)
|
||||
-- o = string.format("%9.3f", 123.45)
|
||||
-- p = string.format("%+9.3f", 123.45)
|
||||
-- q = string.format("%-9.3f", 123.45)
|
||||
-- r = string.format("%-+9.3f", 123.45)
|
||||
-- s = string.format("%.0f", 123.45)
|
||||
-- t = string.format("%.4f", 123.05)
|
||||
|
||||
-- assertTrue (a == '123.450000', 'string.format() should format floats correctly[1]')
|
||||
-- assertTrue (b == '123.45000000', 'string.format() should format floats correctly[2]')
|
||||
-- assertTrue (c == '123.5', 'string.format() should format floats correctly[3]')
|
||||
-- assertTrue (d == ' 123.45', 'string.format() should format floats correctly[4]')
|
||||
-- assertTrue (e == ' +123.45', 'string.format() should format floats correctly[5]')
|
||||
-- assertTrue (f == '123.450 ', 'string.format() should format floats correctly[6]')
|
||||
-- assertTrue (g == '0123.450', 'string.format() should format floats correctly[7]')
|
||||
-- assertTrue (h == ' 123.450', 'string.format() should format floats correctly[8]')
|
||||
-- assertTrue (i == ' 123.450', 'string.format() should format floats correctly[9]')
|
||||
-- assertTrue (j == '+123.45 ', 'string.format() should format floats correctly[10]')
|
||||
-- assertTrue (k == '123.45', 'string.format() should format floats correctly[11]')
|
||||
-- assertTrue (l == '+123.45000000', 'string.format() should format floats correctly[12]')
|
||||
-- assertTrue (m == '123.45000000', 'string.format() should format floats correctly[13]')
|
||||
-- assertTrue (n == '123.45000000', 'string.format() should format floats correctly[14]')
|
||||
-- assertTrue (o == ' 123.450', 'string.format() should format floats correctly[15]')
|
||||
-- assertTrue (p == ' +123.450', 'string.format() should format floats correctly[16]')
|
||||
-- assertTrue (q == '123.450 ', 'string.format() should format floats correctly[17]')
|
||||
-- assertTrue (r == '+123.450 ', 'string.format() should format floats correctly[18]')
|
||||
-- assertTrue (s == '123', 'string.format() should format floats correctly[19]')
|
||||
-- assertTrue (t == '123.0500', 'string.format() should format floats correctly[20]')
|
||||
|
||||
-- a = string.format("%x", 123)
|
||||
-- b = string.format("%x", 123.45)
|
||||
-- c = string.format("%x", -123)
|
||||
-- d = string.format("%4x", 123)
|
||||
-- e = string.format("%.4x", 123)
|
||||
-- f = string.format("%8.4x", 123)
|
||||
-- g = string.format("%+8.4x", 123)
|
||||
-- h = string.format("%-8.4x", 123)
|
||||
-- i = string.format("%#8.4x", 123)
|
||||
-- j = string.format("%08.4x", 123)
|
||||
-- k = string.format("% 8.4x", 123)
|
||||
-- l = string.format("%+-#0 8.4x", 123)
|
||||
-- m = string.format("%08x", 123)
|
||||
-- n = string.format("% x", 123)
|
||||
|
||||
-- assertTrue (a == '7b', 'string.format() should format hex correctly[1]')
|
||||
-- assertTrue (b == '7b', 'string.format() should format hex correctly[2]')
|
||||
-- assertTrue (c == 'ffffffffffffff85', 'string.format() should format hex correctly[3]')
|
||||
-- assertTrue (d == ' 7b', 'string.format() should format hex correctly[4]')
|
||||
-- assertTrue (e == '007b', 'string.format() should format hex correctly[5]')
|
||||
-- assertTrue (f == ' 007b', 'string.format() should format hex correctly[6]')
|
||||
-- assertTrue (g == ' 007b', 'string.format() should format hex correctly[7]')
|
||||
-- assertTrue (h == '007b ', 'string.format() should format hex correctly[8]')
|
||||
-- assertTrue (i == ' 0x007b', 'string.format() should format hex correctly[9]')
|
||||
-- assertTrue (k == ' 007b', 'string.format() should format hex correctly[11]')
|
||||
-- assertTrue (l == '0x007b ', 'string.format() should format hex correctly[12]')
|
||||
-- assertTrue (n == '7b', 'string.format() should format hex correctly[14]')
|
||||
|
||||
-- a = string.format("%8.2f\n", 1.234)
|
||||
-- b = string.format("\n%8.2f", 1.234)
|
||||
-- c = string.format("\n%8.2f\n", 1.234)
|
||||
|
||||
-- assertTrue (a == ' 1.23\n', 'string.format() should correctly format patterns that contain new lines.[1]')
|
||||
-- assertTrue (b == '\n 1.23', 'string.format() should correctly format patterns that contain new lines.[2]')
|
||||
-- assertTrue (c == '\n 1.23\n', 'string.format() should correctly format patterns that contain new lines.[3]')
|
||||
|
||||
-- -- TODO!!!!
|
||||
-- -- assertTrue (j == ' 007b', 'string.format() should format hex correctly[10]')
|
||||
-- -- assertTrue (m == '0000007b', 'string.format() should format hex correctly[13]')
|
||||
|
||||
-- -- print (c)
|
||||
|
||||
-- end
|
||||
|
||||
-- gmatch
|
||||
|
||||
local s = "from=world, to=Lua"
|
||||
local x = string.gmatch(s, "(%w+)=(%w+)")
|
||||
|
||||
assertTrue(type(x) == 'function', 'string.gmatch() should return an iterator function')
|
||||
|
||||
local a, b, c = x()
|
||||
assertTrue(a == 'from', 'string.gmatch() iterator should return the first group matched in the string [1]')
|
||||
assertTrue(b == 'world', 'string.gmatch() iterator should return the second group matched in the string [1]')
|
||||
assertTrue(c == nil, 'string.gmatch() iterator should return nil after all groups are matched [1]')
|
||||
|
||||
local a, b, c = x()
|
||||
assertTrue(a == 'to', 'string.gmatch() iterator should return the first group matched in the string [2]')
|
||||
assertTrue(b == 'Lua', 'string.gmatch() iterator should return the second group matched in the string [2]')
|
||||
assertTrue(c == nil, 'string.gmatch() iterator should return nil after all groups are matched [2]')
|
||||
|
||||
local a = x()
|
||||
assertTrue(a == nil, 'string.gmatch() iterator should return nil after all matches have ben returned')
|
||||
|
||||
local x = string.gmatch(s, "%w+=%w+")
|
||||
local a, b = x()
|
||||
assertTrue(a == 'from=world', 'string.gmatch() iterator should return the first match when no groups are specified')
|
||||
assertTrue(b == nil,
|
||||
'string.gmatch() iterator should return nil as second return value when no groups are specified [1]')
|
||||
|
||||
local a, b = x()
|
||||
assertTrue(a == 'to=Lua', 'string.gmatch() iterator should return the second match when no groups are specified')
|
||||
assertTrue(b == nil,
|
||||
'string.gmatch() iterator should return nil as second return value when no groups are specified [2]')
|
||||
|
||||
do
|
||||
local x = string.gmatch(';a;', 'a*')
|
||||
local a, b, c, d, e, f = x(), x(), x(), x(), x(), x();
|
||||
|
||||
assertEqual(a, '', 'string.gmatch() iterator should return correct values [1]')
|
||||
assertEqual(b, 'a', 'string.gmatch() iterator should return correct values [2]')
|
||||
assertEqual(c, '', 'string.gmatch() iterator should return correct values [3]')
|
||||
-- assertEqual(d, '', 'string.gmatch() iterator should return correct values [4]')
|
||||
assertEqual(e, nil, 'string.gmatch() iterator should return correct values [5]')
|
||||
assertEqual(e, nil, 'string.gmatch() iterator should return correct values [6]')
|
||||
end
|
||||
|
||||
-- gsub
|
||||
|
||||
a = '<%?xml version="1.0" encoding="UTF%-8"%?>'
|
||||
b = '<?xml version="1.0" encoding="UTF-8"?><my-xml></my-xml>'
|
||||
|
||||
c = string.gsub(b, a, 'moo')
|
||||
|
||||
assertTrue(c == 'moo<my-xml></my-xml>', 'string.gsub() should replace the matched part of the string[1]')
|
||||
-- Not even scraping the surface
|
||||
|
||||
a = '%%1'
|
||||
b = 'Hello %1'
|
||||
|
||||
c = string.gsub(b, a, 'world')
|
||||
assertTrue(c == 'Hello world', 'string.gsub() should replace the matched part of the string[2]')
|
||||
|
||||
a = '%d'
|
||||
b = 'ab5kfd8scf4lll'
|
||||
c = function(x)
|
||||
return '(' .. x .. ')'
|
||||
end
|
||||
|
||||
d = string.gsub(b, a, c, 2)
|
||||
assertTrue(d == 'ab(5)kfd(8)scf4lll',
|
||||
'string.gsub() should replace the matched part of the string with the value returned from the given map function')
|
||||
|
||||
a = "[^:]+"
|
||||
b = ":aa:bbb:cccc:ddddd:eee:"
|
||||
c = function(subStr)
|
||||
end
|
||||
|
||||
d = string.gsub(b, a, c)
|
||||
assertTrue(d == ':aa:bbb:cccc:ddddd:eee:',
|
||||
'string.gsub() should not replace the matched part of the string if the value returned from the map function is nil')
|
||||
|
||||
c = function(subStr)
|
||||
return 'X'
|
||||
end
|
||||
|
||||
d = string.gsub(b, a, c)
|
||||
assertTrue(d == ':X:X:X:X:X:',
|
||||
'string.gsub() should replace the matched part of the string if the value returned from the map function is not nil')
|
||||
|
||||
-- c = string.gsub(';a;', 'a*', 'ITEM')
|
||||
-- assertTrue(c == 'ITEM;ITEMITEM;ITEM', 'string.gsub() should replace the matched part of the string[2]')
|
||||
|
||||
a = 'abc\\def'
|
||||
b = string.gsub(a, '\\', '\\\\')
|
||||
assertEqual(b, 'abc\\\\def', 'string.gsub() should allow backslashes')
|
||||
|
||||
a = "a = 'a', b = 'b', c = 'c',"
|
||||
b = string.gsub(a, ",$", "")
|
||||
assertEqual(b, "a = 'a', b = 'b', c = 'c'", 'string.gsub() should match $ with end of string')
|
||||
|
||||
-- len
|
||||
|
||||
local a = 'McLaren Mercedes'
|
||||
|
||||
local b = string.len('');
|
||||
local c = string.len(a);
|
||||
|
||||
assertTrue(b == 0, 'string.len() should return 0 if passed an empty string')
|
||||
assertTrue(c == 16, 'string.len() should return the length of the string in the first argument')
|
||||
|
||||
-- lower
|
||||
|
||||
local a = 'McLaren Mercedes'
|
||||
|
||||
local b = string.lower('');
|
||||
local c = string.lower(a);
|
||||
|
||||
assertTrue(b == '', 'string.lower() should return an empty string if passed an empty string')
|
||||
assertTrue(c == 'mclaren mercedes',
|
||||
'string.lower() should return the string in the first argument with all character in lower case')
|
||||
|
||||
-- match
|
||||
|
||||
local a = string.match('20/11/1988', "^%d+%p%d+%p%d%d%d%d$")
|
||||
assertEqual(a, '20/11/1988', 'string.match() should handle punctuation.')
|
||||
|
||||
local a = ('foo@bar.com'):match("^[%w+%.%-_]+@[%w+%.%-_]+%.%a%a+$")
|
||||
|
||||
local a = ('test-123_test.2@a-b_c.movie'):match("^[%w+%.%-_]+@[%w+%.%-_]+%.%a%a+$")
|
||||
assertEqual(a, 'test-123_test.2@a-b_c.movie', 'string.match() should flatten nested groups.')
|
||||
|
||||
local a = ('-=[]\';'):match("%W")
|
||||
assertEqual(a, '-', 'string.match() match non-word chars.')
|
||||
|
||||
-- rep
|
||||
|
||||
local a = 'Ho'
|
||||
|
||||
local b = string.rep(a, 0);
|
||||
local c = string.rep(a, 1);
|
||||
local d = string.rep(a, 3);
|
||||
|
||||
assertTrue(b == '', 'string.rep() should return an empty string if the second argument is 0')
|
||||
assertTrue(c == 'Ho', 'string.rep() should return the first argument if the second argument is 1')
|
||||
assertTrue(d == 'HoHoHo',
|
||||
'string.rep() should return a string containing the first argument repeated the second argument number of times')
|
||||
|
||||
-- reverse
|
||||
|
||||
local a = string.reverse('');
|
||||
local b = string.reverse('x');
|
||||
local c = string.reverse('tpircSavaJ');
|
||||
|
||||
assertTrue(a == '', 'string.reverse() should return an empty string if passed an empty string')
|
||||
assertTrue(b == 'x', 'string.reverse() should return the first argument if its length is 1')
|
||||
assertTrue(c == 'JavaScript', 'string.reverse() should return a string containing the first argument reversed')
|
||||
|
||||
-- sub
|
||||
|
||||
local a = 'Pub Standards'
|
||||
|
||||
local b = string.sub(a, 1)
|
||||
local c = string.sub(a, 5)
|
||||
local d = string.sub(a, -4)
|
||||
|
||||
local e = string.sub(a, 1, 3)
|
||||
local f = string.sub(a, 7, 9)
|
||||
local g = string.sub(a, -4, -2)
|
||||
|
||||
local h = string.sub(a, 5, -2)
|
||||
local i = string.sub(a, 0)
|
||||
|
||||
assertTrue(b == 'Pub Standards', 'string.sub() should return the first argument if the second argument is 1')
|
||||
assertTrue(c == 'Standards',
|
||||
'string.sub() should return a subset of the first argument from the nth character onwards, when n is the second argument and positive')
|
||||
assertTrue(d == 'ards',
|
||||
'string.sub() should return the last n characters of the first argument, where n is the absolute value of the second argument and the second argument is negative')
|
||||
assertTrue(e == 'Pub',
|
||||
'string.sub() should return the first n characters of the first argument when the second argument is one and n is the third argument')
|
||||
assertTrue(f == 'and',
|
||||
'string.sub() should return a subset of the first argument from the nth character to the mth character, when n is the second argument and positive and m is the third argument and negative')
|
||||
|
||||
assertTrue(h == 'Standard',
|
||||
'string.sub() should return a subset of the first argument from the nth character to the last but mth character, when n is the second argument and positive and m is the third argument and negative')
|
||||
assertTrue(i == 'Pub Standards',
|
||||
'string.sub() should return a subset of the first argument from the last but nth character to the last but mth character, when n is the second argument and negative and m is the third argument and negative')
|
||||
|
||||
-- upper
|
||||
|
||||
local a = string.upper('');
|
||||
local b = string.upper('JavaScript');
|
||||
|
||||
assertTrue(a == '', 'string.upper() should return an empty string if passed an empty string')
|
||||
assertTrue(b == 'JAVASCRIPT', 'string.upper() should return the first argument in uppercase')
|
||||
|
||||
-- `string` lib as metatable of strings.
|
||||
local strMeta = getmetatable('')
|
||||
assertEqual(strMeta.__index, string, 'String lib should be metamethod of string instances.')
|
||||
|
||||
a = ('Hey'):lower()
|
||||
assertEqual(a, 'hey', 'String lib should be metamethod of string instances.')
|
||||
|
||||
Reference in New Issue
Block a user