Search code examples
typescriptalgorithmdata-structures

Expressions Algorithm in TypeScript


I have some list of expressions,

const expressions = [
    'A&(B|C)',
    'A|(B&C)',
    'A|(B&(C|D))',
    'A|(B&C&D)',
];

Out of this, I need to get an output like,

[[A, B], [A, C]]
[[A], [B, C]]
[A, [B, C], [B, D]]
[A, [B, C, D]] 

I tried to do this in the below way, But getting an empty arrays instead of correct result.

class ParsedExpression {
    operator: string | null;
    variables: string[];
    subExpressions: ParsedExpression[];

    constructor(operator: string | null, variables: string[], subExpressions: ParsedExpression[]) {
        this.operator = operator;
        this.variables = variables;
        this.subExpressions = subExpressions;
    }
}

function parseExpression(expression: string): ParsedExpression {
    const stack: ParsedExpression[] = [];
    let currentExpr: ParsedExpression | null = null;
    let currentVariable = '';

    for (const char of expression) {
        if (char === '(') {
            if (currentExpr !== null) {
                stack.push(currentExpr);
            }
            currentExpr = null;
        } else if (char === ')') {
            if (stack.length > 0) {
                const prevExpr = stack.pop();
                if (prevExpr !== null && currentExpr !== null) {
                    prevExpr.subExpressions.push(currentExpr);
                    currentExpr = prevExpr;
                }
            }
        } else if (char === '&' || char === '|') {
            if (currentExpr === null) {
                currentExpr = new ParsedExpression(char, [], []);
            } else {
                currentExpr.operator = char;
            }
        } else {
            if (currentExpr === null) {
                currentExpr = new ParsedExpression(null, [], []);
            }
            currentExpr.variables.push(char);
        }
    }

    return currentExpr as ParsedExpression;
}

function generateCombinations(parsedExpr: ParsedExpression): string[][] {
    if (!parsedExpr.operator) {
        return [parsedExpr.variables];
    }

    const leftCombinations = parsedExpr.subExpressions.length > 0 ? generateCombinations(parsedExpr.subExpressions[0]) : [[]];
    const rightCombinations = parsedExpr.subExpressions.length > 1 ? generateCombinations(parsedExpr.subExpressions[1]) : [[]];

    const combinations: string[][] = [];
    for (const left of leftCombinations) {
        for (const right of rightCombinations) {
            combinations.push(left.concat(right));
        }
    }

    return combinations;
}

const expressions = [
    'A&(B|C)',
    'A|(B&C)',
    'A|(B&(C|D))',
    'A|(B&C&D)',
];

for (const expr of expressions) {
    const parsedExpression = parseExpression(expr);
    const combinations = generateCombinations(parsedExpression);
    console.log(`Expression: ${expr}`);
    console.log(`Combinations: ${JSON.stringify(combinations)}`);
    console.log('-------------------');
}

Output I get,

Expression: A&(B|C)
Combinations: [[]]
-------------------
Expression: A|(B&C)
Combinations: [[]]
-------------------
Expression: A|(B&(C|D))
Combinations: [[]]
-------------------
Expression: A|(B&C&D)
Combinations: [[]]
-------------------

Anyway to resolve this?


Solution

  • The generateCombinations function does not do anything that relates to the task. It should actually flatten the tree to two levels, where the top level is a disjuction, and the second level consists of conjunctions.

    You can do this with recursion, and flatten on the way out of recursion.

    I have chosen an implementation where the parser allows parentheses to be optional, in which case & gets precedence over |, and the variable names can consist of more than one character. The parser builds a binary tree. I wouldn't store variables in a separate array, but allow a child to be a variable (string) or a node.

    In a second phase the tree can be converted to an array of arrays, bottom-up, where this nested array will represent either a CNF (conjunction normal form) or DNF (disjunction normal form). This representation will need to convert from one to the other to allow a node's children to be merged. This conversion is nothing else than a Cartesian product.

    Finally some simplifications could be applied, so that the bottom level arrays only have unique values (so to change A&A&B to A&B). More simplification rules could be added (as can be seen in the additional test case I added), but I didn't pursue this further.

    Below is a JavaScript implementation. It should not be difficult to add typing:

    class BinaryNode {
        constructor(value, left, right) {
            this.value = value;
            this.left = left;
            this.right = right;
        }
    }
    
    function parseToBinaryTree(expression) {
        const precedence = { "&": 2, "|": 1, "(": -1, ")": 0, "": 0 };
        const operators = [];
        const operands = [];
        for (const token of expression.match(/[^&|()\s]+|\S?/gs)) {
            if (token in precedence) {
                if (token !== "(") {
                    while (precedence[operators.at(-1)] >= precedence[token]) {
                        if (operands.length < 2) throw new Error("Parser: Syntax error: operands length < 2"); 
                        operands.push(new BinaryNode(operators.pop(), ...operands.splice(-2)));
                    }
                }
                if (token && token !== ")") operators.push(token);
                else operators.pop(); // "("
            } else {
                operands.push(token);
            }
        }
        if (operands.length != 1) throw new Error("Parser: Syntax error: left over");
        return operands.pop();
    }
    
    const unique = values => [...new Set(values)];
    const uniqueEach = arrays => arrays.map(unique);
    const unwrapSingle = values => values.length === 1 ? values[0] : values;
    const unwrapSingleEach = arrays => arrays.map(unwrapSingle);
    
    function product(arrays) {
        if (arrays.length === 0) {
            return [[]];
        }
        const result = [];
        for (const x of arrays[0]) {
            for (const rest of product(arrays.slice(1))) {
                result.push([x, ...rest]);
            }
        }
        return result;
    }
    
    function normalize(root, operator="|") {
        if (typeof root === "string") { // Leaf node
            return [[root]];
        }
        const terms = uniqueEach([
            ...normalize(root.left, root.value),
            ...normalize(root.right, root.value)
        ]);
        if (root.value !== operator) {
            return uniqueEach(product(terms));
        }
        return terms;
    }
    
    const expressions = [
        'A&(B|C)',
        'A|(B&C)',
        'A|(B&(C|D))',
        'A|(B&C&D)',
        'A&(B|C)&(C|D)',
    ];
    
    for (const expr of expressions) {
        const root = parseToBinaryTree(expr);
        const disj = unwrapSingleEach(normalize(root));
        console.log(`Expression: ${expr}`);
        console.log(JSON.stringify(disj));
        console.log('-------------------');
    }