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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions codecov.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
coverage:
status:
project:
default:
target: auto
threshold: 1%
32 changes: 32 additions & 0 deletions src/Procedures/procMatchRegExp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createRequestMatchRegExp, procMatchRegExp, isMatchRegExpResponse, } from './procMatchRegExp';
import { Request, isErrorResponse } from './procedure';
import { createId } from './uniqueId';

describe('procMatchRegExp', () => {
test('basic', () => {
const req = createRequestMatchRegExp({ text: 'two words', regexp: /w\w+/g });
const result = procMatchRegExp(req);
expect(isMatchRegExpResponse(result)).toBe(true);
const response = isMatchRegExpResponse(result) ? result : undefined;
expect(response?.data.elapsedTimeMs).toBeGreaterThan(0);
expect(response?.data.ranges).toEqual(new Uint32Array([1, 3, 4, 9]));
});

test('non-RequestMatchRegExp', () => {
const req: Request = { id: createId(), requestType: 'unknown', data: { text: 'two words', regexp: /w\w+/g }};
const result = procMatchRegExp(req);
expect(isMatchRegExpResponse(result)).toBe(false);
expect(result).toBeUndefined();
});

test('RequestExecRegExp bad regex', () => {
const req: Request = createRequestMatchRegExp({ text: 'two words', regexp: '/[/g' });
const result = procMatchRegExp(req);
const response = isErrorResponse(result) ? result : undefined;
expect(isMatchRegExpResponse(result)).toBe(false);
expect(isErrorResponse(result)).toBe(true);
expect(response?.id).toBe(req.id);
expect(response?.data.requestType).toBe(req.requestType);
expect(response?.data.message).toContain('SyntaxError');
});
});
53 changes: 53 additions & 0 deletions src/Procedures/procMatchRegExp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
createErrorResponse,
createRequest,
createResponse,
ErrorResponse,
genIsRequest,
genIsResponse,
Request,
Response,
} from './procedure';
import { toRegExp, matchRegExp, MatchRegExpResult } from '../helpers/evaluateRegExp';

export const requestTypeMatchRegExp = 'MatchRegExp';
export type MatchRegExpRequestType = typeof requestTypeMatchRegExp;
export type MatchRegExpResponseType = MatchRegExpRequestType;

export interface RequestMatchRegExp extends Request {
requestType: MatchRegExpRequestType;
data: {
text: string;
regexp: RegExp | string;
};
}

export interface ResponseMatchRegExp extends Response {
responseType: MatchRegExpRequestType;
data: MatchRegExpResult;
}

export const isMatchRegExpRequest = genIsRequest<RequestMatchRegExp>(requestTypeMatchRegExp);
export const isMatchRegExpResponse = genIsResponse<ResponseMatchRegExp>(requestTypeMatchRegExp);

export function procMatchRegExp(r: RequestMatchRegExp): ResponseMatchRegExp | ErrorResponse;
export function procMatchRegExp(r: Request): Response | undefined;
export function procMatchRegExp(r: RequestMatchRegExp | Request): ResponseMatchRegExp | ErrorResponse | undefined {
if (!isMatchRegExpRequest(r)) return undefined;
try {
const regex = toRegExp(r.data.regexp);
const regexResult = matchRegExp(r.data.text, regex);

return createResponseMatchRegExp(r, regexResult);
} catch (e) {
return createErrorResponse(r, e.toString());
}
}

export function createRequestMatchRegExp(data: RequestMatchRegExp['data']): RequestMatchRegExp {
return createRequest(requestTypeMatchRegExp, data);
}

export function createResponseMatchRegExp(request: RequestMatchRegExp, data: ResponseMatchRegExp['data']): ResponseMatchRegExp {
return createResponse(request.id, request.requestType, data);
}
32 changes: 32 additions & 0 deletions src/Procedures/procMatchRegExpArray.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createRequestMatchRegExpArray, procMatchRegExpArray, isMatchRegExpArrayResponse, } from './procMatchRegExpArray';
import { Request, isErrorResponse } from './procedure';
import { createId } from './uniqueId';

