Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changed alias expansion to allow for nested aliases #125

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
176 changes: 133 additions & 43 deletions src/core/utilities/generate_aliases.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,133 @@
function cartesianProduct<T>(arr: T[][]): T[][] {
return arr.reduce(
(a, b) => {
return a
.map(x => {
return b.map(y => {
return x.concat(y);
});
})
.reduce((c, d) => c.concat(d), []);
},
[[]] as T[][]
);
}

export function* aliasesFromPattern(query: string) {
if (!query || query.length === 0) {
throw new Error('Tried to parse null or empty string');
}

let i = 0;
const currentChar = () => query[i];

function match(c: string) {
const char = currentChar();
if (c.includes(char)) {
i++;
return char;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} should be on the same line as else if

Run gts check at project root to run linter to see style issues in the project as a whole.

else if (i >= query.length) {
throw new Error(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider throwing your own class here that inherits from Error. Then when you catch, use instanceof operator to detect your error. Re-throw other errors.

`Ran off the end of the string expecting to find: '${c}', i: ${i}`
);
} else {
throw new Error(
`Unexpected character: '${currentChar()}'. Expected '${c}'.`
);
}
}

//Intrinsics
const whitespace = () => match(' ');
const letter = () =>
match(
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using on of the built-in iswhitespace methods here to handle other locals.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not, then define the constant outside of the function. (To avoid performing string concat every call)

'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
'abcdefghijklmnopqrstuvwxyz' +
'ÁáÍíøOóÚúÜüÑñ' +
' '
);
const token = () => [kleenePlus(letter).join('')];
const constant = (s: string) => {
for (let i = 0; i < s.length; i++) match(s.charAt(i));
};
const separator = () => match('|,;');

//Lists
const listGap = () => {
separator();
kleene(whitespace);
};
const group = () => kleenePlus(phrase, listGap);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be good, for the sake of readability to define phrase before introducing group


//Branches
function choice(): string[] {
constant('(');
const contents = group();
constant(')');
return ([] as string[]).concat(...contents);
}

function optional(): string[] {
constant('[');
const contents = group();
constant(']');
const result = ([] as string[]).concat(...contents);
result.push('');
return result;
}

const term = () => union(choice, optional, token);
const phrase = () => cartesianProduct(kleenePlus(term)).map(a => a.join(''));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, took a second to decompose this one.

kleenePlus(...) generates a list of branches (where each branch is a list of tokens), then the Cartesian enumerates all paths through these branches.

Wouldn't be a bad idea to document this! A little explanation by the dev saves a lot of reverse engineering by the reader.

const grammar = () => phrase();

const results = grammar();
if (i !== query.length) {
throw new TypeError('Failed expanding alias at index ' + i);
}

yield* aliasesFromPatternHelper(results);

function kleene<T>(match: () => T, separator?: () => void): T[] {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overloading the term match. Within this namespace, the word match means very different things within different lexical scopes.

Please avoid instances of variable overshadowing here, and in other locations (such as i in for loops, since i is defined in the outer scope)

const results: T[] = [];
try {
while (true) {
results.concat(match());
if (separator) separator();
}
} catch (e) {
return results;
}
}

function kleenePlus<T>(match: () => T, separator?: () => void): T[] {
const results: T[] = [];
results.push(match());
try {
while (true) {
if (separator) separator();
results.push(match());
}
} catch (e) {
return results;
}
}

function union<T>(...matches: Array<() => T>) {
const backtrackPointer = i;
const errors: string[] = [];
for (const match of matches) {
try {
return match();
} catch (e) {
errors.push(e);
i = backtrackPointer;
}
}
throw new Error('Matched none of union:\n- ' + errors.join('\n- '));
}
}

function combine(left: string, right: string) {
if (left.length === 0) {
return right;
Expand All @@ -9,52 +139,12 @@ function combine(left: string, right: string) {
}

function* aliasesFromPatternHelper(
prefix: string,
options: string[][]
options: string[]
): IterableIterator<string> {
if (options.length > 0) {
for (const option of options[0]) {
yield* aliasesFromPatternHelper(
combine(prefix, option),
options.slice(1)
);
for (const option of options) {
yield option.replace(/\s\s+/g, ' ').trim();
}
} else {
yield prefix;
}
}

export function* aliasesFromPattern(query: string) {
const m = /(\[[^\]]*\])|(\([^\)]*\))|([^\[^\()]*)/g;

// Remove leading, trailing, and consecutive spaces.
const query2 = query.replace(/\s+/g, ' ').trim();

// Throw on comma before ] and ).
if (query2.search(/(,\])|(,\))/g) !== -1) {
throw TypeError(`generateAliases: illegal trailing comma in "${query}".`);
}

const matches = query2.match(m);

if (matches !== null) {
const options = matches
.map(match => {
if (match.startsWith('[')) {
// Selects an option or leaves blank
return [...match.slice(1, -1).split(','), ''].map(x => x.trim());
} else if (match.startsWith('(')) {
// Must select from one of the options
return match
.slice(1, -1)
.split(',')
.map(x => x.trim());
} else {
return [match.trim()];
}
})
.filter(match => match[0].length > 0);
yield* aliasesFromPatternHelper('', options);
}
}

Expand Down
18 changes: 18 additions & 0 deletions test/utilities/generate_aliases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import 'mocha';
import { aliasesFromPattern } from '../../src/core/utilities/generate_aliases';

describe('Alias Generation', () => {
it('should expand plaintext to itself', () => {
const pattern = 'helloworld';

const expected = ['helloworld'];

const observed = [...aliasesFromPattern(pattern)];
assert.deepEqual(observed, expected);
});

it('should enumerate optionals', () => {
const pattern = 'a [b,c] d';

Expand Down Expand Up @@ -52,4 +61,13 @@ describe('Alias Generation', () => {
const observed = [...aliasesFromPattern(pattern)];
assert.deepEqual(observed, expected);
});

it('should expand recursively', () => {
const pattern = '(red,green,[light] blue)';

const expected = ['red', 'green', 'light blue', 'blue'];

const observed = [...aliasesFromPattern(pattern)];
assert.deepEqual(observed, expected);
});
});