diff --git a/src/components/__tests__/character-class.test.ts b/src/components/__tests__/character-class.test.ts index 68f47a4..f73481b 100644 --- a/src/components/__tests__/character-class.test.ts +++ b/src/components/__tests__/character-class.test.ts @@ -5,9 +5,11 @@ import { anyOf, digit, encodeCharacterClass, + inverted, whitespace, word, } from '../character-class'; +import { execRegex } from '../../test-utils'; test('"whitespace" character class', () => { expect(buildPattern(whitespace)).toEqual(`\\s`); @@ -60,17 +62,33 @@ test('"anyOf" moves hyphen to the first position', () => { expect(buildPattern(anyOf('a-bc'))).toBe('[-abc]'); }); -test('`anyOf` throws on empty text', () => { +test('"anyOf" throws on empty text', () => { expect(() => anyOf('')).toThrowErrorMatchingInlineSnapshot( `"\`anyOf\` should received at least one character"` ); }); +test('"inverted" character class', () => { + expect(buildPattern(inverted(anyOf('a')))).toBe('[^a]'); + expect(buildPattern(inverted(anyOf('abc')))).toBe('[^abc]'); +}); + +test('"inverted" character class double inversion', () => { + expect(buildPattern(inverted(inverted(anyOf('a'))))).toBe('a'); + expect(buildPattern(inverted(inverted(anyOf('abc'))))).toBe('[abc]'); +}); + +test('"inverted" character class execution', () => { + expect(execRegex('aa', [inverted(anyOf('a'))])).toBeNull(); + expect(execRegex('aba', [inverted(anyOf('a'))])).toEqual(['b']); +}); + test('buildPattern throws on empty text', () => { expect(() => encodeCharacterClass({ type: 'characterClass', characters: [], + inverted: false, }) ).toThrowErrorMatchingInlineSnapshot( `"Character class should contain at least one character"` diff --git a/src/components/character-class.ts b/src/components/character-class.ts index 7c6dbd3..1de3e6f 100644 --- a/src/components/character-class.ts +++ b/src/components/character-class.ts @@ -5,21 +5,25 @@ import type { CharacterClass } from './types'; export const any: CharacterClass = { type: 'characterClass', characters: ['.'], + inverted: false, }; export const whitespace: CharacterClass = { type: 'characterClass', characters: ['\\s'], + inverted: false, }; export const digit: CharacterClass = { type: 'characterClass', characters: ['\\d'], + inverted: false, }; export const word: CharacterClass = { type: 'characterClass', characters: ['\\w'], + inverted: false, }; export function anyOf(characters: string): CharacterClass { @@ -31,26 +35,36 @@ export function anyOf(characters: string): CharacterClass { return { type: 'characterClass', characters: charactersArray, + inverted: false, }; } -export function encodeCharacterClass({ - characters, -}: CharacterClass): EncoderNode { - if (characters.length === 0) { +export function inverted(characterClass: CharacterClass): CharacterClass { + return { + type: 'characterClass', + characters: characterClass.characters, + inverted: !characterClass.inverted, + }; +} + +export function encodeCharacterClass( + characterClass: CharacterClass +): EncoderNode { + if (characterClass.characters.length === 0) { throw new Error('Character class should contain at least one character'); } - if (characters.length === 1) { + if (characterClass.characters.length === 1 && !characterClass.inverted) { return { precedence: EncoderPrecedence.Atom, - pattern: characters[0]!, + pattern: characterClass.characters[0]!, }; } + const characterString = reorderHyphen(characterClass.characters).join(''); return { precedence: EncoderPrecedence.Atom, - pattern: `[${reorderHyphen(characters).join('')}]`, + pattern: `[${characterClass.inverted ? '^' : ''}${characterString}]`, }; } diff --git a/src/components/types.ts b/src/components/types.ts index 648b679..9d26d5c 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -5,6 +5,7 @@ export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat; export type CharacterClass = { type: 'characterClass'; characters: string[]; + inverted: boolean; }; // Components diff --git a/src/test-utils.ts b/src/test-utils.ts index cea4dde..520f235 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -6,5 +6,6 @@ export function execRegex( elements: Array ) { const regex = buildRegex(...elements); - return [...regex.exec(text)!]; + const result = regex.exec(text); + return result ? [...result] : null; }