regexgen
Version:
Generate regular expressions that match a set of strings
183 lines (153 loc) • 4.66 kB
JavaScript
const {Alternation, CharClass, Concatenation, Repetition, Literal} = require('./ast');
/**
* Implements Brzozowski's algebraic method to convert a DFA into a regular
* expression pattern.
* http://cs.stackexchange.com/questions/2016/how-to-convert-finite-automata-to-regular-expressions#2392
*
* @param {State} root - the initial state of the DFA
* @param {string} flags - The flags to add to the regex.
* @return {String} - the converted regular expression pattern
*/
function toRegex(root, flags) {
let states = Array.from(root.visit());
// Setup the system of equations A and B from Arden's Lemma.
// A represents a state transition table for the given DFA.
// B is a vector of accepting states in the DFA, marked as epsilons.
let A = [];
let B = [];
for (let i = 0; i < states.length; i++) {
let a = states[i];
if (a.accepting) {
B[i] = new Literal('');
}
A[i] = [];
for (let [t, s] of a.transitions) {
let j = states.indexOf(s);
A[i][j] = A[i][j] ? union(A[i][j], new Literal(t)) : new Literal(t);
}
}
// Solve the of equations
for (let n = states.length - 1; n >= 0; n--) {
if (A[n][n] != null) {
B[n] = concat(star(A[n][n]), B[n]);
for (let j = 0; j < n; j++) {
A[n][j] = concat(star(A[n][n]), A[n][j]);
}
}
for (let i = 0; i < n; i++) {
if (A[i][n] != null) {
B[i] = union(B[i], concat(A[i][n], B[n]));
for (let j = 0; j < n; j++) {
A[i][j] = union(A[i][j], concat(A[i][n], A[n][j]));
}
}
}
}
return B[0].toString(flags);
}
/**
* Creates a repetition if `exp` exists.
*/
function star(exp) {
return exp ? new Repetition(exp, '*') : null;
}
/**
* Creates a union between two expressions
*/
function union(a, b) {
if (a != null && b != null && a !== b) {
// Hoist common substrings at the start and end of the options
let start, end, res;
[a, b, start] = removeCommonSubstring(a, b, 'start');
[a, b, end] = removeCommonSubstring(a, b, 'end');
// If a or b is empty, make an optional group instead
if (a.isEmpty || b.isEmpty) {
res = new Repetition(a.isEmpty ? b : a, '?');
} else if (a instanceof Repetition && a.type === '?') {
res = new Repetition(new Alternation(a.expr, b), '?');
} else if (b instanceof Repetition && b.type === '?') {
res = new Repetition(new Alternation(a, b.expr), '?');
} else {
// Check if we can make a character class instead of an alternation
let ac = a.getCharClass && a.getCharClass();
let bc = b.getCharClass && b.getCharClass();
if (ac && bc) {
res = new CharClass(ac, bc);
} else {
res = new Alternation(a, b);
}
}
if (start) {
res = new Concatenation(new Literal(start), res);
}
if (end) {
res = new Concatenation(res, new Literal(end));
}
return res;
}
return a || b;
}
/**
* Removes the common prefix or suffix from the two expressions
*/
function removeCommonSubstring(a, b, side) {
let al = a.getLiteral && a.getLiteral(side);
let bl = b.getLiteral && b.getLiteral(side);
if (!al || !bl) {
return [a, b, null];
}
let s = commonSubstring(al, bl, side);
if (!s) {
return [a, b, ''];
}
a = a.removeSubstring(side, s.length);
b = b.removeSubstring(side, s.length);
return [a, b, s];
}
/**
* Finds the common prefix or suffix between to strings
*/
function commonSubstring(a, b, side) {
let dir = side === 'start' ? 1 : -1;
a = Array.from(a);
b = Array.from(b);
let ai = dir === 1 ? 0 : a.length - 1;
let ae = dir === 1 ? a.length : -1;
let bi = dir === 1 ? 0 : b.length - 1;
let be = dir === 1 ? b.length : -1;
let res = '';
for (; ai !== ae && bi !== be && a[ai] === b[bi]; ai += dir, bi += dir) {
if (dir === 1) {
res += a[ai];
} else {
res = a[ai] + res;
}
}
return res;
}
/**
* Creates a concatenation between expressions a and b
*/
function concat(a, b) {
if (a == null || b == null) {
return null;
}
if (a.isEmpty) {
return b;
}
if (b.isEmpty) {
return a;
}
// Combine literals
if (a instanceof Literal && b instanceof Literal) {
return new Literal(a.value + b.value);
}
if (a instanceof Literal && b instanceof Concatenation && b.a instanceof Literal) {
return new Concatenation(new Literal(a.value + b.a.value), b.b);
}
if (b instanceof Literal && a instanceof Concatenation && a.b instanceof Literal) {
return new Concatenation(a.a, new Literal(a.b.value + b.value));
}
return new Concatenation(a, b);
}
module.exports = toRegex;