/*
Chalkboard - Boolean Algebra Namespace
Version 3.0.4 Euler
Released July 20th, 2026
*/
/*
This Source Code Form is subject to the terms of the
Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
///
namespace Chalkboard {
/**
* The boolean algebra namespace
* @namespace
*/
export namespace bool {
/** @ignore */
const $ = (x: boolean): boolean | 0 | 1 => mode === "boolean" ? x : (x ? 1 : 0);
/**
* Calculates the logical AND operation on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.AND(true, true); // Returns true
* const y = Chalkboard.bool.AND(true, true, false); // Returns false
*/
export const AND = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.AND: Parameter "vals" must contain only true, false, 0, or 1.`);
if (!(vals[i] === true || vals[i] === 1)) result = false;
}
return $(result);
};
/**
* Calculates the biconditional (if and only if) operation on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values.
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.BICOND(true, true, true); // Returns true
* const y = Chalkboard.bool.BICOND(true, false, true); // Returns false
* const z = Chalkboard.bool.BICOND(false, false, false); // Returns true
*/
export const BICOND = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
if (vals.length === 0) return $(true);
let first = true, result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.BICOND: Parameter "vals" must contain only true, false, 0, or 1.`);
if (i === 0) first = vals[i] === true || vals[i] === 1;
else if (first !== (vals[i] === true || vals[i] === 1)) result = false;
}
return $(result);
};
/**
* Calculates the conditional (implication) operation on two or more values. It works as a chain, i.e. it returns true if every adjacent pair (p, q) satisfies p ∧ ¬q.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.COND(false, false, true); // Returns true
* const y = Chalkboard.bool.COND(true, false, true); // Returns false
*/
export const COND = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.COND: Parameter "vals" must contain only true, false, 0, or 1.`);
if (i > 0 && (vals[i - 1] === true || vals[i - 1] === 1) && !(vals[i] === true || vals[i] === 1)) result = false;
}
return $(result);
};
/**
* Calculates the converse of the conditional operation on two or more values. It works as a chain, i.e. it returns true if every adjacent pair (p, q) satisfies q ∧ ¬p.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.CONV(false, false, true); // Returns true
* const y = Chalkboard.bool.CONV(true, false, true); // Returns false
*/
export const CONV = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.CONV: Parameter "vals" must contain only true, false, 0, or 1.`);
if (i > 0 && (vals[i] === true || vals[i] === 1) && !(vals[i - 1] === true || vals[i - 1] === 1)) result = false;
}
return $(result);
};
/**
* Checks if two boolean expressions are logically equivalent.
* @param {string} expr1 - First boolean expression
* @param {string} expr2 - Second boolean expression
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.isEqual("x & y", "y & x"); // Returns true
* const y = Chalkboard.bool.isEqual("x & y | z", "(x | z) & (y | z)"); // Returns false
* const z = Chalkboard.bool.isEqual("x & (y | z)", "(x & y) | (x & z)"); // Returns true
*/
export const isEqual = (expr1: string, expr2: string): boolean | 0 | 1 => {
if (typeof expr1 !== "string") throw new Error(`Chalkboard.bool.isEqual: Parameter "expr1" must be a string.`);
if (typeof expr2 !== "string") throw new Error(`Chalkboard.bool.isEqual: Parameter "expr2" must be a string.`);
const variables: string[] = [];
const varextract = (expr: string): void => {
const ast = Chalkboard.bool.parse(expr, { returnAST: true }) as { type: string, [key: string]: any };
const traverse = (node: { type: string, [key: string]: any }): void => {
if (node.type === "var" && !variables.includes(node.name)) {
variables.push(node.name);
} else if (node.type === "not") {
traverse(node.expr);
} else if (node.type === "and" || node.type === "or") {
traverse(node.left);
traverse(node.right);
}
};
traverse(ast);
};
varextract(expr1);
varextract(expr2);
const generateAssignments = (vars: string[], index: number = 0, current: Record = {}): Record[] => {
if (index >= vars.length) return [current];
const withTrue = { ...current, [vars[index]]: true };
const withFalse = { ...current, [vars[index]]: false };
return [...generateAssignments(vars, index + 1, withTrue), ...generateAssignments(vars, index + 1, withFalse)];
};
const assignments = generateAssignments(variables);
for (const values of assignments) {
const result1 = Chalkboard.bool.parse(expr1, { values});
const result2 = Chalkboard.bool.parse(expr2, { values});
if (result1 !== result2) return $(false);
}
return $(true);
};
/**
* Calculates a Karnaugh map (K-map) for a boolean expression. It supports 2, 3, or 4 variables and returns a 2D array representing the K-map with cells as 0 or 1.
* @param {string} input - The boolean expression to map
* @param {string[]} variables - An array of variable names (order matters)
* @returns {(boolean | 0 | 1)[][]}
* @example
* // For a 3-variable expression
* const kmap = Chalkboard.bool.Karnaugh("x & !y | z", ["x", "y", "z"]);
*/
export const Karnaugh = (input: string, variables: string[]): (boolean | 0 | 1)[][] => {
if (typeof input !== "string") throw new Error(`Chalkboard.bool.Karnaugh: Parameter "input" must be a string.`);
if (!Array.isArray(variables) || variables.length > 0 && typeof variables[0] !== "string" || variables.length > 1 && typeof variables[variables.length - 1] !== "string") throw new Error(`Chalkboard.bool.Karnaugh: Parameter "variables" must be an array of strings.`);
const n = variables.length;
if (n !== 2 && n !== 3 && n !== 4) throw new Error(`Chalkboard.bool.Karnaugh: Parameter "variables" can only be an array with length 2, 3, or 4.`);
let rowvars: string[];
let colvars: string[];
let rows: string[];
let cols: string[];
const grayCodes = (bits: number): string[] => {
if (bits === 0) return [""];
const prev = grayCodes(bits - 1);
const result: string[] = [];
for (let code of prev) {
result.push("0" + code);
}
for (let code of prev.slice().reverse()) {
result.push("1" + code);
}
return result;
};
if (n === 2) {
rowvars = [variables[0]];
colvars = [variables[1]];
rows = grayCodes(1);
cols = grayCodes(1);
} else if (n === 3) {
rowvars = [variables[0]];
colvars = variables.slice(1);
rows = grayCodes(1);
cols = grayCodes(2);
} else {
rowvars = variables.slice(0,2);
colvars = variables.slice(2);
rows = grayCodes(2);
cols = grayCodes(2);
}
const result: (boolean | 0 | 1)[][] = [];
for (let r of rows) {
const row: (boolean | 0 | 1)[] = [];
for (let c of cols) {
const values: Record = {};
for (let i = 0; i < rowvars.length; i++) {
values[rowvars[i]] = r[i] === "1";
}
for (let j = 0; j < colvars.length; j++) {
values[colvars[j]] = c[j] === "1";
}
const parsed = Chalkboard.bool.parse(input, { values });
const booled = parsed === true || parsed === 1;
row.push($(booled));
}
result.push(row);
}
return result;
};
/**
* Calculates a function that maps inputs to outputs based on provided truth tables.
* @param {(boolean | 0 | 1)[][]} inputs - Array of input rows, each row containing values for all inputs
* @param {(boolean | 0 | 1)[][]} outputs - Array of output rows corresponding to each input row
* @returns {((...args: (boolean | 0 | 1)[]) => (boolean | 0 | 1)[])}
* @example
* // f(0, 0, 0) returns [0, 0]
* // f(1, 0, 0) returns [0, 1]
* const f = Chalkboard.bool.mapping([
* [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]
* ], [
* [0, 0], [0, 1], [1, 0], [1, 1]
* ]);
*/
export const mapping = (inputs: (boolean | 0 | 1)[][], outputs: (boolean | 0 | 1)[][]): ((...args: (boolean | 0 | 1)[]) => (boolean | 0 | 1)[]) => {
if (!Array.isArray(inputs) || !Array.isArray(outputs)) throw new Error(`Chalkboard.bool.mapping: Parameters "inputs" and "outputs" must be arrays.`);
if (inputs.length !== outputs.length) throw new Error(`Chalkboard.bool.mapping: Parameter "inputs" and "outputs" must have the same length.`);
if (inputs.length === 0) throw new Error(`Chalkboard.bool.mapping: Parameter "inputs" and "outputs" cannot be empty.`);
if (!Array.isArray(inputs[0]) || !Array.isArray(outputs[0]) || inputs.length > 1 && !Array.isArray(inputs[inputs.length - 1]) || outputs.length > 1 && !Array.isArray(outputs[outputs.length - 1])) throw new Error(`Chalkboard.bool.mapping: Parameters "inputs" and "outputs" must begin and end with arrays.`);
const m = inputs[0].length;
const n = outputs[0].length;
if (inputs.length > 1 && inputs[inputs.length - 1].length !== m) throw new Error(`Chalkboard.bool.mapping: Parameter "inputs" must begin and end with rows of the same length.`);
if (outputs.length > 1 && outputs[outputs.length - 1].length !== n) throw new Error(`Chalkboard.bool.mapping: Parameter "outputs" must begin and end with rows of the same length.`);
const variables = Array.from({length: m}, (_, i) => String.fromCharCode(97 + i));
const expressions: string[] = [];
for (let outCol = 0; outCol < n; outCol++) {
const trueRows: number[] = [];
for (let row = 0; row < inputs.length; row++) {
if (outputs[row][outCol] === true || outputs[row][outCol] === 1) {
trueRows.push(row);
}
}
if (trueRows.length === 0) {
expressions.push("false");
continue;
}
if (trueRows.length === inputs.length) {
expressions.push("true");
continue;
}
const terms: string[] = [];
for (const row of trueRows) {
const literals: string[] = [];
for (let i = 0; i < m; i++) {
const value = inputs[row][i] === true || inputs[row][i] === 1;
literals.push(value ? variables[i] : `!${variables[i]}`);
}
terms.push(`(${literals.join(" & ")})`);
}
const expr = terms.join(" | ");
if (m <= 4) {
expressions.push(Chalkboard.bool.minimize(expr, variables));
} else {
expressions.push(expr);
}
}
return (...args: (boolean | 0 | 1)[]): (boolean | 0 | 1)[] => {
if (args.length !== m) throw new Error(`Chalkboard.bool.mapping: Expected ${m} arguments, but got ${args.length}.`);
if (args.length > 0 && args[0] !== true && args[0] !== false && args[0] !== 0 && args[0] !== 1 || args.length > 1 && args[args.length - 1] !== true && args[args.length - 1] !== false && args[args.length - 1] !== 0 && args[args.length - 1] !== 1) throw new Error(`Chalkboard.bool.mapping: Parameter "args" must begin and end with true, false, 0, or 1.`);
const values: Record = {};
for (let i = 0; i < m; i++) {
values[variables[i]] = args[i];
}
return expressions.map((expr) => {
const parsed = Chalkboard.bool.parse(expr, { values });
const booled = parsed === true || parsed === 1;
return $(booled);
});
};
};
/**
* Calculates the minimization of a boolean expression using the Quine-McCluskey algorithm.
* @param {string} input - The boolean expression
* @param {string[]} variables - An array of variable names (order matters)
* @returns {string}
* @example
* const min = Chalkboard.bool.minimize("x & y | x & z", ["x", "y", "z"]); // Returns "x & (y | z)"
*/
export const minimize = (input: string, variables: string[]): string => {
if (typeof input !== "string") throw new Error(`Chalkboard.bool.minimize: Parameter "input" must be a string.`);
if (!Array.isArray(variables) || variables.length > 0 && typeof variables[0] !== "string" || variables.length > 1 && typeof variables[variables.length - 1] !== "string") throw new Error(`Chalkboard.bool.minimize: Parameter "variables" must be an array of strings.`);
if (variables.length === 0) {
const result = Chalkboard.bool.parse(input);
return result ? "true" : "false";
}
if (variables.length !== 2 && variables.length !== 3 && variables.length !== 4) throw new Error(`Chalkboard.bool.minimize: Parameter "variables" can only be an array with length 2, 3, or 4.`);
try {
const primes = Chalkboard.bool.primeImplicants(input, variables);
if (primes.length === 0) {
return "false";
}
if (primes.some((term) => term === "true")) {
return "true";
}
return Chalkboard.bool.parse(primes.join(" | ")) as string;
} catch (e) {
if (e instanceof Error) throw new Error(`Chalkboard.bool.minimize: Error minimizing expression: ${e.message.replace(/\.*$/, "")}.`);
else throw new Error(`Chalkboard.bool.minimize: Error minimizing expression: ${String(e).replace(/\.*$/, "")}.`);
}
};
/** @ignore */
let mode: "boolean" | "binary" = "boolean";
/**
* Configures the output mode for boolean operations. The default is "boolean".
* @param {"boolean" | "binary"} config - The new mode to set, which can be either "boolean" or "binary"
* @returns {void}
* @example
* Chalkboard.bool.modeConfig("binary"); // Sets the mode to binary
* const x = Chalkboard.bool.AND(true, false); // Returns 0 in binary mode
* Chalkboard.bool.modeConfig("boolean"); // Sets the mode to boolean
* const y = Chalkboard.bool.AND(true, false); // Returns false in boolean mode
*/
export const modeConfig = (config: "boolean" | "binary"): void => {
if (typeof config !== "string") throw new Error(`Chalkboard.bool.modeConfig: Parameter "config" must be a string.`);
const _config = config.toLowerCase();
if (_config !== "boolean" && _config !== "binary") throw new Error(`Chalkboard.bool.modeConfig: Parameter "config" must be either "boolean" or "binary".`);
mode = _config;
};
/**
* Calculates the logical NAND (NOT AND) operation on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.NAND(true, true, true); // Returns false
* const y = Chalkboard.bool.NAND(true, true, false); // Returns true
*/
export const NAND = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.NAND: Parameter "vals" must contain only true, false, 0, or 1.`);
if (!(vals[i] === true || vals[i] === 1)) result = false;
}
return $(!result);
};
/**
* Calculates the opposite of the biconditional operation on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.BICOND(true, true, true); // Returns false
* const y = Chalkboard.bool.BICOND(true, false, true); // Returns true
* const z = Chalkboard.bool.BICOND(false, false, false); // Returns false
*/
export const NBICOND = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
if (vals.length === 0) return $(false);
let first = true, result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.NBICOND: Parameter "vals" must contain only true, false, 0, or 1.`);
if (i === 0) first = vals[i] === true || vals[i] === 1;
else if (first !== (vals[i] === true || vals[i] === 1)) result = false;
}
return $(!result);
};
/**
* Calculates the opposite of the conditional (non-implication) on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.COND(false, false, true); // Returns false
* const y = Chalkboard.bool.COND(true, false, true); // Returns true
*/
export const NCOND = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.NCOND: Parameter "vals" must contain only true, false, 0, or 1.`);
if (i > 0 && (vals[i - 1] === true || vals[i - 1] === 1) && !(vals[i] === true || vals[i] === 1)) result = false;
}
return $(!result);
};
/**
* Calculates the opposite of the converse of the conditional on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.CONV(false, false, true); // Returns false
* const y = Chalkboard.bool.CONV(true, false, true); // Returns true
*/
export const NCONV = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.NCONV: Parameter "vals" must contain only true, false, 0, or 1.`);
if (i > 0 && (vals[i] === true || vals[i] === 1) && !(vals[i - 1] === true || vals[i - 1] === 1)) result = false;
}
return $(!result);
};
/**
* Calculates the logical NOR (NOT OR) operation on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.NOR(false, false, false); // Returns true
* const y = Chalkboard.bool.NOR(true, false, false); // Returns false
*/
export const NOR = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.NOR: Parameter "vals" must contain only true, false, 0, or 1.`);
if (vals[i] === true || vals[i] === 1) result = false;
}
return $(result);
};
/**
* Calculates the logical NOT operation on one or more values. When more than one value is inputted, it applies NOT to each and then returns true only if all values are false.
* @param {...(boolean | 0 | 1)[]} vals - One or more values.
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.NOT(false); // Returns true
* const y = Chalkboard.bool.NOT(true, false, false); // Returns false
* const z = Chalkboard.bool.NOT(false, false, false); // Returns true
*/
export const NOT = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
if (vals.length === 0) return $(true);
let result = true;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.NOT: Parameter "vals" must contain only true, false, 0, or 1.`);
result = result && !(vals[i] === true || vals[i] === 1);
}
return $(result);
};
/**
* Calculates the logical OR operation on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.OR(false, false, true); // Returns true
* const y = Chalkboard.bool.OR(false, false, false); // Returns false
*/
export const OR = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let result = false;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.OR: Parameter "vals" must contain only true, false, 0, or 1.`);
if (vals[i] === true || vals[i] === 1) result = true;
}
return $(result);
};
/**
* Parses, simplifies, and optionally evaluates a boolean expression. When called with values, it evaluates the expression with the provided variable values, and otherwise, it simplifies the expression to its minimal form. Supports variables (a-z, A-Z, 0-9, _), operators (!, &, |), and parentheses.
* @param {string} expr - The boolean expression to parse
* @param {Record} [config.values] - Optional object mapping variable names to values
* @param {boolean} [config.returnAST=false] - If true, returns an abstract syntax tree (AST) instead of a string
* @param {boolean} [config.returnJSON=false] - If true, returns an AST in JSON instead of a string
* @param {boolean} [config.returnLaTeX=false] - If true, returns LaTeX code instead of a string
* @returns {string | boolean | 0 | 1 | { type: string, [key: string]: any }}
* @example
* // Simplify expression
* const p = Chalkboard.bool.parse("x & !x | y & x | y & !x"); // Returns "y"
* const q = Chalkboard.bool.parse("(x & y) | (x & z)"); // Returns "x & (y | z)"
*
* // Evaluate expression with values
* const r = Chalkboard.bool.parse("x & y | z", { values: { x: true, y: false, z: true } }); // Returns true
* const s = Chalkboard.bool.parse("a & !b", { values: { a: 1, b: 0 } }); // Returns true
*
* // Get AST representation
* const t = Chalkboard.bool.parse("x & y", { returnAST: true }); // Returns AST object
*/
export const parse = (
expr: string,
config: {
values?: Record,
returnAST?: boolean,
returnJSON?: boolean,
returnLaTeX?: boolean
} = { returnAST: false, returnJSON: false, returnLaTeX: false }
): string | boolean | 0 | 1 | { type: string, [key: string]: any } => {
const tokenize = (input: string): string[] => {
const tokens: string[] = [];
let i = 0;
while (i < input.length) {
const ch = input[i];
if (/\s/.test(ch)) {
i++;
continue;
}
if ("!&|()".indexOf(ch) !== -1) {
tokens.push(ch);
i++;
} else if (!/[a-zA-Z0-9_]/.test(ch)) throw new Error(`Chalkboard.bool.parse: Unsupported character "${ch}".`); else {
let name = "";
while (i < input.length && /[a-zA-Z0-9_]/.test(input[i])) {
name += input[i++];
}
tokens.push(name);
}
}
return tokens;
};
const parseTokens = (tokens: string[]): { type: string, [key: string]: any } => {
let pos = 0;
const peek = (): string => tokens[pos];
const consume = (token?: string): string => {
if (token && tokens[pos] !== token) throw new Error(`Chalkboard.bool.parse: Expected token "${token}" but found "${tokens[pos]}".`);
return tokens[pos++];
};
const parseExpression = (): { type: string, [key: string]: any } => parseOr();
const parseOr = (): { type: string, [key: string]: any } => {
let node = parseAnd();
while (peek() === "|") {
consume("|");
node = { type: "or", left: node, right: parseAnd() };
}
return node;
};
const parseAnd = (): { type: string, [key: string]: any } => {
let node = parseNot();
while (peek() === "&") {
consume("&");
node = { type: "and", left: node, right: parseNot() };
}
return node;
};
const parseNot = (): { type: string, [key: string]: any } => {
if (peek() === "!") {
consume("!");
return { type: "not", expr: parseNot() };
}
return parsePrimary();
};
const parsePrimary = (): { type: string, [key: string]: any } => {
const token = peek();
if (token === "(") {
consume("(");
const node = parseExpression();
consume(")");
return node;
}
if (token === "true") {
consume();
return { type: "bool", value: true };
}
if (token === "false") {
consume();
return { type: "bool", value: false };
}
consume();
return { type: "var", name: token };
};
const ast = parseExpression();
if (pos < tokens.length) throw new Error(`Chalkboard.bool.parse: Unexpected tokens at end.`);
return ast;
};
const nodeEqual = (a: { type: string, [key: string]: any }, b: { type: string, [key: string]: any }): boolean => {
if (a.type !== b.type) return false;
switch(a.type) {
case "bool": {
return a.value === b.value;
}
case "var": {
return a.name === b.name;
}
case "not": {
return nodeEqual(a.expr, b.expr);
}
case "and": {
return nodeEqual(a.left, b.left) && nodeEqual(a.right, b.right);
}
case "or": {
return nodeEqual(a.left, b.left) && nodeEqual(a.right, b.right);
}
}
return false;
};
const nodeToString = (node: { type: string, [key: string]: any }): string => {
switch (node.type) {
case "bool": {
return node.value ? "true" : "false";
}
case "var": {
return node.name;
}
case "not": {
const inner = node.expr.type === "var" ? nodeToString(node.expr) : `(${nodeToString(node.expr)})`;
return `!${inner}`;
}
case "and": {
const leftAnd = node.left.type === "or" ? `(${nodeToString(node.left)})` : nodeToString(node.left);
const rightAnd = node.right.type === "or" ? `(${nodeToString(node.right)})` : nodeToString(node.right);
return `${leftAnd} & ${rightAnd}`;
}
case "or": {
return `${nodeToString(node.left)} | ${nodeToString(node.right)}`;
}
}
return "";
};
const nodeToLaTeX = (node: { type: string, [key: string]: any }): string => {
switch (node.type) {
case "bool": {
return node.value ? "1" : "0";
}
case "var": {
return node.name;
}
case "not": {
const inner = (node.expr.type === "var" || node.expr.type === "bool") ? nodeToLaTeX(node.expr) : `\\left(${nodeToLaTeX(node.expr)}\\right)`;
return `\\neg ${inner}`;
}
case "and": {
const left = node.left.type === "or" ? `\\left(${nodeToLaTeX(node.left)}\\right)` : nodeToLaTeX(node.left);
const right = node.right.type === "or" ? `\\left(${nodeToLaTeX(node.right)}\\right)` : nodeToLaTeX(node.right);
return `${left} \\land ${right}`;
}
case "or": {
const left = node.left.type === "and" ? `\\left(${nodeToLaTeX(node.left)}\\right)` : nodeToLaTeX(node.left);
const right = node.right.type === "and" ? `\\left(${nodeToLaTeX(node.right)}\\right)` : nodeToLaTeX(node.right);
return `${left} \\lor ${right}`;
}
default: {
throw new Error(`Chalkboard.bool.parse: Unknown node type "${node.type}".`);
}
}
};
const getAndFactors = (node: { type: string, [key: string]: any }): Array<{ type: string, [key: string]: any }> => {
if (node.type === "and") return [...getAndFactors(node.left), ...getAndFactors(node.right)];
return [node];
};
const detectTautology = (left: { type: string, [key: string]: any }, right: { type: string, [key: string]: any }): boolean => {
if (left.type === "not" && right.type === "var" && nodeEqual(left.expr, right)) return true;
if (right.type === "not" && left.type === "var" && nodeEqual(right.expr, left)) return true;
return false;
};
const simplifyOrNode = (node: { type: string, left: any, right: any, [key: string]: any }): { type: string, [key: string]: any } => {
if (node.type !== "or") return node;
const left = simplifyNode(node.left);
const right = simplifyNode(node.right);
if (detectTautology(left, right)) return { type: "bool", value: true };
if (left.type === "bool" && left.value === true) return { type: "bool", value: true };
if (right.type === "bool" && right.value === true) return { type: "bool", value: true };
if (left.type === "bool" && left.value === false) return right;
if (right.type === "bool" && right.value === false) return left;
const leftFactors = left.type === "and" ? getAndFactors(left) : [left];
const rightFactors = right.type === "and" ? getAndFactors(right) : [right];
const commons: Array<{ type: string, [key: string]: any }> = [];
leftFactors.forEach((fa) => {
if (rightFactors.some((fb) => nodeEqual(fa, fb)) && !commons.some((c) => nodeEqual(c, fa))) {
commons.push(fa);
}
});
const leftRemainder = leftFactors.filter((f) => !commons.some((c) => nodeEqual(c, f)));
const rightRemainder = rightFactors.filter((f) => !commons.some((c) => nodeEqual(c, f)));
if (leftRemainder.length === 1 && rightRemainder.length === 1 && detectTautology(leftRemainder[0], rightRemainder[0])) return commons.length === 0 ? { type: "bool", value: true } : commons.reduce((acc, cur) => ({ type: "and", left: acc, right: cur }));
if (commons.length > 0) {
const removeCommon = (factors: Array<{ type: string, [key: string]: any }>): { type: string, [key: string]: any } => {
const remaining = factors.filter((f) => !commons.some(c => nodeEqual(c, f)));
if (remaining.length === 0) return { type: "bool", value: true };
if (remaining.length === 1) return remaining[0];
return remaining.reduce((acc, cur) => ({ type: "and", left: acc, right: cur }));
};
const newLeft = removeCommon(leftFactors);
const newRight = removeCommon(rightFactors);
let combined: { type: string, [key: string]: any };
if ((newLeft.type === "bool" && newLeft.value === true) || (newRight.type === "bool" && newRight.value === true)) {
combined = { type: "bool", value: true };
} else {
combined = { type: "or", left: newLeft, right: newRight };
}
return commons.reduce((acc, cur) => ({ type: "and", left: acc, right: cur }), combined);
}
return { type: "or", left, right };
};
const simplifyNode = (node: { type: string, [key: string]: any }): { type: string, [key: string]: any } => {
switch (node.type) {
case "bool": {
return node;
}
case "var": {
return node;
}
case "not": {
const expr = simplifyNode(node.expr);
if (expr.type === "not") return simplifyNode(expr.expr);
if (expr.type === "bool") return { type: "bool", value: !expr.value };
return { type: "not", expr };
}
case "and": {
const left = simplifyNode(node.left);
const right = simplifyNode(node.right);
if ((left.type === "bool" && left.value === false) || (right.type === "bool" && right.value === false)) return { type: "bool", value: false };
if (left.type === "bool" && left.value === true) return right;
if (right.type === "bool" && right.value === true) return left;
if (nodeEqual(left, right)) return left;
if ((left.type === "not" && nodeEqual(left.expr, right)) || (right.type === "not" && nodeEqual(right.expr, left))) return { type: "bool", value: false };
return { type: "and", left, right };
}
case "or": {
return simplifyOrNode(node as { type: string, left: any, right: any, [key: string]: any });
}
}
return node;
};
const evaluateNode = (node: { type: string, [key: string]: any }, values: Record): boolean => {
switch (node.type) {
case "bool": {
return node.value;
}
case "var": {
const varname = node.name;
if (!(varname in values)) throw new Error(`Chalkboard.bool.parse: Variable "${varname}" is not defined in "config.values".`);
const value = values[varname];
return value === true || value === 1;
}
case "not": {
return !evaluateNode(node.expr, values);
}
case "and": {
return evaluateNode(node.left, values) && evaluateNode(node.right, values);
}
case "or": {
return evaluateNode(node.left, values) || evaluateNode(node.right, values);
}
}
throw new Error(`Chalkboard.bool.parse: Unknown node type "${node.type}".`);
};
try {
const tokens = tokenize(expr);
const ast = parseTokens(tokens);
if (config.returnAST) return ast;
if (config.returnJSON) return JSON.stringify(ast);
if (config.values && Object.keys(config.values).length > 0) return $(evaluateNode(ast, config.values));
let simplified = simplifyNode(ast);
let normalizedast = parseTokens(tokenize(nodeToString(simplified)));
simplified = simplifyNode(normalizedast);
simplified = simplifyNode(simplified);
if (config.returnLaTeX) return nodeToLaTeX(simplified);
return nodeToString(simplified);
} catch (err) {
if (err instanceof Error) throw new Error(`Chalkboard.bool.parse: Error parsing expression: ${err.message.replace(/\.*$/, "")}.`); else {
throw new Error(`Chalkboard.bool.parse: Error parsing expression: ${String(err).replace(/\.*$/, "")}.`);
}
}
};
/**
* Calculates the prime implicants of a boolean expression using its Karnaugh map.
* @param {string} input - The boolean expression
* @param {string[]} variables - An array of variable names (order matters)
* @returns {string[]}
* @example
* const primes = Chalkboard.bool.primeImplicants("x & y | x & z", ["x", "y", "z"]); // Returns ["x & y", "x & z"]
*/
export const primeImplicants = (input: string, variables: string[]): string[] => {
if (typeof input !== "string") throw new Error(`Chalkboard.bool.primeImplicants: Parameter "input" must be a string.`);
if (!Array.isArray(variables) || variables.length > 0 && typeof variables[0] !== "string" || variables.length > 1 && typeof variables[variables.length - 1] !== "string") throw new Error(`Chalkboard.bool.primeImplicants: Parameter "variables" must be an array of strings.`);
if (variables.length !== 2 && variables.length !== 3 && variables.length !== 4) throw new Error(`Chalkboard.bool.primeImplicants: Parameter "variables" can only be an array with length 2, 3, or 4.`);
const kmap = Chalkboard.bool.Karnaugh(input, variables);
const grayCodes = (bits: number): string[] => {
if (bits === 0) return [""];
const prev = grayCodes(bits - 1);
const result: string[] = [];
for (let code of prev) {
result.push("0" + code);
}
for (let code of prev.slice().reverse()) {
result.push("1" + code);
}
return result;
};
let rowbits: number, colbits: number;
let rowvars: string[], colvars: string[];
if (variables.length === 2) {
rowbits = 1;
colbits = 1;
rowvars = [variables[0]];
colvars = [variables[1]];
} else if (variables.length === 3) {
rowbits = 1;
colbits = 2;
rowvars = [variables[0]];
colvars = variables.slice(1);
} else {
rowbits = 2;
colbits = 2;
rowvars = variables.slice(0, 2);
colvars = variables.slice(2);
}
const rows = grayCodes(rowbits);
const cols = grayCodes(colbits);
const cells: ({ row: number; col: number; rowcode: string; colcode: string; })[] = [];
for (let i = 0; i < rows.length; i++) {
for (let j = 0; j < cols.length; j++) {
if (kmap[i][j] === true || kmap[i][j] === 1) {
cells.push({ row: i, col: j, rowcode: rows[i], colcode: cols[j] });
}
}
}
const isAdjacent = (c1: { row: number; col: number; rowcode: string; colcode: string; }, c2: { row: number; col: number; rowcode: string; colcode: string; }): boolean => {
const rowdiff = c1.row === rows.length - 1 && c2.row === 0 || c2.row === rows.length - 1 && c1.row === 0 ? 1 : Math.abs(c1.row - c2.row);
const coldiff = c1.col === cols.length - 1 && c2.col === 0 || c2.col === cols.length - 1 && c1.col === 0 ? 1 : Math.abs(c1.col - c2.col);
return (rowdiff === 1 && coldiff === 0) || (rowdiff === 0 && coldiff === 1);
};
const isGroup = (groupcells: ({ row: number; col: number; rowcode: string; colcode: string; })[]): boolean => {
const size = groupcells.length;
if ((size & (size - 1)) !== 0) return false;
if (size === 1) return true;
const uniquerows = new Set(groupcells.map((c) => c.row)).size;
const uniquecols = new Set(groupcells.map((c) => c.col)).size;
const rowswrap = groupcells.some(c => c.row === 0) && groupcells.some(c => c.row === rows.length - 1);
const colswrap = groupcells.some(c => c.col === 0) && groupcells.some(c => c.col === cols.length - 1);
const effectiverows = rowswrap ? 1 : uniquerows;
const effectivecols = colswrap ? 1 : uniquecols;
const isRectangle = (size === effectiverows * effectivecols) && (size === 1 || size === 2 || size === 4 || size === 8 || size === 16);
if (!isRectangle) return false;
const visited = new Set();
const queue: ({ row: number; col: number; rowcode: string; colcode: string; })[] = [groupcells[0]];
visited.add(`${groupcells[0].row},${groupcells[0].col}`);
while (queue.length > 0) {
const current = queue.shift()!;
for (const neighbor of groupcells) {
const key = `${neighbor.row},${neighbor.col}`;
if (!visited.has(key) && isAdjacent(current, neighbor)) {
visited.add(key);
queue.push(neighbor);
}
}
}
return visited.size === size;
};
const generateGroups = (): ({ cells: ({ row: number; col: number; rowcode: string; colcode: string; })[]; size: number; term: string; })[] => {
const groups: ({ cells: ({ row: number; col: number; rowcode: string; colcode: string; })[]; size: number; term: string; })[] = [];
const maxsize = Chalkboard.stat.min([16, Chalkboard.real.pow(2, variables.length) as number]);
for (const cell of cells) {
groups.push({
cells: [cell],
size: 1,
term: ""
});
}
for (let size = 2; size <= maxsize; size *= 2) {
const prevgroups = groups.filter((g) => g.size === size / 2);
for (let i = 0; i < prevgroups.length; i++) {
for (let j = i + 1; j < prevgroups.length; j++) {
const group1 = prevgroups[i];
const group2 = prevgroups[j];
const merged = [...group1.cells, ...group2.cells];
const unique = new Set(merged.map((c) => `${c.row},${c.col}`));
if (unique.size === size && isGroup(merged)) {
groups.push({
cells: merged,
size: size,
term: ""
});
}
}
}
}
return groups;
};
const groups = generateGroups();
const primes: ({ cells: ({ row: number; col: number; rowcode: string; colcode: string; })[]; size: number; term: string; })[] = [];
groups.sort((a, b) => b.size - a.size);
const covered = new Set();
for (const group of groups) {
const uncovered = group.cells.some((cell) => !covered.has(`${cell.row},${cell.col}`));
if (uncovered) {
primes.push(group);
group.cells.forEach((cell) => covered.add(`${cell.row},${cell.col}`));
}
}
primes.forEach((group) => {
const constants: Record = {};
variables.forEach((variable) => {
constants[variable] = true;
});
for (let i = 0; i < rowvars.length; i++) {
const varname = rowvars[i];
const values = new Set(group.cells.map((c) => c.rowcode[i]));
if (values.size > 1) {
constants[varname] = false;
}
}
for (let i = 0; i < colvars.length; i++) {
const varname = colvars[i];
const values = new Set(group.cells.map((c) => c.colcode[i]));
if (values.size > 1) {
constants[varname] = false;
}
}
const terms: string[] = [];
for (const [varname, isConstant] of Object.entries(constants)) {
if (isConstant) {
const sampleCell = group.cells[0];
let value: boolean;
const rowindex = rowvars.indexOf(varname);
if (rowindex >= 0) {
value = sampleCell.rowcode[rowindex] === '1';
} else {
const colindex = colvars.indexOf(varname);
value = sampleCell.colcode[colindex] === '1';
}
terms.push(value ? varname : `!${varname}`);
}
}
group.term = terms.length > 0 ? terms.join(" & ") : "true";
});
return primes.map((group) => group.term);
};
/**
* Converts a boolean expression to conjunctive normal form (CNF).
* @param {string} input - The boolean expression
* @returns {string}
* @example
* const cnf = Chalkboard.bool.toCNF("x & (y | z)"); // Returns "(x) & (y | z)"
*/
export const toCNF = (input: string): string => {
if (typeof input !== "string") throw new Error(`Chalkboard.bool.toCNF: Parameter "input" must be a string.`);
const simplified = Chalkboard.bool.parse(input) as string;
if (simplified.includes(" & ") && !simplified.includes(" | ")) {
return simplified;
}
const ast = Chalkboard.bool.parse(input, { returnAST: true }) as { type: string, [key: string]: any };
const convertToCNF = (node: { type: string, [key: string]: any }): { type: string, [key: string]: any } => {
switch (node.type) {
case "bool":
case "var":
return node;
case "not":
if (node.expr.type === "not") {
return convertToCNF(node.expr.expr);
} else if (node.expr.type === "and") {
return convertToCNF({
type: "or",
left: { type: "not", expr: node.expr.left },
right: { type: "not", expr: node.expr.right }
});
} else if (node.expr.type === "or") {
return convertToCNF({
type: "and",
left: { type: "not", expr: node.expr.left },
right: { type: "not", expr: node.expr.right }
});
}
return { type: "not", expr: convertToCNF(node.expr) };
case "and":
const leftCNF = convertToCNF(node.left);
const rightCNF = convertToCNF(node.right);
return { type: "and", left: leftCNF, right: rightCNF };
case "or":
const left = convertToCNF(node.left);
const right = convertToCNF(node.right);
if (right.type === "and") {
return convertToCNF({
type: "and",
left: { type: "or", left, right: right.left },
right: { type: "or", left, right: right.right }
});
}
if (left.type === "and") {
return convertToCNF({
type: "and",
left: { type: "or", left: left.left, right },
right: { type: "or", left: left.right, right }
});
}
return { type: "or", left, right };
}
return node;
};
const cnfAST = convertToCNF(ast);
const nodeToString = (node: { type: string, [key: string]: any }): string => {
switch (node.type) {
case "bool":
return node.value ? "true" : "false";
case "var":
return node.name;
case "not":
const innerExpr = node.expr.type === "var" ?
nodeToString(node.expr) :
`(${nodeToString(node.expr)})`;
return `!${innerExpr}`;
case "and":
return `${nodeToString(node.left)} & ${nodeToString(node.right)}`;
case "or":
return `(${nodeToString(node.left)} | ${nodeToString(node.right)})`;
}
return "";
};
return nodeToString(cnfAST);
};
/**
* Converts a boolean expression to disjunctive normal form (DNF).
* @param {string} input - The boolean expression
* @returns {string}
* @example
* const dnf = Chalkboard.bool.toDNF("x & (y | z)"); // Returns "(x & y) | (x & z)"
*/
export const toDNF = (input: string): string => {
if (typeof input !== "string") throw new Error(`Chalkboard.bool.toDNF: Parameter "input" must be a string.`);
const simplified = Chalkboard.bool.parse(input) as string;
if (simplified.includes(" | ") && !simplified.includes(" & ")) {
return simplified;
}
const ast = Chalkboard.bool.parse(input, { returnAST: true }) as { type: string, [key: string]: any };
const convertToDNF = (node: { type: string, [key: string]: any }): { type: string, [key: string]: any } => {
switch (node.type) {
case "bool":
case "var":
return node;
case "not":
if (node.expr.type === "not") {
return convertToDNF(node.expr.expr);
} else if (node.expr.type === "and") {
return convertToDNF({
type: "or",
left: { type: "not", expr: node.expr.left },
right: { type: "not", expr: node.expr.right }
});
} else if (node.expr.type === "or") {
return convertToDNF({
type: "and",
left: { type: "not", expr: node.expr.left },
right: { type: "not", expr: node.expr.right }
});
}
return { type: "not", expr: convertToDNF(node.expr) };
case "or":
const leftDNF = convertToDNF(node.left);
const rightDNF = convertToDNF(node.right);
return { type: "or", left: leftDNF, right: rightDNF };
case "and":
const left = convertToDNF(node.left);
const right = convertToDNF(node.right);
if (right.type === "or") {
return convertToDNF({
type: "or",
left: { type: "and", left, right: right.left },
right: { type: "and", left, right: right.right }
});
}
if (left.type === "or") {
return convertToDNF({
type: "or",
left: { type: "and", left: left.left, right },
right: { type: "and", left: left.right, right }
});
}
return { type: "and", left, right };
}
return node;
};
const dnfAST = convertToDNF(ast);
const nodeToString = (node: { type: string, [key: string]: any }): string => {
switch (node.type) {
case "bool":
return node.value ? "true" : "false";
case "var":
return node.name;
case "not":
const innerExpr = node.expr.type === "var" ?
nodeToString(node.expr) :
`(${nodeToString(node.expr)})`;
return `!${innerExpr}`;
case "and":
const leftAnd = nodeToString(node.left);
const rightAnd = nodeToString(node.right);
return `(${leftAnd} & ${rightAnd})`;
case "or":
return `${nodeToString(node.left)} | ${nodeToString(node.right)}`;
}
return "";
};
return nodeToString(dnfAST);
};
/**
* Calculates a truth table for one or more binary boolean operators. The truth table is represented as a 2D array where each row is [p, q, op1(p,q), op2(p,q), ...].
* @param {...((p: boolean | 0 | 1, q: boolean | 0 | 1) => boolean | 0 | 1)[]} operations - One or more binary boolean operators
* @returns {(boolean | 0 | 1)[][]}
* @example
* const table = Chalkboard.bool.truthTable(Chalkboard.bool.AND, Chalkboard.bool.OR);
* // Returns:
* // [
* // [false, false, false, false],
* // [false, true, false, true],
* // [true, false, false, true],
* // [true, true, true, true]
* // ]
*/
export const truthTable = (...operations: ((p: boolean | 0 | 1, q: boolean | 0 | 1) => boolean | 0 | 1)[]): (boolean | 0 | 1)[][] => {
if (operations.length > 0 && typeof operations[0] !== "function" || operations.length > 1 && typeof operations[operations.length - 1] !== "function") throw new Error(`Chalkboard.bool.truthTable: Parameter "operations" must begin and end with functions.`);
const result: (boolean | 0 | 1)[][] = [];
const inputs: (boolean | 0 | 1)[] = [false, true];
for (let p of inputs) {
for (let q of inputs) {
const row: (boolean | 0 | 1)[] = [$(p === true), $(q === true)];
for (let op of operations) {
const result = op(p, q);
row.push((result === true || result === 1 ? $(true) : $(false)));
}
result.push(row);
}
}
return result;
};
/**
* Calculates the logical XOR (exclusive OR) operation on two or more values.
* @param {...(boolean | 0 | 1)[]} vals - Two or more values
* @returns {boolean | 0 | 1}
* @example
* const x = Chalkboard.bool.XOR(true, false, false); // Returns true
* const y = Chalkboard.bool.XOR(true, true, false); // Returns false
*/
export const XOR = (...vals: (boolean | 0 | 1)[]): boolean | 0 | 1 => {
let count = 0;
for (let i = 0; i < vals.length; i++) {
if (vals[i] !== true && vals[i] !== false && vals[i] !== 0 && vals[i] !== 1) throw new Error(`Chalkboard.bool.XOR: Parameter "vals" must contain only true, false, 0, or 1.`);
if (vals[i] === true || vals[i] === 1) count++;
}
return $(count % 2 === 1);
};
}
}