Skip to content

Commit fc3a2fb

Browse files
Merge pull request #1 from lfbittencourt/feature/inclusive-or
Feature: inclusive groups
2 parents b37b0a6 + 32052e2 commit fc3a2fb

9 files changed

Lines changed: 172 additions & 88 deletions

File tree

README.md

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,35 +32,43 @@ console.log(result); // ['a', 'b']
3232
The syntax is similar to regular expressions and its composed by just a few
3333
different tokens.
3434

35+
### Literals
36+
3537
Literal strings output themselves:
3638

3739
```
3840
a => ['a']
3941
```
4042

43+
### Optional characters
44+
4145
Optional characters are followed by a `?`:
4246

4347
```
4448
ab? => ['a', 'ab']
4549
```
4650

51+
### Groups
52+
4753
Groups are used to group options. The group is defined by parentheses and the
4854
options are separated by `|`:
4955

5056
```
5157
(a|b) => ['a', 'b']
5258
```
5359

54-
Optional groups are followed by a `?`:
60+
Groups can have a single option:
5561

5662
```
57-
(a|b)? => ['a', 'b', '']
63+
(a) => ['a']
5864
```
5965

60-
Groups can have a single option:
66+
### Optional groups
67+
68+
Optional groups are followed by a `?`:
6169

6270
```
63-
(a) => ['a']
71+
(a|b)? => ['a', 'b', '']
6472
```
6573

