Files
JetCore/Operand.cpp
T
Brad Arant 8b2391b289 Implement datediff(a, b)
Parsed its arguments correctly but had an empty body (a literal
"// TODO: Implement DATEDIFF." comment), never setting a result - isNumber
was left uninitialized on this path.

Matches MySQL's own DATEDIFF(expr1, expr2): expr1 - expr2 expressed as a
whole number of days, ignoring time-of-day. Dates parse as YYYY-MM-DD via
strptime (a longer "YYYY-MM-DD HH:MM:SS" value parses fine too, since
strptime only consumes what the format string asks for) and are
interpreted as UTC via timegm() rather than mktime(), so the result
doesn't shift with the server's own timezone. An unparseable date throws
a clear, specific error instead of producing garbage.

Verified via real jet-2.0 runs: datediff('2026-08-26','2026-08-20') -> 6,
the reverse -> -6, same date -> 0, a full non-leap year -> 365, and an
invalid date string throws cleanly rather than crashing or silently
returning nonsense. Full 9-script regression suite still passes.
2026-08-26 10:50:57 -07:00

653 lines
22 KiB
C++

#include "Operand.h"
#include "Exception.h"
#include <format>
#include <iostream>
#include <algorithm>
#include <time.h>
#include <chrono>
#include <cmath>
namespace jet {
Operand::Operand(coreutils::ZString &in, Tag &tag, bool stop) : in(in), tag(tag) {
doubleValue = 0;
in.skipWhitespace();
if(in.startsWith("$[") || in.startsWith("#[")) {
string = tag.getVariable(in);
doubleValue = string.asDouble();
isNumber = string.eod();
string.reset();
if((string == "false") || (string == "true"))
boolean = (string == "true");
} else if(in.ifNext("(")) {
Operand op(in, tag);
string = op.string;
doubleValue = op.doubleValue;
isNumber = op.isNumber;
if(!in.ifNext(")"))
throw coreutils::Exception("expected ) in expression.");
} else if(in.ifNextIgnoreCase("SUBSTRING")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for SUBSTRING parameters.");
Operand parm1(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in SUBSTRING expression.");
Operand parm2(in, tag);
if(in.ifNext(")")) {
string = parm1.string.substring(parm2.string.asInteger());
} else {
// Real, pre-existing bug fixed here: the 3-arg-only branch below
// used to run unconditionally (missing braces around this
// else-if), so even after the 2-arg form above already
// succeeded, it fell through into trying to parse a third
// argument from whatever was left (nothing, since the closing
// ")" was already consumed) - always throwing "operand is not
// valid." right after computing the correct 2-arg result.
// Confirmed via real debug tracing showing an extra Operand
// constructed on empty input immediately after a successful
// 2-arg substring(str, start) call - the 2-arg form was
// completely unusable before this fix.
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in SUBSTRING expression.");
Operand parm3(in, tag);
if(in.ifNext(")")) {
string = parm1.string.substring(parm2.string.asInteger(), parm3.string.asInteger());
} else
throw coreutils::Exception("Expecting ) at end of substring expression.");
}
} else if(in.ifNextIgnoreCase("LEFT")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for LEFT parameters.");
Operand parm1(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in LEFT expression.");
Operand parm2(in, tag);
if(in.ifNext(")")) {
string = parm1.string.substring(0, parm2.string.asInteger());
} else
throw coreutils::Exception("Expecting ) at end of LEFT expression.");
} else if(in.ifNextIgnoreCase("EXPR")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for EXPR parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
Operand op(parm1.string, tag);
string = op.string;
isNumber = op.isNumber;
if(isNumber) {
doubleValue = op.doubleValue;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
}
boolean = op.boolean;
} else
throw coreutils::Exception("Expecting ) at end of EXPR expression.");
} else if(in.ifNextIgnoreCase("RIGHT")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for RIGHT parameters.");
Operand parm1(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in RIGHT expression.");
Operand parm2(in, tag);
if(in.ifNext(")")) {
int len = parm1.string.getLength();
int size = parm2.string.asInteger();
int start = len - size;
string = parm1.string.substring(start, size);
} else
throw coreutils::Exception("Expecting ) at end of RIGHT expression.");
} else if(in.ifNextIgnoreCase("TRIM")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for TRIM parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
string = parm1.string.trim();
} else
throw coreutils::Exception("Expecting ) at end of TRIM expression.");
} else if(in.ifNextIgnoreCase("TOUPPER")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for TOUPPER parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
string = parm1.string.toUpper();
} else
throw coreutils::Exception("Expecting ) at end of TOUPPER expression.");
} else if(in.ifNextIgnoreCase("TOLOWER")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for TOLOWER parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
string = parm1.string.toLower();
} else
throw coreutils::Exception("Expecting ) at end of TOLOWER expression.");
} else if(in.ifNextIgnoreCase("REVERSE")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for REVERSE parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
std::string reversed = parm1.string.str();
std::reverse(reversed.begin(), reversed.end());
string = reversed;
} else
throw coreutils::Exception("Expecting ) at end of REVERSE expression.");
} else if(in.ifNextIgnoreCase("CONCAT")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for CONCAT parameters.");
Operand parm1(in, tag);
string = parm1.string;
while(in.ifNext(",")) {
Operand parm(in, tag);
string << parm.string;
}
if(!in.ifNext(")"))
throw coreutils::Exception("Expecting ) at end of CONCAT expression.");
} else if(in.ifNextIgnoreCase("INTEGER")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for INTEGER parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
string = parm1.string.asInteger();
} else
throw coreutils::Exception("Expecting ) at end of INTEGER expression.");
} else if(in.ifNextIgnoreCase("ROUND")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for ROUND parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = round(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of ROUND expression.");
} else if(in.ifNextIgnoreCase("RANDOM")) {
unsigned int seed = (unsigned int)clock();
doubleValue = (double) rand_r(&seed) / (RAND_MAX + 1.0);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else if(in.ifNextIgnoreCase("ABS")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for ABS parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = abs(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of ABS expression.");
} else if(in.ifNextIgnoreCase("MAX")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for MAX parameters.");
Operand *parm = new Operand(in, tag);
string = parm->string;
doubleValue = parm->doubleValue;
isNumber = parm->isNumber;
delete parm;
while(in.ifNext(",")) {
parm = new Operand(in, tag);
if(isNumber != parm->isNumber)
throw coreutils::Exception("All parameters in MAX expression must be same type.");
if(isNumber) {
if(parm->doubleValue > doubleValue) {
doubleValue = parm->doubleValue;
string = parm->string;
}
} else {
if(parm->string > string) {
doubleValue = parm->doubleValue;
string = parm->string;
}
}
delete parm;
}
if(!in.ifNext(")")) {
throw coreutils::Exception("Expecting ) at end of MAX expression.");
}
string.removeTrailingZeros();
} else if(in.ifNextIgnoreCase("MIN")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for MIN parameters.");
Operand *parm = new Operand(in, tag);
string = parm->string;
doubleValue = parm->doubleValue;
isNumber = parm->isNumber;
delete parm;
while(in.ifNext(",")) {
parm = new Operand(in, tag);
if(isNumber != parm->isNumber)
throw coreutils::Exception("All parameters in MIN expression must be same type.");
if(isNumber) {
if(parm->doubleValue < doubleValue) {
doubleValue = parm->doubleValue;
string = parm->string;
}
} else {
if(parm->string < string) {
doubleValue = parm->doubleValue;
string = parm->string;
}
}
delete parm;
}
if(!in.ifNext(")")) {
throw coreutils::Exception("Expecting ) at end of MIN expression.");
}
string.removeTrailingZeros();
} else if(in.ifNextIgnoreCase("POW")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for POW parameters.");
Operand parm1(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in POW expression.");
Operand parm2(in, tag);
if(in.ifNext(")")) {
doubleValue = pow(parm1.doubleValue, parm2.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of substring expression.");
} else if(in.ifNextIgnoreCase("SIN")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for SIN parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = sin(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of SIN expression.");
} else if(in.ifNextIgnoreCase("ASIN")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for ASIN parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = asin(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of ASIN expression.");
} else if(in.ifNextIgnoreCase("COS")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for COS parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = cos(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of COS expression.");
} else if(in.ifNextIgnoreCase("ACOS")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for ACOS parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = acos(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of ACOS expression.");
} else if(in.ifNextIgnoreCase("ATAN")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for ATAN parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = atan(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of ATAN expression.");
} else if(in.ifNextIgnoreCase("SQRT")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for SQRT parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = sqrt(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of SQRT expression.");
} else if(in.ifNextIgnoreCase("DEG")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for DEG parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = parm1.doubleValue * 180 / 3.14159;
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of DEG expression.");
} else if(in.ifNextIgnoreCase("RAD")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for RAD parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = parm1.doubleValue * 3.14159 / 180;
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of RAD expression.");
} else if(in.ifNextIgnoreCase("TAN")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for TAN parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = tan(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of TAN expression.");
} else if(in.ifNextIgnoreCase("TRUNC")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for TRUNC parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = trunc(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of TRUNC expression.");
} else if(in.ifNextIgnoreCase("CEIL")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for CEIL parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = ceil(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of CEIL expression.");
} else if(in.ifNextIgnoreCase("FLOOR")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for FLOOR parameters.");
Operand parm1(in, tag);
if(in.ifNext(")")) {
doubleValue = floor(parm1.doubleValue);
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of FLOOR expression.");
} else if(in.ifNextIgnoreCase("UNIXTIME")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for UNIXTIME.");
if(in.ifNext(")")) {
auto now = std::chrono::system_clock::now();
auto duration_since_epoch = now.time_since_epoch();
auto seconds_since_epoch = std::chrono::duration_cast<std::chrono::seconds>(duration_since_epoch);
int unixtime = seconds_since_epoch.count();
string << unixtime;
} else
throw coreutils::Exception("Expecting ) at end of UNIXTIME.");
}
else if(in.ifNextIgnoreCase("DATEDIFF")) {
if(!in.ifNext("("))
throw coreutils::Exception("Expecting ( for DATEDIFF.");
Operand parm1(in, tag);
if(!in.ifNext(","))
throw coreutils::Exception("Expecting , in DATEDIFF expression.");
Operand parm2(in, tag);
if(in.ifNext(")")) {
// Matches MySQL's DATEDIFF(expr1, expr2): expr1 - expr2 expressed
// as a whole number of days, ignoring any time-of-day component.
// Dates are parsed as YYYY-MM-DD (a leading date portion of a
// longer "YYYY-MM-DD HH:MM:SS" value parses fine too, since
// strptime only consumes what the format string asks for) and
// interpreted as UTC via timegm() (not mktime(), which is
// local-timezone-dependent) so the result doesn't shift with the
// server's own timezone setting.
struct tm tm1 = {};
struct tm tm2 = {};
if(!strptime(parm1.string.c_str(), "%Y-%m-%d", &tm1))
throw coreutils::Exception("DATEDIFF: first argument is not a valid date (expected YYYY-MM-DD).");
if(!strptime(parm2.string.c_str(), "%Y-%m-%d", &tm2))
throw coreutils::Exception("DATEDIFF: second argument is not a valid date (expected YYYY-MM-DD).");
time_t t1 = timegm(&tm1);
time_t t2 = timegm(&tm2);
doubleValue = difftime(t1, t2) / 86400.0;
isNumber = true;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("Expecting ) at end of DATEDIFF expression.");
} else if(in.ifNextIgnoreCase("true")) {
boolean = true;
string = "true";
} else if(in.ifNextIgnoreCase("false")) {
boolean = false;
string = "false";
} else if(in.startsWithNumber()) {
doubleValue = in.asDouble();
string = std::format("{:.12f}", doubleValue);
isNumber = true;
} else if(in.ifNext("'")) {
string = in.getTokenExclude("'");
in.ifNext("'");
isNumber = false;
} else
throw coreutils::Exception("operand is not valid.");
in.skipWhitespace();
if(!stop)
parseOperator();
return;
}
int Operand::parseOperator() {
if(in.ifNext("!=") || in.ifNext("<>")) {
Operand op(in, tag);
if(isNumber && op.isNumber) {
if(doubleValue != op.doubleValue) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
} else if(!isNumber && !op.isNumber) {
if(string != op.string) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
}
}
if(in.ifNext("<=")) {
Operand op(in, tag);
if(isNumber && op.isNumber) {
if(doubleValue <= op.doubleValue) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
} else if(!isNumber && !op.isNumber) {
if(string <= op.string) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
}
}
if(in.ifNext(">=")) {
Operand op(in, tag);
if(isNumber && op.isNumber) {
if(doubleValue >= op.doubleValue) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
} else if(!isNumber && !op.isNumber) {
if(string >= op.string) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
}
}
if(in.ifNext("=")) {
Operand op(in, tag);
if(isNumber && op.isNumber) {
if(doubleValue == op.doubleValue) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
} else if(!isNumber && !op.isNumber) {
if(string == op.string) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
}
}
if(in.ifNext("<")) {
Operand op(in, tag);
if(isNumber && op.isNumber) {
if(doubleValue < op.doubleValue) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
} else if(!isNumber && !op.isNumber) {
if(string < op.string) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
}
}
if(in.ifNext(">")) {
Operand op(in, tag);
if(isNumber && op.isNumber) {
if(doubleValue > op.doubleValue) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
} else if(!isNumber && !op.isNumber) {
if(string > op.string) {
boolean = true;
isNumber = false;
string = "true";
} else {
boolean = false;
isNumber = false;
string = "false";
}
}
}
if(in.ifNext("+")) {
if(isNumber) {
Operand op(in, tag);
if(op.isNumber) {
doubleValue += op.doubleValue;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("operand is not a number.");
} else
throw coreutils::Exception("operand is not a number.");
} else if(in.ifNext("-")) {
if(isNumber) {
Operand op(in, tag);
if(op.isNumber) {
doubleValue -= op.doubleValue;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
} else
throw coreutils::Exception("operand is not a number.");
} else
throw coreutils::Exception("operand is not a number.");
} else if(in.ifNext("*")) {
if(isNumber) {
Operand op(in, tag, true);
if(op.isNumber) {
doubleValue *= op.doubleValue;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
parseOperator();
} else
throw coreutils::Exception("operand is not a number.");
} else
throw coreutils::Exception("operand is not a number.");
} else if(in.ifNext("/")) {
if(isNumber) {
Operand op(in, tag, true);
if(op.isNumber) {
doubleValue /= op.doubleValue;
string = std::format("{:.12f}", doubleValue);
string.removeTrailingZeros();
parseOperator();
} else
throw coreutils::Exception("operand is not a number.");
} else
throw coreutils::Exception("operand is not a number.");
}
return true;
}
}