From 079bcb2bdab194ce629fc82ec2dcf315f3b93119 Mon Sep 17 00:00:00 2001 From: Duane Nykamp Date: Tue, 21 Nov 2023 14:46:33 -0600 Subject: [PATCH] subset and elementof inside boolean (#58) --- .../src/components/SubsetOfReals.js | 1 + .../doenetml-worker/src/utils/booleanLogic.js | 387 +++++++-- packages/doenetml/src/Viewer/PageViewer.jsx | 15 +- .../cypress/e2e/tagSpecific/boolean.cy.js | 742 +++++++++++++++++- packages/utils/src/math/subset-of-reals.js | 121 ++- 5 files changed, 1162 insertions(+), 104 deletions(-) diff --git a/packages/doenetml-worker/src/components/SubsetOfReals.js b/packages/doenetml-worker/src/components/SubsetOfReals.js index 067e10e38..2feaee1c6 100644 --- a/packages/doenetml-worker/src/components/SubsetOfReals.js +++ b/packages/doenetml-worker/src/components/SubsetOfReals.js @@ -11,6 +11,7 @@ export default class SubsetOfReals extends MathComponent { // used when creating new component via adapter or copy prop static primaryStateVariableForDefinition = "subsetValue"; + static stateVariableToBeShadowed = "subsetValue"; static createAttributesObject() { let attributes = super.createAttributesObject(); diff --git a/packages/doenetml-worker/src/utils/booleanLogic.js b/packages/doenetml-worker/src/utils/booleanLogic.js index 3c344caf8..9c087ac79 100644 --- a/packages/doenetml-worker/src/utils/booleanLogic.js +++ b/packages/doenetml-worker/src/utils/booleanLogic.js @@ -1,6 +1,6 @@ import checkEquality from "./checkEquality"; import me from "math-expressions"; -import { deepCompare } from "@doenet/utils"; +import { buildSubsetFromMathExpression, deepCompare } from "@doenet/utils"; import { appliedFunctionSymbolsDefault, getTextToMathConverter, @@ -218,6 +218,9 @@ export function evaluateLogic({ } } + // Note: foundMath, foundText, foundBoolean, and foundOther will all be false + // if all operands are strings. + // In this case, we will default to treating the strings as math let foundMath = false; let foundText = false; let foundBoolean = false; @@ -325,6 +328,10 @@ export function evaluateLogic({ "gts", "in", "notin", + "subset", + "notsubset", + "superset", + "notsuperset", ].includes(operator) ) { if (foundText || foundBoolean || foundOther) { @@ -434,6 +441,65 @@ export function evaluateLogic({ }).fraction_equal; return fraction_equal === 0 ? 1 : 0; + } else if (operator === "in" || operator === "notin") { + let boolean1 = operands[0]; + if ( + !( + operands.length === 2 && + typeof boolean1 === "boolean" && + Array.isArray(operands[1]) && + operands[1].every((b) => typeof b === "boolean") + ) + ) { + return valueOnInvalid; + } + + // Have: [boolean1] in/notin [booleanlist] + // check if one of the elements in booleanlist is boolean1 + let isInList = operands[1].includes(boolean1); + if (operator === "in") { + return isInList ? 1 : 0; + } else { + // notin + return isInList ? 0 : 1; + } + } else if ( + ["subset", "notsubset", "superset", "notsuperset"].includes( + operator, + ) + ) { + let booleanList1 = operands[0]; + let booleanList2 = operands[1]; + + if ( + !( + operands.length === 2 && + Array.isArray(booleanList1) && + booleanList1.every((b) => typeof b === "boolean") && + Array.isArray(booleanList2) && + booleanList2.every((b) => typeof b === "boolean") + ) + ) { + return valueOnInvalid; + } + + // Have: [booleanList1] operator [booleanList2], + // where operator is subset, notsubset, superset, or notsuperset + + if (operator === "superset" || operator === "notsuperset") { + // swap operands so can use subset convention + [booleanList1, booleanList2] = [booleanList2, booleanList1]; + } + + // check if every element of booleanList1 is in booleanList2 + let haveContainment = booleanList1.every((b) => + booleanList2.includes(b), + ); + if (operator.substring(0, 3) === "not") { + return haveContainment ? 0 : 1; + } else { + return haveContainment ? 1 : 0; + } } else { return valueOnInvalid; } @@ -442,10 +508,9 @@ export function evaluateLogic({ return valueOnInvalid; } - let foundInvalidFormat = false; let foundUnorderedList = false; - let extractText = function (tree, recurse = false) { + let replaceTextAndFindUnordered = function (tree, recurse = true) { if (typeof tree === "string") { let child = dependencyValues.textChildrenByCode[tree]; if (child !== undefined) { @@ -469,17 +534,19 @@ export function evaluateLogic({ // multiple words would become multiplication if (!(recurse && Array.isArray(tree) && tree[0] === "*")) { - foundInvalidFormat = true; - return ""; + throw Error("Invalid format"); } - return tree.slice(1).map(extractText).join(" "); + return tree + .slice(1) + .map((x) => replaceTextAndFindUnordered(x, false)) + .join(" "); }; - // every operand must be a text or string - operands = operands.map((x) => extractText(x, true)); - - if (foundInvalidFormat) { + try { + // every operand must be a text or string + operands = operands.map(replaceTextAndFindUnordered); + } catch (e) { return valueOnInvalid; } @@ -541,6 +608,91 @@ export function evaluateLogic({ }).fraction_equal; return fraction_equal === 0 ? 1 : 0; + } else if (operator === "in" || operator === "notin") { + let text1 = operands[0]; + if (operands.length !== 2 || typeof text1 !== "string") { + return valueOnInvalid; + } + + if (dependencyValues.caseInsensitiveMatch) { + text1 = text1.toLowerCase(); + } + + if (typeof operands[1] === "string") { + let text2 = operands[1]; + if (dependencyValues.caseInsensitiveMatch) { + text2 = text2.toLowerCase(); + } + // Have: [text1] in/notin [text2] + // check if text1 is a substring of text2 + let isSubstring = text2.includes(text1); + if (operator === "in") { + return isSubstring ? 1 : 0; + } else { + // notin + return isSubstring ? 0 : 1; + } + } else if ( + Array.isArray(operands[1]) && + operands[1].every((s) => typeof s === "string") + ) { + let textlist = operands[1]; + if (dependencyValues.caseInsensitiveMatch) { + textlist = textlist.map((s) => s.toLowerCase()); + } + + // Have: [text1] in/notin [textlist] + // check if one of the elements in textlist is text1 + let isInList = textlist.includes(text1); + if (operator === "in") { + return isInList ? 1 : 0; + } else { + // notin + return isInList ? 0 : 1; + } + } else { + return valueOnInvalid; + } + } else if ( + ["subset", "notsubset", "superset", "notsuperset"].includes( + operator, + ) + ) { + let textList1 = operands[0]; + let textList2 = operands[1]; + + if ( + !( + operands.length === 2 && + Array.isArray(textList1) && + textList1.every((b) => typeof b === "string") && + Array.isArray(textList2) && + textList2.every((b) => typeof b === "string") + ) + ) { + return valueOnInvalid; + } + + if (dependencyValues.caseInsensitiveMatch) { + textList1 = textList1.map((s) => s.toLowerCase()); + textList2 = textList2.map((s) => s.toLowerCase()); + } + + // Have: [textList1] operator [textList2], + // where operator is subset, notsubset, superset, or notsuperset + + if (operator === "superset" || operator === "notsuperset") { + // swap operands so can use subset convention + [textList1, textList2] = [textList2, textList1]; + } + + // check if every element of textList1 is in textList2 + let haveContainment = textList1.every((b) => textList2.includes(b)); + if (operator.substring(0, 3) === "not") { + return haveContainment ? 0 : 1; + } else { + return haveContainment ? 1 : 0; + } } else { return valueOnInvalid; } @@ -740,51 +892,16 @@ export function evaluateLogic({ } if (operator === "in" || operator === "notin") { - let element = mathOperands[0]; - let set = mathOperands[1]; - let set_tree = set.tree; - if (!(Array.isArray(set_tree) && set_tree[0] === "set")) { + if (mathOperands.length !== 2) { return valueOnInvalid; } - if (dependencyValues.matchPartial) { - let results = set_tree.slice(1).map((x) => - checkEquality({ - object1: element, - object2: me.fromAst(x), - isUnordered: unorderedCompare, - partialMatches: dependencyValues.matchPartial, - matchByExactPositions: - dependencyValues.matchByExactPositions, - symbolicEquality: dependencyValues.symbolicEquality, - simplify: dependencyValues.simplifyOnCompare, - expand: dependencyValues.expandOnCompare, - allowedErrorInNumbers: - dependencyValues.allowedErrorInNumbers, - includeErrorInNumberExponents: - dependencyValues.includeErrorInNumberExponents, - allowedErrorIsAbsolute: - dependencyValues.allowedErrorIsAbsolute, - numSignErrorsMatched: dependencyValues.numSignErrorsMatched, - numPeriodicSetMatchesRequired: - dependencyValues.numPeriodicSetMatchesRequired, - caseInsensitiveMatch: dependencyValues.caseInsensitiveMatch, - matchBlanks: dependencyValues.matchBlanks, - }), - ); - - let max_fraction = results.reduce( - (a, c) => Math.max(a, c.fraction_equal), - 0, - ); - if (operator === "in") { - return max_fraction; - } else { - return 1 - max_fraction; - } - } else { - let result = set_tree.slice(1).some( - (x) => + let element = mathOperands[0]; + let set = mathOperands[1]; + let set_tree = set.tree; + if (Array.isArray(set_tree) && ["set", "list"].includes(set_tree[0])) { + if (dependencyValues.matchPartial) { + let results = set_tree.slice(1).map((x) => checkEquality({ object1: element, object2: me.fromAst(x), @@ -808,15 +925,173 @@ export function evaluateLogic({ caseInsensitiveMatch: dependencyValues.caseInsensitiveMatch, matchBlanks: dependencyValues.matchBlanks, - }).fraction_equal === 1, + }), + ); + + let max_fraction = results.reduce( + (a, c) => Math.max(a, c.fraction_equal), + 0, + ); + if (operator === "in") { + return max_fraction; + } else { + return 1 - max_fraction; + } + } else { + let result = set_tree.slice(1).some( + (x) => + checkEquality({ + object1: element, + object2: me.fromAst(x), + isUnordered: unorderedCompare, + partialMatches: dependencyValues.matchPartial, + matchByExactPositions: + dependencyValues.matchByExactPositions, + symbolicEquality: dependencyValues.symbolicEquality, + simplify: dependencyValues.simplifyOnCompare, + expand: dependencyValues.expandOnCompare, + allowedErrorInNumbers: + dependencyValues.allowedErrorInNumbers, + includeErrorInNumberExponents: + dependencyValues.includeErrorInNumberExponents, + allowedErrorIsAbsolute: + dependencyValues.allowedErrorIsAbsolute, + numSignErrorsMatched: + dependencyValues.numSignErrorsMatched, + numPeriodicSetMatchesRequired: + dependencyValues.numPeriodicSetMatchesRequired, + caseInsensitiveMatch: + dependencyValues.caseInsensitiveMatch, + matchBlanks: dependencyValues.matchBlanks, + }).fraction_equal === 1, + ); + + if (operator === "in") { + return result ? 1 : 0; + } else { + return result ? 0 : 1; + } + } + } + + // operator is in or notin, but second operand is not a set or list + // If first operand is a number and second operand can be turned into a subset of reals, + // then we can check for inclusion. + + let number1 = element.evaluate_to_constant(); + let number2 = set.evaluate_to_constant(); + + // Note: since buildSubsetFromMathExpression will create a subset from a number, + // we exclude this case to make it consistent with the fact that non-numerical + // single values are not treated as sets. + if (Number.isFinite(number1) && !Number.isFinite(number2)) { + let subsetOfReals = buildSubsetFromMathExpression(set); + + if (subsetOfReals.isValid()) { + let containsNumber = subsetOfReals.containsElement(number1); + if (operator === "in") { + return containsNumber ? 1 : 0; + } else { + // notin + return containsNumber ? 0 : 1; + } + } + } + + return valueOnInvalid; + } + + if (["subset", "notsubset", "superset", "notsuperset"].includes(operator)) { + if (mathOperands.length !== 2) { + return valueOnInvalid; + } + + let set1 = mathOperands[0]; + let set2 = mathOperands[1]; + + if (operator === "superset" || operator === "notsuperset") { + // swap operands so can use subset convention + [set1, set2] = [set2, set1]; + } + + let set1_tree = set1.tree; + let set2_tree = set2.tree; + + if ( + Array.isArray(set1_tree) && + ["set", "list"].includes(set1_tree[0]) && + Array.isArray(set2_tree) && + ["set", "list"].includes(set2_tree[0]) + ) { + // check if every element in set 1 is equal to an element in set 2 + let haveContainment = set1_tree.slice(1).every((elt1) => + set2_tree.slice(1).some( + (elt2) => + checkEquality({ + object1: me.fromAst(elt1), + object2: me.fromAst(elt2), + isUnordered: unorderedCompare, + partialMatches: dependencyValues.matchPartial, + matchByExactPositions: + dependencyValues.matchByExactPositions, + symbolicEquality: dependencyValues.symbolicEquality, + simplify: dependencyValues.simplifyOnCompare, + expand: dependencyValues.expandOnCompare, + allowedErrorInNumbers: + dependencyValues.allowedErrorInNumbers, + includeErrorInNumberExponents: + dependencyValues.includeErrorInNumberExponents, + allowedErrorIsAbsolute: + dependencyValues.allowedErrorIsAbsolute, + numSignErrorsMatched: + dependencyValues.numSignErrorsMatched, + numPeriodicSetMatchesRequired: + dependencyValues.numPeriodicSetMatchesRequired, + caseInsensitiveMatch: + dependencyValues.caseInsensitiveMatch, + matchBlanks: dependencyValues.matchBlanks, + }).fraction_equal === 1, + ), ); - if (operator === "in") { - return result ? 1 : 0; + if (operator.substring(0, 3) === "not") { + return haveContainment ? 0 : 1; } else { - return result ? 0 : 1; + return haveContainment ? 1 : 0; + } + } + + // operator is subset, notsubset, superset, or notsuperset, + // but operands are not lists or sets + // If both operands can be turned into a subset of reals, + // then we can check for inclusion. + + // Note: since buildSubsetFromMathExpression will create a subset from a number, + // we exclude this case to make it consistent with the fact that non-numerical + // single values are not treated as sets. + let number1 = set1.evaluate_to_constant(); + let number2 = set2.evaluate_to_constant(); + + if (!(Number.isFinite(number1) || Number.isFinite(number2))) { + let subsetOfReals1 = buildSubsetFromMathExpression(set1); + + if (subsetOfReals1.isValid()) { + let subsetOfReals2 = buildSubsetFromMathExpression(set2); + + if (subsetOfReals2.isValid()) { + let haveContainment = + subsetOfReals2.containsSubset(subsetOfReals1); + + if (operator.substring(0, 3) === "not") { + return haveContainment ? 0 : 1; + } else { + return haveContainment ? 1 : 0; + } + } } } + + return valueOnInvalid; } // since have inequality, all operands must be numbers diff --git a/packages/doenetml/src/Viewer/PageViewer.jsx b/packages/doenetml/src/Viewer/PageViewer.jsx index 8e7c76a5c..92e2d988c 100644 --- a/packages/doenetml/src/Viewer/PageViewer.jsx +++ b/packages/doenetml/src/Viewer/PageViewer.jsx @@ -819,13 +819,14 @@ export function PageViewer({ } } else { // TODO: are there cases where will get an infinite loop here? - sendAlert( - `Reverted page to state saved on device ${changedOnDevice}`, - "info", - ); - - coreId.current = nanoid(); - setPageContentChanged(true); + // TODO: since we removed the pageContentChanged feature, it's not clear what to do here. + // We should either make this work correctly or remove any calls to resetPage. + // sendAlert( + // `Reverted page to state saved on device ${changedOnDevice}`, + // "info", + // ); + // coreId.current = nanoid(); + // setPageContentChanged(true); } } diff --git a/packages/test-cypress/cypress/e2e/tagSpecific/boolean.cy.js b/packages/test-cypress/cypress/e2e/tagSpecific/boolean.cy.js index 93d7868be..0da12884d 100644 --- a/packages/test-cypress/cypress/e2e/tagSpecific/boolean.cy.js +++ b/packages/test-cypress/cypress/e2e/tagSpecific/boolean.cy.js @@ -1,4 +1,4 @@ -import { cesc } from "@doenet/utils"; +import { cesc, cesc2 } from "@doenet/utils"; describe("Boolean Tag Tests", function () { beforeEach(() => { @@ -351,6 +351,746 @@ describe("Boolean Tag Tests", function () { cy.get(cesc("#\\/f19")).should("have.text", "false"); }); + it("element of list, set, or string", () => { + let elements = [ + { + element: "1", + set: "1 2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1 2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1,2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "{1,2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "{1,2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1 2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1 2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1,2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "{1,2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "{1,2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1 2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1 2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1,2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "{1,2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "{1,2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "3", + set: "1 2", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "3", + set: "1 2", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "3", + set: "1,2", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "3", + set: "{1,2}", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "3", + set: "{1,2}", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "3", + set: "3", + isElement: false, + isElementCaseInsensitive: false, + isInvalid: true, + }, + { + element: "2, 3", + set: "{1,2}", + isElement: false, + isElementCaseInsensitive: false, + isInvalid: true, + }, + { + element: "1", + set: "[1,2)", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "[1,2)", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "[1,2)", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "1 <= x < 2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "1", + set: "(1,2)", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "1", + set: "(1,2)", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "1", + set: "(1,2)", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "1", + set: "1 < x < 2", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "3", + set: "(1,4) intersect (2,5)", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2", + set: "(1,4) intersect (2,5)", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "2x", + set: "x+x y/2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "x+x, y/2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "{x+x, y/2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "{x+x, y/2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "x+x y/2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "x+x, y/2", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "{x+x, y/2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "{x+x, y/2}", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "x+X y/2", + isElement: false, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "x+X, y/2", + isElement: false, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "{x+X, y/2}", + isElement: false, + isElementCaseInsensitive: true, + }, + { + element: "2x", + set: "x+X", + isElement: false, + isElementCaseInsensitive: false, + isInvalid: true, + }, + { + element: "x", + set: "x+X y/2", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "x", + set: "x+X, y/2", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "x", + set: "{x+X, y/2}", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "b", + set: "abc", + isElement: false, + isElementCaseInsensitive: false, + isInvalid: true, + }, + { + element: "b", + set: "abc", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "b", + set: "abc", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "b", + set: "abc", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "b", + set: "ABC", + isElement: false, + isElementCaseInsensitive: true, + }, + { + element: "b", + set: "ABC", + isElement: false, + isElementCaseInsensitive: true, + }, + { + element: "b", + set: "ABC", + isElement: false, + isElementCaseInsensitive: true, + }, + { + element: "b", + set: "abc", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "b", + set: "abc def", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "abc", + set: "abc def", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "abc", + set: "ABC def", + isElement: false, + isElementCaseInsensitive: true, + }, + { + element: "truE", + set: "false true", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "true", + set: "false false", + isElement: false, + isElementCaseInsensitive: false, + }, + { + element: "truE", + set: "false true", + isElement: true, + isElementCaseInsensitive: true, + }, + { + element: "true", + set: "false false", + isElement: false, + isElementCaseInsensitive: false, + }, + ]; + + let doenetML = "a"; + + for (let [ind, info] of elements.entries()) { + doenetML += `\n${info.element} elementof ${info.set}`; + doenetML += `\n${info.element} notelementof ${info.set}`; + doenetML += `\n${info.element} elementof ${info.set}`; + doenetML += `\n${info.element} notelementof ${info.set}`; + } + + cy.window().then(async (win) => { + win.postMessage( + { + doenetML, + }, + "*", + ); + }); + + cy.get(cesc2("#/_text1")).should("contain.text", "a"); + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + + for (let [ind, info] of elements.entries()) { + expect( + stateVariables[`/s${ind}`].stateValues.value, + `Checking if ${info.element} is element of ${info.set}`, + ).eq(info.isElement && !info.isInvalid); + expect( + stateVariables[`/n${ind}`].stateValues.value, + `Checking if ${info.element} is not element of ${info.set}`, + ).eq(!info.isElement && !info.isInvalid); + expect( + stateVariables[`/sci${ind}`].stateValues.value, + `Checking if ${info.element} is case-insensitive element of ${info.set}`, + ).eq(info.isElementCaseInsensitive && !info.isInvalid); + expect( + stateVariables[`/nsci${ind}`].stateValues.value, + `Checking if ${info.element} is not case-insensitive element of ${info.set}`, + ).eq(!info.isElementCaseInsensitive && !info.isInvalid); + } + }); + }); + + it("subset or superset of list or set", () => { + let elements = [ + { + set1: "x+x y-y", + set2: "z 2x q 0", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "x+X Y-y", + set2: "z 2X q 0", + isSubset: false, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "z 2x q 0", + set2: "x+x y-y", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: true, + isSupersetCaseInsensitive: true, + }, + { + set1: "z 2X q 0", + set2: "x+X Y-y", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: true, + }, + { + set1: "x+x y-y v", + set2: "z 2x q 0", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "x+x y-y q 0 2x 2x z", + set2: "z 2x q 0 q", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: true, + isSupersetCaseInsensitive: true, + }, + { + set1: "z", + set2: "z 2x", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + { + set1: "z", + set2: "z 2x", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + { + set1: "z", + set2: "z 2x", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "(1,2)", + set2: "(0,3)", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "(0,3)", + set2: "(1,2)", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: true, + isSupersetCaseInsensitive: true, + }, + { + set1: "(0,3)", + set2: "(2,3]", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "{2,3}", + set2: "[2,4)", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "{3}", + set2: "[2,4)", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "3", + set2: "[2,4)", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + { + set1: "2,3", + set2: "[2,4)", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + { + set1: "{3}", + set2: "[2,3] intersect [3,4)", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: true, + isSupersetCaseInsensitive: true, + }, + { + set1: "hello there", + set2: "there bye hello", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "hellO there", + set2: "tHere bye hello", + isSubset: false, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "there bye hello", + set2: "hello there", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: true, + isSupersetCaseInsensitive: true, + }, + { + set1: "tHere bye hello", + set2: "hellO there", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: true, + }, + { + set1: "ere hel", + set2: "there hello", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "hello there there hello hello", + set2: "there hello bye", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "hello", + set2: "there hello bye", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + { + set1: "hello", + set2: "there hello bye", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + { + set1: "true true", + set2: "true false", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "true true", + set2: "false false", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + }, + { + set1: "false true true", + set2: "true false", + isSubset: true, + isSubsetCaseInsensitive: true, + isSuperset: true, + isSupersetCaseInsensitive: true, + }, + { + set1: "true", + set2: "true false", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + { + set1: "true", + set2: "true false", + isSubset: false, + isSubsetCaseInsensitive: false, + isSuperset: false, + isSupersetCaseInsensitive: false, + isInvalid: true, + }, + ]; + + let doenetML = "a"; + + for (let [ind, info] of elements.entries()) { + doenetML += `\n${info.set1} subset ${info.set2}`; + doenetML += `\n${info.set1} notsubset ${info.set2}`; + doenetML += `\n${info.set1} superset ${info.set2}`; + doenetML += `\n${info.set1} notsuperset ${info.set2}`; + doenetML += `\n${info.set1} subset ${info.set2}`; + doenetML += `\n${info.set1} notsubset ${info.set2}`; + doenetML += `\n${info.set1} superset ${info.set2}`; + doenetML += `\n${info.set1} notsuperset ${info.set2}`; + } + + cy.window().then(async (win) => { + win.postMessage( + { + doenetML, + }, + "*", + ); + }); + + cy.get(cesc2("#/_text1")).should("contain.text", "a"); + + cy.window().then(async (win) => { + let stateVariables = await win.returnAllStateVariables1(); + + for (let [ind, info] of elements.entries()) { + expect( + stateVariables[`/sb${ind}`].stateValues.value, + `Checking if ${info.set1} is subset of ${info.set2}`, + ).eq(info.isSubset && !info.isInvalid); + expect( + stateVariables[`/nsb${ind}`].stateValues.value, + `Checking if ${info.set1} is not subset of ${info.set2}`, + ).eq(!info.isSubset && !info.isInvalid); + expect( + stateVariables[`/sp${ind}`].stateValues.value, + `Checking if ${info.set1} is superset of ${info.set2}`, + ).eq(info.isSuperset && !info.isInvalid); + expect( + stateVariables[`/nsp${ind}`].stateValues.value, + `Checking if ${info.set1} is not superset of ${info.set2}`, + ).eq(!info.isSuperset && !info.isInvalid); + expect( + stateVariables[`/sbci${ind}`].stateValues.value, + `Checking if ${info.set1} is case-insensitive subset of ${info.set2}`, + ).eq(info.isSubsetCaseInsensitive && !info.isInvalid); + expect( + stateVariables[`/nsbci${ind}`].stateValues.value, + `Checking if ${info.set1} is not case-insensitive subset of ${info.set2}`, + ).eq(!info.isSubsetCaseInsensitive && !info.isInvalid); + expect( + stateVariables[`/spci${ind}`].stateValues.value, + `Checking if ${info.set1} is case-insensitive superset of ${info.set2}`, + ).eq(info.isSupersetCaseInsensitive && !info.isInvalid); + expect( + stateVariables[`/nspci${ind}`].stateValues.value, + `Checking if ${info.set1} is not case-insensitive superset of ${info.set2}`, + ).eq(!info.isSupersetCaseInsensitive && !info.isInvalid); + } + }); + }); + it("boolean with texts", () => { cy.window().then(async (win) => { win.postMessage( diff --git a/packages/utils/src/math/subset-of-reals.js b/packages/utils/src/math/subset-of-reals.js index 839e9c765..53e5bce96 100644 --- a/packages/utils/src/math/subset-of-reals.js +++ b/packages/utils/src/math/subset-of-reals.js @@ -33,10 +33,22 @@ class Subset { return this.setMinus(that).union(that.setMinus(this)); } + containsSubset(that) { + return this.intersect(that).equals(that); + } + + isSubsetOf(that) { + return that.intersect(this).equals(this); + } + equals(that) { return this.symmetricDifference(that).isEmpty(); } + isValid() { + return true; + } + toJSON() { return { objectType: "subset", @@ -86,7 +98,7 @@ class EmptySet extends Subset { return new EmptySet(); } - contains(/* element */) { + containsElement(/* element */) { return false; } @@ -107,6 +119,42 @@ class EmptySet extends Subset { } } +class InvalidSet extends Subset { + static subsetType = "invalidSet"; + + union(/* subset */) { + return new InvalidSet(); + } + + intersect(/* subset */) { + return new InvalidSet(); + } + + containsElement(/* element */) { + return false; + } + + isEmpty() { + return true; + } + + complement() { + return new InvalidSet(); + } + + isValid() { + return false; + } + + toString() { + return "\uff3f"; + } + + toMathExpression() { + return me.fromAst("\uff3f"); + } +} + /** **************************************************************/ class RealLine extends Subset { static subsetType = "realLine"; @@ -119,7 +167,7 @@ class RealLine extends Subset { return that; } - contains(/* element */) { + containsElement(/* element */) { return true; } @@ -155,7 +203,7 @@ class Singleton extends Subset { } union(that) { - if (that.contains(this.element)) { + if (that.containsElement(this.element)) { return that; } /* else */ @@ -163,7 +211,7 @@ class Singleton extends Subset { } intersect(subset) { - if (subset.contains(this.element)) { + if (subset.containsElement(this.element)) { return new Singleton(this.element); } /* else */ @@ -174,7 +222,7 @@ class Singleton extends Subset { return false; } - contains(element) { + containsElement(element) { return element === this.element; } @@ -373,8 +421,8 @@ class Union extends Subset { return this; } - contains(element) { - return this.subsets.some((s) => s.contains(element)); + containsElement(element) { + return this.subsets.some((s) => s.containsElement(element)); } isEmpty() { @@ -443,7 +491,7 @@ class OpenInterval extends Interval { return this.left >= this.right; } - contains(element) { + containsElement(element) { return element > this.left && element < this.right; } @@ -523,6 +571,7 @@ class ClosedOpenInterval extends Interval { export const subsets = { Subset, EmptySet, + InvalidSet, RealLine, Singleton, Union, @@ -549,7 +598,7 @@ function buildSubsetFromIntervals(tree, variable) { // TODO: eliminate \u2205 once have varnothing integrated into latex parser return new EmptySet(); } else { - return new EmptySet(); + return new InvalidSet(); } } @@ -569,7 +618,7 @@ function buildSubsetFromIntervals(tree, variable) { left === -Infinity ) ) { - return new EmptySet(); + return new InvalidSet(); } } @@ -583,7 +632,7 @@ function buildSubsetFromIntervals(tree, variable) { right === -Infinity ) ) { - return new EmptySet(); + return new InvalidSet(); } } @@ -624,7 +673,7 @@ function buildSubsetFromIntervals(tree, variable) { } else { return pieces.reduce((a, c) => a.intersect(c)); } - } else if (operator === "set") { + } else if (operator === "set" || operator === "list") { let pieces = tree .slice(1) .map((x) => buildSubsetFromIntervals(x, variable)) @@ -652,7 +701,7 @@ function buildSubsetFromIntervals(tree, variable) { left === -Infinity ) ) { - return new EmptySet(); + return new InvalidSet(); } } } @@ -671,14 +720,14 @@ function buildSubsetFromIntervals(tree, variable) { right === -Infinity ) ) { - return new EmptySet(); + return new InvalidSet(); } } } if (varAtLeft) { if (varAtRight) { - return new EmptySet(); + return new InvalidSet(); } else { if (operator === "<") { return new OpenInterval(-Infinity, right); @@ -692,7 +741,7 @@ function buildSubsetFromIntervals(tree, variable) { if (Number.isFinite(right)) { return new Singleton(right); } else { - return new EmptySet(); + return new InvalidSet(); } } else { // operator === "ne" @@ -702,7 +751,7 @@ function buildSubsetFromIntervals(tree, variable) { new OpenInterval(right, Infinity), ]); } else { - return new RealLine(); + return new InvalidSet(); } } } @@ -720,7 +769,7 @@ function buildSubsetFromIntervals(tree, variable) { if (Number.isFinite(left)) { return new Singleton(left); } else { - return new EmptySet(); + return new InvalidSet(); } } else { // operator === "ne" @@ -730,11 +779,11 @@ function buildSubsetFromIntervals(tree, variable) { new OpenInterval(left, Infinity), ]); } else { - return new RealLine(); + return new InvalidSet(); } } } else { - return new EmptySet(); + return new InvalidSet(); } } } else if (["lts", "gts"].includes(operator)) { @@ -742,7 +791,7 @@ function buildSubsetFromIntervals(tree, variable) { let strict = tree[2].slice(1); if (vals.length !== 3 || !deepCompare(vals[1], variable)) { - return new EmptySet(); + return new InvalidSet(); } if (operator === "gts") { @@ -760,7 +809,7 @@ function buildSubsetFromIntervals(tree, variable) { left === -Infinity ) ) { - return new EmptySet(); + return new InvalidSet(); } } @@ -774,7 +823,7 @@ function buildSubsetFromIntervals(tree, variable) { right === -Infinity ) ) { - return new EmptySet(); + return new InvalidSet(); } } @@ -796,46 +845,38 @@ function buildSubsetFromIntervals(tree, variable) { return buildSubsetFromIntervals(tree[2], variable); } else if (operator === "^" && (tree[2] === "C" || tree[2] === "c")) { let orig = buildSubsetFromIntervals(tree[1], variable); - if (orig) { - return orig.complement(); - } else { - return new EmptySet(); - } + return orig.complement(); } else if (operator === "in") { if (deepCompare(tree[1], variable)) { return buildSubsetFromIntervals(tree[2], variable); } else { - return new EmptySet(); + return new InvalidSet(); } } else if (operator === "ni") { if (deepCompare(tree[2], variable)) { return buildSubsetFromIntervals(tree[1], variable); } else { - return new EmptySet(); + return new InvalidSet(); } } else if (operator === "notin") { if (deepCompare(tree[1], variable)) { let orig = buildSubsetFromIntervals(tree[2], variable); - if (orig) { - return orig.complement(); - } + return orig.complement(); } - return new EmptySet(); + return new InvalidSet(); } else if (operator === "notni") { if (deepCompare(tree[2], variable)) { let orig = buildSubsetFromIntervals(tree[1], variable); - if (orig) { - return orig.complement(); - } + return orig.complement(); } - return new EmptySet(); + return new InvalidSet(); } else { let num = me.fromAst(tree).evaluate_to_constant(); if (Number.isFinite(num)) { return new Singleton(num); } else { - return new EmptySet(); + return new InvalidSet(); } } } @@ -924,7 +965,7 @@ export function mathExpressionFromSubsetValue({ } else if (subset instanceof RealLine) { return ["in", variable, "R"]; } else { - return null; + return "\uff3f"; } } }