forked from episphere/quest
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
21 changed files
with
1,734 additions
and
1,539 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
import { getStateManager } from './stateManager.js'; | ||
import { create, all } from 'https://cdn.skypack.dev/pin/[email protected]/mode=imports,min/optimized/mathjs.js'; | ||
import { moduleParams } from './questionnaire.js'; | ||
export const math = create(all); | ||
|
||
// Strip '_<num>' when an id that _0, _1, _2, etc. as a suffix. | ||
|
@@ -70,7 +71,7 @@ export const customMathJSFunctions = { | |
case 'undefined': | ||
return false; | ||
default: | ||
console.error(`unhandled case in EXISTS check. Error: ${x} is not a valid response type.`); | ||
moduleParams.errorLogger(`unhandled case in EXISTS check. Error: ${x} is not a valid response type.`); | ||
return false; | ||
} | ||
}, | ||
|
@@ -94,7 +95,6 @@ export const customMathJSFunctions = { | |
}, | ||
|
||
getKeyedValue: function(x) { | ||
console.warn('TODO: GET KEYED VALUE', x); | ||
const array = x.toString().split('.'); | ||
const key = array.shift(); | ||
const obj = this._value(key); | ||
|
@@ -295,61 +295,32 @@ export const customMathJSFunctions = { | |
let responseValue = this._value(questionId); | ||
|
||
if (Array.isArray(responseValue) || Array.isArray(responseValue[name])) { | ||
// Note: haven't found an instance of this case yet. | ||
console.error('TODO: (selectionCount) remove DOM access and use stateManager', x, countReset); | ||
responseValue = Array.isArray(responseValue) ? responseValue : responseValue[name] | ||
|
||
if (countReset){ | ||
return responseValue.length; | ||
} | ||
|
||
// BUG FIX: if the data-reset ("none of the above") is selected | ||
let questionElement = document.getElementById(questionId) | ||
// there is a chance that nothing is selected (v.length==0) in that case you will the | ||
// (legacy notes) BUG FIX: if the data-reset ("none of the above") is selected | ||
// there is a chance that nothing is selected (v.length==0) in that case you will the | ||
// selector will find nothing. Use the "?" because you cannot find the dataset on a null object. | ||
return questionElement.querySelector(`input[type="checkbox"][name="${name}"]:checked`)?.dataset["reset"]?0:responseValue.length | ||
const questionHTML = this.appState.getQuestionHTMLByID(questionId); | ||
if (questionHTML) { | ||
// Find the checked input and check if it has the dataset["reset"] attribute | ||
const inputs = Array.from(questionHTML.querySelectorAll('input')); | ||
const inputChecked = inputs | ||
.filter(input => input.name === name && input.checked && input.dataset?.reset !== undefined) | ||
.find(input => input.dataset?.reset); | ||
|
||
return inputChecked ? 0 : responseValue.length; | ||
} | ||
|
||
return responseValue.length; | ||
} | ||
|
||
// if we want object to return the number of keys | ||
// Object.keys(v).length | ||
// otherwise: | ||
return 0; | ||
}, | ||
|
||
// TODO: remove if unused | ||
// // For a question in a loop, does the value of the response | ||
// // for ANY ITERATION equal a value from a given set. | ||
// loopQuestionValueIsOneOf: function (id, ...values) { | ||
// // Loops append _n_n to the id, where n is an | ||
// // integer starting from 1... | ||
// for (let i = 1; ; i = i + 1) { | ||
// let tmp_qid = `${id}_${i}_${i}` | ||
// // the Id does not exist, we've gone through | ||
// // all potential question and have not found | ||
// // a value in the set of "acceptable" values... | ||
// if (this.doesNotExist(tmp_qid)) return false; | ||
// if (this.valueIsOneOf(tmp_qid, ...values)) return true | ||
// } | ||
// }, | ||
// gridQuestionsValueIsOneOf: function (gridId, ...values) { | ||
// if (this.doesNotExist(gridId)) return false | ||
// console.warn('TODO: (gridQuestionsValueIsOneOf) remove DOM access and use stateManager', gridId, ...values); | ||
// let gridElement = document.getElementById(gridId) | ||
// if (! "grid" in gridElement.dataset) return false | ||
|
||
// values = values.map(v => v.toString()) | ||
// let gridValues = this._value(gridId) | ||
// for (const gridQuestionId in gridValues) { | ||
// // even if there is only one value, force it into | ||
// // an array. flatten it to make sure that it's a 1-d array | ||
// let test_values = [gridValues[gridQuestionId]].flat() | ||
// if (test_values.some(v => values.includes(v.toString()))) { | ||
// return true; | ||
// } | ||
|
||
// } | ||
// return false; | ||
// }, | ||
yearMonth: function (str) { | ||
let isYM = /^(\d+)-(\d+)$/.test(str) | ||
if (isYM) { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
import { math } from './customMathJSImplementation.js'; | ||
import { knownFunctions } from "./knownFunctions.js"; | ||
import { getStateManager } from "./stateManager.js"; | ||
import { moduleParams } from './questionnaire.js'; | ||
|
||
// RegExp to segment text conditions passed in as a string with '[', '(', ')', ',', and ']'. https://stackoverflow.com/questions/6323417/regex-to-extract-all-matches-from-string-using-regexp-exec | ||
const evaluateConditionRegex = /[(),]/g; | ||
|
||
/** | ||
* Try to evaluate using mathjs. Use fallback evaluation in the catch block. | ||
* math.evaluate(<string>) is a built-in mathjs func to evaluate string as mathematical expression. | ||
* @param {string} evalString - The string condition (markdown) to evaluate. | ||
* @returns {any}- The result of the evaluation. | ||
*/ | ||
|
||
export function evaluateCondition(evalString) { | ||
evalString = decodeURIComponent(evalString); | ||
|
||
try { | ||
return math.evaluate(evalString) | ||
} catch (err) { //eslint-disable-line no-unused-vars | ||
|
||
let displayIfStack = []; | ||
let lastMatchIndex = 0; | ||
|
||
// split the displayif string into a stack of strings and operators | ||
for (const match of evalString.matchAll(evaluateConditionRegex)) { | ||
displayIfStack.push(evalString.slice(lastMatchIndex, match.index)); | ||
displayIfStack.push(match[0]); | ||
lastMatchIndex = match.index + 1; | ||
} | ||
|
||
// remove all blanks | ||
displayIfStack = displayIfStack.filter((x) => x != ""); | ||
|
||
const appState = getStateManager(); | ||
|
||
// Process the stack | ||
while (displayIfStack.indexOf(")") > 0) { | ||
const stackEnd = displayIfStack.indexOf(")"); | ||
|
||
if (isValidFunctionSyntax(displayIfStack, stackEnd)) { | ||
const { func, arg1, arg2 } = getFunctionArgsFromStack(displayIfStack, stackEnd, appState); | ||
const functionResult = knownFunctions[func](arg1, arg2, appState); | ||
|
||
// Replace from stackEnd-5 to stackEnd with the results. Splice and replace the function call with the result. | ||
displayIfStack.splice(stackEnd - 5, 6, functionResult); | ||
|
||
} else { | ||
moduleParams.errorLogger('Error in Displayif Function:', evalString, displayIfStack); | ||
throw { Message: "Bad Displayif Function: " + evalString, Stack: displayIfStack }; | ||
} | ||
} | ||
|
||
return displayIfStack[0]; | ||
} | ||
} | ||
|
||
/** | ||
* Test the string-based function syntax for a valid function call (converting markdown function strings to function calls). | ||
* These are legacy, hardcoded conditions that must apply for 'knownFunctions' to evaluate. | ||
* @param {array} stack - The stack of string-based conditions to evaluate. | ||
* @param {number} stackEnd - The index of the closing parenthesis in the stack. | ||
*/ | ||
|
||
const isValidFunctionSyntax = (stack, stackEnd) => { | ||
return stack[stackEnd - 4] === "(" && | ||
stack[stackEnd - 2] === "," && | ||
stack[stackEnd - 5] in knownFunctions | ||
} | ||
|
||
/** | ||
* Get the current function and arguments to evaluate from the stack. | ||
* func, arg1, arg2 are in the stack at specific locations: callEnd-5, callEnd-3, callEnd-1 | ||
* First, the individual arguments are evaluated to resolve any string-based conditions. | ||
* Then, the function and arguments are returned as an object for evaluation as an expression. | ||
* @param {array} stack - The stack of string-based conditions to evaluate. | ||
* @param {number} callEnd - The index of the closing parenthesis in the stack. | ||
* @param {object} appState - The application state. | ||
* @returns {object} - The function and arguments to evaluate. | ||
*/ | ||
|
||
function getFunctionArgsFromStack(stack, callEnd, appState) { | ||
const func = stack[callEnd - 5]; | ||
|
||
let arg1 = stack[callEnd - 3]; | ||
arg1 = evaluateArg(arg1, appState); | ||
|
||
let arg2 = stack[callEnd - 1]; | ||
arg2 = evaluateArg(arg2, appState); | ||
|
||
return { func, arg1, arg2 }; | ||
} | ||
|
||
/** | ||
* Evaluate the individual args embedded in conditions. | ||
* Return early for: undefined, hardcoded numbers and booleans (they get evaluated in mathjs), and known loop markers. | ||
* Otherwise, search for values in the surveyState. This search covers responses and 'previousResults' (values from prior surveys passed in on initialization). | ||
* @param {string} arg - The argument to evaluate. | ||
* @param {object} appState - The application state. | ||
* @returns {string} - The evaluated argument. | ||
*/ | ||
|
||
function evaluateArg(arg, appState) { | ||
if (arg === null || arg === 'undefined') return arg; | ||
else if (typeof arg === 'number' || parseInt(arg, 10) || parseFloat(arg)) return arg; | ||
else if (['true', true, 'false', false].includes(arg)) return arg; | ||
else if (arg === '#loop') return arg; | ||
else return appState.findResponseValue(arg) ?? ''; | ||
} |
Oops, something went wrong.