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

Add xor predication operator #8661

Merged
merged 1 commit into from
Jul 11, 2023
Merged
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
54 changes: 34 additions & 20 deletions src/module/system/predication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ class PredicatePF2e extends Array<PredicateStatement> {
readonly isValid: boolean;

constructor(...statements: PredicateStatement[] | [PredicateStatement[]]) {
if (Array.isArray(statements[0])) {
super(...statements[0]);
} else {
super(...(statements as PredicateStatement[]));
}
super(...(Array.isArray(statements[0]) ? statements[0] : (statements as PredicateStatement[])));
this.isValid = PredicatePF2e.isValid(this);
}

Expand Down Expand Up @@ -46,7 +42,7 @@ class PredicatePF2e extends Array<PredicateStatement> {
}

const domain = options instanceof Set ? options : new Set(options);
return this.every((s) => this.isTrue(s, domain));
return this.every((s) => this.#isTrue(s, domain));
}

toObject(): RawPredicate {
Expand All @@ -58,15 +54,15 @@ class PredicatePF2e extends Array<PredicateStatement> {
}

/** Is the provided statement true? */
private isTrue(statement: PredicateStatement, domain: Set<string>): boolean {
#isTrue(statement: PredicateStatement, domain: Set<string>): boolean {
return (
(typeof statement === "string" && domain.has(statement)) ||
(StatementValidator.isBinaryOp(statement) && this.testBinaryOp(statement, domain)) ||
(StatementValidator.isCompound(statement) && this.testCompound(statement, domain))
(StatementValidator.isBinaryOp(statement) && this.#testBinaryOp(statement, domain)) ||
(StatementValidator.isCompound(statement) && this.#testCompound(statement, domain))
);
}

private testBinaryOp(statement: BinaryOperation, domain: Set<string>): boolean {
#testBinaryOp(statement: BinaryOperation, domain: Set<string>): boolean {
if ("eq" in statement) {
return domain.has(`${statement.eq[0]}:${statement.eq[1]}`);
} else {
Expand Down Expand Up @@ -102,14 +98,15 @@ class PredicatePF2e extends Array<PredicateStatement> {
}

/** Is the provided compound statement true? */
private testCompound(statement: Exclude<PredicateStatement, Atom>, domain: Set<string>): boolean {
#testCompound(statement: Exclude<PredicateStatement, Atom>, domain: Set<string>): boolean {
return (
("and" in statement && statement.and.every((subProp) => this.isTrue(subProp, domain))) ||
("nand" in statement && !statement.nand.every((subProp) => this.isTrue(subProp, domain))) ||
("or" in statement && statement.or.some((subProp) => this.isTrue(subProp, domain))) ||
("nor" in statement && !statement.nor.some((subProp) => this.isTrue(subProp, domain))) ||
("not" in statement && !this.isTrue(statement.not, domain)) ||
("if" in statement && !(this.isTrue(statement.if, domain) && !this.isTrue(statement.then, domain)))
("and" in statement && statement.and.every((subProp) => this.#isTrue(subProp, domain))) ||
("nand" in statement && !statement.nand.every((subProp) => this.#isTrue(subProp, domain))) ||
("or" in statement && statement.or.some((subProp) => this.#isTrue(subProp, domain))) ||
("xor" in statement && statement.xor.filter((subProp) => this.#isTrue(subProp, domain)).length === 1) ||
("nor" in statement && !statement.nor.some((subProp) => this.#isTrue(subProp, domain))) ||
("not" in statement && !this.#isTrue(statement.not, domain)) ||
("if" in statement && !(this.#isTrue(statement.if, domain) && !this.#isTrue(statement.then, domain)))
);
}
}
Expand All @@ -127,15 +124,15 @@ class StatementValidator {
return (typeof statement === "string" && statement.length > 0) || this.isBinaryOp(statement);
}

private static binaryOperators = new Set(["eq", "gt", "gte", "lt", "lte"]);
static #binaryOperators = new Set(["eq", "gt", "gte", "lt", "lte"]);

static isBinaryOp(statement: unknown): statement is BinaryOperation {
if (!isObject(statement)) return false;
const entries = Object.entries(statement);
if (entries.length > 1) return false;
const [operator, operands]: [string, unknown] = entries[0];
return (
this.binaryOperators.has(operator) &&
this.#binaryOperators.has(operator) &&
Array.isArray(operands) &&
operands.length === 2 &&
typeof operands[0] === "string" &&
Expand All @@ -149,6 +146,7 @@ class StatementValidator {
(this.isAnd(statement) ||
this.isOr(statement) ||
this.isNand(statement) ||
this.isXor(statement) ||
this.isNor(statement) ||
this.isNot(statement) ||
this.isIf(statement))
Expand Down Expand Up @@ -179,6 +177,14 @@ class StatementValidator {
);
}

static isXor(statement: { xor?: unknown }): statement is ExclusiveDisjunction {
return (
Object.keys(statement).length === 1 &&
Array.isArray(statement.xor) &&
statement.xor.every((subProp) => this.isStatement(subProp))
);
}

static isNor(statement: { nor?: unknown }): statement is JointDenial {
return (
Object.keys(statement).length === 1 &&
Expand Down Expand Up @@ -208,11 +214,19 @@ type Atom = string | BinaryOperation;

type Conjunction = { and: PredicateStatement[] };
type Disjunction = { or: PredicateStatement[] };
type ExclusiveDisjunction = { xor: PredicateStatement[] };
type Negation = { not: PredicateStatement };
type AlternativeDenial = { nand: PredicateStatement[] };
type JointDenial = { nor: PredicateStatement[] };
type Conditional = { if: PredicateStatement; then: PredicateStatement };
type CompoundStatement = Conjunction | Disjunction | AlternativeDenial | JointDenial | Negation | Conditional;
type CompoundStatement =
| Conjunction
| Disjunction
| ExclusiveDisjunction
| AlternativeDenial
| JointDenial
| Negation
| Conditional;

type PredicateStatement = Atom | CompoundStatement;

Expand Down
74 changes: 58 additions & 16 deletions tests/module/system/predication.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PredicatePF2e } from "@system/predication.ts";

describe("Predication with string atomics return correct results", () => {
describe("Predication with string atomics returns correct results", () => {
test("conjunctions of atomic statements", () => {
const predicate = new PredicatePF2e("foo", "bar", "baz");
expect(predicate.test(["foo"])).toEqual(false);
Expand All @@ -26,7 +26,7 @@ describe("Predication with string atomics return correct results", () => {
});
});

describe("Predication with numeric-comparison atomics return correct results", () => {
describe("Predication with numeric-comparison atomics returns correct results", () => {
test("greater-than, less-than", () => {
const predicate = new PredicatePF2e({ gt: ["foo", 2] }, { lt: ["bar", 2] });
expect(predicate.test(["foo:1", "bar:3"])).toEqual(false);
Expand Down Expand Up @@ -59,7 +59,7 @@ describe("Predication with numeric-comparison atomics return correct results", (
});
});

describe("Predication with conjunction and negation return correct results", () => {
describe("Predication with conjunction and negation returns correct results", () => {
test("conjunction operator", () => {
const predicate = new PredicatePF2e({ and: ["foo", "bar", "baz"] });
expect(predicate.test(["foo"])).toEqual(false);
Expand Down Expand Up @@ -99,7 +99,7 @@ describe("Predication with conjunction and negation return correct results", ()
});
});

describe("simple disjunction return correct results", () => {
describe("Simple disjunction returns correct results", () => {
test("single disjunction operator", () => {
const predicate = new PredicatePF2e({ or: ["foo", "bar", "baz"] });
expect(predicate.test(["foo"])).toEqual(true);
Expand Down Expand Up @@ -142,9 +142,9 @@ describe("simple disjunction return correct results", () => {
});
});

describe("Predication with joint denial return correct results", () => {
test("conjunction of joint denial", () => {
const predicate = new PredicatePF2e({ and: [{ nor: ["foo", "bar", "baz"] }] });
describe("Predication with joint denial returns correct results", () => {
test("simple joint denial", () => {
const predicate = new PredicatePF2e({ nor: ["foo", "bar", "baz"] });
expect(predicate.test(["foo"])).toEqual(false);
expect(predicate.test(["foo", "bar"])).toEqual(false);
expect(predicate.test(["foo", "bar", "baz"])).toEqual(false);
Expand All @@ -154,15 +154,57 @@ describe("Predication with joint denial return correct results", () => {
expect(predicate.test(["bat"])).toEqual(true);
});

test("joint denial is equivalent to negated disjunction", () => {
const joinDenial = new PredicatePF2e({ nor: ["foo", "bar"] });
const negatedDisjunction = new PredicatePF2e({ not: { or: ["foo", "bar"] } });
expect(joinDenial.test(["foo"])).toEqual(negatedDisjunction.test(["foo"]));
expect(joinDenial.test(["foo", "bar"])).toEqual(negatedDisjunction.test(["foo", "bar"]));
expect(joinDenial.test(["foo", "bar", "baz"])).toEqual(negatedDisjunction.test(["foo", "bar", "baz"]));
expect(joinDenial.test(["baz"])).toEqual(negatedDisjunction.test(["baz"]));
expect(joinDenial.test(["baz", "bat"])).toEqual(negatedDisjunction.test(["baz", "bat"]));
expect(joinDenial.test([])).toEqual(negatedDisjunction.test([]));
test("joint denial with compound operand", () => {
const predicate = new PredicatePF2e({ nor: ["foo", { and: ["bar", "baz"] }] });
expect(predicate.test(["foo"])).toEqual(false);
expect(predicate.test(["foo", "bar"])).toEqual(false);
expect(predicate.test(["foo", "bar", "baz"])).toEqual(false);
expect(predicate.test(["bar", "baz"])).toEqual(false);
expect(predicate.test(["baz"])).toEqual(true);
expect(predicate.test([])).toEqual(true);
expect(predicate.test(["bat"])).toEqual(true);
});
});

describe("Predication with exclusive disjunction returns correct results", () => {
test("simple exclusive disjunction", () => {
const predicate = new PredicatePF2e({ xor: ["foo", "bar", "baz"] });
expect(predicate.test(["foo"])).toEqual(true);
expect(predicate.test(["foo", "bar"])).toEqual(false);
expect(predicate.test(["foo", "bar", "baz"])).toEqual(false);
expect(predicate.test(["bar", "baz"])).toEqual(false);
expect(predicate.test(["baz"])).toEqual(true);
expect(predicate.test([])).toEqual(false);
expect(predicate.test(["bat"])).toEqual(false);
expect(predicate.test(["bar", "bat"])).toEqual(true);
});

test("exclusive disjunction with compound operand", () => {
const predicate = new PredicatePF2e({ xor: ["foo", { or: ["bar", "baz"] }] });
expect(predicate.test(["foo"])).toEqual(true);
expect(predicate.test(["foo", "bar"])).toEqual(false);
expect(predicate.test(["foo", "bar", "baz"])).toEqual(false);
expect(predicate.test(["bar", "baz"])).toEqual(true);
expect(predicate.test(["baz"])).toEqual(true);
expect(predicate.test([])).toEqual(false);
});

test("tautological and contradictory exclusive disjunction", () => {
const tautology = new PredicatePF2e({ xor: ["foo", { not: "foo" }] });
expect(tautology.test(["foo"])).toEqual(true);
expect(tautology.test([])).toEqual(true);
expect(tautology.test(["bar"])).toEqual(true);

const contradiction1 = new PredicatePF2e({ xor: ["foo", "foo"] });
expect(contradiction1.test(["foo"])).toEqual(false);
expect(contradiction1.test(["bar"])).toEqual(false);
expect(contradiction1.test([])).toEqual(false);

const contradiction2 = new PredicatePF2e({ xor: ["foo", { or: ["foo", "foo"] }] });
expect(contradiction2.test(["foo"])).toEqual(false);
expect(contradiction2.test(["bar"])).toEqual(false);
expect(contradiction2.test(["foo", "bar"])).toEqual(false);
expect(contradiction2.test([])).toEqual(false);
});
});

Expand Down