describe('procMatchRegExpArray', () => {
test('basic', () => {
const req = createRequestMatchRegExpArray({ text: 'two words', regexps: [/w\w+/g] });
const result = procMatchRegExpArray(req);
expect(isMatchRegExpArrayResponse(result)).toBe(true);
const response = isMatchRegExpArrayResponse(result) ? result : undefined;
expect(response?.data.elapsedTimeMs).toBeGreaterThan(0);
expect(response?.data.results[0].ranges).toEqual(new Uint32Array([1, 3, 4, 9]));
});

test('non-RequestMatchRegExpArray', () => {
const req: Request = { id: createId(), requestType: 'unknown', data: { text: 'two words', regexps: [/w\w+/g] }};
const result = procMatchRegExpArray(req);
expect(isMatchRegExpArrayResponse(result)).toBe(false);
expect(result).toBeUndefined();
});

test('RequestExecRegExp bad regex', () => {
const req: Request = createRequestMatchRegExpArray({ text: 'two words', regexps: ['/[/g'] });
const result = procMatchRegExpArray(req);
const response = isErrorResponse(result) ? result : undefined;
expect(isMatchRegExpArrayResponse(result)).toBe(false);
expect(isErrorResponse(result)).toBe(true);
expect(response?.id).toBe(req.id);
expect(response?.data.requestType).toBe(req.requestType);
expect(response?.data.message).toContain('SyntaxError');
});
});
53 changes: 53 additions & 0 deletions src/Procedures/procMatchRegExpArray.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
createErrorResponse,
createRequest,
createResponse,
ErrorResponse,
genIsRequest,
genIsResponse,
Request,
Response,
} from './procedure';
import { toRegExp, MatchRegExpArrayResult, matchRegExpArray } from '../helpers/evaluateRegExp';

export const requestTypeMatchRegExpArray = 'MatchRegExpArray';
export type MatchRegExpArrayRequestType = typeof requestTypeMatchRegExpArray;
export type MatchRegExpArrayResponseType = MatchRegExpArrayRequestType;

export interface RequestMatchRegExpArray extends Request {
requestType: MatchRegExpArrayRequestType;
data: {
text: string;
regexps: (RegExp | string)[];
};
}

export interface ResponseMatchRegExpArray extends Response {
responseType: MatchRegExpArrayResponseType;
data: MatchRegExpArrayResult;
}

export const isMatchRegExpArrayRequest = genIsRequest<RequestMatchRegExpArray>(requestTypeMatchRegExpArray);
export const isMatchRegExpArrayResponse = genIsResponse<ResponseMatchRegExpArray>(requestTypeMatchRegExpArray);

export function procMatchRegExpArray(r: RequestMatchRegExpArray): ResponseMatchRegExpArray | ErrorResponse;
export function procMatchRegExpArray(r: Request): undefined | ResponseMatchRegExpArray | ErrorResponse;
export function procMatchRegExpArray(r: RequestMatchRegExpArray | Request): ResponseMatchRegExpArray | ErrorResponse | undefined {
if (!isMatchRegExpArrayRequest(r)) return undefined;
try {
const regex = r.data.regexps.map(r => toRegExp(r));
const regexResult = matchRegExpArray(r.data.text, regex);

return createResponseMatchRegExpArray(r, regexResult);
} catch (e) {
return createErrorResponse(r, e.toString());
}
}

export function createRequestMatchRegExpArray(data: RequestMatchRegExpArray['data']): RequestMatchRegExpArray {
return createRequest(requestTypeMatchRegExpArray, data);
}

export function createResponseMatchRegExpArray(request: RequestMatchRegExpArray, data: ResponseMatchRegExpArray['data']): ResponseMatchRegExpArray {
return createResponse(request.id, request.requestType, data);
}
6 changes: 5 additions & 1 deletion src/Procedures/procedures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { procSleep } from './procSleep';
import { procSpin } from './procSpin';
import { procExecRegExpMatrix } from './procExecRegExpMatrix';
import { procGenError } from './procGenError';
import { procMatchRegExpArray } from './procMatchRegExpArray';
import { procMatchRegExp } from './procMatchRegExp';

type Procedure = (r: Request) => Promise<Response> | Response | undefined;
type Procedure = (r: Request) => any

export const procedures: Procedure[] = [
procExecRegExp,
procExecRegExpMatrix,
procMatchRegExp,
procMatchRegExpArray,
procEcho,
procSleep,
procSpin,
Expand Down
22 changes: 22 additions & 0 deletions src/RegExpWorker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,28 @@ describe('RegExpWorker', () => {
})
);

