Files
GoldenCheetah/src/DataFilter.cpp
Mark Liversedge b703465a83 Formula Microcache 'snips'
.. when running a formula it may use a vector
   operation to calculate e.g. an average for the
   date range.

.. these get called for every ride, but actually they
   perform the same calculation over and over

.. we cache the vector operation and result to avoid
   repeated calculations; these are only cached whilst
   a datafilter is being evaluated, so we do not need
   to worry about stale/refreshing.
2015-08-22 11:34:58 +01:00

1978 lines
66 KiB
C++

/*
* Copyright (c) 2012 Mark Liversedge (liversedge@gmail.com)
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 51
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "DataFilter.h"
#include "Context.h"
#include "Athlete.h"
#include "RideItem.h"
#include "RideNavigator.h"
#include "RideFileCache.h"
#include "PMCData.h"
#include <QDebug>
#include "Zones.h"
#include "PaceZones.h"
#include "HrZones.h"
#include "DataFilter_yacc.h"
// v4 functions
static struct {
QString name;
int parameters; // -1 is end of list, 0 is variable number, >0 is number of parms needed
} DataFilterFunctions[] = {
// ALWAYS ADD TO BOTTOM OF LIST AS ARRAY OFFSET
// USED IN SWITCH STATEMENT AT RUNTIME DO NOT BE
// TEMPTED TO ADD IN THE MIDDLE !!!!
// math.h
{ "cos", 1 },
{ "tan", 1 },
{ "sin", 1 },
{ "acos", 1 },
{ "atan", 1 },
{ "asin", 1 },
{ "cosh", 1 },
{ "tanh", 1 },
{ "sinh", 1 },
{ "acosh", 1 },
{ "atanh", 1 },
{ "asinh", 1 },
{ "exp", 1 },
{ "log", 1 },
{ "log10", 1 },
{ "ceil", 1 },
{ "floor", 1 },
{ "round", 1 },
{ "fabs", 1 },
{ "isinf", 1 },
{ "isnan", 1 },
// primarily for working with vectors, but can
// have variable number of parameters so probably
// quite useful for a number of things
{ "sum", 0 },
{ "mean", 0 },
{ "max", 0 },
{ "min", 0 },
{ "count", 0 },
// PMC functions
{ "lts", 1 },
{ "sts", 1 },
{ "sb", 1 },
{ "rr", 1 },
// estimate
{ "estimate", 2 }, // estimate(model, (cp|ftp|w'|pmax|x))
// more vector operations
{ "which", 0 }, // which(expr, ...) - create vector contain values that pass expr
// add new ones above this line
{ "", -1 }
};
static QStringList pdmodels()
{
QStringList returning;
returning << "2p";
returning << "3p";
returning << "ext";
returning << "ws";
returning << "velo";
return returning;
}
QStringList
DataFilter::functions()
{
QStringList returning;
for(int i=0; DataFilterFunctions[i].parameters != -1; i++) {
QString function;
if (i == 30) { // special case 'estimate' we describe it
foreach(QString model, pdmodels())
returning << "estimate(" + model + ", x)";
} else {
function = DataFilterFunctions[i].name + "(";
for(int j=0; j<DataFilterFunctions[i].parameters; j++) {
if (j) function += ", ";
function += QString("p%1").arg(j+1);
}
if (DataFilterFunctions[i].parameters) function += ")";
else function += "...)";
}
returning << function;
}
return returning;
}
// LEXER VARIABLES WE INTERACT WITH
// Standard yacc/lex variables / functions
extern int DataFilterlex(); // the lexer aka yylex()
extern char *DataFiltertext; // set by the lexer aka yytext
extern void DataFilter_setString(QString);
extern void DataFilter_clearString();
// PARSER STATE VARIABLES
QStringList DataFiltererrors;
extern int DataFilterparse();
Leaf *DataFilterroot; // root node for parsed statement
static RideFile::SeriesType nameToSeries(QString name)
{
if (!name.compare("power", Qt::CaseInsensitive)) return RideFile::watts;
if (!name.compare("apower", Qt::CaseInsensitive)) return RideFile::aPower;
if (!name.compare("cadence", Qt::CaseInsensitive)) return RideFile::cad;
if (!name.compare("hr", Qt::CaseInsensitive)) return RideFile::hr;
if (!name.compare("speed", Qt::CaseInsensitive)) return RideFile::kph;
if (!name.compare("torque", Qt::CaseInsensitive)) return RideFile::nm;
if (!name.compare("NP", Qt::CaseInsensitive)) return RideFile::NP;
if (!name.compare("xPower", Qt::CaseInsensitive)) return RideFile::xPower;
if (!name.compare("VAM", Qt::CaseInsensitive)) return RideFile::vam;
if (!name.compare("wpk", Qt::CaseInsensitive)) return RideFile::wattsKg;
if (!name.compare("lrbalance", Qt::CaseInsensitive)) return RideFile::lrbalance;
return RideFile::none;
}
bool
Leaf::isDynamic(Leaf *leaf)
{
switch(leaf->type) {
default:
case Leaf::Symbol :
return leaf->dynamic;
break;
case Leaf::Logical :
if (leaf->op == 0) return leaf->isDynamic(leaf->lvalue.l);
case Leaf::UnaryOperation :
return leaf->isDynamic(leaf->lvalue.l);
break;
case Leaf::Operation :
case Leaf::BinaryOperation :
return leaf->isDynamic(leaf->lvalue.l) || leaf->isDynamic(leaf->rvalue.l);
break;
case Leaf::Function :
if (leaf->lvalue.l) return leaf->isDynamic(leaf->lvalue.l);
else return leaf->dynamic;
break;
break;
case Leaf::Conditional :
{
return leaf->isDynamic(leaf->cond.l) ||
leaf->isDynamic(leaf->lvalue.l) ||
leaf->isDynamic(leaf->rvalue.l);
break;
}
case Leaf::Vector :
return true;
break;
}
return false;
}
void
DataFilter::setSignature(QString &query)
{
// strip out all whitespace and comments
// from the formula so it can be used as
// a signature when placed into a hash
sig.clear();
bool incomment=false;
bool instring=false;
for (int i=0; i<query.length(); i++) {
// get out of comments and strings at end of a line
if (query[i] == '\n') instring = incomment=false;
// get into comments
if (!instring && query[i] == '#') incomment=true;
// not in comment and not escaped
if (query[i] == '"' && !incomment && (((i && query[i-1] != '\\') || i==0))) instring = !instring;
// keep anything that isn't whitespace, or a comment
if (instring || (!incomment && !query[i].isSpace())) sig += query[i];
}
}
void
DataFilter::colorSyntax(QTextDocument *document, int pos)
{
// matched brace position
int bpos = -1;
// for looking for comments
QString string = document->toPlainText();
// clear formatting and color comments
QTextCharFormat normal;
normal.setFontWeight(QFont::Normal);
normal.setUnderlineStyle(QTextCharFormat::NoUnderline);
normal.setForeground(Qt::black);
QTextCharFormat cyanbg;
cyanbg.setBackground(Qt::cyan);
QTextCharFormat redbg;
redbg.setBackground(QColor(255,153,153));
QTextCharFormat function;
function.setUnderlineStyle(QTextCharFormat::NoUnderline);
function.setForeground(Qt::blue);
QTextCharFormat symbol;
symbol.setUnderlineStyle(QTextCharFormat::NoUnderline);
symbol.setForeground(Qt::red);
QTextCharFormat literal;
literal.setFontWeight(QFont::Normal);
literal.setUnderlineStyle(QTextCharFormat::NoUnderline);
literal.setForeground(Qt::magenta);
QTextCharFormat comment;
comment.setFontWeight(QFont::Normal);
comment.setUnderlineStyle(QTextCharFormat::NoUnderline);
comment.setForeground(Qt::darkGreen);
QTextCursor cursor(document);
// set all black
cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.setCharFormat(normal);
// color comments
bool instring=false;
bool innumber=false;
bool incomment=false;
bool insymbol=false;
int commentstart=0;
int stringstart=0;
int numberstart=0;
int symbolstart=0;
int brace=0;
for(int i=0; i<string.length(); i++) {
// enter into symbol
if (!insymbol && !incomment && !instring && string[i].isLetter()) {
insymbol = true;
symbolstart = i;
// it starts with numbers but ends with letters - number becomes symbol
if (innumber) { symbolstart=numberstart; innumber=false; }
}
// end of symbol ?
if (insymbol && (!string[i].isLetterOrNumber() && string[i] != '_')) {
bool isfunction = false;
insymbol = false;
QString sym = string.mid(symbolstart, i-symbolstart);
bool found=false;
QString lookup = lookupMap.value(sym, "");
if (lookup == "") {
// isRun isa special, we may add more later (e.g. date)
if (!sym.compare("Date", Qt::CaseInsensitive) ||
!sym.compare("best", Qt::CaseInsensitive) ||
!sym.compare("tiz", Qt::CaseInsensitive) ||
!sym.compare("const", Qt::CaseInsensitive) ||
!sym.compare("config", Qt::CaseInsensitive) ||
!sym.compare("ctl", Qt::CaseInsensitive) ||
!sym.compare("tsb", Qt::CaseInsensitive) ||
!sym.compare("atl", Qt::CaseInsensitive) ||
!sym.compare("daterange", Qt::CaseInsensitive) ||
!sym.compare("Today", Qt::CaseInsensitive) ||
!sym.compare("Current", Qt::CaseInsensitive) ||
sym == "isSwim" || sym == "isRun") {
isfunction = found = true;
}
// still not found ?
// is it a function then ?
for(int j=0; DataFilterFunctions[j].parameters != -1; j++) {
if (DataFilterFunctions[j].name == sym) {
isfunction = found = true;
break;
}
}
} else {
found = true;
}
if (found) {
// lets color it red, its a literal.
cursor.setPosition(symbolstart, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.setCharFormat(isfunction ? function : symbol);
}
}
// numeric literal
if (!insymbol && string[i].isNumber()) {
// start of number ?
if (!incomment && !instring && !innumber) {
innumber = true;
numberstart = i;
}
} else if (!insymbol && !incomment && !instring &&innumber) {
// now out of number
innumber = false;
// lets color it red, its a literal.
cursor.setPosition(numberstart, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.setCharFormat(literal);
}
// watch for being in a string, but remember escape!
if ((i==0 || string[i-1] != '\\') && string[i] == '"') {
if (!incomment && instring) {
instring = false;
// lets color it red, its a literal.
cursor.setPosition(stringstart, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.setCharFormat(literal);
} else if (!incomment && !instring) {
stringstart=i;
instring=true;
}
}
// watch for being in a comment
if (string[i] == '#' && !instring && !incomment) {
incomment = true;
commentstart=i;
}
// end of text or line
if (i+1 == string.length() || (string[i] == '\r' || string[i] == '\n')) {
// mark if we have a comment
if (incomment) {
// we have a line of comments to here from commentstart
incomment = false;
// comments are blue
cursor.setPosition(commentstart, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.setCharFormat(comment);
} else {
int start=0;
if (instring) start=stringstart;
if (innumber) start=numberstart;
// end of string ...
if (instring || innumber) {
innumber = instring = false; // always quit on end of line
// lets color it red, its a literal.
cursor.setPosition(start, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.setCharFormat(literal);
}
}
}
// are the braces balanced ?
if (!instring && !incomment && string[i]=='(') {
brace++;
// match close/open if over cursor
if (i==pos-1) {
cursor.setPosition(i, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.mergeCharFormat(cyanbg);
// run forward looking for match
int bb=0;
for(int j=i; j<string.length(); j++) {
if (string[j]=='(') bb++;
if (string[j]==')') {
bb--;
if (bb == 0) {
bpos = j; // matched brace here, don't change color!
cursor.setPosition(j, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(j+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.mergeCharFormat(cyanbg);
break;
}
}
}
}
}
if (!instring && !incomment && string[i]==')') {
brace--;
if (i==pos-1) {
cursor.setPosition(i, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.mergeCharFormat(cyanbg);
// run backward looking for match
int bb=0;
for(int j=i; j>=0; j--) {
if (string[j]==')') bb++;
if (string[j]=='(') {
bb--;
if (bb == 0) {
bpos = j; // matched brace here, don't change color!
cursor.setPosition(j, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(j+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.mergeCharFormat(cyanbg);
break;
}
}
}
} else if (brace < 0 && i != bpos-1) {
cursor.setPosition(i, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.mergeCharFormat(redbg);
}
}
}
// unbalanced braces - do same as above, backwards?
//XXX braces in comments fuck things up ... XXX
if (brace > 0) {
brace = 0;
for(int i=string.length(); i>=0; i--) {
if (string[i] == ')') brace++;
if (string[i] == '(') brace--;
if (brace < 0 && string[i] == '(' && i != pos-1 && i != bpos-1) {
cursor.setPosition(i, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(i+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
cursor.mergeCharFormat(redbg);
}
}
}
// apply selective coloring to the symbols and expressions
if(treeRoot) treeRoot->color(treeRoot, document);
}
void Leaf::color(Leaf *leaf, QTextDocument *document)
{
// nope
if (leaf == NULL) return;
QTextCharFormat apply;
switch(leaf->type) {
case Leaf::Float :
case Leaf::Integer :
case Leaf::String :
break;
case Leaf::Symbol :
break;
case Leaf::Logical :
leaf->color(leaf->lvalue.l, document);
if (leaf->op) leaf->color(leaf->rvalue.l, document);
return;
break;
case Leaf::Operation :
leaf->color(leaf->lvalue.l, document);
leaf->color(leaf->rvalue.l, document);
return;
break;
case Leaf::UnaryOperation :
leaf->color(leaf->lvalue.l, document);
return;
break;
case Leaf::BinaryOperation :
leaf->color(leaf->lvalue.l, document);
leaf->color(leaf->rvalue.l, document);
return;
break;
case Leaf::Function :
if (leaf->series) {
if (leaf->lvalue.l) leaf->color(leaf->lvalue.l, document);
return;
} else {
foreach(Leaf*l, leaf->fparms) leaf->color(l, document);
}
break;
case Leaf::Vector :
leaf->color(leaf->lvalue.l, document);
leaf->color(leaf->fparms[0], document);
leaf->color(leaf->fparms[1], document);
return;
break;
case Leaf::Conditional :
{
leaf->color(leaf->cond.l, document);
leaf->color(leaf->lvalue.l, document);
leaf->color(leaf->rvalue.l, document);
return;
}
break;
default:
return;
break;
}
// all we do now is highlight if in error.
if (leaf->inerror == true) {
QTextCursor cursor(document);
// highlight this token and apply
cursor.setPosition(leaf->loc, QTextCursor::MoveAnchor);
cursor.selectionStart();
cursor.setPosition(leaf->leng+1, QTextCursor::KeepAnchor);
cursor.selectionEnd();
apply.setUnderlineStyle(QTextCharFormat::WaveUnderline);
apply.setUnderlineColor(Qt::red);
cursor.mergeCharFormat(apply);
}
}
// convert expression to string, without white space
// this can be used as a signature when caching values etc
QString
Leaf::toString()
{
switch(type) {
case Leaf::Float : return QString("%1").arg(lvalue.f); break;
case Leaf::Integer : return QString("%1").arg(lvalue.i); break;
case Leaf::String : return *lvalue.s; break;
case Leaf::Symbol : return *lvalue.n; break;
case Leaf::Logical :
case Leaf::Operation :
case Leaf::BinaryOperation :
return QString("%1%2%3")
.arg(lvalue.l->toString())
.arg(op)
.arg(op ? rvalue.l->toString() : "");
break;
case Leaf::UnaryOperation :
return QString("-%1").arg(lvalue.l->toString());
break;
case Leaf::Function :
if (series) {
if (lvalue.l) return QString("%1(%2,%3)")
.arg(function).arg(*(series->lvalue.n))
.arg(lvalue.l->toString());
} else {
QString f= function + "(";
bool first=true;
foreach(Leaf*l, fparms) {
f+= (first ? "," : "") + l->toString();
first = false;
}
f += ")";
return f;
}
break;
case Leaf::Vector :
return QString("%1[%2:%3]")
.arg(lvalue.l->toString())
.arg(fparms[0]->toString())
.arg(fparms[1]->toString());
case Leaf::Conditional : qDebug()<<"cond";
{
return QString("%1?%2:%3")
.arg(cond.l->toString())
.arg(lvalue.l->toString())
.arg(rvalue.l->toString());
}
break;
case Leaf::Parameters :
break;
default:
break;
}
return "";
}
void Leaf::print(Leaf *leaf, int level)
{
qDebug()<<"LEVEL"<<level;
if (leaf == NULL) {
qDebug()<<"NULL";
return;
}
switch(leaf->type) {
case Leaf::Float : qDebug()<<"float"<<leaf->lvalue.f<<leaf->dynamic; break;
case Leaf::Integer : qDebug()<<"integer"<<leaf->lvalue.i<<leaf->dynamic; break;
case Leaf::String : qDebug()<<"string"<<*leaf->lvalue.s<<leaf->dynamic; break;
case Leaf::Symbol : qDebug()<<"symbol"<<*leaf->lvalue.n<<leaf->dynamic; break;
case Leaf::Logical : qDebug()<<"lop"<<leaf->op;
leaf->print(leaf->lvalue.l, level+1);
if (leaf->op) // nonzero ?
leaf->print(leaf->rvalue.l, level+1);
break;
case Leaf::Operation : qDebug()<<"cop"<<leaf->op;
leaf->print(leaf->lvalue.l, level+1);
leaf->print(leaf->rvalue.l, level+1);
break;
case Leaf::UnaryOperation : qDebug()<<"uop"<<leaf->op;
leaf->print(leaf->lvalue.l, level+1);
break;
case Leaf::BinaryOperation : qDebug()<<"bop"<<leaf->op;
leaf->print(leaf->lvalue.l, level+1);
leaf->print(leaf->rvalue.l, level+1);
break;
case Leaf::Function :
if (leaf->series) {
qDebug()<<"function"<<leaf->function<<"parm="<<*(leaf->series->lvalue.n);
if (leaf->lvalue.l) leaf->print(leaf->lvalue.l, level+1);
} else {
qDebug()<<"function"<<leaf->function<<"parms:"<<leaf->fparms.count();
foreach(Leaf*l, leaf->fparms) leaf->print(l, level+1);
}
break;
case Leaf::Vector : qDebug()<<"vector";
leaf->print(leaf->lvalue.l, level+1);
leaf->print(leaf->fparms[0], level+1);
leaf->print(leaf->fparms[1], level+1);
case Leaf::Conditional : qDebug()<<"cond";
{
leaf->print(leaf->cond.l, level+1);
leaf->print(leaf->lvalue.l, level+1);
leaf->print(leaf->rvalue.l, level+1);
}
break;
case Leaf::Parameters :
{
qDebug()<<"parameters"<<leaf->fparms.count();
foreach(Leaf*l, fparms) leaf->print(l, level+1);
}
break;
default:
break;
}
}
static bool isCoggan(QString symbol)
{
if (!symbol.compare("ctl", Qt::CaseInsensitive)) return true;
if (!symbol.compare("tsb", Qt::CaseInsensitive)) return true;
if (!symbol.compare("atl", Qt::CaseInsensitive)) return true;
return false;
}
bool Leaf::isNumber(DataFilter *df, Leaf *leaf)
{
switch(leaf->type) {
case Leaf::Float : return true;
case Leaf::Integer : return true;
case Leaf::String :
{
// strings that evaluate as a date
// will be returned as a number of days
// since 1900!
QString string = *(leaf->lvalue.s);
if (QDate::fromString(string, "yyyy/MM/dd").isValid())
return true;
else {
return false;
}
}
case Leaf::Symbol :
{
QString symbol = *(leaf->lvalue.n);
if (symbol == "isRun") return true;
else if (symbol == "isSwim") return true;
else if (!symbol.compare("Date", Qt::CaseInsensitive)) return true;
else if (!symbol.compare("Today", Qt::CaseInsensitive)) return true;
else if (!symbol.compare("Current", Qt::CaseInsensitive)) return true;
else if (isCoggan(symbol)) return true;
else return df->lookupType.value(symbol, false);
}
break;
case Leaf::Logical : return true; // not possible!
case Leaf::Operation : return true;
case Leaf::UnaryOperation : return true;
case Leaf::BinaryOperation : return true;
case Leaf::Function : return true;
case Leaf::Vector :
case Leaf::Conditional :
{
return true;
}
break;
case Leaf::Parameters : return false; break;
default:
return false;
break;
}
}
void Leaf::clear(Leaf *leaf)
{
Q_UNUSED(leaf);
#if 0 // memory leak!!!
switch(leaf->type) {
case Leaf::String : delete leaf->lvalue.s; break;
case Leaf::Symbol : delete leaf->lvalue.n; break;
case Leaf::Logical :
case Leaf::BinaryOperation :
case Leaf::Operation : clear(leaf->lvalue.l);
clear(leaf->rvalue.l);
delete(leaf->lvalue.l);
delete(leaf->rvalue.l);
break;
case Leaf::Function : clear(leaf->lvalue.l);
delete(leaf->lvalue.l);
break;
default:
break;
}
#endif
}
void Leaf::validateFilter(DataFilter *df, Leaf *leaf)
{
leaf->inerror = false;
switch(leaf->type) {
case Leaf::Symbol :
{
// are the symbols correct?
// if so set the type to meta or metric
// and save the technical name used to do
// a lookup at execution time
QString symbol = *(leaf->lvalue.n);
QString lookup = df->lookupMap.value(symbol, "");
if (lookup == "") {
// isRun isa special, we may add more later (e.g. date)
if (symbol.compare("Date", Qt::CaseInsensitive) &&
symbol.compare("Today", Qt::CaseInsensitive) &&
symbol.compare("Current", Qt::CaseInsensitive) &&
symbol != "isSwim" && symbol != "isRun" && !isCoggan(symbol)) {
DataFiltererrors << QString(QObject::tr("%1 is unknown")).arg(symbol);
leaf->inerror = true;
}
if (symbol.compare("Current", Qt::CaseInsensitive))
leaf->dynamic = true;
}
}
break;
case Leaf::Vector :
{
leaf->validateFilter(df, leaf->lvalue.l);
leaf->validateFilter(df, leaf->fparms[0]);
leaf->validateFilter(df, leaf->fparms[1]);
}
return;
case Leaf::Function :
{
// is the symbol valid?
QRegExp bestValidSymbols("^(apower|power|hr|cadence|speed|torque|vam|xpower|np|wpk)$", Qt::CaseInsensitive);
QRegExp tizValidSymbols("^(power|hr)$", Qt::CaseInsensitive);
QRegExp configValidSymbols("^(cp|w\\'|pmax|cv|d\\'|scv|sd\\'|height|weight|lthr|maxhr|rhr|units)$", Qt::CaseInsensitive);
QRegExp constValidSymbols("^(e|pi)$", Qt::CaseInsensitive); // just do basics for now
QRegExp dateRangeValidSymbols("^(start|stop)$", Qt::CaseInsensitive); // date range
if (leaf->series) { // old way of hand crafting each function in the lexer including support for literal parameter e.g. (power, 1)
QString symbol = leaf->series->lvalue.n->toLower();
if (leaf->function == "best" && !bestValidSymbols.exactMatch(symbol)) {
DataFiltererrors << QString(QObject::tr("invalid data series for best(): %1")).arg(symbol);
leaf->inerror = true;
}
if (leaf->function == "tiz" && !tizValidSymbols.exactMatch(symbol)) {
DataFiltererrors << QString(QObject::tr("invalid data series for tiz(): %1")).arg(symbol);
leaf->inerror = true;
}
if (leaf->function == "daterange") {
if (!dateRangeValidSymbols.exactMatch(symbol)) {
DataFiltererrors << QString(QObject::tr("invalid literal for daterange(): %1")).arg(symbol);
leaf->inerror = true;
} else {
// convert to int days since using current date range config
// should be able to get from parent somehow
leaf->type = Leaf::Integer;
if (symbol == "start") leaf->lvalue.i = QDate(1900,01,01).daysTo(df->context->currentDateRange().from);
else if (symbol == "stop") leaf->lvalue.i = QDate(1900,01,01).daysTo(df->context->currentDateRange().to);
else leaf->lvalue.i = 0;
}
}
if (leaf->function == "config" && !configValidSymbols.exactMatch(symbol)) {
DataFiltererrors << QString(QObject::tr("invalid literal for config(): %1")).arg(symbol);
leaf->inerror = true;
}
if (leaf->function == "const") {
if (!constValidSymbols.exactMatch(symbol)) {
DataFiltererrors << QString(QObject::tr("invalid literal for const(): %1")).arg(symbol);
leaf->inerror = true;
} else {
// convert to a float
leaf->type = Leaf::Float;
leaf->lvalue.f = 0.0L;
if (symbol == "e") leaf->lvalue.f = MATHCONST_E;
if (symbol == "pi") leaf->lvalue.f = MATHCONST_PI;
}
}
if (leaf->function == "best" || leaf->function == "tiz") {
// now set the series type used as parameter 1 to best/tiz
leaf->seriesType = nameToSeries(symbol);
}
} else { // generic functions, math etc
bool found=false;
// are the parameters well formed ?
if (leaf->function == "estimate") {
// we only want two parameters and they must be
// a model name and then either ftp, cp, pmax, w'
// or a duration
if (leaf->fparms.count() > 0) {
// check the model name
if (leaf->fparms[0]->type != Leaf::Symbol) {
leaf->fparms[0]->inerror = true;
DataFiltererrors << QString(QObject::tr("estimate function expects model name as first parameter"));
} else {
if (!pdmodels().contains(*(leaf->fparms[0]->lvalue.n))) {
leaf->inerror = leaf->fparms[0]->inerror = true;
DataFiltererrors << QString(QObject::tr("estimate function expects model name as first parameter"));
}
}
if (leaf->fparms.count() > 1) {
// check symbol name if it is a symbol
if (leaf->fparms[1]->type == Leaf::Symbol) {
QRegExp estimateValidSymbols("^(cp|ftp|pmax|w')$", Qt::CaseInsensitive);
if (!estimateValidSymbols.exactMatch(*(leaf->fparms[1]->lvalue.n))) {
leaf->inerror = leaf->fparms[1]->inerror = true;
DataFiltererrors << QString(QObject::tr("estimate function expects parameter or duration as second parameter"));
}
} else {
validateFilter(df, leaf->fparms[1]);
}
}
}
} else {
// normal parm check !
foreach(Leaf *p, leaf->fparms) validateFilter(df, p);
}
// does it exist?
for(int i=0; DataFilterFunctions[i].parameters != -1; i++) {
if (DataFilterFunctions[i].name == leaf->function) {
// with the right number of parameters?
if (DataFilterFunctions[i].parameters && leaf->fparms.count() != DataFilterFunctions[i].parameters) {
DataFiltererrors << QString(QObject::tr("function '%1' expects %2 parameter(s) not %3")).arg(leaf->function)
.arg(DataFilterFunctions[i].parameters).arg(fparms.count());
leaf->inerror = true;
}
found = true;
break;
}
}
// not known!
if (!found) {
DataFiltererrors << QString(QObject::tr("unknown function %1")).arg(leaf->function);
leaf->inerror=true;
}
}
}
break;
case Leaf::UnaryOperation :
{ // only 1 at present, unary minus return the value * -1
if (!Leaf::isNumber(df, leaf->lvalue.l)) {
DataFiltererrors << QString(QObject::tr("unary negation on a string!"));
leaf->inerror = true;
}
}
break;
case Leaf::BinaryOperation :
case Leaf::Operation :
{
// first lets make sure the lhs and rhs are of the same type
bool lhsType = Leaf::isNumber(df, leaf->lvalue.l);
bool rhsType = Leaf::isNumber(df, leaf->rvalue.l);
if (lhsType != rhsType) {
DataFiltererrors << QString(QObject::tr("comparing strings with numbers"));
leaf->inerror = true;
}
// what about using string operations on a lhs/rhs that
// are numeric?
if ((lhsType || rhsType) && leaf->op >= MATCHES && leaf->op <= CONTAINS) {
DataFiltererrors << QObject::tr("using a string operations with a number");
leaf->inerror = true;
}
validateFilter(df, leaf->lvalue.l);
validateFilter(df, leaf->rvalue.l);
}
break;
case Leaf::Logical :
{
validateFilter(df, leaf->lvalue.l);
if (leaf->op) validateFilter(df, leaf->rvalue.l);
}
break;
case Leaf::Conditional :
{
// three expressions to validate
validateFilter(df, leaf->cond.l);
validateFilter(df, leaf->lvalue.l);
validateFilter(df, leaf->rvalue.l);
}
break;
case Leaf::Parameters :
{
// should never get here !
DataFiltererrors << QObject::tr("internal parser error: parms");
leaf->inerror = true;
}
break;
default:
break;
}
}
DataFilter::DataFilter(QObject *parent, Context *context) : QObject(parent), context(context), isdynamic(false), treeRoot(NULL)
{
// set up the models we support
models << new CP2Model(context);
models << new CP3Model(context);
models << new MultiModel(context);
models << new ExtendedModel(context);
models << new WSModel(context);
configChanged(CONFIG_FIELDS);
connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
connect(context, SIGNAL(rideSelected(RideItem*)), this, SLOT(dynamicParse()));
}
DataFilter::DataFilter(QObject *parent, Context *context, QString formula) : QObject(parent), context(context), isdynamic(false), treeRoot(NULL)
{
// set up the models we support
models << new CP2Model(context);
models << new CP3Model(context);
models << new MultiModel(context);
models << new ExtendedModel(context);
models << new WSModel(context);
configChanged(CONFIG_FIELDS);
// regardless of success or failure set signature
setSignature(formula);
DataFiltererrors.clear(); // clear out old errors
DataFilter_setString(formula);
DataFilterparse();
DataFilter_clearString();
treeRoot = DataFilterroot;
// if it parsed (syntax) then check logic (semantics)
if (treeRoot && DataFiltererrors.count() == 0)
treeRoot->validateFilter(this, treeRoot);
else
treeRoot=NULL;
// save away the results if it passed semantic validation
if (DataFiltererrors.count() != 0)
treeRoot= NULL;
}
Result DataFilter::evaluate(RideItem *item)
{
// clear the micro-cache
snips.clear();
if (!item || !treeRoot || DataFiltererrors.count()) return Result(0);
Result res = treeRoot->eval(context, this, treeRoot, item);
return res;
}
QStringList DataFilter::check(QString query)
{
// since we may use it afterwards
setSignature(query);
// remember where we apply
isdynamic=false;
// Parse from string
DataFiltererrors.clear(); // clear out old errors
DataFilter_setString(query);
DataFilterparse();
DataFilter_clearString();
// save away the results
treeRoot = DataFilterroot;
// if it passed syntax lets check semantics
if (treeRoot && DataFiltererrors.count() == 0) treeRoot->validateFilter(this, treeRoot);
// ok, did it pass all tests?
if (!treeRoot || DataFiltererrors.count() > 0) { // nope
// no errors just failed to finish
if (!treeRoot) DataFiltererrors << tr("malformed expression.");
}
errors = DataFiltererrors;
return errors;
}
QStringList DataFilter::parseFilter(QString query, QStringList *list)
{
// remember where we apply
this->list = list;
isdynamic=false;
// regardless of fail/pass set the signature
setSignature(query);
//DataFilterdebug = 2; // no debug -- needs bison -t in src.pro
DataFilterroot = NULL;
// if something was left behind clear it up now
clearFilter();
// Parse from string
DataFiltererrors.clear(); // clear out old errors
DataFilter_setString(query);
DataFilterparse();
DataFilter_clearString();
// save away the results
treeRoot = DataFilterroot;
// if it passed syntax lets check semantics
if (treeRoot && DataFiltererrors.count() == 0) treeRoot->validateFilter(this, treeRoot);
// ok, did it pass all tests?
if (!treeRoot || DataFiltererrors.count() > 0) { // nope
// no errors just failed to finish
if (!treeRoot) DataFiltererrors << tr("malformed expression.");
// Bzzzt, malformed
emit parseBad(DataFiltererrors);
clearFilter();
} else { // yep! .. we have a winner!
isdynamic = treeRoot->isDynamic(treeRoot);
// successfully parsed, lets check semantics
//treeRoot->print(treeRoot);
emit parseGood();
// clear current filter list
filenames.clear();
// get all fields...
foreach(RideItem *item, context->athlete->rideCache->rides()) {
// evaluate each ride...
Result result = treeRoot->eval(context, this, treeRoot, item);
if (result.isNumber && result.number) {
filenames << item->fileName;
}
}
emit results(filenames);
if (list) *list = filenames;
}
errors = DataFiltererrors;
return errors;
}
void
DataFilter::dynamicParse()
{
if (isdynamic) {
// need to reapply on current state
// clear current filter list
filenames.clear();
// get all fields...
foreach(RideItem *item, context->athlete->rideCache->rides()) {
// evaluate each ride...
Result result = treeRoot->eval(context, this, treeRoot, item);
if (result.isNumber && result.number)
filenames << item->fileName;
}
emit results(filenames);
if (list) *list = filenames;
}
}
void DataFilter::clearFilter()
{
if (treeRoot) {
treeRoot->clear(treeRoot);
treeRoot = NULL;
}
isdynamic = false;
sig = "";
}
void DataFilter::configChanged(qint32)
{
lookupMap.clear();
lookupType.clear();
// create lookup map from 'friendly name' to INTERNAL-name used in summaryMetrics
// to enable a quick lookup && the lookup for the field type (number, text)
const RideMetricFactory &factory = RideMetricFactory::instance();
for (int i=0; i<factory.metricCount(); i++) {
QString symbol = factory.metricName(i);
QString name = context->specialFields.internalName(factory.rideMetric(symbol)->name());
lookupMap.insert(name.replace(" ","_"), symbol);
lookupType.insert(name.replace(" ","_"), true);
}
// now add the ride metadata fields -- should be the same generally
foreach(FieldDefinition field, context->athlete->rideMetadata()->getFields()) {
QString underscored = field.name;
if (!context->specialFields.isMetric(underscored)) {
// translate to internal name if name has non Latin1 characters
underscored = context->specialFields.internalName(underscored);
field.name = context->specialFields.internalName((field.name));
lookupMap.insert(underscored.replace(" ","_"), field.name);
lookupType.insert(underscored.replace(" ","_"), (field.type > 2)); // true if is number
}
}
}
Result Leaf::eval(Context *context, DataFilter *df, Leaf *leaf, RideItem *m)
{
// if error state all bets are off
if (inerror) return Result(0);
switch(leaf->type) {
//
// LOGICAL EXPRESSION
//
case Leaf::Logical :
{
switch (leaf->op) {
case AND :
{
Result left = eval(context, df, leaf->lvalue.l, m);
if (left.isNumber && left.number) {
Result right = eval(context, df, leaf->rvalue.l, m);
if (right.isNumber && right.number) return Result(true);
}
return Result(false);
}
case OR :
{
Result left = eval(context, df, leaf->lvalue.l, m);
if (left.isNumber && left.number) return Result(true);
Result right = eval(context, df, leaf->rvalue.l, m);
if (right.isNumber && right.number) return Result(true);
return Result(false);
}
default : // parenthesis
return (eval(context, df, leaf->lvalue.l, m));
}
}
break;
//
// FUNCTIONS
//
case Leaf::Function :
{
double duration;
if (leaf->function == "config") {
//
// Get CP and W' estimates for date of ride
//
double CP = 0;
double WPRIME = 0;
double PMAX = 0;
int zoneRange;
if (context->athlete->zones()) {
// if range is -1 we need to fall back to a default value
zoneRange = context->athlete->zones()->whichRange(m->dateTime.date());
CP = zoneRange >= 0 ? context->athlete->zones()->getCP(zoneRange) : 0;
WPRIME = zoneRange >= 0 ? context->athlete->zones()->getWprime(zoneRange) : 0;
PMAX = zoneRange >= 0 ? context->athlete->zones()->getPmax(zoneRange) : 0;
// did we override CP in metadata ?
int oCP = m->getText("CP","0").toInt();
int oW = m->getText("W'","0").toInt();
int oPMAX = m->getText("Pmax","0").toInt();
if (oCP) CP=oCP;
if (oW) WPRIME=oW;
if (oPMAX) PMAX=oPMAX;
}
//
// LTHR, MaxHR, RHR
//
int hrZoneRange = context->athlete->hrZones() ?
context->athlete->hrZones()->whichRange(m->dateTime.date())
: -1;
int LTHR = hrZoneRange != -1 ? context->athlete->hrZones()->getLT(hrZoneRange) : 0;
int RHR = hrZoneRange != -1 ? context->athlete->hrZones()->getRestHr(hrZoneRange) : 0;
int MaxHR = hrZoneRange != -1 ? context->athlete->hrZones()->getMaxHr(hrZoneRange) : 0;
//
// CV' D'
//
int paceZoneRange = context->athlete->paceZones(false) ?
context->athlete->paceZones(false)->whichRange(m->dateTime.date()) :
-1;
double CV = (paceZoneRange != -1) ? context->athlete->paceZones(false)->getCV(paceZoneRange) : 0.0;
double DPRIME = 0; //XXX(paceZoneRange != -1) ? context->athlete->paceZones(false)->getDPrime(paceZoneRange) : 0.0;
int spaceZoneRange = context->athlete->paceZones(true) ?
context->athlete->paceZones(true)->whichRange(m->dateTime.date()) :
-1;
double SCV = (spaceZoneRange != -1) ? context->athlete->paceZones(true)->getCV(spaceZoneRange) : 0.0;
double SDPRIME = 0; //XXX (spaceZoneRange != -1) ? context->athlete->paceZones(true)->getDPrime(spaceZoneRange) : 0.0;
//
// HEIGHT and WEIGHT
//
double HEIGHT = m->getText("Height","0").toDouble();
if (HEIGHT == 0) HEIGHT = context->athlete->getHeight(NULL);
double WEIGHT = m->getWeight();
QString symbol = leaf->series->lvalue.n->toLower();
if (symbol == "cp") {
return Result(CP);
}
if (symbol == "w'") {
return Result(WPRIME);
}
if (symbol == "pmax") {
return Result(PMAX);
}
if (symbol == "scv") {
return Result(SCV);
}
if (symbol == "sd'") {
return Result(SDPRIME);
}
if (symbol == "cv") {
return Result(CV);
}
if (symbol == "d'") {
return Result(DPRIME);
}
if (symbol == "lthr") {
return Result(LTHR);
}
if (symbol == "rhr") {
return Result(RHR);
}
if (symbol == "maxhr") {
return Result(MaxHR);
}
if (symbol == "weight") {
return Result(WEIGHT);
}
if (symbol == "height") {
return Result(HEIGHT);
}
if (symbol == "units") {
return Result(context->athlete->useMetricUnits ? 1 : 0);
}
}
// get here for tiz and best
if (leaf->function == "best" || leaf->function == "tiz") {
switch (leaf->lvalue.l->type) {
default:
case Leaf::Function :
{
duration = eval(context, df, leaf->lvalue.l, m).number; // duration will be zero if string
}
break;
case Leaf::Symbol :
{
QString rename;
// get symbol value
if (df->lookupType.value(*(leaf->lvalue.l->lvalue.n)) == true) {
// numeric
duration = m->getForSymbol(rename=df->lookupMap.value(*(leaf->lvalue.l->lvalue.n),""));
} else {
duration = 0;
}
}
break;
case Leaf::Float :
duration = leaf->lvalue.l->lvalue.f;
break;
case Leaf::Integer :
duration = leaf->lvalue.l->lvalue.i;
break;
case Leaf::String :
duration = (leaf->lvalue.l->lvalue.s)->toDouble();
break;
break;
}
if (leaf->function == "best")
return Result(RideFileCache::best(df->context, m->fileName, leaf->seriesType, duration));
if (leaf->function == "tiz") // duration is really zone number
return Result(RideFileCache::tiz(df->context, m->fileName, leaf->seriesType, duration));
}
// if we get here its general function handling
// what function is being called?
int fnum=-1;
for (int i=0; DataFilterFunctions[i].parameters != -1; i++) {
if (DataFilterFunctions[i].name == leaf->function) {
// parameter mismatch not allowed; function signature mismatch
// should be impossible...
if (DataFilterFunctions[i].parameters && DataFilterFunctions[i].parameters != leaf->fparms.count())
fnum=-1;
else
fnum = i;
break;
}
}
// not found...
if (fnum < 0) return Result(0);
switch (fnum) {
case 0 : { return Result(cos(eval(context, df, leaf->fparms[0], m).number)); } // COS(x)
case 1 : { return Result(tan(eval(context, df, leaf->fparms[0], m).number)); } // TAN(x)
case 2 : { return Result(sin(eval(context, df, leaf->fparms[0], m).number)); } // SIN(x)
case 3 : { return Result(acos(eval(context, df, leaf->fparms[0], m).number)); } // ACOS(x)
case 4 : { return Result(atan(eval(context, df, leaf->fparms[0], m).number)); } // ATAN(x)
case 5 : { return Result(asin(eval(context, df, leaf->fparms[0], m).number)); } // ASIN(x)
case 6 : { return Result(cosh(eval(context, df, leaf->fparms[0], m).number)); } // COSH(x)
case 7 : { return Result(tanh(eval(context, df, leaf->fparms[0], m).number)); } // TANH(x)
case 8 : { return Result(sinh(eval(context, df, leaf->fparms[0], m).number)); } // SINH(x)
case 9 : { return Result(acosh(eval(context, df, leaf->fparms[0], m).number)); } // ACOSH(x)
case 10 : { return Result(atanh(eval(context, df, leaf->fparms[0], m).number)); } // ATANH(x)
case 11 : { return Result(asinh(eval(context, df, leaf->fparms[0], m).number)); } // ASINH(x)
case 12 : { return Result(exp(eval(context, df, leaf->fparms[0], m).number)); } // EXP(x)
case 13 : { return Result(log(eval(context, df, leaf->fparms[0], m).number)); } // LOG(x)
case 14 : { return Result(log10(eval(context, df, leaf->fparms[0], m).number)); } // LOG10(x)
case 15 : { return Result(ceil(eval(context, df, leaf->fparms[0], m).number)); } // CEIL(x)
case 16 : { return Result(floor(eval(context, df, leaf->fparms[0], m).number)); } // FLOOR(x)
case 17 : { return Result(round(eval(context, df, leaf->fparms[0], m).number)); } // ROUND(x)
case 18 : { return Result(fabs(eval(context, df, leaf->fparms[0], m).number)); } // FABS(x)
case 19 : { return Result(std::isinf(eval(context, df, leaf->fparms[0], m).number)); } // ISINF(x)
case 20 : { return Result(std::isnan(eval(context, df, leaf->fparms[0], m).number)); } // ISNAN(x)
case 21 : { /* SUM( ... ) */
double sum=0;
foreach(Leaf *l, leaf->fparms) {
sum += eval(context, df, l, m).number; // for vectors number is sum
}
return Result(sum);
}
break;
case 22 : { /* MEAN( ... ) */
double sum=0;
int count=0;
foreach(Leaf *l, leaf->fparms) {
Result res = eval(context, df, l, m); // for vectors number is sum
sum += res.number;
if (res.vector.count()) count += res.vector.count();
else count++;
}
return count ? Result(sum/double(count)) : Result(0);
}
break;
case 23 : { /* MAX( ... ) */
double max=0;
bool set=false;
foreach(Leaf *l, leaf->fparms) {
Result res = eval(context, df, l, m);
if (res.vector.count()) {
foreach(double x, res.vector) {
if (set && x>max) max=x;
else if (!set) { set=true; max=x; }
}
} else {
if (set && res.number>max) max=res.number;
else if (!set) { set=true; max=res.number; }
}
}
return Result(max);
}
break;
case 24 : { /* MIN( ... ) */
double min=0;
bool set=false;
foreach(Leaf *l, leaf->fparms) {
Result res = eval(context, df, l, m);
if (res.vector.count()) {
foreach(double x, res.vector) {
if (set && x<min) min=x;
else if (!set) { set=true; min=x; }
}
} else {
if (set && res.number<min) min=res.number;
else if (!set) { set=true; min=res.number; }
}
}
return Result(min);
}
break;
case 25 : { /* COUNT( ... ) */
int count = 0;
foreach(Leaf *l, leaf->fparms) {
Result res = eval(context, df, l, m);
if (res.vector.count()) count += res.vector.count();
else count++;
}
return Result(count);
}
break;
case 26 : { /* LTS (expr) */
PMCData *pmcData = context->athlete->getPMCFor(leaf->fparms[0], df);
return Result(pmcData->lts(m->dateTime.date()));
}
break;
case 27 : { /* STS (expr) */
PMCData *pmcData = context->athlete->getPMCFor(leaf->fparms[0], df);
return Result(pmcData->sts(m->dateTime.date()));
}
break;
case 28 : { /* SB (expr) */
PMCData *pmcData = context->athlete->getPMCFor(leaf->fparms[0], df);
return Result(pmcData->sb(m->dateTime.date()));
}
break;
case 29 : { /* RR (expr) */
PMCData *pmcData = context->athlete->getPMCFor(leaf->fparms[0], df);
return Result(pmcData->rr(m->dateTime.date()));
}
break;
case 30 :
{ /* ESTIMATE( model, CP | FTP | W' | PMAX | duration ) */
// which model ?
QString model = *leaf->fparms[0]->lvalue.n;
if (model == "2p") model = "2 Parm";
if (model == "3p") model = "3 Parm";
if (model == "ws") model = "WS";
if (model == "velo") model = "Velo";
if (model == "ext") model = "Ext";
// what we looking for ?
QString parm = leaf->fparms[1]->type == Leaf::Symbol ? *leaf->fparms[1]->lvalue.n : "";
bool toDuration = parm == "" ? true : false;
double duration = toDuration ? eval(context, df, leaf->fparms[1], m).number : 0;
// get the PD Estimate for this date - note we always work with the absolulte
// power estimates in formulas, since the user can just divide by config(weight)
// or Athlete_Weight (which takes into account values stored in ride files.
PDEstimate pde = context->athlete->getPDEstimateFor(m->dateTime.date(), model, false);
// no model estimate for this date
if (pde.parameters.count() == 0) return Result(0);
// get a duration
if (toDuration == true) {
double value = 0;
// we need to find the model
foreach(PDModel *pdm, df->models) {
// not the one we want
if (pdm->code() != model) continue;
// set the parameters previously derived
pdm->loadParameters(pde.parameters);
// use seconds
pdm->setMinutes(false);
// get the model estimate for our duration
value = pdm->y(duration);
// our work here is done
return Result(value);
}
} else {
if (parm == "cp") return Result(pde.CP);
if (parm == "w'") return Result(pde.WPrime);
if (parm == "ftp") return Result(pde.FTP);
if (parm == "pmax") return Result(pde.PMax);
}
return Result(0);
}
break;
default:
return Result(0);
}
}
break;
//
// SYMBOLS
//
case Leaf::Symbol :
{
double lhsdouble=0.0f;
bool lhsisNumber=false;
QString lhsstring;
QString rename;
QString symbol = *(leaf->lvalue.n);
// is it isRun ?
if (symbol == "isRun") {
lhsdouble = m->isRun ? 1 : 0;
lhsisNumber = true;
} else if (symbol == "isSwim") {
lhsdouble = m->isSwim ? 1 : 0;
lhsisNumber = true;
} else if (!symbol.compare("Current", Qt::CaseInsensitive)) {
if (context->currentRideItem())
lhsdouble = QDate(1900,01,01).
daysTo(context->currentRideItem()->dateTime.date());
else
lhsdouble = 0;
lhsisNumber = true;
} else if (!symbol.compare("Today", Qt::CaseInsensitive)) {
lhsdouble = QDate(1900,01,01).daysTo(QDate::currentDate());
lhsisNumber = true;
} else if (!symbol.compare("Date", Qt::CaseInsensitive)) {
lhsdouble = QDate(1900,01,01).daysTo(m->dateTime.date());
lhsisNumber = true;
} else if (isCoggan(symbol)) {
// a coggan PMC metric
PMCData *pmcData = context->athlete->getPMCFor("coggan_tss");
if (!symbol.compare("ctl", Qt::CaseInsensitive)) lhsdouble = pmcData->lts(m->dateTime.date());
if (!symbol.compare("atl", Qt::CaseInsensitive)) lhsdouble = pmcData->sts(m->dateTime.date());
if (!symbol.compare("tsb", Qt::CaseInsensitive)) lhsdouble = pmcData->sb(m->dateTime.date());
lhsisNumber = true;
} else if ((lhsisNumber = df->lookupType.value(*(leaf->lvalue.n))) == true) {
// get symbol value
// check metadata string to number first ...
QString meta = m->getText(rename=df->lookupMap.value(symbol,""), "unknown");
if (meta == "unknown")
lhsdouble = m->getForSymbol(rename=df->lookupMap.value(symbol,""));
else
lhsdouble = meta.toDouble();
lhsisNumber = true;
//qDebug()<<"symbol" << *(lvalue.n) << "is" << lhsdouble << "via" << rename;
} else {
// string symbol will evaluate to zero as unary expression
lhsstring = m->getText(rename=df->lookupMap.value(symbol,""), "");
//qDebug()<<"symbol" << *(lvalue.n) << "is" << lhsstring << "via" << rename;
}
if (lhsisNumber) return Result(lhsdouble);
else return Result(lhsstring);
}
break;
//
// LITERALS
//
case Leaf::Float :
{
return Result(leaf->lvalue.f);
}
break;
case Leaf::Integer :
{
return Result(leaf->lvalue.i);
}
break;
case Leaf::String :
{
QString string = *(leaf->lvalue.s);
// dates are returned as numbers
QDate date = QDate::fromString(string, "yyyy/MM/dd");
if (date.isValid()) return Result(QDate(1900,01,01).daysTo(date));
else return Result(string);
}
break;
//
// UNARY EXPRESSION
//
case Leaf::UnaryOperation :
{
// get result
Result lhs = eval(context, df, leaf->lvalue.l, m);
return Result(lhs.number * -1);
}
break;
//
// BINARY EXPRESSION
//
case Leaf::BinaryOperation :
case Leaf::Operation :
{
// lhs and rhs
Result lhs = eval(context, df, leaf->lvalue.l, m);
Result rhs = eval(context, df, leaf->rvalue.l, m);
// NOW PERFORM OPERATION
switch (leaf->op) {
case ADD:
{
if (lhs.isNumber) return Result(lhs.number + rhs.number);
else return Result(0);
}
break;
case SUBTRACT:
{
if (lhs.isNumber) return Result(lhs.number - rhs.number);
else return Result(0);
}
break;
case DIVIDE:
{
// avoid divide by zero
if (lhs.isNumber && rhs.number) return Result(lhs.number / rhs.number);
else return Result(0);
}
break;
case MULTIPLY:
{
if (lhs.isNumber && rhs.isNumber) return Result(lhs.number * rhs.number);
else return Result(0);
}
break;
case POW:
{
if (lhs.isNumber && rhs.number) return Result(pow(lhs.number,rhs.number));
else return Result(0);
}
break;
case EQ:
{
if (lhs.isNumber) return Result(lhs.number == rhs.number);
else return Result(lhs.string == rhs.string);
}
break;
case NEQ:
{
if (lhs.isNumber) return Result(lhs.number != rhs.number);
else return Result(lhs.string != rhs.string);
}
break;
case LT:
{
if (lhs.isNumber) return Result(lhs.number < rhs.number);
else return Result(lhs.string < rhs.string);
}
break;
case LTE:
{
if (lhs.isNumber) return Result(lhs.number <= rhs.number);
else return Result(lhs.string <= rhs.string);
}
break;
case GT:
{
if (lhs.isNumber) return Result(lhs.number > rhs.number);
else return Result(lhs.string > rhs.string);
}
break;
case GTE:
{
if (lhs.isNumber) return Result(lhs.number >= rhs.number);
else return Result(lhs.string >= rhs.string);
}
break;
case MATCHES:
if (!lhs.isNumber && !rhs.isNumber) return Result(QRegExp(rhs.string).exactMatch(lhs.string));
else return Result(false);
break;
case ENDSWITH:
if (!lhs.isNumber && !rhs.isNumber) return Result(lhs.string.endsWith(rhs.string));
else return Result(false);
break;
case BEGINSWITH:
if (!lhs.isNumber && !rhs.isNumber) return Result(lhs.string.startsWith(rhs.string));
else return Result(false);
break;
case CONTAINS:
{
if (!lhs.isNumber && !rhs.isNumber) return Result(lhs.string.contains(rhs.string) ? true : false);
else return Result(false);
}
break;
default:
break;
}
}
break;
case Leaf::Conditional :
{
Result cond = eval(context, df, leaf->cond.l, m);
if (cond.isNumber && cond.number) return eval(context, df, leaf->lvalue.l, m);
else return eval(context, df, leaf->rvalue.l, m);
}
break;
case Leaf::Vector :
{
// places results in vector, and number is sum of all
// explicit funcions sum/avg/max/min will return non-sum
// values. No vector operations are supported at present
// as a DESIGN DECISION. They are too complex for the average
// user to understand. We will integrate to R for users
// that want that kind of power.
//
// Vectors are about collecting data from across a date range
// so you can use them within a formula for simple kinds of
// operations; e.g. how much of todays' workouts in time
// does this workout represent would be:
// Duration / Duration[today:today] * 100.00
Specification spec;
// is this already snipped?
Result snipped = df->snips.value(leaf->signature(), Result(0));
if (snipped.vector.count() != 0) return snipped;
// get date range
int fromDS = eval(context, df, leaf->fparms[0], m).number;
int toDS = eval(context, df, leaf->fparms[1], m).number;
// swap dates if needed
if (toDS < fromDS) {
int swap=fromDS;
fromDS = toDS;
toDS = swap;
}
spec.setDateRange(DateRange(QDate(1900,01,01).addDays(fromDS),QDate(1900,01,01).addDays(toDS)));
Result returning(0);
// now iterate and evaluate for each
foreach(RideItem *ride, context->athlete->rideCache->rides()) {
if (!spec.pass(ride)) continue;
// calculate value
Result res = eval(context, df, leaf->lvalue.l, ride);
if (res.isNumber) {
returning.number += res.number; // sum for easy access
returning.vector << res.number;
}
}
// vectors of more than 100 items need snipping
if (returning.vector.count() > 100)
df->snips.insert(leaf->signature(), returning);
// always return as sum number (for now)
return returning;
}
default: // we don't need to evaluate any lower - they are leaf nodes handled above
break;
}
return Result(false);
}