I’ve got a Javascript class I’ve written that decodes a nested function system, recursively. When assigning the “value” to the object, in the DB function, it has to do a database call by ajax/post request to a PHP page which does the DB call and returns the value, which then gets assigned to the function object.
Here is an example of the nested function:
IF([DB("client", 2, "dob") > DATE("2024-04-24")], "TRUE", CONCAT$("The report is due on ", DATE$(-4)))
The problem I’m having is that because the DB takes a second to respond, the recursion does the comparison between DB() and DATE() before DB() gets it’s value in the promise “then()”
I was able to set async to false and it works great (even the UI pause isn’t noticeable), but I want to find a better way.
I’ve thought about checking the children to ensure they have the values they need and forcing delays as needed till they get a response, but that isn’t much different than using synchronous calls.
Here is the class:
class Function {
// this.name, this.params, this.defintion, this.function
constructor(definition) {
this.definition = definition;
this.cat = "not a function";
if (this.checkSyntax()) this.cat = "function";
this.checkParam();
}
checkSyntax() {
if (this.definition.match(/^/)) throw new Error("You are not allowed to have '^' in the formula: " + this.definition); // Reject it because it has a '^' char.
const regex = /^([A-Z$]+)((.*))$/;
const match = this.definition.match(regex);
if (match) { // We have proper function form.
this.name = match[1]; // Extracting the function name
if (!function_names[match[1]]) throw new Error(match[1] + " is not a valid function name: " + this.definition);
this.params = SpecialSplit(match[2]).map(param => {
const trimmedParam = param.trim();
return trimmedParam ? new Function(trimmedParam) : null;
}).filter(param => param !== null);// Extracting and cleaning up parameters
const openParenthesesCount = (this.definition.match(/(/g) || []).length;
const closeParenthesesCount = (this.definition.match(/)/g) || []).length;
if (openParenthesesCount === closeParenthesesCount)
return true;
throw new Error("The number of parenthesis doesn't match: " + this.definition);
}
else
return false;
}
checkParam()
{
if (this.cat != "function")
{
if (this.definition.match(/^[.*]$/))
{
this.cat = "[COMPARISON]";
var a = parseExpression(this.definition);
this.left = new Function(a.pre);
this.test = a.test;
this.right = new Function(a.post);
this.output = "[COMPARISON]";
if (this.left.output != this.right.output && this.left.output != "VARY" && this.right.output != "VARY")
throw new Error("Can not compare '" + this.left.output + "' to '" + this.right.output + "': " + this.definition);
else if (this.left.output == "DATE" || this.left.output == "VARY")
this.value = eval("new Date(this.left.value) " + this.test + " new Date(this.right.value)");
else if (this.left.output == "INTEGER" || this.left.output == "STRING")
this.value = eval(this.left.value + " " + this.test + " " + this.right.value);
else
throw new Error(this.left.output + " evaluation has not been programmed: " + this.definition);
this.eval = this.left.value + " " + this.test + " " + this.right.value;
}
else if (this.definition.match(/^"[^"]*"$/))
{ this.cat = "STRING"; this.value = this.definition.replace(/"/g, ""); this.output = "STRING"; }
else if (!isNaN(this.definition))
{ this.cat = "INTEGER"; this.value = +this.definition; this.output = "INTEGER"; }
else if (this.definition == "TRUE")
{ this.cat = "BOOLEAN"; this.value = true; this.output = "BOOLEAN"; }
else if (this.definition == "FALSE")
{ this.cat = "BOOLEAN"; this.value = false; this.output = "BOOLEAN"; }
else
throw new Error("Unknown parameter: " + this.definition);
}
else
{
var param_string = this.params.map(function(element) { return element.cat === "function" ? element.output : element.cat; }).join(", ");
var the_formula;
var param_options = "";
function_names[this.name].forEach(element => {
param_options += "<br>" + element["parameters"];
var testParts = element["parameters"].split(", ");
if (element["parameters"] == "[STRING]") testParts = this.params.map(function() { return "STRING"; });
var conditionParts = param_string.split(", ");
var match = true;
if (testParts.length === conditionParts.length) {
for (var i = 0; i < testParts.length; i++) {
if (testParts[i] !== "VARY" && testParts[i] !== conditionParts[i]) {
match = false;
break;
}
}
} else {
match = false;
}
if (match) the_formula = element;
});
if (!the_formula) throw new Error("The function '" + this.name + "' does not have a definition that matches '"+param_string+"'<br><br>Options are: <br>" + param_options);
//this.formula = the_formula;
this.syntax = the_formula["name"] + "(" + the_formula["parameters"] + ")";
this.output = the_formula["response"];
switch (this.name)
{
case "DATE":
var date = new Date();
if (this.syntax == "DATE(INTEGER)") date.setDate(date.getDate() + this.params[0].value);
if (this.syntax == "DATE(STRING)") date = new Date(this.params[0].value);
this.value = date;
break;
case "DATE$":
var date = new Date();
if (this.syntax == "DATE$(INTEGER)") date.setDate(date.getDate() + this.params[0].value);
this.value = date.toLocaleDateString('en-CA').replace(///g, '-');
break;
case "IF":
if (this.params[0].value)
this.value = this.params[1].value;
else
this.value = this.params[2].value;
break;
case "CONCAT$":
this.value = concatenateValues(this.params);
break;
case "DB":
var self = this;
$.ajax({
url: 'includes/external_db.php',
type: 'POST',
data: {
action: "get_column_value",
table: this.params[0].value,
id: this.params[1].value,
col: this.params[2].value,
debug: "false"
},
async: false, // Setting async to false makes the request synchronous
success: function(response) {
// Handle success
self.value = response;
},
error: function(xhr, status, error) {
// Handle errors
throw new Error("Error accessing the DB: " + xhr.responseText);
}
});
break;
}
}
}
}