6674
You can use a single-option group combined with the optional character `?` to
@@ -70,27 +78,56 @@ make more than one character optional:
7078
(ab)? => ['ab', '']
7179
```
7280

81+
### Nested groups
82+
7383
Groups can be nested:
7484

7585
```
7686
(a(b|c)) => ['ab', 'ac']
7787
```
7888

89+
### Inclusive groups
90+
91+
You can also use inclusive groups (or simply igroups) by using a plus sign (`+`)
92+
before the options. Different from the standard groups, igroups also output an
93+
option that concatenates all of its options:
94+
95+
```
96+
(+a|b) => ['a', 'b', 'ab']
97+
```
98+
99+
As with standard groups, igroups can be optional:
100+
101+
```
102+
(+a|b)? => ['a', 'b', 'ab', '']
103+
```
104+
105+
Igroups can also be nested:
106+
107+
```
108+
(+a|(+b|c)) => ['a', 'ab', 'abc', 'ac', 'b', 'bc', 'c']
109+
```
110+
111+
### Complex patterns
112+
79113
Finally, you can mix all of them into complex patterns:
80114

81115
```
82116
Mary( Eli(z|s)abeth)?( Simmons)? Smith(son)? => [
83-
'Mary Elizabeth Simmons Smithson',
84-
'Mary Elizabeth Simmons Smith',
85-
'Mary Elizabeth Smithson',
86-
'Mary Elizabeth Smith',
87-
'Mary Elisabeth Simmons Smithson',
117+
'Mary Elisabeth Simmons',
88118
'Mary Elisabeth Simmons Smith',
89-
'Mary Elisabeth Smithson',
119+
'Mary Elisabeth Simmons Smithson',
90120
'Mary Elisabeth Smith',
91-
'Mary Simmons Smithson',
121+
'Mary Elisabeth Smithson',
122+
'Mary Elizabeth Simmons',
123+
'Mary Elizabeth Simmons Smith',
124+
'Mary Elizabeth Simmons Smithson',
125+
'Mary Elizabeth Smith',
126+
'Mary Elizabeth Smithson',
127+
'Mary Simmons',
92128
'Mary Simmons Smith',
129+
'Mary Simmons Smithson',
130+
'Mary Smith',
93131
'Mary Smithson',
94-
'Mary Smith'
95132
]
96133
```

src/logical.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ export type LogicalChildren = LogicalChild[];
55
class Logical {
66
children: LogicalChildren;
77

8-
constructor(...args: LogicalChildren) {
9-
this.children = args;
8+
constructor(...children: LogicalChildren) {
9+
this.children = children;
1010
}
1111
}
1212

src/parser.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { EmbeddedActionsParser } from 'chevrotain';
33
import { And, LogicalChildren, Or } from './logical';
44

55
import tokens, {
6-
Text, LeftParenthesis, RightParenthesis, QuestionMark, Pipe,
6+
Text, LeftParenthesis, RightParenthesis, QuestionMark, Pipe, PlusSign,
77
} from './tokens';
88

99
/**
@@ -16,7 +16,11 @@ import tokens, {
1616
* Text QuestionMark
1717
*
1818
* optionableGroup:
19-
* LeftParenthesis tree (Pipe tree)* RightParenthesis (QuestionMark)?
19+
* LeftParenthesis
20+
* (PlusSign)?
21+
* tree (Pipe tree)*
22+
* RightParenthesis
23+
* (QuestionMark)?
2024
*
2125
* tree:
2226
* element+
@@ -39,18 +43,24 @@ export default class Parser extends EmbeddedActionsParser {
3943
});
4044

4145
private optionableGroup = this.RULE('optionableGroup', () => {
42-
const alternatives = [];
46+
const alternatives: LogicalChildren = [];
4347

4448
this.CONSUME(LeftParenthesis);
4549

50+
const isIgroup = this.OPTION1(() => this.CONSUME(PlusSign));
51+
4652
this.AT_LEAST_ONE_SEP({
4753
SEP: Pipe,
4854
DEF: () => alternatives.push(this.SUBRULE(this.tree)),
4955
});
5056

5157
this.CONSUME(RightParenthesis);
5258

53-
if (this.OPTION(() => this.CONSUME(QuestionMark))) {
59+
if (isIgroup && alternatives.length > 1) {
60+
alternatives.push(new And(...alternatives));
61+
}
62+
63+
if (this.OPTION2(() => this.CONSUME(QuestionMark))) {
5464
alternatives.push('');
5565
}
5666

@@ -68,14 +78,14 @@ export default class Parser extends EmbeddedActionsParser {
6878
]));
6979

7080
public tree = this.RULE('tree', () => {
71-
const variations: LogicalChildren = [];
81+
const children: LogicalChildren = [];
7282

73-
this.AT_LEAST_ONE(() => variations.push(this.SUBRULE(this.element)));
83+
this.AT_LEAST_ONE(() => children.push(this.SUBRULE(this.element)));
7484

75-
if (variations.length === 1) {
76-
return variations[0];
85+
if (children.length === 1) {
86+
return children[0];
7787
}
7888

79-
return new And(...variations);
89+
return new And(...children);
8090
});
8191
}

src/tokens.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ export const LeftParenthesis = createToken({ name: 'LeftParenthesis', pattern: '
55
export const RightParenthesis = createToken({ name: 'RightParenthesis', pattern: ')' });
66
export const QuestionMark = createToken({ name: 'QuestionMark', pattern: '?' });
77
export const Pipe = createToken({ name: 'Pipe', pattern: '|' });
8+
export const PlusSign = createToken({ name: 'PlusSign', pattern: '+' });
89

9-
export default [Text, LeftParenthesis, RightParenthesis, QuestionMark, Pipe];
10+
export default [Text, LeftParenthesis, RightParenthesis, QuestionMark, Pipe, PlusSign];

tests/basic.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import expand from '../src';
2+
3+
describe('basic', () => {
4+
it('should expand literal strings', () => {
5+
const result = expand('a');
6+
7+
expect(result).toEqual(['a']);
8+
});
9+
10+
it('should expand optional characters', () => {
11+
const result = expand('ab?').sort();
12+
13+
expect(result).toEqual(['a', 'ab'].sort());
14+
});
15+
});

tests/expand.spec.ts

Lines changed: 0 additions & 64 deletions
This file was deleted.

tests/extra.spec.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import expand from '../src';
2+
3+
describe('extra', () => {
4+
it('should expand complex patterns', () => {
5+
const result = expand('Mary( Eli(z|s)abeth)?(+ Simmons| Smith(son)?)').sort();
6+
7+
expect(result).toEqual([
8+
'Mary Elisabeth Simmons',
9+
'Mary Elisabeth Simmons Smith',
10+
'Mary Elisabeth Simmons Smithson',
11+
'Mary Elisabeth Smith',
12+
'Mary Elisabeth Smithson',
13+
'Mary Elizabeth Simmons',
14+
'Mary Elizabeth Simmons Smith',
15+
'Mary Elizabeth Simmons Smithson',
16+
'Mary Elizabeth Smith',
17+
'Mary Elizabeth Smithson',
18+
'Mary Simmons',
19+
'Mary Simmons Smith',
20+
'Mary Simmons Smithson',
21+
'Mary Smith',
22+
'Mary Smithson',
23+
].sort());
24+
});
25+
});

tests/groups.spec.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import expand from '../src';
2+
3+
describe('groups', () => {
4+
it('should expand groups', () => {
5+
const result = expand('(a|b)').sort();
6+
7+
expect(result).toEqual(['a', 'b'].sort());
8+
});
9+
10+
it('should expand optional groups', () => {
11+
const result = expand('(a|b)?').sort();
12+
13+
expect(result).toEqual(['a', 'b', ''].sort());
14+
});
15+
16+
it('should expand single-option groups', () => {
17+
const result = expand('(a)').sort();
18+
19+
expect(result).toEqual(['a'].sort());
20+
});
21+
22+
it('should expand single-option groups with optional characters', () => {
23+
const result = expand('(ab)?').sort();
24+
25+
expect(result).toEqual(['ab', ''].sort());
26+
});
27+
28+
it('should expand nested groups', () => {
29+
const result = expand('(a(b|c))').sort();
30+
31+
expect(result).toEqual(['ab', 'ac'].sort());
32+
});
33+
});

tests/igroups.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import expand from '../src';
2+
3+
describe('inclusive groups', () => {
4+
it('should expand inclusive groups', () => {
5+
const result = expand('(+a|b)').sort();
6+
7+
expect(result).toEqual(['a', 'b', 'ab'].sort());
8+
});
9+
10+
it('should expand optional inclusive groups', () => {
11+
const result = expand('(+a|b)?').sort();
12+
13+
expect(result).toEqual(['a', 'b', 'ab', ''].sort());
14+
});
15+
16+
it('should not expand single-option inclusive groups', () => {
17+
const result = expand('(+a)').sort();
18+
19+
expect(result).toEqual(['a'].sort());
20+
});
21+
22+
it('should expand nested inclusive groups', () => {
23+
const result = expand('(+a|(+b|c))').sort();
24+
25+
expect(result).toEqual(['a', 'ab', 'abc', 'ac', 'b', 'bc', 'c'].sort());
26+
});
27+
});

0 commit comments

Comments
 (0)