Calculated fields
The form question set endpoint will sometimes return questions as calculated fields. These questions represent form worksheet fields that require the employee to calculate a value using values from other questions. Calculated fields are returned to help provide a complete context of the worksheet that the employee is completing.
Calculated fields, if displayed, should be disabled to prevent users from setting their value, instead the value should be derived using the calculation provided in the calculation object. Calculated questions are not required when submitting question responses to the (fillpdf
POST) endpoint and will be ignored if provided. Current clients can log into Symmetry's Client Support Center to view a demo of this element in the API.
Below is an example of a calculated field from the form question set endpoint.
{
"id": "worksheetB_line8",
"questionText": "Line 8. Divide the amount on line 7 by $1,000, round any fraction to the nearest whole number enter this number on line 1b of the DE 4. Complete Worksheet C, if needed, otherwise stop here",
"htmlType": "INPUT",
"displayType": "DOLLAR",
"required": {
"whenRequired": "NEVER"
},
"calculation": {
"formula": "worksheetB_line7 / 1000",
"roundingMode": "HALF_UP",
"scale": 0
},
"isCalculated": true
}
All questions returned from SPF API will contain a boolean flag, isCalculated
, that will have a value of true if that question is a calculated field. These questions will not provide a validationRegex
norvalidationErrorMessage
, and instead returning a calculation
object.
The calculation
represents a formula, scaling, and rounding mode to calculate the value for a calculated field. Calculation is only returned if isCalculated
is true and will always contain a formula and a scale, with a rounding mode returned when required by the particular form. The formula will contain the question IDs, for which, the question's value should be substituted to process the calculation. Please note, the formula can contain values from a previous answer. If you choose to implement calculated fields, as you collect answers to questions, it will be necessary to compare the previously answered question ID to see if it is a value included in the calculation formula.
Supported rounding modes
SPF API supports 3 different rounding modes:
CEILING: Rounding should be performed to the next largest value, for example rounding up.
Ex: 1.5 -> 2FLOOR: Rounding should be performed to the next smallest value, for example rounding down.
Ex: 1.5 -> 1HALF_UP: Rounding should be performed to the nearest value with ties rounding up.
Ex:
1.2 -> 1
1.7 -> 2
1.5 -> 2The API uses the Java definition of HALF_UP rounding: Rounding mode to round towards "nearest neighbor" unless both neighbors are equidistant, in which case round up. Behaves as for RoundingMode.UP if the discarded fraction is >= 0.5; otherwise, behaves as for RoundingMode.DOWN.
Note that it is possible to receive negative numbers, but it will likely depend on the other variables in the expression. Many forms do not allow for negative values on fields, in which there is an expected default value of 0, which would be included in the expression. There maybe some forms that do allow negative values.
Ex:
-2.5 -> -3
-2.3 -> -2
-2.7 -> -3
Processing a calculation
The process for resolving the value of a calculated field involves substituting the question IDs in the formula with the associated values that the user has provided and evaluating the resulting equation.
Formula symbols
In addition to question IDs and numerical values, SPF API supports the following symbols in calculation formulas:
Symbol token Description ( Open parenthesis ) Close parenthesis + Addition - Subtraction * Multiplication / Division ? Ternary condition operator : Ternary expression operator < Less than > Greater than <= Less than or equal == Equals >= Greater than or equal != Not equals | Logical OR || Short-circuiting logical OR & Logical AND && Short-circuiting logical AND null Null, None, Nil, NaN, etc.
Question values
Question values in a calculation can be null, numeric, or boolean.
Full example
/*
* Converts the calculation object of a given question into an anonymous function
* that can be run to return the result.
*
* Calculation object from API:
* {
* "formula": "( field1 > field2 ) ? field1 - field2 : 0",
* "scale": 1,
* "roundingMode": "FLOOR"
* }
*
* Converted to anonymous function:
* function() {
* var result = ( 4.5 > 2.0 ) ? 4.5 - 2.0 : 0;
* if (isNaN(result) || !isFinite(result)) { result = 0; }
* result = Math.floor(result * 10) / 10;
* return result;
* }
*/
export const processCalculation = (calculationObj, allAnswersForForm) => {
let processedFormula = "";
// split formula into individual tokens and get question IDs
const questionIds = getQuestionIds(calculationObj.formula.split(" "));
// create variables for question IDs
questionIds.forEach(questionId => {
const value = allAnswersForForm[questionId] === undefined
? "null" : (isValueBoolean(allAnswersForForm[questionId])
? "(String('" + allAnswersForForm[questionId] + "').toLowerCase() === 'true')" : "Number('" + allAnswersForForm[questionId] + "'.split(',').join(''))");
processedFormula += "var " + questionId + " = " + value + "; ";
});
// add calculation formula
processedFormula += "var result = " + calculationObj.formula + "; ";
// make sure result is a finite number, otherwise set to 0
processedFormula += "if (isNaN(result) || !isFinite(result)) { result = 0; } ";
// process rounding mode and/or scale if necessary
if (calculationObj.hasOwnProperty("scale") || calculationObj.hasOwnProperty("roundingMode")) {
processedFormula += handleRoundingAndScale(calculationObj.roundingMode, calculationObj.scale);
}
// return result of formula from anonymous function
processedFormula += "return result;";
// create and call anonymous function to process, returning result
const calculationFunction = new Function(processedFormula);
return calculationFunction();
}
const handleRoundingAndScale = (roundingMode, scale) => {
if (scale === undefined || isNaN(parseInt(scale)) || parseInt(scale) < 0) {
scale = 0;
}
else {
scale = parseInt(scale);
}
// scale formula: round(value * (10 ^ scale)) / (10 ^ scale)
const scalingFactor = Math.pow(10, scale);
let processedFormulaPart = "result = ";
if (roundingMode !== undefined) {
if (roundingMode === "CEILING") {
processedFormulaPart += scale > 0 ? ("Math.ceil(result * " + scalingFactor + ") / " + scalingFactor + "; ") : "Math.ceil(result); ";
}
else if (roundingMode === "FLOOR") {
processedFormulaPart += scale > 0 ? ("Math.floor(result * " + scalingFactor + ") / " + scalingFactor + "; ") : "Math.floor(result); ";
}
else {
processedFormulaPart += scale > 0 ? ("Math.round(result * " + scalingFactor + ") / " + scalingFactor + "; ") : "Math.round(result); ";
}
if (scale === 0) {
// drop any remaining fraction if scale is 0
processedFormulaPart += "Math.trunc(result); ";
}
}
else {
// no rounding needed, just set scale
processedFormulaPart += scale > 0 ? ("Math.floor(result * " + scalingFactor + ") / " + scalingFactor + "; ") : "Math.trunc(result); ";
}
return processedFormulaPart;
}
const isSymbol = (token) => {
switch (token) {
case "(": // open parenthesis
case ")": // close parenthesis
case "+": // addition
case "-": // subtraction
case "*": // multiplication
case "/": // division
case "?": // ternary condition operator
case ":": // ternary expression operator
case "<": // less than
case ">": // greater than
case "<=": // less than or equal
case ">=": // greater than or equal
case "==": // equals
case "!=": // not equals
case "|": // logical OR
case "||": // short-circuiting logical OR
case "&": // logical AND
case "&&": // short-circuiting logical AND
case "null": // null, none, NaN, etc
return true;
default:
return false;
}
}
const isValueBoolean = (value) => {
switch (value) {
case "true":
case "false":
return true;
default:
return false;
}
}
const getQuestionIds = (tokens) => {
const questionIds = [];
if (tokens) {
tokens.forEach((token) => {
// check if token is a symbol or a digit
if (!isSymbol(token) && isNaN(token)) {
questionIds.push(token);
}
});
}
// remove duplicates and return
return [ ...new Set(questionIds) ];
}
Updated 5 days ago