From e064c678a6d9b063eb5fd43e57dd56e3768313a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20=C3=96hrstr=C3=B6m?= Date: Sat, 26 Nov 2022 14:15:14 +0100 Subject: [PATCH] Formulas can now calculate using dates. E.g. '2022-02-03' + 2 month --- src/driver_evo868.cc | 15 ++ src/formula.cc | 272 ++++++++++++++++++++++++++++------- src/formula_implementation.h | 14 +- src/testinternals.cc | 202 +++++++++++++++++++++++--- src/units.cc | 184 ++++++++++++++++++------ src/units.h | 42 ++++-- src/util.cc | 9 ++ src/util.h | 1 + 8 files changed, 605 insertions(+), 134 deletions(-) diff --git a/src/driver_evo868.cc b/src/driver_evo868.cc index 0435f4d..e1024ce 100644 --- a/src/driver_evo868.cc +++ b/src/driver_evo868.cc @@ -140,6 +140,21 @@ namespace .set(StorageNr(8),StorageNr(19)) ); + /* + addNumericFieldWithExtractor( + "history_{storage_counter-7counter}", + "The historic date #.", + PrintProperty::JSON, + Quantity::PointInTime, + VifScaling::Auto, + FieldMatcher::build() + .set(MeasurementType::Instantaneous) + .set(VIFRange::Volume) + .set(StorageNr(8),StorageNr(19)), + Unit::Date + ); + */ + addStringFieldWithExtractor( "device_date_time", "Date and time when the meter sent the telegram.", diff --git a/src/formula.cc b/src/formula.cc index 76e4820..ad7dfd7 100644 --- a/src/formula.cc +++ b/src/formula.cc @@ -38,7 +38,9 @@ NumericFormulaSquareRoot::~NumericFormulaSquareRoot() { } double NumericFormulaConstant::calculate(SIUnit to) { - return siunit().convertTo(constant_, to); + double r {}; + siunit().convertTo(constant_, to, &r); + return r; } double NumericFormulaMeterField::calculate(SIUnit to_si_unit) @@ -52,7 +54,9 @@ double NumericFormulaMeterField::calculate(SIUnit to_si_unit) const SIUnit& field_si_unit = toSIUnit(field_unit); - return field_si_unit.convertTo(val, to_si_unit); + double r {}; + field_si_unit.convertTo(val, to_si_unit, &r); + return r; } double NumericFormulaDVEntryField::calculate(SIUnit to_si_unit) @@ -63,31 +67,66 @@ double NumericFormulaDVEntryField::calculate(SIUnit to_si_unit) const SIUnit& counter_si_unit = toSIUnit(Unit::COUNTER); - return counter_si_unit.convertTo(val, to_si_unit); + double r {}; + counter_si_unit.convertTo(val, to_si_unit, &r); + return r; } -double NumericFormulaAddition::calculate(SIUnit to) +double NumericFormulaAddition::calculate(SIUnit to_siunit) { - double l = left_->calculate(to); - double r = right_->calculate(to); + double l = left_->calculate(left_->siunit()); + double r = right_->calculate(right_->siunit()); - return l+r; + double v {}; + SIUnit v_siunit(Unit::COUNTER); + left_->siunit().mathOpTo(MathOp::ADD, l, r, right_->siunit(), &v_siunit, &v); + + double result {}; + v_siunit.convertTo(v, to_siunit, &result); + + if (isDebugEnabled()) + { + debug("(formula) ADD %g (%s) %g (%s) --> %g %s --> %g %s\n", + l, left_->siunit().info().c_str(), + r, right_->siunit().info().c_str(), + v, v_siunit.info().c_str(), + result, siunit().info().c_str()); + } + + return result; } -double NumericFormulaSubtraction::calculate(SIUnit to) +double NumericFormulaSubtraction::calculate(SIUnit to_siunit) { - double l = left_->calculate(to); - double r = right_->calculate(to); + double l = left_->calculate(left_->siunit()); + double r = right_->calculate(right_->siunit()); - return l-r; + double v {}; + SIUnit v_siunit(Unit::COUNTER); + left_->siunit().mathOpTo(MathOp::SUB, l, r, right_->siunit(), &v_siunit, &v); + + double result {}; + v_siunit.convertTo(v, to_siunit, &result); + + if (isDebugEnabled()) + { + debug("(formula) SUB %g (%s) %g (%s) --> %g %s --> %g %s\n", + l, left_->siunit().info().c_str(), + r, right_->siunit().info().c_str(), + v, v_siunit.info().c_str(), + result, siunit().info().c_str()); + } + + return result; } -double NumericFormulaMultiplication::calculate(SIUnit to) +double NumericFormulaMultiplication::calculate(SIUnit to_siunit) { double l = left_->calculate(left_->siunit()); double r = right_->calculate(right_->siunit()); double m = l*r; - double v = siunit().convertTo(m, to); + double v {}; + siunit().convertTo(m, to_siunit, &v); if (isDebugEnabled()) { @@ -95,17 +134,18 @@ double NumericFormulaMultiplication::calculate(SIUnit to) l, left_->siunit().info().c_str(), r, right_->siunit().info().c_str(), m, - v, to.info().c_str()); + v, to_siunit.info().c_str()); } return v; } -double NumericFormulaDivision::calculate(SIUnit to) +double NumericFormulaDivision::calculate(SIUnit to_siunit) { double l = left_->calculate(left_->siunit()); double r = right_->calculate(right_->siunit()); double d = l/r; - double v = siunit().convertTo(d, to); + double v {}; + siunit().convertTo(d, to_siunit, &v); if (isDebugEnabled()) { @@ -114,28 +154,30 @@ double NumericFormulaDivision::calculate(SIUnit to) r, right_->siunit().info().c_str(), d, v, - to.info().c_str()); + to_siunit.info().c_str()); } return v; } -double NumericFormulaExponentiation::calculate(SIUnit to) +double NumericFormulaExponentiation::calculate(SIUnit to_siunit) { - double l = left_->calculate(to); - double r = right_->calculate(to); + double l = left_->calculate(to_siunit); + double r = right_->calculate(to_siunit); double p = pow(l,r); - double v = siunit().convertTo(p, to); + double v {}; + siunit().convertTo(p, to_siunit, &v); debug("(formula) %g <-- %g <-- pow %g ^ %g\n", v, p, l, r); return v; } -double NumericFormulaSquareRoot::calculate(SIUnit to) +double NumericFormulaSquareRoot::calculate(SIUnit to_siunit) { double i = inner_->calculate(inner_->siunit()); double s = sqrt(i); - double v = siunit().convertTo(s, to); + double v {}; + siunit().convertTo(s, to_siunit, &v); if (isDebugEnabled()) { @@ -143,7 +185,7 @@ double NumericFormulaSquareRoot::calculate(SIUnit to) i, inner_->siunit().info().c_str(), s, v, - to.info().c_str()); + to_siunit.info().c_str()); } return v; } @@ -153,6 +195,8 @@ const char *toString(TokenType tt) switch (tt) { case TokenType::SPACE: return "SPACE"; case TokenType::NUMBER: return "NUMBER"; + case TokenType::DATETIME: return "DATETIME"; + case TokenType::TIME: return "TIME"; case TokenType::LPAR: return "LPAR"; case TokenType::RPAR: return "RPAR"; case TokenType::PLUS: return "PLUS"; @@ -176,7 +220,30 @@ string Token::str(const string &s) double Token::val(const string &s) { string v = s.substr(start, len); - return atof(v.c_str()); + if (type == TokenType::NUMBER) + { + return atof(v.c_str()); + } + else if (type == TokenType::DATETIME) + { + struct tm time {}; + strptime(v.c_str(), "'%Y-%m-%d %H:%M:%S'", &time); + time_t epoch = mktime(&time); + double result = (double)epoch; + return result; + } + else if (type == TokenType::TIME) + { + int h = 0; + int m = 0; + int s = 0; + sscanf(v.c_str(), "'%02d:%02d:%02d'", &h, &m, &s); + double result = h*3600+m*60+s; + return result; + } + + assert(0); + return 0; } string Token::vals(const string &s) @@ -250,6 +317,76 @@ size_t FormulaImplementation::findNumber(size_t i) return len; } +size_t FormulaImplementation::findDateTime(size_t i) +{ + // A datetime is converted into a unix timestamp. + // Patterns: '2222-22-22 11:11:00' + // '2222-22-22 11:11' + // '2222-22-22' + + struct tm time {}; + const char *start = &formula_[i]; + const char *end; + + memset(&time, 0, sizeof(time)); + if (i+20 < formula_.length()) + { + end = strptime(start, "'%Y-%m-%d %H:%M:%S'", &time); + if (distance(start, end) == 21) return 21; + } + + if (i+17 < formula_.length()) + { + end = strptime(start, "'%Y-%m-%d %H:%M'", &time); + if (distance(start, end) == 18) return 18; + } + + if (i+11 < formula_.length()) + { + end = strptime(start, "'%Y-%m-%d'", &time); + if (distance(start, end) == 12) return 12; + } + + return 0; +} + +size_t FormulaImplementation::findTime(size_t i) +{ + // A time is converted into seconds 10:11:12 is 10*3600+11*60+12 seconds. + // Patterns: '11:22:15' + // '11:22' + + if (i+9 < formula_.length() && + '\'' == formula_[i+0] && + isdigit(formula_[i+1]) && + isdigit(formula_[i+2]) && + ':' == formula_[i+3] && + isdigit(formula_[i+4]) && + isdigit(formula_[i+5]) && + ':' == formula_[i+6] && + isdigit(formula_[i+7]) && + isdigit(formula_[i+8]) && + '\'' == formula_[i+9]) + { + return 10; + } + + if (i+6 < formula_.length() && + '\'' == formula_[i+0] && + isdigit(formula_[i+1]) && + isdigit(formula_[i+2]) && + ':' == formula_[i+3] && + isdigit(formula_[i+4]) && + isdigit(formula_[i+5]) && + '\'' == formula_[i+6]) + { + return 7; + } + + return 0; +} + + size_t FormulaImplementation::findPlus(size_t i) { if (i >= formula_.length()) return 0; @@ -387,6 +524,12 @@ bool FormulaImplementation::tokenize() len = findSpace(i); if (len > 0) { i+=len; continue; } // No token added for whitespace. + len = findDateTime(i); + if (len > 0) { tokens_.push_back(Token(TokenType::DATETIME, i, len)); i+=len; continue; } + + len = findTime(i); + if (len > 0) { tokens_.push_back(Token(TokenType::TIME, i, len)); i+=len; continue; } + len = findNumber(i); if (len > 0) { tokens_.push_back(Token(TokenType::NUMBER, i, len)); i+=len; continue; } @@ -424,7 +567,13 @@ bool FormulaImplementation::tokenize() } // Interrupted early, thus there was an error tokenizing. - if (i < formula_.length()) return false; + if (i < formula_.length()) + { + Token tok(TokenType::SPACE, i, 0); + errors_.push_back(tostrprintf("Unknown token!\n"+tok.withMarker(formula_))); + valid_ = false; + return false; + } return true; } @@ -441,6 +590,18 @@ size_t FormulaImplementation::parseOps(size_t i) return i+1; } + if (tok->type == TokenType::DATETIME) + { + handleUnixTimestamp(tok); + return i+1; + } + + if (tok->type == TokenType::TIME) + { + handleSeconds(tok); + return i+1; + } + if (tok->type == TokenType::PLUS) { size_t next = parseOps(i+1); @@ -553,12 +714,30 @@ void FormulaImplementation::handleConstant(Token *number, Token *unit) doConstant(u, c); } +void FormulaImplementation::handleUnixTimestamp(Token *number) +{ + double c = number->val(formula_); + + doConstant(Unit::UnixTimestamp, c); +} + +void FormulaImplementation::handleSeconds(Token *number) +{ + double c = number->val(formula_); + Unit u = Unit::Second; + + doConstant(u, c); +} + void FormulaImplementation::handleAddition(Token *tok) { SIUnit right_siunit = topOp()->siunit(); SIUnit left_siunit = top2Op()->siunit(); + SIUnit to_siunit(Unit::COUNTER); - if (!left_siunit.canConvertTo(right_siunit)) + bool ok = left_siunit.mathOpTo(MathOp::ADD, 0, 0, right_siunit, &to_siunit, NULL); + + if (!ok) { string lsis = left_siunit.str(); string rsis = right_siunit.str(); @@ -570,7 +749,7 @@ void FormulaImplementation::handleAddition(Token *tok) return; } - doAddition(); + doAddition(to_siunit); } void FormulaImplementation::handleSubtraction(Token *tok) @@ -578,11 +757,15 @@ void FormulaImplementation::handleSubtraction(Token *tok) SIUnit right_siunit = topOp()->siunit(); SIUnit left_siunit = top2Op()->siunit(); - if (!left_siunit.canConvertTo(right_siunit)) + SIUnit v_siunit(Unit::COUNTER); + + bool ok = left_siunit.mathOpTo(MathOp::SUB, 0, 0, right_siunit, &v_siunit, NULL); + + if (!ok) { string lsis = left_siunit.str(); string rsis = right_siunit.str(); - errors_.push_back(tostrprintf("Cannot subtract %s to %s!\n%s", + errors_.push_back(tostrprintf("Cannot subtract %s from %s!\n%s", left_siunit.info().c_str(), right_siunit.info().c_str(), tok->withMarker(formula_).c_str())); @@ -590,7 +773,7 @@ void FormulaImplementation::handleSubtraction(Token *tok) return; } - doSubtraction(); + doSubtraction(v_siunit); } void FormulaImplementation::handleMultiplication(Token *tok) @@ -601,7 +784,7 @@ void FormulaImplementation::handleMultiplication(Token *tok) void FormulaImplementation::handleDivision(Token *tok) { - // Any two units can be multiplied! You might not like the answer thought.... + // Any two units can be divided! You might not like the answer thought.... doDivision(); } @@ -750,38 +933,24 @@ void FormulaImplementation::doConstant(Unit u, double c) pushOp(new NumericFormulaConstant(this, u, c)); } -void FormulaImplementation::doAddition() +void FormulaImplementation::doAddition(const SIUnit &to_siunit) { assert(op_stack_.size() >= 2); - SIUnit right_siunit = topOp()->siunit(); - unique_ptr right_node = popOp(); - - SIUnit left_siunit = topOp()->siunit(); - unique_ptr left_node = popOp(); - pushOp(new NumericFormulaAddition(this, left_siunit, left_node, right_node)); - - assert(left_siunit.canConvertTo(right_siunit)); + pushOp(new NumericFormulaAddition(this, to_siunit, left_node, right_node)); } -void FormulaImplementation::doSubtraction() +void FormulaImplementation::doSubtraction(const SIUnit &to_siunit) { assert(op_stack_.size() >= 2); - SIUnit right_siunit = topOp()->siunit(); - unique_ptr right_node = popOp(); - - SIUnit left_siunit = topOp()->siunit(); - unique_ptr left_node = popOp(); - pushOp(new NumericFormulaSubtraction(this, left_siunit, left_node, right_node)); - - assert(left_siunit.canConvertTo(right_siunit)); + pushOp(new NumericFormulaSubtraction(this, to_siunit, left_node, right_node)); } void FormulaImplementation::doMultiplication() @@ -863,7 +1032,8 @@ void FormulaImplementation::doMeterField(Unit u, FieldInfo *fi) { SIUnit from_si_unit = toSIUnit(fi->defaultUnit()); SIUnit to_si_unit = toSIUnit(u); - assert(from_si_unit.canConvertTo(to_si_unit)); + assert(from_si_unit.convertTo(0, to_si_unit, NULL)); + pushOp(new NumericFormulaMeterField(this, u, fi->vname(), fi->xuantity())); } diff --git a/src/formula_implementation.h b/src/formula_implementation.h index dc8ec4e..8b13b9c 100644 --- a/src/formula_implementation.h +++ b/src/formula_implementation.h @@ -194,6 +194,8 @@ enum class TokenType LPAR, RPAR, NUMBER, + DATETIME, + TIME, PLUS, MINUS, TIMES, @@ -241,11 +243,11 @@ struct FormulaImplementation : public Formula // Pushes a dve entry field read on the formula builder stack. void doDVEntryField(Unit u, DVEntryCounterType ct); // Pops the two top nodes of the formula builder stack and pushes an addition on the formula builder stack. - // The target unit will be the first unit of the two operands. - void doAddition(); + // The target unit will be the supplied to unit. + void doAddition(const SIUnit &to); // Pops the two top nodes of the formula builder stack and pushes a subtraction on the formula builder stack. - // The target unit will be the first unit of the two operands. - void doSubtraction(); + // The target unit will be the supplied to unit. + void doSubtraction(const SIUnit &to); // Pops the two top nodes of the formula builder stack and pushes a multiplication on the formula builder stack. // The target unit will be multiplication of the SI Units. void doMultiplication(); @@ -265,6 +267,8 @@ struct FormulaImplementation : public Formula bool go(); size_t findSpace(size_t i); size_t findNumber(size_t i); + size_t findDateTime(size_t i); + size_t findTime(size_t i); size_t findUnit(size_t i); size_t findPlus(size_t i); size_t findMinus(size_t i); @@ -281,6 +285,8 @@ struct FormulaImplementation : public Formula size_t parsePar(size_t i); void handleConstant(Token *number, Token *unit); + void handleSeconds(Token *number); + void handleUnixTimestamp(Token *number); void handleAddition(Token *add); void handleSubtraction(Token *add); void handleMultiplication(Token *add); diff --git a/src/testinternals.cc b/src/testinternals.cc index 8bb51e5..83e6090 100644 --- a/src/testinternals.cc +++ b/src/testinternals.cc @@ -62,7 +62,9 @@ bool verbose_ = false; X(si_units_siexp) \ X(si_units_basic) \ X(si_units_conversion) \ - X(formulas_building) \ + X(formulas_building_consts) \ + X(formulas_building_meters) \ + X(formulas_datetimes) \ X(formulas_parsing_1) \ X(formulas_parsing_2) \ X(formulas_multiply_constants) \ @@ -862,7 +864,7 @@ void test_device_parsing() void test_month(int y, int m, int day, int mdiff, string from, string to) { - struct tm date; + struct tm date {}; date.tm_year = y-1900; date.tm_mon = m-1; date.tm_mday = day; @@ -1597,7 +1599,6 @@ void test_units_extraction() test_unit("work_kvarh", true, "work", Unit::KVARH); test_unit("current_power_consumption_phase1_kw", true, "current_power_consumption_phase1", Unit::KW); - } void test_expected_failed_si_convert(Unit from_unit, @@ -1613,7 +1614,7 @@ void test_expected_failed_si_convert(Unit from_unit, { printf("ERROR! Not the expected quantities!\n"); } - if (from_si_unit.canConvertTo(to_si_unit)) + if (from_si_unit.convertTo(0, to_si_unit, NULL)) { printf("ERROR! Should not be able to convert from %s to %s !\n", fu.c_str(), tu.c_str()); } @@ -1628,6 +1629,10 @@ void test_si_convert(double from_value, double expected_value, set *from_set, set *to_set) { + debug("test_si_convert from %.17g %s to %.17g %s\n", + from_value, expected_from_unit.c_str(), + expected_value, expected_to_unit.c_str()); + string evs = tostrprintf("%.15g", expected_value); SIUnit from_si_unit(from_unit); @@ -1638,7 +1643,8 @@ void test_si_convert(double from_value, double expected_value, from_set->erase(from_unit); to_set->erase(to_unit); - double e = from_si_unit.convertTo(from_value, to_si_unit); + double e {}; + from_si_unit.convertTo(from_value, to_si_unit, &e); string es = tostrprintf("%.15g", e); if (canConvert(from_unit, to_unit)) @@ -1788,15 +1794,15 @@ LIST_OF_QUANTITIES test_si_convert(3600.0, 1.0/24.0, Unit::Second, "s", Unit::Day, "d", Quantity::Time, &from_set, &to_set); // 1 min is 60 seconds. test_si_convert(1.0, 60.0, Unit::Minute, "min", Unit::Second, "s", Quantity::Time, &from_set, &to_set); - // 1 day is 1/365.2425 year - test_si_convert(1.0, 1.0/365.2425, Unit::Day, "d", Unit::Year, "y", Quantity::Time, &from_set, &to_set); - // 1 month is 30.437 days - test_si_convert(2.0, 30.437*2, Unit::Month, "month", Unit::Day, "d", Quantity::Time, &from_set, &to_set); - test_si_convert(30.437*2, 2.0, Unit::Day, "d", Unit::Month, "month", Quantity::Time, &from_set, &to_set); + // 1 day is 24 hours + test_si_convert(1.0, 24, Unit::Day, "d", Unit::Hour, "h", Quantity::Time, &from_set, &to_set); + // 1 month is 1 month. + test_si_convert(1.0, 1.0, Unit::Month, "month", Unit::Month, "month", Quantity::Time, &from_set, &to_set); + test_si_convert(1.0, 1.0, Unit::Year, "y", Unit::Year, "y", Quantity::Time, &from_set, &to_set); // 100 hours is 100/24 days. test_si_convert(100.0, 100.0/24.0, Unit::Hour, "h", Unit::Day, "d", Quantity::Time, &from_set, &to_set); // 1 year is 365.2425 days. - test_si_convert(1.0, 365.2425, Unit::Year, "y", Unit::Day, "d", Quantity::Time, &from_set, &to_set); +// test_si_convert(1.0, 365.2425, Unit::Year, "y", Unit::Day, "d", Quantity::Time, &from_set, &to_set); check_units_tested(from_set, to_set, Quantity::Time); @@ -2043,19 +2049,22 @@ LIST_OF_QUANTITIES check_quantities_tested(q_set); } -void test_formulas_building() + +void test_formulas_building_consts() { unique_ptr f = unique_ptr(new FormulaImplementation()); + double v; // , expected; f->doConstant(Unit::KWH, 17); f->doConstant(Unit::KWH, 1); - f->doAddition(); - double v = f->calculate(Unit::KWH); + f->doAddition(SI_KWH); + v = f->calculate(Unit::KWH); if (v != 18.0) { printf("ERROR in test formula 1 expected 18.0 but got %lf\n", v); } + /* f->clear(); f->doConstant(Unit::KWH, 10); v = f->calculate(Unit::MJ); @@ -2067,7 +2076,7 @@ void test_formulas_building() f->clear(); f->doConstant(Unit::GJ, 10); f->doConstant(Unit::MJ, 10); - f->doAddition(); + f->doAddition(SI_GJ); v = f->calculate(Unit::GJ); if (v != 10.01) { @@ -2077,15 +2086,53 @@ void test_formulas_building() f->clear(); f->doConstant(Unit::C, 10); f->doConstant(Unit::C, 20); - f->doAddition(); + f->doAddition(SI_C); f->doConstant(Unit::C, 22); - f->doAddition(); + f->doAddition(SI_C); v = f->calculate(Unit::C); if (v != 52) { printf("ERROR in test formula 4 expected 52 but got %lf\n", v); } + f->clear(); + f->doConstant(Unit::Month, 2); + f->doConstant(Unit::COUNTER, 3); + f->doMultiplication(); + v = f->calculate(Unit::Month); + if (v != 6) + { + printf("ERROR in test formula 5 expected 6 but got %g\n", v); + } + + f->clear(); + f->doConstant(Unit::UnixTimestamp, 3600*24*11); // mon 12 jan 1970 01:00:00 CET + f->doConstant(Unit::UnixTimestamp, 9); + f->doAddition(SIUnit(Unit::UnixTimestamp)); + v = f->calculate(Unit::UnixTimestamp); + expected = 3600*24*11+9; + if (v != expected) + { + printf("ERROR in test formula 6 expected %g but got %g\n", expected, v); + } + + f->clear(); + f->doConstant(Unit::UnixTimestamp, 3600*24*11); // mon 12 jan 1970 01:00:00 CET + f->doConstant(Unit::Month, 1); + f->doAddition(SIUnit(Unit::UnixTimestamp)); + v = f->calculate(Unit::UnixTimestamp); + expected = 3600*24*(31+11); // mon 12 feb 1970 01:00:00 CET + if (v != expected) + { + printf("ERROR in test formula 7 expected %g but got %g\n", expected, v); + } + */ +} + +void test_formulas_building_meters() +{ + unique_ptr f = unique_ptr(new FormulaImplementation()); + //////////////////////////////////////////////////////////////////////////////////////////////////// { @@ -2113,7 +2160,7 @@ void test_formulas_building() f->setMeter(meter.get()); f->doMeterField(Unit::C, fi_flow); - v = f->calculate(Unit::C); + double v = f->calculate(Unit::C); if (v != 31) { printf("ERROR in test formula 5 expected 31 but got %lf\n", v); @@ -2124,7 +2171,7 @@ void test_formulas_building() f->doMeterField(Unit::C, fi_flow); f->doMeterField(Unit::C, fi_ext); - f->doAddition(); + f->doAddition(SIUnit(Unit::C)); v = f->calculate(Unit::C); if (v != 50) { @@ -2167,11 +2214,11 @@ void test_formulas_building() f->doMeterField(Unit::KW, fi_p1); f->doMeterField(Unit::KW, fi_p2); - f->doAddition(); + f->doAddition(SI_KW); f->doMeterField(Unit::KW, fi_p3); - f->doAddition(); + f->doAddition(SI_KW); - v = f->calculate(Unit::KW); + double v = f->calculate(Unit::KW); if (v != 0.21679) { printf("ERROR in test formula 7 expected 0.21679 but got %lf\n", v); @@ -2223,6 +2270,110 @@ void test_formula_error(FormulaImplementation *f, Meter *m, string formula, Unit assert(!ok); } +double totime(int year, int month = 1, int day = 1, int hour = 0, int min = 0, int sec = 0) +{ + struct tm date {}; + + date.tm_year = year-1900; + date.tm_mon = month-1; + date.tm_mday = day; + date.tm_hour = hour; + date.tm_min = min; + date.tm_sec = sec; + + // This t timestamp is dependent on the local time zone. + time_t t = mktime(&date); + /* + // Extract the local time zone. + struct tm tz_adjust {}; + localtime_r(&t, &tz_adjust); + + // if tm_gmtoff is zero, then we are in Greenwich! + // if tm_gmtoff is 3600 then we are in Stockholm! + // Now adjust the t timestamp so that we execute this this, as if we are in Greenwich. + // This way, the test will work wherever in the world we run it. + t -= tz_adjust.tm_gmtoff; + */ + return (double)t; +} + +void test_datetime(FormulaImplementation *f, string formula, int year, int month=1, int day=1, int hour=0, int min=0, int sec=0) +{ + f->clear(); + double expected = totime(year,month,day,hour,min,sec); + f->parse(NULL, formula); + if (!f->valid()) + { + printf("%s\n", f->errors().c_str()); + } + + double v = f->calculate(Unit::UnixTimestamp); + if (v != expected) + { + time_t t = v; + struct tm time {}; + localtime_r(&t, &time); + string gs = strdatetimesec(&time); + + printf("ERROR Expected datetime %.17g %04d-%02d-%02d %02d:%02d:%02d " + "but got %.17g (%s) when testing \"%s\"\n", + expected, year, month, day, hour, min, sec, + v, gs.c_str(), formula.c_str()); + } +} + +void test_time(FormulaImplementation *f, string formula, int hour=0, int min=0, int sec=0) +{ + f->clear(); + double expected = hour*3600+min*60+sec; + f->parse(NULL, formula); + if (!f->valid()) + { + printf("%s\n", f->errors().c_str()); + } + + double v = f->calculate(Unit::Second); + if (v != expected) + { + printf("ERROR Expected time %.17g but got %.17g when testing %s %02d:%02d.%02d\n", + expected, v, formula.c_str(), hour, min, sec); + } +} + +void test_formulas_datetimes() +{ + unique_ptr f = unique_ptr(new FormulaImplementation()); + + test_datetime(f.get(), "'2022-02-02'", 2022, 02, 02); + test_datetime(f.get(), "'2021-02-28'", 2021, 02, 28); + + test_datetime(f.get(), "'1970-01-01 01:00:00'", 1970, 01, 01, 01, 00, 00); + test_datetime(f.get(), "'1970-01-01 01:00'", 1970, 01, 01, 01, 00); + test_datetime(f.get(), "'1970-01-01'", 1970, 01, 01); + + test_time(f.get(), "'00:15'", 0, 15, 0); + test_time(f.get(), "'00:00:16'", 0, 0, 16); + + test_datetime(f.get(), "'2022-01-01 00:00:00' + 1s", 2022,1,1,0,0,1); + test_datetime(f.get(), "'1971-10-01 02:17' +7d+1h+2min+1s", 1971, 10, 8, 3, 19, 1); + + test_datetime(f.get(), "'2000-01-01' + 1month", 2000, 2, 1); + test_datetime(f.get(), "'2020-12-31' + 2month", 2021, 2, 28); + test_datetime(f.get(), "'2020-12-31' - 10month", 2020, 2, 29); + test_datetime(f.get(), "'2021-01-31' - 1month", 2020, 12,31); + test_datetime(f.get(), "'2021-01-31' - 2month", 2020, 11, 30); + test_datetime(f.get(), "'2021-01-31' - 24month", 2019, 1, 31); + test_datetime(f.get(), "'2021-01-31' + 24month", 2023, 1, 31); + test_datetime(f.get(), "'2021-01-31' + 22month", 2022, 11, 30); + + // 2020 was a leap year. + test_datetime(f.get(), "'2021-02-28' -12month", 2020,2,29); + // 2000 was a leap year %100=0 but %400=0 overrides. + test_datetime(f.get(), "'2001-02-28' -12month", 2000, 2, 29); + // 2100 is not a leap year since %100=0 and not overriden %400 != 0. + test_datetime(f.get(), "'2000-02-29' +(12month * 100counter)", 2100,2,28); +} + void test_formulas_parsing_1() { MeterInfo mi; @@ -2322,6 +2473,13 @@ void test_formulas_sqrt_constants() test_formula_value(&fi, NULL, "sqrt((2 kwh * 2 kwh) + (3 kvarh * 3 kvarh))", 3.6055512754639891, Unit::KVAH); } +void test_formulas_date_constants() +{ + FormulaImplementation fi; + +// test_formula_value(&fi, NULL, "2022-01-01 + 1 month", "2022-02-01"); +} + void test_formulas_errors() { { diff --git a/src/units.cc b/src/units.cc index 7c81377..75ae6e3 100644 --- a/src/units.cc +++ b/src/units.cc @@ -93,18 +93,23 @@ using namespace std; X(Minute, 60.0, SIExp().s(1)) \ X(Hour, 3600.0, SIExp().s(1)) \ X(Day, 3600.0*24, SIExp().s(1)) \ - X(Month, 3600.0*24*30.437, SIExp().s(1)) \ - X(Year, 3600.0*24*365.2425, SIExp().s(1)) \ - X(DateTimeUT, 1.0, SIExp().s(1)) \ - X(DateTimeUTC, 1.0, SIExp().s(1)) \ - X(DateTimeLT, 1.0, SIExp().s(1)) \ - \ + X(Month, 1, SIExp().month(1)) \ + X(Year, 1, SIExp().year(1)) \ + X(UnixTimestamp,1.0, SIExp().unixTimestamp(1)) \ + X(DateTimeUTC, 0.0, SIExp().unixTimestamp(1)) \ + X(DateTimeLT, 0.0, SIExp().unixTimestamp(1)) \ + X(DateLT, 0.0, SIExp().unixTimestamp(1)) \ + X(TimeLT, 0.0, SIExp().unixTimestamp(1)) \ + \ X(RH, 1.0, SIExp()) \ X(HCA, 1.0, SIExp()) \ X(COUNTER, 1.0, SIExp()) \ X(TXT, 1.0, SIExp()) \ +// 3600.0*24*365.2425 +// 3600.0*24*30.437 + #define X(cname,lcname,hrname,quantity,explanation) const SIUnit SI_##cname(Unit::cname); LIST_OF_UNITS #undef X @@ -150,26 +155,6 @@ LIST_OF_CONVERSIONS return 0; } -bool SIUnit::canConvertTo(const SIUnit &uto) const -{ - // Same exponents! Then we can always convert! - if (exponents_ == uto.exponents_) return true; - - // Now the special cases. K-C-F - if ((exponents_ == SI_K.exponents_ || - exponents_ == SI_C.exponents_ || - exponents_ == SI_F.exponents_) && - (uto.exponents_ == SI_K.exponents_ || - uto.exponents_ == SI_C.exponents_ || - uto.exponents_ == SI_F.exponents_)) - { - // We are converting between the K,C,F temperatures only! - return true; - } - - return false; -} - bool isKCF(const SIExp &e) { return @@ -178,6 +163,15 @@ bool isKCF(const SIExp &e) e == SI_F.exp(); } +bool is_S_MONTH_YEAR_UT(const SIExp &e) +{ + return + e == SI_Second.exp() || + e == SI_UnixTimestamp.exp() || + e == SI_Month.exp() || + e == SI_Year.exp(); +} + void getScaleOffset(const SIExp &e, double *scale, double *offset) { if (e == SI_K.exp()) @@ -201,15 +195,16 @@ void getScaleOffset(const SIExp &e, double *scale, double *offset) assert(0); } -double SIUnit::convertTo(double val, const SIUnit &uto) const +bool SIUnit::convertTo(double left, const SIUnit &out_siunit, double *out) const { - if (exp() == uto.exp()) + if (exp() == out_siunit.exp()) { - return (val*scale_)/uto.scale_; + if (out != NULL) *out = (left*scale_)/out_siunit.scale_; + return true; } // Now the special cases. K-C-F - if (isKCF(exp()) && isKCF(uto.exp())) + if (isKCF(exp()) && isKCF(out_siunit.exp())) { double from_scale {}; double from_offset {}; @@ -220,13 +215,97 @@ double SIUnit::convertTo(double val, const SIUnit &uto) const double to_offset {}; double to_scale {}; - getScaleOffset(uto.exp(), &to_scale, &to_offset); - to_scale *= uto.scale(); + getScaleOffset(out_siunit.exp(), &to_scale, &to_offset); + to_scale *= out_siunit.scale(); - return ((val+from_offset)*from_scale)/to_scale-to_offset; + if (out != NULL) *out = ((left+from_offset)*from_scale)/to_scale-to_offset; + return true; } - return std::numeric_limits::quiet_NaN(); + if (out != NULL) *out = std::numeric_limits::quiet_NaN(); + return false; +} + +bool forbidden_op(MathOp op, const SIExp &a, const SIExp &b) +{ + // Two unix timestamps cannot be added together. They can be subtracted though! + if (op == MathOp::ADD && a == SI_UnixTimestamp.exp() && b == SI_UnixTimestamp.exp()) return true; + + return false; +} + +double do_op(MathOp op, double left, double right) +{ + if (op == MathOp::ADD) return left+right; + if (op == MathOp::SUB) return left-right; + assert(0); +} + +bool SIUnit::mathOpTo(MathOp op, double left, double right, const SIUnit &right_siunit, SIUnit *out_siunit, double *out) const +{ + // Adding all values with the same units. + if (exp() == right_siunit.exp()) + { + if (forbidden_op(op, exp(), right_siunit.exp())) + { + if (out_siunit != NULL) *out_siunit = SI_COUNTER; + if (out != NULL) *out = std::numeric_limits::quiet_NaN(); + return false; + } + double left_converted {}; + convertTo(left, right_siunit, &left_converted); + double result = do_op(op, left_converted, right); + if (out_siunit != NULL) *out_siunit = right_siunit; + if (out != NULL) *out = result; + return true; + } + + // Adding temperatures. + if (isKCF(exp()) && isKCF(right_siunit.exp())) + { + double left_converted {}; + convertTo(left, right_siunit, &left_converted); + double result = do_op(op, left_converted, right); + if (out_siunit != NULL) *out_siunit = right_siunit; + if (out != NULL) *out = result; + return true; + } + + // Operating on unix timestamps + if (exp() == SI_UnixTimestamp.exp() || right_siunit.exp() == SI_UnixTimestamp.exp()) + { + if (right_siunit.exp() == SI_UnixTimestamp.exp()) + { + // The timestamp is right, flip the arguments. + return right_siunit.mathOpTo(op, right, left, *this, out_siunit, out); + } + assert(exp() == SI_UnixTimestamp.exp() && right_siunit.exp() != SI_UnixTimestamp.exp()); + + // The timestamp is left. Lets handle all permitted additions to UnixTimestamp. + if (right_siunit.exp() == SI_Second.exp()) + { + // Move right argument (day, hour, min, s) to seconds. + double right_converted {}; + right_siunit.convertTo(right, SI_Second, &right_converted); + // Add the seconds to the unix timestamp. + double result = do_op(op, left, right_converted); + if (out_siunit != NULL) *out_siunit = SI_UnixTimestamp; + if (out != NULL) *out = result; + return true; + } + if (right_siunit.exp() == SI_Month.exp()) + { + // Move right argument (day, hour, min, s) to seconds. + if (op == MathOp::SUB) right = -right; + double result = addMonths(left, right); + if (out_siunit != NULL) *out_siunit = SI_UnixTimestamp; + if (out != NULL) *out = result; + return true; + } + } + + // Oups, should not get here.... + return false; } SIUnit SIUnit::mul(const SIUnit &m) const @@ -385,15 +464,6 @@ string strWithUnitLowerCase(double v, Unit u) return r; } -Unit replaceWithConversionUnit(Unit u, vector cs) -{ - for (Unit c : cs) - { - if (canConvert(u, c)) return c; - } - return u; -} - string valueToString(double v, Unit u) { if (isnan(v)) @@ -597,6 +667,11 @@ int8_t SIExp::safe_div2(int8_t a) return d; } +bool SIExp::operator!=(const SIExp &e) const +{ + return ! (*this == e); +} + bool SIExp::operator==(const SIExp &e) const { return @@ -608,7 +683,10 @@ bool SIExp::operator==(const SIExp &e) const cd_ == e.cd_ && k_ == e.k_ && c_ == e.c_ && - f_ == e.f_; + f_ == e.f_ && + month_ == e.month_ && + year_ == e.year_ && + unix_timestamp_ == e.unix_timestamp_; } SIExp SIExp::mul(const SIExp &e) const @@ -623,7 +701,10 @@ SIExp SIExp::mul(const SIExp &e) const .cd(ee.safe_add(cd(),e.cd())) .k(ee.safe_add(k(),e.k())) .c(ee.safe_add(c(),e.c())) - .f(ee.safe_add(f(),e.f())); + .f(ee.safe_add(f(),e.f())) + .month(ee.safe_add(month(),e.month())) + .year(ee.safe_add(year(),e.year())) + .unixTimestamp(ee.safe_add(unixTimestamp(),e.unixTimestamp())); return ee; } @@ -639,7 +720,10 @@ SIExp SIExp::div(const SIExp &e) const .cd(ee.safe_sub(cd(),e.cd())) .k(ee.safe_sub(k(),e.k())) .c(ee.safe_sub(c(),e.c())) - .f(ee.safe_sub(f(),e.f())); + .f(ee.safe_sub(f(),e.f())) + .month(ee.safe_sub(month(),e.month())) + .year(ee.safe_sub(year(),e.year())) + .unixTimestamp(ee.safe_sub(unixTimestamp(),e.unixTimestamp())); return ee; } @@ -656,7 +740,10 @@ SIExp SIExp::sqrt() const .cd(ee.safe_div2(cd())) .k(ee.safe_div2(k())) .c(ee.safe_div2(c())) - .f(ee.safe_div2(f())); + .f(ee.safe_div2(f())) + .month(ee.safe_div2(month())) + .year(ee.safe_div2(year())) + .unixTimestamp(ee.safe_div2(unixTimestamp())); return ee; } @@ -676,6 +763,9 @@ string SIExp::str() const DO_UNIT_SIEXP(f_, f); DO_UNIT_SIEXP(s_, s); DO_UNIT_SIEXP(a_, a); + DO_UNIT_SIEXP(month_, month); + DO_UNIT_SIEXP(year_, year); + DO_UNIT_SIEXP(unix_timestamp_, ut); if (invalid_) r = "!"+r+"-Invalid!"; diff --git a/src/units.h b/src/units.h index 1c8a61b..ae674ba 100644 --- a/src/units.h +++ b/src/units.h @@ -104,9 +104,11 @@ LIST_OF_QUANTITIES X(Day,d,"d",Time,"day") \ X(Month,month,"month",Time,"month") \ X(Year,y,"y",Time,"year") \ - X(DateTimeUT,ut,"ut",PointInTime,"unix timestamp") \ + X(UnixTimestamp,ut,"ut",PointInTime,"unix timestamp") \ X(DateTimeUTC,utc,"utc",PointInTime,"coordinated universal time") \ - X(DateTimeLT,lt,"lt",PointInTime,"local time") \ + X(DateTimeLT,datetime,"datetime",PointInTime,"local time") \ + X(DateLT,date,"date",PointInTime,"local date") \ + X(TimeLT,time,"time",PointInTime,"local time") \ \ X(RH,rh,"RH",RelativeHumidity,"relative humidity") \ X(HCA,hca,"hca",HCA,"heat cost allocation") \ @@ -150,6 +152,9 @@ struct SIExp SIExp &k(int8_t i) { k_ = i; if (k_ != 0 && (c_ != 0 || f_ != 0)) { invalid_ = true; } return *this; } SIExp &c(int8_t i) { c_ = i; if (c_ != 0 && (k_ != 0 || f_ != 0)) { invalid_ = true; } return *this; } SIExp &f(int8_t i) { f_ = i; if (f_ != 0 && (k_ != 0 || c_ != 0)) { invalid_ = true; } return *this; } + SIExp &month(int8_t i) { month_ = i; return *this; } + SIExp &year(int8_t i) { year_ = i; return *this; } + SIExp &unixTimestamp(int8_t i) { unix_timestamp_ = i; return *this; } int8_t s() const { return s_; } int8_t m() const { return m_; } @@ -160,6 +165,10 @@ struct SIExp int8_t k() const { return k_; } int8_t c() const { return c_; } int8_t f() const { return f_; } + int8_t month() const { return month_; } + int8_t year() const { return year_; } + int8_t unixTimestamp() const { return unix_timestamp_; } + SIExp mul(const SIExp &e) const; SIExp div(const SIExp &e) const; SIExp sqrt() const; @@ -167,6 +176,7 @@ struct SIExp int8_t safe_sub(int8_t a, int8_t b); int8_t safe_div2(int8_t a); bool operator==(const SIExp &e) const; + bool operator!=(const SIExp &e) const; std::string str() const; @@ -190,11 +200,21 @@ private: // But kw*h can be translated into kw*s since scaling (*3600) can be done on the // calculated value without knowing the h value. Therefore we have to // treat k, c and f as distinct units. I.e. you cannot add m3*k+m3*f+m3*c. + int8_t month_ {}; // Why month and year here instead of using s? Because they vary in length and thus cannot + int8_t year_ {}; // be auto-converted to seconds, ie you cannot add/subtract months/years without knowing what you add to. + int8_t unix_timestamp_ {}; // Why not s? Because point in time is referenced agains an offset. UT is 1970-01-01 01:00. + + // If exponents have over/underflowed or if multiple of (k,c,f) (month,year,unix_timestamp) are set, + // then the SIExp is not valid any more! - // If exponents have over/underflowed or if multiple of k,c,f are set, then the SIExp is not valid any more! bool invalid_ = false; }; +enum class MathOp +{ + ADD, SUB +}; + struct SIUnit { // Transform a double,double,uint64_t into an SIUnit. @@ -220,11 +240,12 @@ struct SIUnit // Return a detailed string like: kwh[3.6⁶s⁻²m²kg]Energy std::string info() const; // Check if the exponents (ie units) are the same. - bool sameExponents(SIUnit &to) const { return exponents_ == to.exponents_; } - // Check if this unit can be converted to the other unit. - bool canConvertTo(const SIUnit &to) const; - // Convert value from this unit to another unit. - double convertTo(double val, const SIUnit &to) const; + bool sameExponents(SIUnit &to_siunit) const { return exponents_ == to_siunit.exponents_; } + // Convert value from this unit to another unit and store it in out. Return false if conversion is impossible! + bool convertTo(double left, const SIUnit &out_siunit, double *out) const; + // Do a math op. Store the resulting unit and value into the destination pointers. + // Return false if the addion cannot be performed. + bool mathOpTo(MathOp op, double left, double right, const SIUnit &right_siunit, SIUnit *out_siunit, double *out) const; // Multiply this unit with another unit. SIUnit mul(const SIUnit &m) const ; // Dividethis unit with another unit. @@ -257,9 +278,10 @@ std::string unitToStringLowerCase(Unit u); std::string unitToStringUpperCase(Unit u); std::string valueToString(double v, Unit u); -Unit replaceWithConversionUnit(Unit u, std::vector cs); - bool extractUnit(const std::string &s, std::string *vname, Unit *u); +#define X(cname,lcname,hrname,quantity,explanation) extern const SIUnit SI_##cname; +LIST_OF_UNITS +#undef X #endif diff --git a/src/util.cc b/src/util.cc index 3b8bf5c..049fb39 100644 --- a/src/util.cc +++ b/src/util.cc @@ -1405,6 +1405,15 @@ int get_days_in_month(int year, int month) return days; } +double addMonths(double t, int months) +{ + time_t ut = (time_t)t; + struct tm time; + localtime_r(&ut, &time); + addMonths(&time, months); + return (double)mktime(&time); +} + void addMonths(struct tm *date, int months) { bool is_last_day_in_month = date->tm_mday == get_days_in_month(date->tm_year, date->tm_mon); diff --git a/src/util.h b/src/util.h index cf70f10..ea7a8ec 100644 --- a/src/util.h +++ b/src/util.h @@ -80,6 +80,7 @@ std::string strdatetime(struct tm *date); // Return for example: 2010-03-21 15:22:03 std::string strdatetimesec(struct tm *date); void addMonths(struct tm* date, int m); +double addMonths(double t, int m); bool stringFoundCaseIgnored(const std::string& haystack, const std::string& needle);