test(
'matchRegExp',
run(async (w) => {
const text = 'hello\nthere';
const r = await w.matchRegExp(text, /\w/g, );
expect([...r.ranges].map((m) => text.slice(...m))).toEqual('hellothere'.split(''));
expect(r.elapsedTimeMs).toBeGreaterThan(0);
})
);

test(
'matchRegExpArray',
run(async (w) => {
const text = 'hello\nthere';
const r = await w.matchRegExpArray(text, [/\w/g], );
expect([...r.results[0].ranges].map((m) => text.slice(...m))).toEqual('hellothere'.split(''));
expect(r.elapsedTimeMs).toBeGreaterThan(0);
expect(r.results[0].elapsedTimeMs).toBeGreaterThan(0);
expect(r.elapsedTimeMs).toBeGreaterThanOrEqual(r.results[0].elapsedTimeMs)
})
);

test(
'set timeout',
run(async (worker) => {
Expand Down
73 changes: 70 additions & 3 deletions src/RegExpWorker.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { ExecRegExpResult, ExecRegExpMatrixResult } from './helpers/evaluateRegExp';
import {
ExecRegExpMatrixResult,
ExecRegExpResult,
FlatRanges,
flatRangesToRanges,
MatchRegExpArrayResult as _MatchRegExpArrayResult,
MatchRegExpResult as _MatchRegExpResult,
Range,
} from './helpers/evaluateRegExp';
import { Scheduler } from './scheduler';
import {
createRequestExecRegExp,
Expand All @@ -7,8 +15,10 @@ import {
RequestExecRegExp,
RequestExecRegExpMatrix,
} from './Procedures';
import { RequestMatchRegExp, createRequestMatchRegExp } from './Procedures/procMatchRegExp';
import { RequestMatchRegExpArray, createRequestMatchRegExpArray } from './Procedures/procMatchRegExpArray';

export { ExecRegExpResult, ExecRegExpMatrixResult, toRegExp } from './helpers/evaluateRegExp';
export { ExecRegExpResult, ExecRegExpMatrixResult, toRegExp, Range } from './helpers/evaluateRegExp';

export interface TimeoutError {
message: string;
Expand All @@ -33,9 +43,31 @@ export class RegExpWorker {
return this.makeRequest(req, timeLimitMs);
}

public matchRegExp(text: string, regExp: RegExp, timeLimitMs?: number): Promise<MatchRegExpResult> {
const req = createRequestMatchRegExp({ regexp: regExp, text });
return this.makeRequest(req, timeLimitMs)
.then(MatchRegExpResult.create);
}

public matchRegExpArray(text: string, regExp: RegExp[], timeLimitMs?: number): Promise<MatchRegExpArrayResult> {
const req = createRequestMatchRegExpArray({ regexps: regExp, text });
return this.makeRequest(req, timeLimitMs).then(MatchRegExpArrayResult.create);
}

private makeRequest(req: RequestExecRegExp, timeLimitMs: number | undefined): Promise<ExecRegExpResult>;
private makeRequest(req: RequestExecRegExpMatrix, timeLimitMs: number | undefined): Promise<ExecRegExpMatrixResult>;
private makeRequest(req: RequestExecRegExp | RequestExecRegExpMatrix, timeLimitMs: number | undefined): Promise<ExecRegExpResult> | Promise<ExecRegExpMatrixResult> {
private makeRequest(req: RequestMatchRegExp, timeLimitMs: number | undefined): Promise<_MatchRegExpResult>;
private makeRequest(req: RequestMatchRegExpArray, timeLimitMs: number | undefined): Promise<_MatchRegExpArrayResult>;
private makeRequest(
req: RequestExecRegExp
| RequestExecRegExpMatrix
| RequestMatchRegExp
| RequestMatchRegExpArray,
timeLimitMs: number | undefined
): Promise<ExecRegExpResult>
| Promise<ExecRegExpMatrixResult>
| Promise<_MatchRegExpResult>
| Promise<_MatchRegExpArrayResult> {
return this.scheduler.scheduleRequest(req, timeLimitMs).then(extractResult, timeoutRejection);
}

Expand Down Expand Up @@ -76,3 +108,38 @@ export function execRegExpMatrixOnWorker(regExpArray: RegExp[], textArray: strin
const worker = new RegExpWorker();
return worker.execRegExpMatrix(regExpArray, textArray, timeLimitMs).finally(worker.dispose);
}

export class MatchRegExpResult {
private constructor(
readonly elapsedTimeMs: number,
readonly raw_ranges: FlatRanges
) {
}

/**
* The range tuples that matched the full regular expression.
* Each tuple is: [startIndex, endIndex]
*/
get ranges(): IterableIterator<Range> {
return flatRangesToRanges(this.raw_ranges);
}

static create(res: _MatchRegExpResult): MatchRegExpResult {
return new MatchRegExpResult(res.elapsedTimeMs, res.ranges);
}
}

export class MatchRegExpArrayResult {
constructor(
readonly elapsedTimeMs: number,
readonly results: MatchRegExpResult[]
) {
}

static create(res: _MatchRegExpArrayResult): MatchRegExpArrayResult {
return new MatchRegExpArrayResult(
res.elapsedTimeMs,
res.results.map(MatchRegExpResult.create),
);
}
}
47 changes: 42 additions & 5 deletions src/helpers/evaluateRegExp.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { execRegExp, ExecRegExpResult, toRegExp, execRegExpMatrix } from './evaluateRegExp';
import { execRegExp, ExecRegExpResult, toRegExp, execRegExpMatrix, execRegExpArray, FlatRanges, flatRangesToRanges, matchRegExp, matchRegExpArray } from './evaluateRegExp';
import * as fs from 'fs';
import * as Path from 'path';

Expand All @@ -14,7 +14,7 @@ Numbers: 1, 2, 3, 4, 1000, -55.0, 1.34e2
const x2 = 'hello';
`
const code = fs.readFileSync(Path.join(__filename), 'utf8');
const w = (result: ExecRegExpResult) => matchesToText(result.matches);
const w = (result: ExecRegExpResult) => resultsToTexts(result.matches);

test('evaluateRegExp', () => {
const words = w(execRegExp(/\w+/g, text));
Expand Down Expand Up @@ -52,16 +52,53 @@ const x2 = 'hello';
expect(result.matrix[2].results).toHaveLength(2)
expect(result.matrix[2].results[1].matches).toHaveLength(1)
});

test('execRegExpArray', () => {
const r = execRegExpArray([/\w+/g], text);
expect(r.results.map(r => r.matches).map(resultsToTexts))
.toEqual([w(execRegExp(/\w+/g, text))]);
});

test('matchRegExp', () => {
const r = matchRegExp(text, /\w+/g);
const words = [...flatRangesToTexts(r.ranges, text)];
expect(words).toEqual(text.split(/\b/g).map(s => s.replace(/[^\w]/g, '')).filter(notEmpty));
expect(r.elapsedTimeMs).toBeGreaterThan(0);
expect(r.elapsedTimeMs).toBeLessThan(100);
});

test('matchRegExpArray', () => {
const regExps = [/\w+/g, /\d+/g];
const r = matchRegExpArray(text, regExps);
expect(r.elapsedTimeMs).toBeGreaterThan(0);
expect(r.elapsedTimeMs).toBeLessThan(100);
expect(r.elapsedTimeMs).toBeGreaterThan(r.results.map(a => a.elapsedTimeMs).reduce( (a, b) => a + b,0));
for (let i = 0; i < r.results.length; ++i) {
const result = r.results[i];
const regexp = regExps[i];
expect(result.elapsedTimeMs).toBeGreaterThan(0);
expect(result.elapsedTimeMs).toBeLessThan(100);
const expectedWords = w(execRegExp(regexp, text));
const words = [...flatRangesToTexts(result.ranges, text)];
expect(words).toEqual(expectedWords);
}
});
});

function notEmpty<T>(v: T | null | undefined | '' | 0): v is T {
return !!v;
}

function matchToText(match: RegExpExecArray): string {
function regExpExecArrayToText(match: RegExpExecArray): string {
return match[0];
}

function matchesToText(matches: RegExpExecArray[]): string[] {
return matches.map(matchToText);
function resultsToTexts(matches: RegExpExecArray[]): string[] {
return matches.map(regExpExecArrayToText);
}

function *flatRangesToTexts(ranges: FlatRanges, text: string): IterableIterator<string> {
for (const [start, end] of flatRangesToRanges(ranges)) {
yield text.slice(start, end);
}
}
Loading