Skip to content

Commit f45e8a5

Browse files
feat: inverted character class (#27)
1 parent 1b4e445 commit f45e8a5

File tree

4 files changed

+43
-9
lines changed

4 files changed

+43
-9
lines changed

src/components/__tests__/character-class.test.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import {
55
anyOf,
66
digit,
77
encodeCharacterClass,
8+
inverted,
89
whitespace,
910
word,
1011
} from '../character-class';
12+
import { execRegex } from '../../test-utils';
1113

1214
test('"whitespace" character class', () => {
1315
expect(buildPattern(whitespace)).toEqual(`\\s`);
@@ -60,17 +62,33 @@ test('"anyOf" moves hyphen to the first position', () => {
6062
expect(buildPattern(anyOf('a-bc'))).toBe('[-abc]');
6163
});
6264

63-
test('`anyOf` throws on empty text', () => {
65+
test('"anyOf" throws on empty text', () => {
6466
expect(() => anyOf('')).toThrowErrorMatchingInlineSnapshot(
6567
`"\`anyOf\` should received at least one character"`
6668
);
6769
});
6870

71+
test('"inverted" character class', () => {
72+
expect(buildPattern(inverted(anyOf('a')))).toBe('[^a]');
73+
expect(buildPattern(inverted(anyOf('abc')))).toBe('[^abc]');
74+
});
75+
76+
test('"inverted" character class double inversion', () => {
77+
expect(buildPattern(inverted(inverted(anyOf('a'))))).toBe('a');
78+
expect(buildPattern(inverted(inverted(anyOf('abc'))))).toBe('[abc]');
79+
});
80+
81+
test('"inverted" character class execution', () => {
82+
expect(execRegex('aa', [inverted(anyOf('a'))])).toBeNull();
83+
expect(execRegex('aba', [inverted(anyOf('a'))])).toEqual(['b']);
84+
});
85+
6986
test('buildPattern throws on empty text', () => {
7087
expect(() =>
7188
encodeCharacterClass({
7289
type: 'characterClass',
7390
characters: [],
91+
inverted: false,
7492
})
7593
).toThrowErrorMatchingInlineSnapshot(
7694
`"Character class should contain at least one character"`

src/components/character-class.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,25 @@ import type { CharacterClass } from './types';
55
export const any: CharacterClass = {
66
type: 'characterClass',
77
characters: ['.'],
8+
inverted: false,
89
};
910

1011
export const whitespace: CharacterClass = {
1112
type: 'characterClass',
1213
characters: ['\\s'],
14+
inverted: false,
1315
};
1416

1517
export const digit: CharacterClass = {
1618
type: 'characterClass',
1719
characters: ['\\d'],
20+
inverted: false,
1821
};
1922

2023
export const word: CharacterClass = {
2124
type: 'characterClass',
2225
characters: ['\\w'],
26+
inverted: false,
2327
};
2428

2529
export function anyOf(characters: string): CharacterClass {
@@ -31,26 +35,36 @@ export function anyOf(characters: string): CharacterClass {
3135
return {
3236
type: 'characterClass',
3337
characters: charactersArray,
38+
inverted: false,
3439
};
3540
}
3641

37-
export function encodeCharacterClass({
38-
characters,
39-
}: CharacterClass): EncoderNode {
40-
if (characters.length === 0) {
42+
export function inverted(characterClass: CharacterClass): CharacterClass {
43+
return {
44+
type: 'characterClass',
45+
characters: characterClass.characters,
46+
inverted: !characterClass.inverted,
47+
};
48+
}
49+
50+
export function encodeCharacterClass(
51+
characterClass: CharacterClass
52+
): EncoderNode {
53+
if (characterClass.characters.length === 0) {
4154
throw new Error('Character class should contain at least one character');
4255
}
4356

44-
if (characters.length === 1) {
57+
if (characterClass.characters.length === 1 && !characterClass.inverted) {
4558
return {
4659
precedence: EncoderPrecedence.Atom,
47-
pattern: characters[0]!,
60+
pattern: characterClass.characters[0]!,
4861
};
4962
}
5063

64+
const characterString = reorderHyphen(characterClass.characters).join('');
5165
return {
5266
precedence: EncoderPrecedence.Atom,
53-
pattern: `[${reorderHyphen(characters).join('')}]`,
67+
pattern: `[${characterClass.inverted ? '^' : ''}${characterString}]`,
5468
};
5569
}
5670

src/components/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type Quantifier = One | OneOrMore | Optionally | ZeroOrMore | Repeat;
55
export type CharacterClass = {
66
type: 'characterClass';
77
characters: string[];
8+
inverted: boolean;
89
};
910

1011
// Components

src/test-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export function execRegex(
66
elements: Array<RegexElement | string>
77
) {
88
const regex = buildRegex(...elements);
9-
return [...regex.exec(text)!];
9+
const result = regex.exec(text);
10+
return result ? [...result] : null;
1011
}

0 commit comments

Comments
 (0)