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: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "1",
+ set: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "1",
+ set: "{1,2}",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "1 2",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "1 2",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ 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: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "1",
+ set: "",
+ 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: "",
+ isElement: false,
+ isElementCaseInsensitive: false,
+ },
+ {
+ element: "3",
+ set: "",
+ 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: "",
+ 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: "",
+ 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: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "2x",
+ set: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "2x",
+ set: "{x+x, y/2}",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "x+x y/2",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "",
+ set: "{x+x, y/2}",
+ isElement: true,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "2x",
+ set: "x+X y/2",
+ isElement: false,
+ isElementCaseInsensitive: true,
+ },
+ {
+ element: "2x",
+ set: "",
+ 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: "",
+ 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: "",
+ 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";
}
}
}