From 0a8125e45cf5ab401ae71562bf5d1c35cce8231b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 21 Jan 2024 22:55:38 +0000 Subject: [PATCH 1/5] refactor: add hashtags example --- docs/Examples.md | 30 ++++++++++++- jest-setup.ts | 1 + src/__tests__/example-hashtags.ts | 24 ++++++++++ src/__tests__/example-ipv4.ts | 43 ++++++++++++++++++ ...{examples.test.ts => example-js-number.ts} | 36 --------------- test-utils/to-match-all-groups.ts | 45 +++++++++++++++++++ 6 files changed, 142 insertions(+), 37 deletions(-) create mode 100644 src/__tests__/example-hashtags.ts create mode 100644 src/__tests__/example-ipv4.ts rename src/__tests__/{examples.test.ts => example-js-number.ts} (57%) create mode 100644 test-utils/to-match-all-groups.ts diff --git a/docs/Examples.md b/docs/Examples.md index bd96a87..ef6e1e3 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -1,21 +1,47 @@ # Regex Examples -## JavaScript number +## Match hashtags + +```ts +const hashtags = buildRegExp( + [ + '#', + capture(oneOrMore(word)), + ], + { global: true }, + ); + +const hashtagMatches = '#hello #world'.matchAll(hashtags); +``` + +It matches and captures all hashtags in a given string. + +See tests: [exampple-hashtags.ts](../src/__tests__/example-hashtags.ts). + +## JavaScript number validation ```ts const sign = anyOf('+-'); const exponent = [anyOf('eE'), optional(sign), oneOrMore(digit)]; const regex = buildRegExp([ + startOfString, optional(sing), choiceOf( [oneOrMore(digit), optional(['.', zeroOrMore(digit)])], // leading digit ['.', oneOrMore(digit)], // leading dot ), optional(exponent), // exponent + endOfString, ]); + +const isValid = regex.test("1.0e+27"); ``` +It validates if given string is a valid JavaScript number. + +See tests: [exampple-hashtags.ts](../src/__tests__/example-js-number.ts). + ## IPv4 address validation ```ts @@ -43,3 +69,5 @@ This code generates the following regex pattern: const regex = /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/; ``` + +See tests: [exampple-hashtags.ts](../src/__tests__/example-ipv4.ts). \ No newline at end of file diff --git a/jest-setup.ts b/jest-setup.ts index b9a2c19..6919444 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -1,3 +1,4 @@ import './test-utils/to-have-pattern'; import './test-utils/to-match-groups'; +import './test-utils/to-match-all-groups'; import './test-utils/to-match-string'; diff --git a/src/__tests__/example-hashtags.ts b/src/__tests__/example-hashtags.ts new file mode 100644 index 0000000..9f55fb8 --- /dev/null +++ b/src/__tests__/example-hashtags.ts @@ -0,0 +1,24 @@ +import { buildRegExp } from '../builders'; +import { capture, oneOrMore, word } from '../index'; + +test('example: extracting hashtags', () => { + const regex = buildRegExp( + [ + '#', // prettier break-line + capture(oneOrMore(word)), + ], + { global: true }, + ); + + expect(regex).toMatchAllGroups('Hello #world!', [['#world', 'world']]); + expect(regex).toMatchAllGroups('#Hello #world!', [ + ['#Hello', 'Hello'], + ['#world', 'world'], + ]); + + expect(regex).not.toMatchString('aa'); + expect(regex).not.toMatchString('#'); + expect(regex).not.toMatchString('a# '); + + expect(regex).toHavePattern(/#(\w+)/g); +}); diff --git a/src/__tests__/example-ipv4.ts b/src/__tests__/example-ipv4.ts new file mode 100644 index 0000000..cb766c2 --- /dev/null +++ b/src/__tests__/example-ipv4.ts @@ -0,0 +1,43 @@ +import { + buildRegExp, + charRange, + choiceOf, + digit, + endOfString, + repeat, + startOfString, +} from '../index'; + +test('example: IPv4 address validator', () => { + const octet = choiceOf( + [digit], + [charRange('1', '9'), digit], + ['1', repeat(digit, 2)], + ['2', charRange('0', '4'), digit], + ['25', charRange('0', '5')], + ); + + const regex = buildRegExp([ + startOfString, // prettier break-line + repeat([octet, '.'], 3), + octet, + endOfString, + ]); + + expect(regex).toMatchString('0.0.0.0'); + expect(regex).toMatchString('192.168.0.1'); + expect(regex).toMatchString('1.99.100.249'); + expect(regex).toMatchString('255.255.255.255'); + expect(regex).toMatchString('123.45.67.89'); + + expect(regex).not.toMatchString('0.0.0.'); + expect(regex).not.toMatchString('0.0.0.0.'); + expect(regex).not.toMatchString('0.-1.0.0'); + expect(regex).not.toMatchString('0.1000.0.0'); + expect(regex).not.toMatchString('0.0.300.0'); + expect(regex).not.toMatchString('255.255.255.256'); + + expect(regex).toHavePattern( + /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/, + ); +}); diff --git a/src/__tests__/examples.test.ts b/src/__tests__/example-js-number.ts similarity index 57% rename from src/__tests__/examples.test.ts rename to src/__tests__/example-js-number.ts index 6455069..0fa4e6e 100644 --- a/src/__tests__/examples.test.ts +++ b/src/__tests__/example-js-number.ts @@ -1,13 +1,11 @@ import { anyOf, buildRegExp, - charRange, choiceOf, digit, endOfString, oneOrMore, optional, - repeat, startOfString, zeroOrMore, } from '../index'; @@ -50,37 +48,3 @@ test('example: validate JavaScript number', () => { expect(regex).toHavePattern(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/); }); - -test('example: IPv4 address validator', () => { - const octet = choiceOf( - [digit], - [charRange('1', '9'), digit], - ['1', repeat(digit, 2)], - ['2', charRange('0', '4'), digit], - ['25', charRange('0', '5')], - ); - - const regex = buildRegExp([ - startOfString, // - repeat([octet, '.'], 3), - octet, - endOfString, - ]); - - expect(regex).toMatchString('0.0.0.0'); - expect(regex).toMatchString('192.168.0.1'); - expect(regex).toMatchString('1.99.100.249'); - expect(regex).toMatchString('255.255.255.255'); - expect(regex).toMatchString('123.45.67.89'); - - expect(regex).not.toMatchString('0.0.0.'); - expect(regex).not.toMatchString('0.0.0.0.'); - expect(regex).not.toMatchString('0.-1.0.0'); - expect(regex).not.toMatchString('0.1000.0.0'); - expect(regex).not.toMatchString('0.0.300.0'); - expect(regex).not.toMatchString('255.255.255.256'); - - expect(regex).toHavePattern( - /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/, - ); -}); diff --git a/test-utils/to-match-all-groups.ts b/test-utils/to-match-all-groups.ts new file mode 100644 index 0000000..9023183 --- /dev/null +++ b/test-utils/to-match-all-groups.ts @@ -0,0 +1,45 @@ +import type { RegexSequence } from '../src/types'; +import { wrapRegExp } from './utils'; + +export function toMatchAllGroups( + this: jest.MatcherContext, + received: RegExp | RegexSequence, + expectedString: string, + expectedGroups: string[], +) { + const receivedRegex = wrapRegExp(received); + const receivedGroups = toNestedArray(expectedString.matchAll(receivedRegex)); + const options = { + isNot: this.isNot, + }; + + return { + pass: this.equals(receivedGroups, expectedGroups), + message: () => + this.utils.matcherHint('toMatchGroups', undefined, undefined, options) + + '\n\n' + + `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedGroups)}\n` + + `Received: ${this.utils.printReceived(receivedGroups)}`, + }; +} + +expect.extend({ toMatchAllGroups }); + +declare global { + namespace jest { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + interface Matchers { + toMatchAllGroups(input: string, expected: string[][]): R; + } + } +} + +function toNestedArray(iterator: IterableIterator) { + const result: string[][] = []; + + for (const match of iterator) { + result.push([...match]); + } + + return result; +} From 50e6fc2479fc5fa54f949dd54ad45e5d27b02e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 21 Jan 2024 22:57:36 +0000 Subject: [PATCH 2/5] refactor: improve matcher toEqualRegex --- src/__tests__/example-hashtags.ts | 2 +- src/__tests__/example-ipv4.ts | 2 +- src/__tests__/example-js-number.ts | 2 +- src/constructs/__tests__/anchors.test.tsx | 8 +-- src/constructs/__tests__/capture.test.tsx | 8 +-- .../__tests__/character-class.test.ts | 72 +++++++++---------- src/constructs/__tests__/choice-of.test.ts | 32 ++++----- src/constructs/__tests__/quantifiers.test.tsx | 24 +++---- src/constructs/__tests__/repeat.test.tsx | 16 ++--- src/encoder/__tests__/encoder.test.tsx | 58 +++++++-------- test-utils/to-have-pattern.ts | 15 ++-- 11 files changed, 119 insertions(+), 120 deletions(-) diff --git a/src/__tests__/example-hashtags.ts b/src/__tests__/example-hashtags.ts index 9f55fb8..55b420c 100644 --- a/src/__tests__/example-hashtags.ts +++ b/src/__tests__/example-hashtags.ts @@ -20,5 +20,5 @@ test('example: extracting hashtags', () => { expect(regex).not.toMatchString('#'); expect(regex).not.toMatchString('a# '); - expect(regex).toHavePattern(/#(\w+)/g); + expect(regex).toEqualRegex(/#(\w+)/g); }); diff --git a/src/__tests__/example-ipv4.ts b/src/__tests__/example-ipv4.ts index cb766c2..cea179c 100644 --- a/src/__tests__/example-ipv4.ts +++ b/src/__tests__/example-ipv4.ts @@ -37,7 +37,7 @@ test('example: IPv4 address validator', () => { expect(regex).not.toMatchString('0.0.300.0'); expect(regex).not.toMatchString('255.255.255.256'); - expect(regex).toHavePattern( + expect(regex).toEqualRegex( /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/, ); }); diff --git a/src/__tests__/example-js-number.ts b/src/__tests__/example-js-number.ts index 0fa4e6e..89bcf10 100644 --- a/src/__tests__/example-js-number.ts +++ b/src/__tests__/example-js-number.ts @@ -46,5 +46,5 @@ test('example: validate JavaScript number', () => { expect(regex).not.toMatchString('.1.1'); expect(regex).not.toMatchString('.'); - expect(regex).toHavePattern(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/); + expect(regex).toEqualRegex(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/); }); diff --git a/src/constructs/__tests__/anchors.test.tsx b/src/constructs/__tests__/anchors.test.tsx index ca9db58..1855c36 100644 --- a/src/constructs/__tests__/anchors.test.tsx +++ b/src/constructs/__tests__/anchors.test.tsx @@ -2,8 +2,8 @@ import { endOfString, startOfString } from '../anchors'; import { oneOrMore } from '../quantifiers'; test('`startOfString` basic cases', () => { - expect(startOfString).toHavePattern(/^/); - expect([startOfString, 'a', 'b']).toHavePattern(/^ab/); + expect(startOfString).toEqualRegex(/^/); + expect([startOfString, 'a', 'b']).toEqualRegex(/^ab/); }); test('`startOfString` regex tests', () => { @@ -11,8 +11,8 @@ test('`startOfString` regex tests', () => { }); test('`endOfString` basic cases', () => { - expect(endOfString).toHavePattern(/$/); - expect(['a', 'b', endOfString]).toHavePattern(/ab$/); + expect(endOfString).toEqualRegex(/$/); + expect(['a', 'b', endOfString]).toEqualRegex(/ab$/); }); test('`endOfString` regex tests', () => { diff --git a/src/constructs/__tests__/capture.test.tsx b/src/constructs/__tests__/capture.test.tsx index ae4d200..32d13c6 100644 --- a/src/constructs/__tests__/capture.test.tsx +++ b/src/constructs/__tests__/capture.test.tsx @@ -2,10 +2,10 @@ import { capture } from '../capture'; import { oneOrMore } from '../quantifiers'; test('`capture` base cases', () => { - expect(capture('a')).toHavePattern(/(a)/); - expect(capture('abc')).toHavePattern(/(abc)/); - expect(capture(oneOrMore('abc'))).toHavePattern(/((?:abc)+)/); - expect(oneOrMore(capture('abc'))).toHavePattern(/(abc)+/); + expect(capture('a')).toEqualRegex(/(a)/); + expect(capture('abc')).toEqualRegex(/(abc)/); + expect(capture(oneOrMore('abc'))).toEqualRegex(/((?:abc)+)/); + expect(oneOrMore(capture('abc'))).toEqualRegex(/(abc)+/); }); test('`capture` captures group', () => { diff --git a/src/constructs/__tests__/character-class.test.ts b/src/constructs/__tests__/character-class.test.ts index bc8d703..ab74564 100644 --- a/src/constructs/__tests__/character-class.test.ts +++ b/src/constructs/__tests__/character-class.test.ts @@ -12,34 +12,34 @@ import { import { buildRegExp } from '../../builders'; test('`any` character class', () => { - expect(any).toHavePattern(/./); - expect(['x', any]).toHavePattern(/x./); - expect(['x', any, 'x']).toHavePattern(/x.x/); + expect(any).toEqualRegex(/./); + expect(['x', any]).toEqualRegex(/x./); + expect(['x', any, 'x']).toEqualRegex(/x.x/); }); test('`digit` character class', () => { - expect(digit).toHavePattern(/\d/); - expect(['x', digit]).toHavePattern(/x\d/); - expect(['x', digit, 'x']).toHavePattern(/x\dx/); + expect(digit).toEqualRegex(/\d/); + expect(['x', digit]).toEqualRegex(/x\d/); + expect(['x', digit, 'x']).toEqualRegex(/x\dx/); }); test('`word` character class', () => { - expect(word).toHavePattern(/\w/); - expect(['x', word]).toHavePattern(/x\w/); - expect(['x', word, 'x']).toHavePattern(/x\wx/); + expect(word).toEqualRegex(/\w/); + expect(['x', word]).toEqualRegex(/x\w/); + expect(['x', word, 'x']).toEqualRegex(/x\wx/); }); test('`whitespace` character class', () => { - expect(whitespace).toHavePattern(/\s/); - expect(['x', whitespace]).toHavePattern(/x\s/); - expect(['x', whitespace, 'x']).toHavePattern(/x\sx/); + expect(whitespace).toEqualRegex(/\s/); + expect(['x', whitespace]).toEqualRegex(/x\s/); + expect(['x', whitespace, 'x']).toEqualRegex(/x\sx/); }); test('`charClass` base cases', () => { - expect(charClass(charRange('a', 'z'))).toHavePattern(/[a-z]/); - expect(charClass(charRange('a', 'z'), charRange('A', 'Z'))).toHavePattern(/[a-zA-Z]/); - expect(charClass(charRange('a', 'z'), anyOf('05'))).toHavePattern(/[a-z05]/); - expect(charClass(charRange('a', 'z'), whitespace, anyOf('05'))).toHavePattern(/[a-z\s05]/); + expect(charClass(charRange('a', 'z'))).toEqualRegex(/[a-z]/); + expect(charClass(charRange('a', 'z'), charRange('A', 'Z'))).toEqualRegex(/[a-zA-Z]/); + expect(charClass(charRange('a', 'z'), anyOf('05'))).toEqualRegex(/[a-z05]/); + expect(charClass(charRange('a', 'z'), whitespace, anyOf('05'))).toEqualRegex(/[a-z\s05]/); }); test('`charClass` throws on inverted arguments', () => { @@ -49,9 +49,9 @@ test('`charClass` throws on inverted arguments', () => { }); test('`charRange` base cases', () => { - expect(charRange('a', 'z')).toHavePattern(/[a-z]/); - expect(['x', charRange('0', '9')]).toHavePattern(/x[0-9]/); - expect([charRange('A', 'F'), 'x']).toHavePattern(/[A-F]x/); + expect(charRange('a', 'z')).toEqualRegex(/[a-z]/); + expect(['x', charRange('0', '9')]).toEqualRegex(/x[0-9]/); + expect([charRange('A', 'F'), 'x']).toEqualRegex(/[A-F]x/); }); test('`charRange` throws on incorrect arguments', () => { @@ -67,31 +67,31 @@ test('`charRange` throws on incorrect arguments', () => { }); test('`anyOf` base cases', () => { - expect(anyOf('a')).toHavePattern(/a/); - expect(['x', anyOf('a'), 'x']).toHavePattern(/xax/); - expect(anyOf('ab')).toHavePattern(/[ab]/); - expect(['x', anyOf('ab')]).toHavePattern(/x[ab]/); - expect(['x', anyOf('ab'), 'x']).toHavePattern(/x[ab]x/); + expect(anyOf('a')).toEqualRegex(/a/); + expect(['x', anyOf('a'), 'x']).toEqualRegex(/xax/); + expect(anyOf('ab')).toEqualRegex(/[ab]/); + expect(['x', anyOf('ab')]).toEqualRegex(/x[ab]/); + expect(['x', anyOf('ab'), 'x']).toEqualRegex(/x[ab]x/); }); test('`anyOf` with quantifiers', () => { - expect(['x', oneOrMore(anyOf('abc')), 'x']).toHavePattern(/x[abc]+x/); - expect(['x', optional(anyOf('abc')), 'x']).toHavePattern(/x[abc]?x/); - expect(['x', zeroOrMore(anyOf('abc')), 'x']).toHavePattern(/x[abc]*x/); + expect(['x', oneOrMore(anyOf('abc')), 'x']).toEqualRegex(/x[abc]+x/); + expect(['x', optional(anyOf('abc')), 'x']).toEqualRegex(/x[abc]?x/); + expect(['x', zeroOrMore(anyOf('abc')), 'x']).toEqualRegex(/x[abc]*x/); }); test('`anyOf` escapes special characters', () => { - expect(anyOf('abc-+.]\\')).toHavePattern(/[abc+.\]\\-]/); + expect(anyOf('abc-+.]\\')).toEqualRegex(/[abc+.\]\\-]/); }); test('`anyOf` moves hyphen to the last position', () => { - expect(anyOf('a-bc')).toHavePattern(/[abc-]/); + expect(anyOf('a-bc')).toEqualRegex(/[abc-]/); }); test('`anyOf` edge case caret and hyphen', () => { - expect(anyOf('^-')).toHavePattern(/[\^-]/); - expect(anyOf('-^')).toHavePattern(/[\^-]/); - expect(anyOf('-^a')).toHavePattern(/[a^-]/); + expect(anyOf('^-')).toEqualRegex(/[\^-]/); + expect(anyOf('-^')).toEqualRegex(/[\^-]/); + expect(anyOf('-^a')).toEqualRegex(/[a^-]/); }); test('`anyOf` throws on empty text', () => { @@ -101,13 +101,13 @@ test('`anyOf` throws on empty text', () => { }); test('`inverted` character class', () => { - expect(inverted(anyOf('a'))).toHavePattern(/[^a]/); - expect(inverted(anyOf('abc'))).toHavePattern(/[^abc]/); + expect(inverted(anyOf('a'))).toEqualRegex(/[^a]/); + expect(inverted(anyOf('abc'))).toEqualRegex(/[^abc]/); }); test('`inverted` character class double inversion', () => { - expect(inverted(inverted(anyOf('a')))).toHavePattern(/a/); - expect(inverted(inverted(anyOf('abc')))).toHavePattern(/[abc]/); + expect(inverted(inverted(anyOf('a')))).toEqualRegex(/a/); + expect(inverted(inverted(anyOf('abc')))).toEqualRegex(/[abc]/); }); test('`inverted` character class execution', () => { diff --git a/src/constructs/__tests__/choice-of.test.ts b/src/constructs/__tests__/choice-of.test.ts index 8a54634..1bc4206 100644 --- a/src/constructs/__tests__/choice-of.test.ts +++ b/src/constructs/__tests__/choice-of.test.ts @@ -3,33 +3,33 @@ import { repeat } from '../repeat'; import { choiceOf } from '../choice-of'; test('`choiceOf` using basic strings', () => { - expect(choiceOf('a')).toHavePattern(/a/); - expect(choiceOf('a', 'b')).toHavePattern(/a|b/); - expect(choiceOf('a', 'b', 'c')).toHavePattern(/a|b|c/); - expect(choiceOf('aaa', 'bbb')).toHavePattern(/aaa|bbb/); + expect(choiceOf('a')).toEqualRegex(/a/); + expect(choiceOf('a', 'b')).toEqualRegex(/a|b/); + expect(choiceOf('a', 'b', 'c')).toEqualRegex(/a|b|c/); + expect(choiceOf('aaa', 'bbb')).toEqualRegex(/aaa|bbb/); }); test('`choiceOf` used in sequence', () => { - expect(['x', choiceOf('a'), 'x']).toHavePattern(/xax/); - expect([choiceOf('a', 'b'), 'x']).toHavePattern(/(?:a|b)x/); - expect(['x', choiceOf('a', 'b')]).toHavePattern(/x(?:a|b)/); + expect(['x', choiceOf('a'), 'x']).toEqualRegex(/xax/); + expect([choiceOf('a', 'b'), 'x']).toEqualRegex(/(?:a|b)x/); + expect(['x', choiceOf('a', 'b')]).toEqualRegex(/x(?:a|b)/); - expect(choiceOf('a', 'b', 'c')).toHavePattern(/a|b|c/); - expect(['x', choiceOf('a', 'b', 'c')]).toHavePattern(/x(?:a|b|c)/); - expect([choiceOf('a', 'b', 'c'), 'x']).toHavePattern(/(?:a|b|c)x/); + expect(choiceOf('a', 'b', 'c')).toEqualRegex(/a|b|c/); + expect(['x', choiceOf('a', 'b', 'c')]).toEqualRegex(/x(?:a|b|c)/); + expect([choiceOf('a', 'b', 'c'), 'x']).toEqualRegex(/(?:a|b|c)x/); - expect(choiceOf('aaa', 'bbb')).toHavePattern(/aaa|bbb/); + expect(choiceOf('aaa', 'bbb')).toEqualRegex(/aaa|bbb/); }); test('`choiceOf` with sequence options', () => { - expect([choiceOf(['a', 'b'])]).toHavePattern(/ab/); - expect([choiceOf(['a', 'b'], ['c', 'd'])]).toHavePattern(/ab|cd/); - expect([choiceOf(['a', zeroOrMore('b')], [oneOrMore('c'), 'd'])]).toHavePattern(/ab*|c+d/); + expect([choiceOf(['a', 'b'])]).toEqualRegex(/ab/); + expect([choiceOf(['a', 'b'], ['c', 'd'])]).toEqualRegex(/ab|cd/); + expect([choiceOf(['a', zeroOrMore('b')], [oneOrMore('c'), 'd'])]).toEqualRegex(/ab*|c+d/); }); test('`choiceOf` using nested regex', () => { - expect(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toHavePattern(/a+|b*/); - expect(choiceOf(repeat('a', { min: 1, max: 3 }), repeat('bx', 5))).toHavePattern( + expect(choiceOf(oneOrMore('a'), zeroOrMore('b'))).toEqualRegex(/a+|b*/); + expect(choiceOf(repeat('a', { min: 1, max: 3 }), repeat('bx', 5))).toEqualRegex( /a{1,3}|(?:bx){5}/, ); }); diff --git a/src/constructs/__tests__/quantifiers.test.tsx b/src/constructs/__tests__/quantifiers.test.tsx index cb882c0..ae7d24b 100644 --- a/src/constructs/__tests__/quantifiers.test.tsx +++ b/src/constructs/__tests__/quantifiers.test.tsx @@ -2,18 +2,18 @@ import { digit } from '../character-class'; import { oneOrMore, optional, zeroOrMore } from '../quantifiers'; test('`oneOrMore` quantifier', () => { - expect(oneOrMore('a')).toHavePattern(/a+/); - expect(oneOrMore('ab')).toHavePattern(/(?:ab)+/); + expect(oneOrMore('a')).toEqualRegex(/a+/); + expect(oneOrMore('ab')).toEqualRegex(/(?:ab)+/); }); test('`optional` quantifier', () => { - expect(optional('a')).toHavePattern(/a?/); - expect(optional('ab')).toHavePattern(/(?:ab)?/); + expect(optional('a')).toEqualRegex(/a?/); + expect(optional('ab')).toEqualRegex(/(?:ab)?/); }); test('`zeroOrMore` quantifier', () => { - expect(zeroOrMore('a')).toHavePattern(/a*/); - expect(zeroOrMore('ab')).toHavePattern(/(?:ab)*/); + expect(zeroOrMore('a')).toEqualRegex(/a*/); + expect(zeroOrMore('ab')).toEqualRegex(/(?:ab)*/); }); test('`oneOrMore` does not generate capture when grouping', () => { @@ -29,11 +29,11 @@ test('`zeroOrMore` does not generate capture when grouping', () => { }); test('base quantifiers optimize grouping for atoms', () => { - expect(oneOrMore(digit)).toHavePattern(/\d+/); - expect(optional(digit)).toHavePattern(/\d?/); - expect(zeroOrMore(digit)).toHavePattern(/\d*/); + expect(oneOrMore(digit)).toEqualRegex(/\d+/); + expect(optional(digit)).toEqualRegex(/\d?/); + expect(zeroOrMore(digit)).toEqualRegex(/\d*/); - expect(oneOrMore('a')).toHavePattern(/a+/); - expect(optional('a')).toHavePattern(/a?/); - expect(zeroOrMore('a')).toHavePattern(/a*/); + expect(oneOrMore('a')).toEqualRegex(/a+/); + expect(optional('a')).toEqualRegex(/a?/); + expect(zeroOrMore('a')).toEqualRegex(/a*/); }); diff --git a/src/constructs/__tests__/repeat.test.tsx b/src/constructs/__tests__/repeat.test.tsx index 74b0fd6..1782919 100644 --- a/src/constructs/__tests__/repeat.test.tsx +++ b/src/constructs/__tests__/repeat.test.tsx @@ -3,18 +3,18 @@ import { oneOrMore, zeroOrMore } from '../quantifiers'; import { repeat } from '../repeat'; test('`repeat` quantifier', () => { - expect(['a', repeat('b', { min: 1, max: 5 })]).toHavePattern(/ab{1,5}/); - expect(['a', repeat('b', { min: 1 })]).toHavePattern(/ab{1,}/); - expect(['a', repeat('b', 1)]).toHavePattern(/ab{1}/); + expect(['a', repeat('b', { min: 1, max: 5 })]).toEqualRegex(/ab{1,5}/); + expect(['a', repeat('b', { min: 1 })]).toEqualRegex(/ab{1,}/); + expect(['a', repeat('b', 1)]).toEqualRegex(/ab{1}/); - expect(['a', repeat(['a', zeroOrMore('b')], 1)]).toHavePattern(/a(?:ab*){1}/); - expect(repeat(['text', ' ', oneOrMore('d')], 5)).toHavePattern(/(?:text d+){5}/); + expect(['a', repeat(['a', zeroOrMore('b')], 1)]).toEqualRegex(/a(?:ab*){1}/); + expect(repeat(['text', ' ', oneOrMore('d')], 5)).toEqualRegex(/(?:text d+){5}/); }); test('`repeat` optimizes grouping for atoms', () => { - expect(repeat(digit, 2)).toHavePattern(/\d{2}/); - expect(repeat(digit, { min: 2 })).toHavePattern(/\d{2,}/); - expect(repeat(digit, { min: 1, max: 5 })).toHavePattern(/\d{1,5}/); + expect(repeat(digit, 2)).toEqualRegex(/\d{2}/); + expect(repeat(digit, { min: 2 })).toEqualRegex(/\d{2,}/); + expect(repeat(digit, { min: 1, max: 5 })).toEqualRegex(/\d{1,5}/); }); test('`repeat` throws on no children', () => { diff --git a/src/encoder/__tests__/encoder.test.tsx b/src/encoder/__tests__/encoder.test.tsx index 86f9ce2..642fe63 100644 --- a/src/encoder/__tests__/encoder.test.tsx +++ b/src/encoder/__tests__/encoder.test.tsx @@ -3,44 +3,44 @@ import { oneOrMore, optional, zeroOrMore } from '../../constructs/quantifiers'; import { repeat } from '../../constructs/repeat'; test('basic quantifies', () => { - expect('a').toHavePattern(/a/); - expect(['a', 'b']).toHavePattern(/ab/); + expect('a').toEqualRegex(/a/); + expect(['a', 'b']).toEqualRegex(/ab/); - expect(oneOrMore('a')).toHavePattern(/a+/); - expect(optional('a')).toHavePattern(/a?/); + expect(oneOrMore('a')).toEqualRegex(/a+/); + expect(optional('a')).toEqualRegex(/a?/); - expect(['a', oneOrMore('b')]).toHavePattern(/ab+/); - expect(['a', oneOrMore('bc')]).toHavePattern(/a(?:bc)+/); - expect(['a', oneOrMore('bc')]).toHavePattern(/a(?:bc)+/); + expect(['a', oneOrMore('b')]).toEqualRegex(/ab+/); + expect(['a', oneOrMore('bc')]).toEqualRegex(/a(?:bc)+/); + expect(['a', oneOrMore('bc')]).toEqualRegex(/a(?:bc)+/); - expect(['a', repeat('b', { min: 1, max: 5 })]).toHavePattern(/ab{1,5}/); + expect(['a', repeat('b', { min: 1, max: 5 })]).toEqualRegex(/ab{1,5}/); - expect(['a', zeroOrMore('b')]).toHavePattern(/ab*/); - expect(['a', zeroOrMore('bc')]).toHavePattern(/a(?:bc)*/); - expect(['a', zeroOrMore('bc')]).toHavePattern(/a(?:bc)*/); + expect(['a', zeroOrMore('b')]).toEqualRegex(/ab*/); + expect(['a', zeroOrMore('bc')]).toEqualRegex(/a(?:bc)*/); + expect(['a', zeroOrMore('bc')]).toEqualRegex(/a(?:bc)*/); - expect([optional('a'), 'b']).toHavePattern(/a?b/); + expect([optional('a'), 'b']).toEqualRegex(/a?b/); - expect([optional('a'), 'b', oneOrMore('d')]).toHavePattern(/a?bd+/); + expect([optional('a'), 'b', oneOrMore('d')]).toEqualRegex(/a?bd+/); }); test('`buildPattern` escapes special characters', () => { - expect('.').toHavePattern(/\./); - expect('*').toHavePattern(/\*/); - expect('+').toHavePattern(/\+/); - expect('?').toHavePattern(/\?/); - expect('^').toHavePattern(/\^/); - expect('$').toHavePattern(/\$/); - expect('{').toHavePattern(/\{/); - expect('}').toHavePattern(/\}/); - expect('|').toHavePattern(/\|/); - expect('[').toHavePattern(/\[/); - expect(']').toHavePattern(/\]/); - expect('\\').toHavePattern(/\\/); - - expect('*.*').toHavePattern(/\*\.\*/); - - expect([oneOrMore('.*'), zeroOrMore('[]{}')]).toHavePattern(/(?:\.\*)+(?:\[\]\{\})*/); + expect('.').toEqualRegex(/\./); + expect('*').toEqualRegex(/\*/); + expect('+').toEqualRegex(/\+/); + expect('?').toEqualRegex(/\?/); + expect('^').toEqualRegex(/\^/); + expect('$').toEqualRegex(/\$/); + expect('{').toEqualRegex(/\{/); + expect('}').toEqualRegex(/\}/); + expect('|').toEqualRegex(/\|/); + expect('[').toEqualRegex(/\[/); + expect(']').toEqualRegex(/\]/); + expect('\\').toEqualRegex(/\\/); + + expect('*.*').toEqualRegex(/\*\.\*/); + + expect([oneOrMore('.*'), zeroOrMore('[]{}')]).toEqualRegex(/(?:\.\*)+(?:\[\]\{\})*/); }); test('`buildRegExp` throws error on unknown element', () => { diff --git a/test-utils/to-have-pattern.ts b/test-utils/to-have-pattern.ts index ce27116..81e79df 100644 --- a/test-utils/to-have-pattern.ts +++ b/test-utils/to-have-pattern.ts @@ -1,35 +1,34 @@ import type { RegexSequence } from '../src/types'; import { wrapRegExp } from './utils'; -export function toHavePattern( +export function toEqualRegex( this: jest.MatcherContext, received: RegExp | RegexSequence, expected: RegExp, ) { - const receivedPattern = wrapRegExp(received).source; - const expectedPattern = expected.source; + received = wrapRegExp(received); const options = { isNot: this.isNot, }; return { - pass: expectedPattern === receivedPattern, + pass: expected.source === received.source && expected.flags === received.flags, message: () => this.utils.matcherHint('toHavePattern', undefined, undefined, options) + '\n\n' + - `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expectedPattern)}\n` + - `Received: ${this.utils.printReceived(receivedPattern)}`, + `Expected: ${this.isNot ? 'not ' : ''}${this.utils.printExpected(expected)}\n` + + `Received: ${this.utils.printReceived(received)}`, }; } -expect.extend({ toHavePattern }); +expect.extend({ toEqualRegex }); declare global { namespace jest { // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Matchers { - toHavePattern(expected: RegExp): R; + toEqualRegex(expected: RegExp): R; } } } From 721c60bfee741ecfda05740252a7378a796fa80c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 21 Jan 2024 23:50:53 +0000 Subject: [PATCH 3/5] docs: add more examples --- README.md | 4 +- docs/Examples.md | 118 ++++++++++++++++++++++++----- src/__tests__/example-email.ts | 42 ++++++++++ src/__tests__/example-hex-color.ts | 46 +++++++++++ src/__tests__/example-url.ts | 50 ++++++++++++ 5 files changed, 241 insertions(+), 19 deletions(-) create mode 100644 src/__tests__/example-email.ts create mode 100644 src/__tests__/example-hex-color.ts create mode 100644 src/__tests__/example-url.ts diff --git a/README.md b/README.md index b969f84..3ebc817 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ const hexDigit = charClass( charRange('0', '9'), ); -const hexColor = buildRegExp( +const hexColor = buildRegExp([ startOfString, optional('#'), capture( @@ -31,7 +31,7 @@ const hexColor = buildRegExp( ), ), endOfString, -); +]); ``` ## Installation diff --git a/docs/Examples.md b/docs/Examples.md index ef6e1e3..c7d45d4 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -2,24 +2,113 @@ ## Match hashtags +This regex matches and captures all hashtags in a given string. + ```ts const hashtags = buildRegExp( - [ - '#', - capture(oneOrMore(word)), - ], - { global: true }, - ); + [ + '#', + capture(oneOrMore(word)), + ], + { global: true }, +); const hashtagMatches = '#hello #world'.matchAll(hashtags); ``` -It matches and captures all hashtags in a given string. +Encoded regex: `/#(\w+)/g`. + +See tests: [example-hashtags.ts](../src/__tests__/example-hashtags.ts). + +## Hex color validation + +This regex validate whether given string is a valid hex color, with 6 or 3 hex digits. + +```ts +const hexDigit = charClass( + charRange('a', 'f'), // + charRange('A', 'F'), + charRange('0', '9'), +); + +const hexColor = buildRegExp( + startOfString, + optional('#'), + capture( + choiceOf( + repeat(hexDigit, 6), // #rrggbb + repeat(hexDigit, 3), // #rgb + ), + ), + endOfString, +); +``` + +Encoded regex: `/^#?(?:[a-f\d]{6}|[a-f\d]{3})$/i`. + +See tests: [example-hex-color.ts](../src/__tests__/example-hex-color.ts). + +## Simple URL validation -See tests: [exampple-hashtags.ts](../src/__tests__/example-hashtags.ts). +This regex validates (in simplified way) whether given string is a URL. + +```ts +const protocol = [choiceOf('http', 'https'), '://']; +const domainChars = charClass(charRange('a', 'z'), digit); +const domainCharsHypen = charClass(domainChars, anyOf('-')); + +const domainSegment = choiceOf( + domainChars, // single char + [domainChars, zeroOrMore(domainCharsHypen), domainChars], // multi char +); + +const regex = buildRegExp([ + startOfString, + optional(protocol), + oneOrMore([domainSegment, '.']), // domain segment + charRange('a', 'z'), // TLD first char + oneOrMore(domainChars), // TLD remaining chars + endOfString, +]); +``` + +Encoded regex: `/^(?:(?:http|https):\/\/)?(?:(?:[a-z\d]|[a-z\d][a-z\d-]*[a-z\d])\.)+[a-z][a-z\d]+$/`. + +See tests: [example-url.ts](../src/__tests__/example-url.ts). + +## Email address validation + +This regex validates whether given string is a properly formatted email address. + +```ts +const hostnameChars = charClass(charRange('a', 'z'), digit, anyOf('-.')); +const domainChars = charRange('a', 'z'); + +const regex = buildRegExp( + [ + startOfString, + oneOrMore(usernameChars), + '@', + oneOrMore(hostnameChars), + '.', + repeat(domainChars, { min: 2 }), + endOfString, + ], + { ignoreCase: true }, +); + +const isValid = regex.test("user@example.com"); +``` + +Encoded regex: `/^[a-z\d._%+-]+@[a-z\d.-]+\.[a-z]{2,}$/i`. + +See tests: [example-email.ts](../src/__tests__/example-email.ts). ## JavaScript number validation +This regex validates if given string is a valid JavaScript number. + + ```ts const sign = anyOf('+-'); const exponent = [anyOf('eE'), optional(sign), oneOrMore(digit)]; @@ -38,9 +127,9 @@ const regex = buildRegExp([ const isValid = regex.test("1.0e+27"); ``` -It validates if given string is a valid JavaScript number. +Encoded regex: `/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/`. -See tests: [exampple-hashtags.ts](../src/__tests__/example-js-number.ts). +See tests: [example-js-number.ts](../src/__tests__/example-js-number.ts). ## IPv4 address validation @@ -63,11 +152,6 @@ const regex = buildRegExp([ ]); ``` -This code generates the following regex pattern: - -```ts -const regex = - /^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/; -``` +Encoded regex: `/^(?:(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d{2}|2[0-4]\d|25[0-5])$/`. -See tests: [exampple-hashtags.ts](../src/__tests__/example-ipv4.ts). \ No newline at end of file +See tests: [example-ipv4.ts](../src/__tests__/example-ipv4.ts). \ No newline at end of file diff --git a/src/__tests__/example-email.ts b/src/__tests__/example-email.ts new file mode 100644 index 0000000..ad508a6 --- /dev/null +++ b/src/__tests__/example-email.ts @@ -0,0 +1,42 @@ +import { + anyOf, + buildRegExp, + charClass, + charRange, + digit, + endOfString, + oneOrMore, + repeat, + startOfString, +} from '../index'; + +test('example: email validation', () => { + const usernameChars = charClass(charRange('a', 'z'), digit, anyOf('._%+-')); + const hostnameChars = charClass(charRange('a', 'z'), digit, anyOf('-.')); + const domainChars = charRange('a', 'z'); + + const regex = buildRegExp( + [ + startOfString, + oneOrMore(usernameChars), + '@', + oneOrMore(hostnameChars), + '.', + repeat(domainChars, { min: 2 }), + endOfString, + ], + { ignoreCase: true }, + ); + + expect(regex).toMatchString('aaa@gmail.co'); + expect(regex).toMatchString('aaa@gmail.com'); + expect(regex).toMatchString('Aaa@GMail.Com'); + expect(regex).toMatchString('aaa@long.domain.example.com'); + + expect(regex).not.toMatchString('@'); + expect(regex).not.toMatchString('aaa@'); + expect(regex).not.toMatchString('a@gmail.c'); + expect(regex).not.toMatchString('@gmail.com'); + + expect(regex).toEqualRegex(/^[a-z\d._%+-]+@[a-z\d.-]+\.[a-z]{2,}$/i); +}); diff --git a/src/__tests__/example-hex-color.ts b/src/__tests__/example-hex-color.ts new file mode 100644 index 0000000..31632f7 --- /dev/null +++ b/src/__tests__/example-hex-color.ts @@ -0,0 +1,46 @@ +import { + buildRegExp, + charClass, + charRange, + choiceOf, + digit, + endOfString, + optional, + repeat, + startOfString, +} from '../index'; + +test('example: hex color validation', () => { + const hexDigit = charClass(digit, charRange('a', 'f')); + + const regex = buildRegExp( + [ + startOfString, + optional('#'), + choiceOf( + repeat(hexDigit, 6), // #rrggbb + repeat(hexDigit, 3), // #rgb + ), + endOfString, + ], + { ignoreCase: true }, + ); + + expect(regex).toMatchString('#ffffff'); + expect(regex).toMatchString('ffffff'); + expect(regex).toMatchString('#eee'); + expect(regex).toMatchString('bbb'); + expect(regex).toMatchString('#000'); + expect(regex).toMatchString('#123456'); + expect(regex).toMatchString('123456'); + expect(regex).toMatchString('#123'); + expect(regex).toMatchString('123'); + + expect(regex).not.toMatchString('#1'); + expect(regex).not.toMatchString('#12'); + expect(regex).not.toMatchString('#1234'); + expect(regex).not.toMatchString('#12345'); + expect(regex).not.toMatchString('#1234567'); + + expect(regex).toEqualRegex(/^#?(?:[a-f\d]{6}|[a-f\d]{3})$/i); +}); diff --git a/src/__tests__/example-url.ts b/src/__tests__/example-url.ts new file mode 100644 index 0000000..1a693db --- /dev/null +++ b/src/__tests__/example-url.ts @@ -0,0 +1,50 @@ +import { + anyOf, + buildRegExp, + charClass, + charRange, + choiceOf, + digit, + endOfString, + oneOrMore, + optional, + startOfString, + zeroOrMore, +} from '../index'; + +// Modified from: https://stackoverflow.com/a/2015516 +test('example: simple url validation', () => { + const protocol = [choiceOf('http', 'https'), '://']; + const domainChars = charClass(charRange('a', 'z'), digit); + const domainCharsHypen = charClass(domainChars, anyOf('-')); + + const domainSegment = choiceOf( + domainChars, // single char + [domainChars, zeroOrMore(domainCharsHypen), domainChars], // multi char + ); + + const regex = buildRegExp([ + startOfString, + optional(protocol), + oneOrMore([domainSegment, '.']), // domain segment + charRange('a', 'z'), // TLD first char + oneOrMore(domainChars), // TLD remaining chars + endOfString, + ]); + + expect(regex).toMatchString('example.com'); + expect(regex).toMatchString('beta.example.com'); + expect(regex).toMatchString('http://beta.example.com'); + expect(regex).toMatchString('https://beta.example.com'); + expect(regex).toMatchString('a.co'); + + expect(regex).not.toMatchString('example'); + expect(regex).not.toMatchString('aaa.a'); + expect(regex).not.toMatchString('a.-a.com'); + expect(regex).not.toMatchString('a.-a.com'); + expect(regex).not.toMatchString('@gmail.com'); + + expect(regex).toEqualRegex( + /^(?:(?:http|https):\/\/)?(?:(?:[a-z\d]|[a-z\d][a-z\d-]*[a-z\d])\.)+[a-z][a-z\d]+$/, + ); +}); From 9aa0b5f73a365e095bc57133f60efde990431603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Sun, 21 Jan 2024 23:53:24 +0000 Subject: [PATCH 4/5] refactor: cleanup --- jest-setup.ts | 2 +- test-utils/{to-have-pattern.ts => to-equal-regex.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename test-utils/{to-have-pattern.ts => to-equal-regex.ts} (100%) diff --git a/jest-setup.ts b/jest-setup.ts index 6919444..3411846 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -1,4 +1,4 @@ -import './test-utils/to-have-pattern'; +import './test-utils/to-equal-regex'; import './test-utils/to-match-groups'; import './test-utils/to-match-all-groups'; import './test-utils/to-match-string'; diff --git a/test-utils/to-have-pattern.ts b/test-utils/to-equal-regex.ts similarity index 100% rename from test-utils/to-have-pattern.ts rename to test-utils/to-equal-regex.ts From c97b90dd2da2f5bde95858c472894ce4a300c271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Jastrze=CC=A8bski?= Date: Mon, 22 Jan 2024 08:55:42 +0000 Subject: [PATCH 5/5] chore: updates --- docs/Examples.md | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/Examples.md b/docs/Examples.md index c7d45d4..e49064b 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -25,22 +25,19 @@ See tests: [example-hashtags.ts](../src/__tests__/example-hashtags.ts). This regex validate whether given string is a valid hex color, with 6 or 3 hex digits. ```ts -const hexDigit = charClass( - charRange('a', 'f'), // - charRange('A', 'F'), - charRange('0', '9'), -); +const hexDigit = charClass(digit, charRange('a', 'f')); -const hexColor = buildRegExp( - startOfString, - optional('#'), - capture( +const regex = buildRegExp( + [ + startOfString, + optional('#'), choiceOf( repeat(hexDigit, 6), // #rrggbb repeat(hexDigit, 3), // #rgb ), - ), - endOfString, + endOfString, + ], + { ignoreCase: true }, ); ```