// delete a range, extend an adjacent (prior if available, otherwise next) // range to cover the same time period, then return the number of the new range // covering the date range of the deleted range int Zones::deleteRange(int rnum) { assert((ranges.size() > 1) && (rnum >= 0) && (rnum < ranges.size())); int return_rnum; // extend an adjacent range if (rnum == 0) { return_rnum = 0; setStartDate(rnum + 1, getStartDate(rnum)); } else { return_rnum = rnum - 1; setEndDate(return_rnum, getEndDate(rnum)); } // eliminate the allocation in the present range delete ranges[rnum]; // drop higher ranges down one slot for (int r = rnum; r < ranges.size() - 1; r ++) ranges[r] = ranges[r + 1]; // reduce the number of ranges by one ranges.removeLast(); return return_rnum; }
// insert a new range starting at the given date extending to the end of the zone currently // containing that date. If the start date of that zone is prior to the specified start // date, then that zone range is shorted. int HrZones::insertRangeAtDate(QDate date, int lt) { assert(date.isValid()); int rnum; if (ranges.empty()) { addHrZoneRange(); rnum = 0; } else { rnum = whichRange(date); assert(rnum >= 0); QDate date1 = getStartDate(rnum); // if the old range has dates before the specified, then truncate // the old range and shift up the existing ranges if (date > date1) { QDate endDate = getEndDate(rnum); setEndDate(rnum, date); ranges.insert(++ rnum, HrZoneRange(date, endDate)); } } if (lt > 0) { setLT(rnum, lt); setHrZonesFromLT(rnum); } return rnum; }
/* * Copyright (c) 2010 Damien Grauser ([email protected]) * * 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 <QMessageBox> #include "HrZones.h" #include "Colors.h" #include "TimeUtils.h" #include <QtGui> #include <QtAlgorithms> #include <qcolor.h> #include <assert.h> #include <cmath> // the infinity endpoints are indicated with extreme date ranges // but not zero dates so we can edit and compare them static const QDate date_zero(1900, 01, 01); static const QDate date_infinity(9999,12,31); // initialize default static zone parameters void HrZones::initializeZoneParameters() { static int initial_zone_default[] = { 0, 68, 83, 94, 105 }; static double initial_zone_default_trimp[] = { 0.9, 1.1, 1.2, 2.0, 5.0 }; static const QString initial_zone_default_desc[] = { tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"), tr("VO2Max") }; static const char *initial_zone_default_name[] = { "Z1", "Z2", "Z3", "Z4", "Z5" }; static int initial_nzones_default = sizeof(initial_zone_default) / sizeof(initial_zone_default[0]); if (run) { fileName_ = "run-hr.zones"; } else { fileName_ = "hr.zones"; } scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_desc.clear(); scheme.zone_default_name.clear(); scheme.zone_default_trimp.clear(); scheme.nzones_default = 0; scheme.nzones_default = initial_nzones_default; for (int z = 0; z < scheme.nzones_default; z ++) { scheme.zone_default.append(initial_zone_default[z]); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(QString(initial_zone_default_name[z])); scheme.zone_default_desc.append(QString(initial_zone_default_desc[z])); scheme.zone_default_trimp.append(initial_zone_default_trimp[z]); } } // read zone file, allowing for zones with or without end dates bool HrZones::read(QFile &file) { // // GET SET // defaults_from_user = false; scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_name.clear(); scheme.zone_default_desc.clear(); scheme.zone_default_trimp.clear(); scheme.nzones_default = 0; ranges.clear(); // set up possible warning dialog warning = QString(); int warning_lines = 0; const int max_warning_lines = 100; // macro to append lines to the warning #define append_to_warning(s) \ if (warning_lines < max_warning_lines) \ warning.append(s); \ else if (warning_lines == max_warning_lines) \ warning.append("...\n"); \ warning_lines ++; // read using text mode takes care of end-lines if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err = tr("can't open %1 file").arg(fileName_); return false; } QTextStream fileStream(&file); QRegExp commentrx("\\s*#.*$"); QRegExp blankrx("^[ \t]*$"); QRegExp rangerx[] = { QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s*([,:]?\\s*(LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)} "\\s*([,:]?\\s*(RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)} "\\s*([,:]?\\s*(MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive), QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s+(?:until|to|-)\\s+" // until "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?" // end date "\\s*:?,?\\s*((LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)} "\\s*:?,?\\s*((RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)} "\\s*:?,?\\s*((MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive) }; QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*" "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+(\\.\\d+)?)\\s*)?$", Qt::CaseInsensitive);// QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$", Qt::CaseInsensitive); int lineno = 0; // the current range in the file // ZoneRange *range = NULL; bool in_range = false; QDate begin = date_zero, end = date_infinity; int lt = 0; int restHr = 0; int maxHr = 0; QList<HrZoneInfo> zoneInfos; // true if zone defaults are found in the file (then we need to write them) bool zones_are_defaults = false; // // READ IN hr.zones FILE // // loop through line by line while (!fileStream.atEnd()) { // starting from line 1 ++lineno; // get a line in QString line = fileStream.readLine(); int pos = commentrx.indexIn(line, 0); // strip comments if (pos != -1) line = line.left(pos); // its a blank line (we check after comments stripped) if (blankrx.indexIn(line, 0) == 0) goto next_line; // who wrote this? bleck. // check for default zone range definition (may be followed by hr zone definitions) if (zonedefaultsx.indexIn(line, 0) != -1) { zones_are_defaults = true; // defaults are allowed only at the beginning of the file if (ranges.size()) { err = tr("HR Zone defaults must be specified at head of %1 file").arg(fileName_); return false; } // only one set of defaults is allowed if (scheme.nzones_default) { err = tr("Only one set of zone defaults may be specified in %1 file").arg(fileName_); return false; } // ok move on to get defaults setup goto next_line; } // check for range specification (may be followed by zone definitions) for (int r=0; r<2; r++) { if (rangerx[r].indexIn(line, 0) != -1) { if (in_range) { // if zones are empty, then generate them HrZoneRange range(begin, end, lt, restHr, maxHr); range.zones = zoneInfos; if (range.zones.empty()) { if (range.lt > 0) setHrZonesFromLT(range); else { err = tr("line %1: read new range without reading " "any zones for previous one").arg(lineno); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } in_range = true; zones_are_defaults = false; zoneInfos.clear(); // process the beginning date if (rangerx[r].cap(1) == "BEGIN") begin = date_zero; else { begin = QDate(rangerx[r].cap(2).toInt(), rangerx[r].cap(3).toInt(), rangerx[r].cap(4).toInt()); } // process an end date, if any, else it is null if (rangerx[r].cap(5) == "END") end = date_infinity; else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) { end = QDate(rangerx[r].cap(6).toInt(), rangerx[r].cap(7).toInt(), rangerx[r].cap(8).toInt()); } else { end = QDate(); } // set up the range, capturing LT if it's specified // range = new ZoneRange(begin, end); int nLT = (r ? 11 : 7); if (rangerx[r].captureCount() >= (nLT)) lt = rangerx[r].cap(nLT).toInt(); else lt = 0; int nRestHr = (r ? 14 : 10); if (rangerx[r].captureCount() >= (nRestHr)) restHr = rangerx[r].cap(nRestHr).toInt(); else restHr = 0; int nMaxHr = (r ? 17 : 13); if (rangerx[r].captureCount() >= (nRestHr)) maxHr = rangerx[r].cap(nMaxHr).toInt(); else maxHr = 0; // bleck goto next_line; } } // check for zone definition if (zonerx.indexIn(line, 0) != -1) { if (! (in_range || zones_are_defaults)) { err = tr("line %1: read zone without preceding date range").arg(lineno); file.close(); return false; } int lo = zonerx.cap(3).toInt(); double trimp = zonerx.cap(5).toDouble(); // allow for zone specified as % of LT bool lo_is_pct = false; if (zonerx.cap(4) == "%") { if (zones_are_defaults) lo_is_pct = true; else if (lt > 0) lo = int(lo * lt / 100); else { err = tr("attempt to set zone based on % of LT without setting LT in line number %1.\n"). arg(lineno); file.close(); return false; } } int hi = -1; // signal an undefined number double tr = zonerx.cap(5).toDouble(); if (zones_are_defaults) { scheme.nzones_default ++; scheme.zone_default_is_pct.append(lo_is_pct); scheme.zone_default.append(lo); scheme.zone_default_name.append(zonerx.cap(1)); scheme.zone_default_desc.append(zonerx.cap(2)); scheme.zone_default_trimp.append(trimp); defaults_from_user = true; } else { HrZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi, tr); zoneInfos.append(zone); } } next_line: ; } // did we drop out mid way through ? if (in_range) { HrZoneRange range(begin, end, lt, restHr, maxHr); range.zones = zoneInfos; if (range.zones.empty()) { if (range.lt > 0) setHrZonesFromLT(range); else { err = tr("file ended without reading any zones for last range"); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } // reading done file.close(); // sort the ranges qSort(ranges); // // POST-PROCESS / FIX-UP ZONES // // set the default zones if not in file if (!scheme.nzones_default) { // do we have a zone which is explicitly set? for (int i=0; i<ranges.count(); i++) { if (ranges[i].hrZonesSetFromLT == false) { // set the defaults using this one! scheme.nzones_default = ranges[i].zones.count(); for (int j=0; j<scheme.nzones_default; j++) { scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].lt) * 100.00); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(ranges[i].zones[j].name); scheme.zone_default_desc.append(ranges[i].zones[j].desc); scheme.zone_default_trimp.append(ranges[i].zones[j].trimp); } } } // still not set then reset to defaults as usual if (!scheme.nzones_default) initializeZoneParameters(); } // resolve undefined endpoints in ranges and zones for (int nr = 0; nr < ranges.size(); nr ++) { // clean up gaps or overlaps in zone ranges if (ranges[nr].end.isNull()) { ranges[nr].end = (nr < ranges.size() - 1) ? ranges[nr + 1].begin : date_infinity; } else if ((nr < ranges.size() - 1) && (ranges[nr + 1].begin != ranges[nr].end)) { append_to_warning(tr("Setting end date of range %1 to start date of range %2.\n").arg(nr + 1).arg(nr + 2)); ranges[nr].end = ranges[nr + 1].begin; } else if ((nr == ranges.size() - 1) && (ranges[nr].end < QDate::currentDate())) { append_to_warning(tr("Extending final range %1 to infinite to include present date.\n").arg(nr + 1)); ranges[nr].end = date_infinity; } if (ranges[nr].lt <= 0) { err = tr("LT must be greater than zero in zone range %1 of %2").arg(nr + 1).arg(fileName_); return false; } if (ranges[nr].zones.size()) { // check that the first zone starts with zero // ranges[nr].zones[0].lo = 0; // there is no reason we should enforce this, so removing it. // resolve zone end powers for (int nz = 0; nz < ranges[nr].zones.size(); nz ++) { if (ranges[nr].zones[nz].hi == -1) ranges[nr].zones[nz].hi = (nz < ranges[nr].zones.size() - 1) ? ranges[nr].zones[nz + 1].lo : INT_MAX; else if ((nz < ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) { if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) { append_to_warning(tr("Range %1: matching top of zone %2 (%3) to bottom of zone %4 (%5).\n") .arg(nr+1).arg(ranges[nr].zones[nz].name).arg(ranges[nr].zones[nz].hi) .arg(ranges[nr].zones[nz + 1].name) .arg(ranges[nr].zones[nz + 1].lo)); } ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo; } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) { append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n") .arg(nr + 1).arg(ranges[nr].zones[nz].name).arg(ranges[nr].zones[nz].hi)); ranges[nr].zones[nz].hi = INT_MAX; } } } } // mark zones as modified so pages which depend on zones can be updated modificationTime = QDateTime::currentDateTime(); return true; } // note empty dates are treated as automatic matches for begin or // end of range int HrZones::whichRange(const QDate &date) const { for (int rnum = 0; rnum < ranges.size(); ++rnum) { const HrZoneRange &range = ranges[rnum]; if (((date >= range.begin) || (range.begin.isNull())) && ((date < range.end) || (range.end.isNull()))) return rnum; } return -1; } int HrZones::numZones(int rnum) const { if (rnum < 0 || rnum >= ranges.size()) return 0; return ranges[rnum].zones.size(); } int HrZones::whichZone(int rnum, double value) const { if (rnum < 0 || rnum > ranges.size()) return 0; const HrZoneRange &range = ranges[rnum]; for (int j = 0; j < range.zones.size(); ++j) { const HrZoneInfo &info = range.zones[j]; // note: the "end" of range is actually in the next zone if ((value >= info.lo) && (value < info.hi)) return j; } // if we got here either it is negative, nan, inf or way high return -1; } void HrZones::zoneInfo(int rnum, int znum, QString &name, QString &description, int &low, int &high, double &trimp) const { assert(rnum < ranges.size()); const HrZoneRange &range = ranges[rnum]; assert(znum < range.zones.size()); const HrZoneInfo &zone = range.zones[znum]; name = zone.name; description = zone.desc; low = zone.lo; high = zone.hi; trimp= zone.trimp; } int HrZones::getLT(int rnum) const { if (rnum < 0 || rnum > ranges.size()) return 0; return ranges[rnum].lt; } void HrZones::setLT(int rnum, int lt) { ranges[rnum].lt = lt; modificationTime = QDateTime::currentDateTime(); } // generate a list of zones from LT int HrZones::lowsFromLT(QList <int> *lows, int lt) const { lows->clear(); for (int z = 0; z < scheme.nzones_default; z++) lows->append(scheme.zone_default_is_pct[z] ? scheme.zone_default[z] * lt / 100 : scheme.zone_default[z]); return scheme.nzones_default; } int HrZones::getRestHr(int rnum) const { if (rnum < 0 || rnum > ranges.size()) return 0; return ranges[rnum].restHr; } void HrZones::setRestHr(int rnum, int restHr) { ranges[rnum].restHr = restHr; modificationTime = QDateTime::currentDateTime(); } int HrZones::getMaxHr(int rnum) const { if (rnum < 0 || rnum > ranges.size()) return 0; return ranges[rnum].maxHr; } void HrZones::setMaxHr(int rnum, int maxHr) { ranges[rnum].maxHr = maxHr; modificationTime = QDateTime::currentDateTime(); } // access the zone name QString HrZones::getDefaultZoneName(int z) const { return scheme.zone_default_name[z]; } // access the zone description QString HrZones::getDefaultZoneDesc(int z) const { return scheme.zone_default_desc[z]; } // set the zones from the LT value (the cp variable) void HrZones::setHrZonesFromLT(HrZoneRange &range) { range.zones.clear(); if (scheme.nzones_default == 0) initializeZoneParameters(); for (int i = 0; i < scheme.nzones_default; i++) { int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.lt / 100 : scheme.zone_default[i]; int hi = lo; double trimp = scheme.zone_default_trimp[i]; HrZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi, trimp); range.zones.append(zone); } // sort the zones (some may be pct, others absolute, so zones need to be sorted, // rather than the defaults qSort(range.zones); // set zone end dates for (int i = 0; i < range.zones.size(); i ++) range.zones[i].hi = (i < scheme.nzones_default - 1) ? range.zones[i + 1].lo : INT_MAX; // mark that the zones were set from LT, so if zones are subsequently // written, only LT is saved range.hrZonesSetFromLT = true; } void HrZones::setHrZonesFromLT(int rnum) { assert((rnum >= 0) && (rnum < ranges.size())); setHrZonesFromLT(ranges[rnum]); } // return the list of starting values of zones for a given range QList <int> HrZones::getZoneLows(int rnum) const { if (rnum >= ranges.size()) return QList <int>(); const HrZoneRange &range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].lo); return return_values; } // return the list of ending values of zones for a given range QList <int> HrZones::getZoneHighs(int rnum) const { if (rnum >= ranges.size()) return QList <int>(); const HrZoneRange &range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].hi); return return_values; } // return the list of zone names QList <QString> HrZones::getZoneNames(int rnum) const { if (rnum >= ranges.size()) return QList <QString>(); const HrZoneRange &range = ranges[rnum]; QList <QString> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].name); return return_values; } // return the list of zone trimp coef QList <double> HrZones::getZoneTrimps(int rnum) const { if (rnum >= ranges.size()) return QList <double>(); const HrZoneRange &range = ranges[rnum]; QList <double> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].trimp); return return_values; } QString HrZones::summarize(int rnum, QVector<double> &time_in_zone, QColor color) const { assert(rnum < ranges.size()); const HrZoneRange &range = ranges[rnum]; if (time_in_zone.size() < range.zones.size()) return ""; QString summary; if(range.lt > 0){ summary += "<table align=\"center\" width=\"70%\" border=\"0\">"; summary += "<tr><td align=\"center\">"; summary += tr("Threshold (bpm): %1").arg(range.lt); summary += "</td></tr></table>"; } summary += "<table align=\"center\" width=\"70%\" "; summary += "border=\"0\">"; summary += "<tr>"; summary += tr("<td align=\"center\">Zone</td>"); summary += tr("<td align=\"center\">Description</td>"); summary += tr("<td align=\"center\">Low (bpm)</td>"); summary += tr("<td align=\"center\">High (bpm)</td>"); summary += tr("<td align=\"center\">Time</td>"); summary += tr("<td align=\"center\">%</td>"); summary += "</tr>"; double duration = 0; foreach(double v, time_in_zone) { duration += v; } for (int zone = 0; zone < time_in_zone.size(); ++zone) { if (time_in_zone[zone] > 0.0) { QString name, desc; int lo, hi; double trimp; zoneInfo(rnum, zone, name, desc, lo, hi, trimp); if (zone % 2 == 0) summary += "<tr bgcolor='" + color.name() + "'>"; else summary += "<tr>"; summary += QString("<td align=\"center\">%1</td>").arg(name); summary += QString("<td align=\"center\">%1</td>").arg(desc); summary += QString("<td align=\"center\">%1</td>").arg(lo); if (hi == INT_MAX) summary += "<td align=\"center\">MAX</td>"; else summary += QString("<td align=\"center\">%1</td>").arg(hi); summary += QString("<td align=\"center\">%1</td>") .arg(time_to_string((unsigned) round(time_in_zone[zone]))); summary += QString("<td align=\"center\">%1</td>") .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0); summary += "</tr>"; } } summary += "</table>"; return summary; } #define USE_SHORT_POWER_ZONES_FORMAT true /* whether a less redundent format should be used */ void HrZones::write(QDir home) { QString strzones; // always write the defaults (config pane can adjust) strzones += QString("DEFAULTS:\n"); for (int z = 0 ; z < scheme.nzones_default; z ++) strzones += QString("%1,%2,%3%4,%5\n"). arg(scheme.zone_default_name[z]). arg(scheme.zone_default_desc[z]). arg(scheme.zone_default[z]). arg(scheme.zone_default_is_pct[z]?"%":""). arg(scheme.zone_default_trimp[z]); strzones += QString("\n"); for (int i = 0; i < ranges.size(); i++) { int lt = getLT(i); int restHr = getRestHr(i); int maxHr = getMaxHr(i); // print header for range // note this explicitly sets the first and last ranges such that all time is spanned // note: BEGIN is not needed anymore // since it becomes Jan 01 1900 strzones += QString("%1: LT=%2, RestHr=%3, MaxHr=%4").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(lt).arg(restHr).arg(maxHr); strzones += QString("\n"); // step through and print the zones if they've been explicitly set if (! ranges[i].hrZonesSetFromLT) { for (int j = 0; j < ranges[i].zones.size(); j ++) { const HrZoneInfo &zi = ranges[i].zones[j]; strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.trimp); } strzones += QString("\n"); } } QFile file(home.canonicalPath() + "/" + fileName_); if (file.open(QFile::WriteOnly)) { QTextStream stream(&file); stream << strzones; file.close(); } else { QMessageBox msgBox; msgBox.setIcon(QMessageBox::Critical); msgBox.setText(tr("Problem Saving Heartrate Zones")); msgBox.setInformativeText(tr("File: %1 cannot be opened for 'Writing'. Please check file properties.").arg(home.canonicalPath() + "/" + fileName_)); msgBox.exec(); return; } }
unsigned int WriMoImp::getRunningTotal(const Date& date) const { unsigned int runningTotal = 0; for (Date currentDate = getStartDate(); currentDate <= date; currentDate = currentDate.next()) { runningTotal += getWordCount(currentDate); } return runningTotal; }
void TestRecurrentTransaction::testGetStartDate() { QFETCH(QDate, startDate); auto amount = .45; auto account = std::make_shared<PublicAccount>("Test account", .0, ""); auto category = std::make_shared<com::chancho::Category>("Sushi", com::chancho::Category::Type::EXPENSE); auto transactionPtr = std::make_shared<com::chancho::Transaction>(account, amount, category); auto recurrentPtr = std::make_shared<com::chancho::RecurrentTransaction>(transactionPtr, std::make_shared<com::chancho::RecurrentTransaction::Recurrence>( com::chancho::RecurrentTransaction::Recurrence::Defaults::DAILY, startDate)); auto qmlTransaction = std::make_shared<com::chancho::tests::PublicRecurrentTransaction>(recurrentPtr); QCOMPARE(qmlTransaction->getStartDate(), startDate); }
/* * Copyright (c) 2010 Damien Grauser ([email protected]) * * 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 <QMessageBox> #include "HrZones.h" #include "Colors.h" #include "TimeUtils.h" #include <QtGui> #include <QtAlgorithms> #include <qcolor.h> #include <assert.h> #include <math.h> #include <boost/crc.hpp> // the infinity endpoints are indicated with extreme date ranges // but not zero dates so we can edit and compare them static const QDate date_zero(1900, 01, 01); static const QDate date_infinity(9999,12,31); // initialize default static zone parameters void HrZones::initializeZoneParameters() { static int initial_zone_default[] = { 0, 68, 83, 94, 105 }; static double initial_zone_default_trimp[] = { 0.9, 1.1, 1.2, 2.0, 5.0 }; static const QString initial_zone_default_desc[] = { tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"), tr("VO2Max") }; static const char *initial_zone_default_name[] = { "Z1", "Z2", "Z3", "Z4", "Z5" }; static int initial_nzones_default = sizeof(initial_zone_default) / sizeof(initial_zone_default[0]); scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_desc.clear(); scheme.zone_default_name.clear(); scheme.zone_default_trimp.clear(); scheme.nzones_default = 0; scheme.nzones_default = initial_nzones_default; for (int z = 0; z < scheme.nzones_default; z ++) { scheme.zone_default.append(initial_zone_default[z]); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(QString(initial_zone_default_name[z])); scheme.zone_default_desc.append(QString(initial_zone_default_desc[z])); scheme.zone_default_trimp.append(initial_zone_default_trimp[z]); } } // read zone file, allowing for zones with or without end dates bool HrZones::read(QFile &file) { defaults_from_user = false; scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_name.clear(); scheme.zone_default_desc.clear(); scheme.zone_default_trimp.clear(); scheme.nzones_default = 0; ranges.clear(); // set up possible warning dialog warning = QString(); int warning_lines = 0; const int max_warning_lines = 100; // macro to append lines to the warning #define append_to_warning(s) \ if (warning_lines < max_warning_lines) \ warning.append(s); \ else if (warning_lines == max_warning_lines) \ warning.append("...\n"); \ warning_lines ++; // read using text mode takes care of end-lines if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err = "can't open file"; return false; } QTextStream fileStream(&file); QRegExp commentrx("\\s*#.*$"); QRegExp blankrx("^[ \t]*$"); QRegExp rangerx[] = { QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s*([,:]?\\s*(LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)} "\\s*([,:]?\\s*(RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)} "\\s*([,:]?\\s*(MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive), QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s+(?:until|to|-)\\s+" // until "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?" // end date "\\s*:?,?\\s*((LT)\\s*=\\s*(\\d+))?" // optional {LT = integer (optional %)} "\\s*:?,?\\s*((RestHr)\\s*=\\s*(\\d+))?" // optional {RestHr = integer (optional %)} "\\s*:?,?\\s*((MaxHr)\\s*=\\s*(\\d+))?" // optional {MaxHr = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive) }; QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*" "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+(\\.\\d+)?)\\s*)?$", Qt::CaseInsensitive);// QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$", Qt::CaseInsensitive); int lineno = 0; // the current range in the file // ZoneRange *range = NULL; bool in_range = false; QDate begin = date_zero, end = date_infinity; int lt = 0; int restHr = 0; int maxHr = 0; QList<HrZoneInfo> zoneInfos; // true if zone defaults are found in the file (then we need to write them) bool zones_are_defaults = false; while (! fileStream.atEnd() ) { ++lineno; QString line = fileStream.readLine(); int pos = commentrx.indexIn(line, 0); if (pos != -1) line = line.left(pos); if (blankrx.indexIn(line, 0) == 0) goto next_line; // check for default zone range definition (may be followed by hr zone definitions) if (zonedefaultsx.indexIn(line, 0) != -1) { zones_are_defaults = true; // defaults are allowed only at the beginning of the file if (ranges.size()) { err = "HR Zone defaults must be specified at head of hr.zones file"; return false; } // only one set of defaults is allowed if (scheme.nzones_default) { err = "Only one set of zone defaults may be specified in hr.zones file"; return false; } goto next_line; } // check for range specification (may be followed by zone definitions) for (int r=0; r<2; r++) { if (rangerx[r].indexIn(line, 0) != -1) { if (in_range) { // if zones are empty, then generate them HrZoneRange range(begin, end, lt, restHr, maxHr); range.zones = zoneInfos; if (range.zones.empty()) { if (range.lt > 0) setHrZonesFromLT(range); else { err = tr("line %1: read new range without reading " "any zones for previous one").arg(lineno); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } in_range = true; zones_are_defaults = false; zoneInfos.clear(); // process the beginning date if (rangerx[r].cap(1) == "BEGIN") begin = date_zero; else { begin = QDate(rangerx[r].cap(2).toInt(), rangerx[r].cap(3).toInt(), rangerx[r].cap(4).toInt()); } // process an end date, if any, else it is null if (rangerx[r].cap(5) == "END") end = date_infinity; else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) { end = QDate(rangerx[r].cap(6).toInt(), rangerx[r].cap(7).toInt(), rangerx[r].cap(8).toInt()); } else { end = QDate(); } // set up the range, capturing LT if it's specified // range = new ZoneRange(begin, end); int nLT = (r ? 11 : 7); if (rangerx[r].numCaptures() >= (nLT)) lt = rangerx[r].cap(nLT).toInt(); else lt = 0; int nRestHr = (r ? 14 : 10); if (rangerx[r].numCaptures() >= (nRestHr)) restHr = rangerx[r].cap(nRestHr).toInt(); else restHr = 0; int nMaxHr = (r ? 17 : 13); if (rangerx[r].numCaptures() >= (nRestHr)) maxHr = rangerx[r].cap(nMaxHr).toInt(); else maxHr = 0; // bleck goto next_line; } } // check for zone definition if (zonerx.indexIn(line, 0) != -1) { if (! (in_range || zones_are_defaults)) { err = tr("line %1: read zone without " "preceeding date range").arg(lineno); file.close(); return false; } int lo = zonerx.cap(3).toInt(); double trimp = zonerx.cap(5).toDouble(); // allow for zone specified as % of LT bool lo_is_pct = false; if (zonerx.cap(4) == "%") { if (zones_are_defaults) lo_is_pct = true; else if (lt > 0) lo = int(lo * lt / 100); else { err = tr("attempt to set zone based on % of " "LT without setting LT in line number %1.\n"). arg(lineno); file.close(); return false; } } int hi = -1; // signal an undefined number double tr = zonerx.cap(5).toDouble(); if (zones_are_defaults) { scheme.nzones_default ++; scheme.zone_default_is_pct.append(lo_is_pct); scheme.zone_default.append(lo); scheme.zone_default_name.append(zonerx.cap(1)); scheme.zone_default_desc.append(zonerx.cap(2)); scheme.zone_default_trimp.append(trimp); defaults_from_user = true; } else { HrZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi, tr); zoneInfos.append(zone); } } next_line: {} } if (in_range) { HrZoneRange range(begin, end, lt, restHr, maxHr); range.zones = zoneInfos; if (range.zones.empty()) { if (range.lt > 0) setHrZonesFromLT(range); else { err = tr("file ended without reading any zones for last range"); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } file.close(); // sort the ranges qSort(ranges); // set the default zones if not in file if (!scheme.nzones_default) { // do we have a zone which is explicitly set? for (int i=0; i<ranges.count(); i++) { if (ranges[i].hrZonesSetFromLT == false) { // set the defaults using this one! scheme.nzones_default = ranges[i].zones.count(); for (int j=0; j<scheme.nzones_default; j++) { scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].lt) * 100.00); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(ranges[i].zones[j].name); scheme.zone_default_desc.append(ranges[i].zones[j].desc); scheme.zone_default_trimp.append(ranges[i].zones[j].trimp); } } } // still not set then reset to defaults as usual if (!scheme.nzones_default) initializeZoneParameters(); } // resolve undefined endpoints in ranges and zones for (int nr = 0; nr < ranges.size(); nr ++) { // clean up gaps or overlaps in zone ranges if (ranges[nr].end.isNull()) ranges[nr].end = (nr < ranges.size() - 1) ? ranges[nr + 1].begin : date_infinity; else if ((nr < ranges.size() - 1) && (ranges[nr + 1].begin != ranges[nr].end)) { append_to_warning(tr("Setting end date of range %1 " "to start date of range %2.\n"). arg(nr + 1). arg(nr + 2) ); ranges[nr].end = ranges[nr + 1].begin; } else if ((nr == ranges.size() - 1) && (ranges[nr].end < QDate::currentDate())) { append_to_warning(tr("Extending final range %1 to infinite " "to include present date.\n").arg(nr + 1)); ranges[nr].end = date_infinity; } if (ranges[nr].lt <= 0) { err = tr("LT must be greater than zero in zone " "range %1 of hr.zones").arg(nr + 1); return false; } if (ranges[nr].zones.size()) { // check that the first zone starts with zero ranges[nr].zones[0].lo = 0; // resolve zone end powers for (int nz = 0; nz < ranges[nr].zones.size(); nz ++) { if (ranges[nr].zones[nz].hi == -1) ranges[nr].zones[nz].hi = (nz < ranges[nr].zones.size() - 1) ? ranges[nr].zones[nz + 1].lo : INT_MAX; else if ((nz < ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) { if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) { append_to_warning(tr("Range %1: matching top of zone %2 " "(%3) to bottom of zone %4 (%5).\n"). arg(nr + 1). arg(ranges[nr].zones[nz].name). arg(ranges[nr].zones[nz].hi). arg(ranges[nr].zones[nz + 1].name). arg(ranges[nr].zones[nz + 1].lo) ); } ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo; } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) { append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n"). arg(nr + 1). arg(ranges[nr].zones[nz].name). arg(ranges[nr].zones[nz].hi) ); ranges[nr].zones[nz].hi = INT_MAX; } } } } // mark zones as modified so pages which depend on zones can be updated modificationTime = QDateTime::currentDateTime(); return true; } // note empty dates are treated as automatic matches for begin or // end of range int HrZones::whichRange(const QDate &date) const { for (int rnum = 0; rnum < ranges.size(); ++rnum) { const HrZoneRange &range = ranges[rnum]; if (((date >= range.begin) || (range.begin.isNull())) && ((date < range.end) || (range.end.isNull()))) return rnum; } return -1; } int HrZones::numZones(int rnum) const { if (rnum < 0 || rnum >= ranges.size()) return 0; return ranges[rnum].zones.size(); } int HrZones::whichZone(int rnum, double value) const { if (rnum < 0 || rnum > ranges.size()) return 0; const HrZoneRange &range = ranges[rnum]; for (int j = 0; j < range.zones.size(); ++j) { const HrZoneInfo &info = range.zones[j]; // note: the "end" of range is actually in the next zone if ((value >= info.lo) && (value < info.hi)) return j; } // if we got here either it is negative, nan, inf or way high if (value < 0 || isnan(value)) return 0; else return range.zones.size()-1; } void HrZones::zoneInfo(int rnum, int znum, QString &name, QString &description, int &low, int &high, double &trimp) const { assert(rnum < ranges.size()); const HrZoneRange &range = ranges[rnum]; assert(znum < range.zones.size()); const HrZoneInfo &zone = range.zones[znum]; name = zone.name; description = zone.desc; low = zone.lo; high = zone.hi; trimp= zone.trimp; } int HrZones::getLT(int rnum) const { if (rnum < 0 || rnum > ranges.size()) return 0; return ranges[rnum].lt; } void HrZones::setLT(int rnum, int lt) { ranges[rnum].lt = lt; modificationTime = QDateTime::currentDateTime(); } // generate a list of zones from LT int HrZones::lowsFromLT(QList <int> *lows, int lt) const { lows->clear(); for (int z = 0; z < scheme.nzones_default; z++) lows->append(scheme.zone_default_is_pct[z] ? scheme.zone_default[z] * lt / 100 : scheme.zone_default[z]); return scheme.nzones_default; } int HrZones::getRestHr(int rnum) const { if (rnum < 0 || rnum > ranges.size()) return 0; return ranges[rnum].restHr; } void HrZones::setRestHr(int rnum, int restHr) { ranges[rnum].restHr = restHr; modificationTime = QDateTime::currentDateTime(); } int HrZones::getMaxHr(int rnum) const { if (rnum < 0 || rnum > ranges.size()) return 0; return ranges[rnum].maxHr; } void HrZones::setMaxHr(int rnum, int maxHr) { ranges[rnum].maxHr = maxHr; modificationTime = QDateTime::currentDateTime(); } // access the zone name QString HrZones::getDefaultZoneName(int z) const { return scheme.zone_default_name[z]; } // access the zone description QString HrZones::getDefaultZoneDesc(int z) const { return scheme.zone_default_desc[z]; } // set the zones from the LT value (the cp variable) void HrZones::setHrZonesFromLT(HrZoneRange &range) { range.zones.clear(); if (scheme.nzones_default == 0) initializeZoneParameters(); for (int i = 0; i < scheme.nzones_default; i++) { int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.lt / 100 : scheme.zone_default[i]; int hi = lo; double trimp = scheme.zone_default_trimp[i]; HrZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi, trimp); range.zones.append(zone); } // sort the zones (some may be pct, others absolute, so zones need to be sorted, // rather than the defaults qSort(range.zones); // set zone end dates for (int i = 0; i < range.zones.size(); i ++) range.zones[i].hi = (i < scheme.nzones_default - 1) ? range.zones[i + 1].lo : INT_MAX; // mark that the zones were set from LT, so if zones are subsequently // written, only LT is saved range.hrZonesSetFromLT = true; } void HrZones::setHrZonesFromLT(int rnum) { assert((rnum >= 0) && (rnum < ranges.size())); setHrZonesFromLT(ranges[rnum]); } // return the list of starting values of zones for a given range QList <int> HrZones::getZoneLows(int rnum) const { if (rnum >= ranges.size()) return QList <int>(); const HrZoneRange &range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].lo); return return_values; } // return the list of ending values of zones for a given range QList <int> HrZones::getZoneHighs(int rnum) const { if (rnum >= ranges.size()) return QList <int>(); const HrZoneRange &range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].hi); return return_values; } // return the list of zone names QList <QString> HrZones::getZoneNames(int rnum) const { if (rnum >= ranges.size()) return QList <QString>(); const HrZoneRange &range = ranges[rnum]; QList <QString> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].name); return return_values; } // return the list of zone trimp coef QList <double> HrZones::getZoneTrimps(int rnum) const { if (rnum >= ranges.size()) return QList <double>(); const HrZoneRange &range = ranges[rnum]; QList <double> return_values; for (int i = 0; i < range.zones.size(); i ++) return_values.append(ranges[rnum].zones[i].trimp); return return_values; } QString HrZones::summarize(int rnum, QVector<double> &time_in_zone) const { assert(rnum < ranges.size()); const HrZoneRange &range = ranges[rnum]; if (time_in_zone.size() < range.zones.size()) return ""; QString summary; if(range.lt > 0){ summary += "<table align=\"center\" width=\"70%\" border=\"0\">"; summary += "<tr><td align=\"center\">"; summary += tr("Threshold (bpm): %1").arg(range.lt); summary += "</td></tr></table>"; } summary += "<table align=\"center\" width=\"70%\" "; summary += "border=\"0\">"; summary += "<tr>"; summary += tr("<td align=\"center\">Zone</td>"); summary += tr("<td align=\"center\">Description</td>"); summary += tr("<td align=\"center\">Low (bpm)</td>"); summary += tr("<td align=\"center\">High (bpm)</td>"); summary += tr("<td align=\"center\">Time</td>"); summary += tr("<td align=\"center\">%</td>"); summary += "</tr>"; QColor color = QApplication::palette().alternateBase().color(); color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); double duration = 0; foreach(double v, time_in_zone) { duration += v; } for (int zone = 0; zone < time_in_zone.size(); ++zone) { if (time_in_zone[zone] > 0.0) { QString name, desc; int lo, hi; double trimp; zoneInfo(rnum, zone, name, desc, lo, hi, trimp); if (zone % 2 == 0) summary += "<tr bgcolor='" + color.name() + "'>"; else summary += "<tr>"; summary += QString("<td align=\"center\">%1</td>").arg(name); summary += QString("<td align=\"center\">%1</td>").arg(desc); summary += QString("<td align=\"center\">%1</td>").arg(lo); if (hi == INT_MAX) summary += "<td align=\"center\">MAX</td>"; else summary += QString("<td align=\"center\">%1</td>").arg(hi); summary += QString("<td align=\"center\">%1</td>") .arg(time_to_string((unsigned) round(time_in_zone[zone]))); summary += QString("<td align=\"center\">%1</td>") .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0); summary += "</tr>"; } } summary += "</table>"; return summary; } #define USE_SHORT_POWER_ZONES_FORMAT true /* whether a less redundent format should be used */ void HrZones::write(QDir home) { QString strzones; // always write the defaults (config pane can adjust) strzones += QString("DEFAULTS:\n"); for (int z = 0 ; z < scheme.nzones_default; z ++) strzones += QString("%1,%2,%3%4,%5\n"). arg(scheme.zone_default_name[z]). arg(scheme.zone_default_desc[z]). arg(scheme.zone_default[z]). arg(scheme.zone_default_is_pct[z]?"%":""). arg(scheme.zone_default_trimp[z]); strzones += QString("\n"); for (int i = 0; i < ranges.size(); i++) { int lt = getLT(i); int restHr = getRestHr(i); int maxHr = getMaxHr(i); // print header for range // note this explicitly sets the first and last ranges such that all time is spanned // note: BEGIN is not needed anymore // since it becomes Jan 01 1900 strzones += QString("%1: LT=%2, RestHr=%3, MaxHr=%4").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(lt).arg(restHr).arg(maxHr); strzones += QString("\n"); // step through and print the zones if they've been explicitly set if (! ranges[i].hrZonesSetFromLT) { for (int j = 0; j < ranges[i].zones.size(); j ++) { const HrZoneInfo &zi = ranges[i].zones[j]; strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.trimp); } strzones += QString("\n"); } } QFile file(home.absolutePath() + "/hr.zones"); if (file.open(QFile::WriteOnly)) { QTextStream stream(&file); stream << strzones; file.close(); } }
// insert a new range starting at the given date extending to the end of the zone currently // containing that date. If the start date of that zone is prior to the specified start // date, then that zone range is shorted. int Zones::insertRangeAtDate(QDate date, int cp) { assert(date.isValid()); int rnum; if (ranges.empty()) { addZoneRange(cp); fprintf( stderr, "Generating first range with CP = %d\n", cp ); rnum = 0; } else { rnum = whichRange(date); assert(rnum >= 0); QDate date1 = getStartDate(rnum); fprintf(stderr, "insertRangeAtDate(%s, %d):\n", date.toString().toAscii().constData(), cp); // if the old range has dates before the specified, then truncate the old range // and shift up the existing ranges if (date > date1) { QDate endDate = getEndDate(rnum); setEndDate(rnum, date); fprintf( stderr, "Inserting range\n" "old range %d: from %s to %s\n" "new range %d: from %s to %s\n" "added range %d: from %s to %s\n", rnum + 1, getStartDateString(rnum).toAscii().constData(), getEndDateString(rnum).toAscii().constData(), rnum + 1, getStartDateString(rnum).toAscii().constData(), (date.isNull() ? "END" : date.toString().toAscii().constData()), rnum + 2, (date.isNull() ? "BEGIN" : date.toString().toAscii().constData()), getEndDateString(rnum).toAscii().constData() ); ranges.insert(++ rnum, new ZoneRange(date, endDate)); } } if (cp > 0) { setCP(rnum, cp); setZonesFromCP(rnum); } return rnum; }
float WriMoImp::getRunningAverageWordCountForDate( const Date& date) const { if (!m_calendar.dateInRange(date)) { throw DateOutOfRangeError(); } unsigned int sum = 0; unsigned int numDays = 0; Date currentDate = getStartDate(); do { sum += getWordCount(currentDate); numDays++; currentDate = currentDate.addDays(1); } while (currentDate <= date); return (float)sum / numDays; }
/* * Copyright (c) 2006 Sean C. Rhea ([email protected]) * Copyright (c) 2014 Mark Liversedge ([email protected]) * * [mostly cut and paste from Zones.cpp] * * 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 <QMessageBox> #include "PaceZones.h" #include "Colors.h" #include "Settings.h" #include "TimeUtils.h" #include "Units.h" #include <QtGui> #include <QtAlgorithms> #include <qcolor.h> #include <assert.h> #include <math.h> // the infinity endpoints are indicated with extreme date ranges // but not zero dates so we can edit and compare them static const QDate date_zero(1900, 01, 01); static const QDate date_infinity(9999,12,31); // initialize default static zone parameters void PaceZones::initializeZoneParameters() { // these default zones are based upon the Skiba pace zones // but expressed as a percentage of Critical Velocity in km/h // rather than as a percentage of LT Pace in minutes/mile // // Name %LT Pace %CV // AR > 125 0 - 80 // Endurance 124 - 115 80 - 87 // Tempo 114 - 105 88 - 95 // Threshold 104 - 95 95 - 105 // Vo2Max 94 - 84 105 - 119 // Anaerobic < 84 > 119 // static int initial_zone_default[] = { 0, 80, 87, 95, 105, 119 }; static const QString initial_zone_default_desc[] = { tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"), tr("VO2Max"), tr("Anaerobic") }; static const char *initial_zone_default_name[] = { "Z1", "Z2", "Z3", "Z4", "Z5", "Z6" }; static int initial_nzones_default = sizeof(initial_zone_default) / sizeof(initial_zone_default[0]); scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_desc.clear(); scheme.zone_default_name.clear(); scheme.nzones_default = 0; scheme.nzones_default = initial_nzones_default; for (int z = 0; z < scheme.nzones_default; z++) { scheme.zone_default.append(initial_zone_default[z]); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(QString(initial_zone_default_name[z])); scheme.zone_default_desc.append(QString(initial_zone_default_desc[z])); } } // read zone file, allowing for zones with or without end dates bool PaceZones::read(QFile &file) { defaults_from_user = false; scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_name.clear(); scheme.zone_default_desc.clear(); scheme.nzones_default = 0; ranges.clear(); // set up possible warning dialog warning = QString(); int warning_lines = 0; const int max_warning_lines = 100; // macro to append lines to the warning #define append_to_warning(s) \ if (warning_lines < max_warning_lines) { \ warning.append(s); } \ else if (warning_lines == max_warning_lines) { \ warning.append("...\n"); } \ warning_lines++; // read using text mode takes care of end-lines if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err = "can't open file"; return false; } QTextStream fileStream(&file); QRegExp commentrx("\\s*#.*$"); QRegExp blankrx("^[ \t]*$"); QRegExp rangerx[] = { QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s*([,:]?\\s*(FTP|CV)\\s*=\\s*([\\d\\.]+))?" // optional {CV/FTP = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive), QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s+(?:until|to|-)\\s+" // until "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?" // end date "\\s*:?,?\\s*((FTP|CV)\\s*=\\s*([\\d\\.]+))?" // optional {CV/FTP = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive) }; QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*" "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+|MAX)\\s*(%?)\\s*)?$", Qt::CaseInsensitive); QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$", Qt::CaseInsensitive); int lineno = 0; // the current range in the file // PaceZoneRange *range = NULL; bool in_range = false; QDate begin = date_zero, end = date_infinity; double cv=0; QList<PaceZoneInfo> zoneInfos; // true if zone defaults are found in the file (then we need to write them) bool zones_are_defaults = false; while (!fileStream.atEnd() ) { ++lineno; QString line = fileStream.readLine(); int pos = commentrx.indexIn(line, 0); if (pos != -1) line = line.left(pos); if (blankrx.indexIn(line, 0) == 0) goto next_line; // check for default zone range definition (may be followed by zone definitions) if (zonedefaultsx.indexIn(line, 0) != -1) { zones_are_defaults = true; // defaults are allowed only at the beginning of the file if (ranges.size()) { err = "Zone defaults must be specified at head of pace.zones file"; return false; } // only one set of defaults is allowed if (scheme.nzones_default) { err = "Only one set of zone defaults may be specified in pace.zones file"; return false; } goto next_line; } // check for range specification (may be followed by zone definitions) for (int r=0; r<2; r++) { if (rangerx[r].indexIn(line, 0) != -1) { if (in_range) { // if zones are empty, then generate them PaceZoneRange range(begin, end, cv); range.zones = zoneInfos; if (range.zones.empty()) { if (range.cv > 0) { setZonesFromCV(range); } else { err = tr("line %1: read new range without reading " "any zones for previous one").arg(lineno); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } in_range = true; zones_are_defaults = false; zoneInfos.clear(); // process the beginning date if (rangerx[r].cap(1) == "BEGIN") { begin = date_zero; } else { begin = QDate(rangerx[r].cap(2).toInt(), rangerx[r].cap(3).toInt(), rangerx[r].cap(4).toInt()); } // process an end date, if any, else it is null if (rangerx[r].cap(5) == "END") { end = date_infinity; } else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) { end = QDate(rangerx[r].cap(6).toInt(), rangerx[r].cap(7).toInt(), rangerx[r].cap(8).toInt()); } else { end = QDate(); } // set up the range, capturing CV if it's specified // range = new PaceZoneRange(begin, end); int nCV = (r ? 11 : 7); if (rangerx[r].captureCount() == (nCV)) cv = rangerx[r].cap(nCV).toDouble(); else cv = 0; // bleck goto next_line; } } // check for zone definition if (zonerx.indexIn(line, 0) != -1) { if (!(in_range || zones_are_defaults)) { err = tr("line %1: read zone without " "preceding date range").arg(lineno); file.close(); return false; } double lo = zonerx.cap(3).toInt(); // allow for zone specified as % of CV bool lo_is_pct = false; if (zonerx.cap(4) == "%") { if (zones_are_defaults) { lo_is_pct = true; } else if (cv > 0) { lo = double(lo * cv / 100.00f); } else { err = tr("attempt to set zone based on % of " "CV without setting CV in line number %1.\n").arg(lineno); file.close(); return false; } } double hi; // if this is not a zone defaults specification, process possible hi end of zones if (zones_are_defaults || zonerx.cap(5).isEmpty()) { hi = -1; // signal an undefined number } else if (zonerx.cap(5) == "MAX") { hi = INT_MAX; } else { hi = zonerx.cap(5).toInt(); // allow for zone specified as % of CV if (zonerx.cap(5) == "%") { if (cv > 0) { hi = double(hi * cv / 100.00f); } else { err = tr("attempt to set zone based on % of CV " "without setting CV in line number %1.\n"). arg(lineno); file.close(); return false; } } } if (zones_are_defaults) { scheme.nzones_default++; scheme.zone_default_is_pct.append(lo_is_pct); scheme.zone_default.append(lo); scheme.zone_default_name.append(zonerx.cap(1)); scheme.zone_default_desc.append(zonerx.cap(2)); defaults_from_user = true; } else { PaceZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi); zoneInfos.append(zone); } } next_line: {} } if (in_range) { PaceZoneRange range(begin, end, cv); range.zones = zoneInfos; if (range.zones.empty()) { if (range.cv > 0) { setZonesFromCV(range); } else { err = tr("file ended without reading any zones for last range"); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } file.close(); // sort the ranges qSort(ranges); // set the default zones if not in file if (!scheme.nzones_default) { // do we have a zone which is explicitly set? for (int i=0; i<ranges.count(); i++) { if (ranges[i].zonesSetFromCV == false) { // set the defaults using this one! scheme.nzones_default = ranges[i].zones.count(); for (int j=0; j<scheme.nzones_default; j++) { scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].cv) * 100.00); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(ranges[i].zones[j].name); scheme.zone_default_desc.append(ranges[i].zones[j].desc); } } } // still not set then reset to defaults as usual if (!scheme.nzones_default) initializeZoneParameters(); } // resolve undefined endpoints in ranges and zones for (int nr = 0; nr < ranges.size(); nr++) { // clean up gaps or overlaps in zone ranges if (ranges[nr].end.isNull()) { ranges[nr].end = (nr < ranges.size() - 1) ? ranges[nr + 1].begin : date_infinity; } else if ((nr < ranges.size() - 1) && (ranges[nr + 1].begin != ranges[nr].end)) { append_to_warning(tr("Setting end date of range %1 " "to start date of range %2.\n"). arg(nr + 1). arg(nr + 2) ); ranges[nr].end = ranges[nr + 1].begin; } else if ((nr == ranges.size() - 1) && (ranges[nr].end < QDate::currentDate())) { append_to_warning(tr("Extending final range %1 to infinite " "to include present date.\n").arg(nr + 1)); ranges[nr].end = date_infinity; } if (ranges[nr].cv <= 0) { err = tr("CV must be greater than zero in zone " "range %1 of pace.zones").arg(nr + 1); return false; } if (ranges[nr].zones.size()) { // check that the first zone starts with zero // ranges[nr].zones[0].lo = 0; // there is no reason we should enforce this // resolve zone end powers for (int nz = 0; nz < ranges[nr].zones.size(); nz++) { if (ranges[nr].zones[nz].hi == -1) { ranges[nr].zones[nz].hi = (nz < ranges[nr].zones.size() - 1) ? ranges[nr].zones[nz + 1].lo : INT_MAX; } else if ((nz < ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) { if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) { append_to_warning(tr("Range %1: matching top of zone %2 " "(%3) to bottom of zone %4 (%5).\n"). arg(nr + 1). arg(ranges[nr].zones[nz].name). arg(ranges[nr].zones[nz].hi). arg(ranges[nr].zones[nz + 1].name). arg(ranges[nr].zones[nz + 1].lo) ); } ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo; } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) { append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n"). arg(nr + 1). arg(ranges[nr].zones[nz].name). arg(ranges[nr].zones[nz].hi) ); ranges[nr].zones[nz].hi = INT_MAX; } } } } // mark zones as modified so pages which depend on zones can be updated modificationTime = QDateTime::currentDateTime(); return true; } // note empty dates are treated as automatic matches for begin or // end of range int PaceZones::whichRange(const QDate &date) const { for (int rnum = 0; rnum < ranges.size(); ++rnum) { const PaceZoneRange &range = ranges[rnum]; if (((date >= range.begin) || (range.begin.isNull())) && ((date < range.end) || (range.end.isNull()))) return rnum; } return -1; } int PaceZones::numZones(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum].zones.size(); } int PaceZones::whichZone(int rnum, double value) const { assert(rnum < ranges.size()); const PaceZoneRange &range = ranges[rnum]; for (int j = 0; j < range.zones.size(); ++j) { const PaceZoneInfo &info = range.zones[j]; // note: the "end" of range is actually in the next zone if ((value >= info.lo) && (value < info.hi)) return j; } // if we got here either it is negative, nan, inf or way high if (value < 0 || isnan(value)) return 0; else return range.zones.size()-1; } void PaceZones::zoneInfo(int rnum, int znum, QString &name, QString &description, double &low, double &high) const { assert(rnum < ranges.size()); const PaceZoneRange &range = ranges[rnum]; assert(znum < range.zones.size()); const PaceZoneInfo &zone = range.zones[znum]; name = zone.name; description = zone.desc; low = zone.lo; high = zone.hi; } double PaceZones::getCV(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum].cv; } void PaceZones::setCV(int rnum, double cv) { ranges[rnum].cv = cv; modificationTime = QDateTime::currentDateTime(); } // generate a list of zones from CV int PaceZones::lowsFromCV(QList <double> *lows, double cv) const { lows->clear(); for (int z = 0; z < scheme.nzones_default; z++) { lows->append(scheme.zone_default_is_pct[z] ? scheme.zone_default[z] * cv / 100.00f : scheme.zone_default[z]); } return scheme.nzones_default; } // access the zone name QString PaceZones::getDefaultZoneName(int z) const { return scheme.zone_default_name[z]; } // access the zone description QString PaceZones::getDefaultZoneDesc(int z) const { return scheme.zone_default_desc[z]; } // set the zones from the CV value (the cv variable) void PaceZones::setZonesFromCV(PaceZoneRange &range) { range.zones.clear(); if (scheme.nzones_default == 0) initializeZoneParameters(); for (int i = 0; i < scheme.nzones_default; i++) { double lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.cv / 100.00f : scheme.zone_default[i]; double hi = lo; PaceZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi); range.zones.append(zone); } // sort the zones (some may be pct, others absolute, so zones need to be sorted, // rather than the defaults qSort(range.zones); // set zone end dates for (int i = 0; i < range.zones.size(); i++) { range.zones[i].hi = (i < scheme.nzones_default - 1) ? range.zones[i + 1].lo : INT_MAX; } // mark that the zones were set from CV, so if zones are subsequently // written, only CV is saved range.zonesSetFromCV = true; } void PaceZones::setZonesFromCV(int rnum) { assert((rnum >= 0) && (rnum < ranges.size())); setZonesFromCV(ranges[rnum]); } // return the list of starting values of zones for a given range QList <double> PaceZones::getZoneLows(int rnum) const { if (rnum >= ranges.size()) return QList <double>(); const PaceZoneRange &range = ranges[rnum]; QList <double> return_values; for (int i = 0; i < range.zones.size(); i++) { return_values.append(ranges[rnum].zones[i].lo); } return return_values; } // return the list of ending values of zones for a given range QList <double> PaceZones::getZoneHighs(int rnum) const { if (rnum >= ranges.size()) return QList <double>(); const PaceZoneRange &range = ranges[rnum]; QList <double> return_values; for (int i = 0; i < range.zones.size(); i++) { return_values.append(ranges[rnum].zones[i].hi); } return return_values; } // return the list of zone names QList <QString> PaceZones::getZoneNames(int rnum) const { if (rnum >= ranges.size()) return QList <QString>(); const PaceZoneRange &range = ranges[rnum]; QList <QString> return_values; for (int i = 0; i < range.zones.size(); i++) { return_values.append(ranges[rnum].zones[i].name); } return return_values; } QString PaceZones::summarize(int rnum, QVector<double> &time_in_zone, QColor color) const { assert(rnum < ranges.size()); const PaceZoneRange &range = ranges[rnum]; if (time_in_zone.size() != range.zones.size()) time_in_zone.resize(range.zones.size()); // are we in metric or imperial ? bool metric = appsettings->value(this, GC_PACE, true).toBool(); QString cvunit = metric ? "kph" : "mph"; QString paceunit = metric ? "min/km" : "min/mile"; double cvfactor = metric ? 1.0f : KM_PER_MILE; QString summary; if(range.cv > 0) { summary += "<table align=\"center\" width=\"70%\" border=\"0\">"; summary += "<tr><td align=\"center\">"; summary += tr("Critical Velocity: %3%4 (%2%1)").arg(cvunit).arg(range.cv / cvfactor, 0, 'f', 2) .arg(kphToPace(range.cv, metric)) .arg(paceunit); summary += "</td></tr></table>"; } summary += "<table align=\"center\" width=\"70%\" "; summary += "border=\"0\">"; summary += "<tr>"; summary += tr("<td align=\"center\">Zone</td>"); summary += tr("<td align=\"center\">Description</td>"); summary += tr("<td align=\"center\">Low (%1)</td>").arg(paceunit); summary += tr("<td align=\"center\">High (%1)</td>").arg(paceunit); summary += tr("<td align=\"center\">Time</td>"); summary += tr("<td align=\"center\">%</td>"); summary += "</tr>"; double duration = 0; foreach(double v, time_in_zone) { duration += v; } for (int zone = 0; zone < time_in_zone.size(); ++zone) { if (time_in_zone[zone] > 0.0) { QString name, desc; double lo, hi; zoneInfo(rnum, zone, name, desc, lo, hi); if (zone % 2 == 0) summary += "<tr bgcolor='" + color.name() + "'>"; else summary += "<tr>"; summary += QString("<td align=\"center\">%1</td>").arg(name); summary += QString("<td align=\"center\">%1</td>").arg(desc); summary += QString("<td align=\"center\">%1</td>").arg(kphToPace(lo, metric)); if (hi == INT_MAX) summary += "<td align=\"center\">MAX</td>"; else summary += QString("<td align=\"center\">%1</td>").arg(kphToPace(hi, metric)); summary += QString("<td align=\"center\">%1</td>").arg(time_to_string((unsigned) round(time_in_zone[zone]))); summary += QString("<td align=\"center\">%1</td>") .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0); summary += "</tr>"; } } summary += "</table>"; return summary; } #define USE_SHORT_POWER_ZONES_FORMAT true /* whether a less redundent format should be used */ void PaceZones::write(QDir home) { QString strzones; // always write the defaults (config pane can adjust) strzones += QString("DEFAULTS:\n"); for (int z = 0; z < scheme.nzones_default; z++) { strzones += QString("%1,%2,%3%4\n"). arg(scheme.zone_default_name[z]). arg(scheme.zone_default_desc[z]). arg(scheme.zone_default[z]). arg(scheme.zone_default_is_pct[z] ? "%" : ""); } strzones += QString("\n"); for (int i = 0; i < ranges.size(); i++) { double cv = getCV(i); // print header for range // note this explicitly sets the first and last ranges such that all time is spanned #if USE_SHORT_POWER_ZONES_FORMAT // note: BEGIN is not needed anymore // since it becomes Jan 01 1900 strzones += QString("%1: CV=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cv); strzones += QString("\n"); // step through and print the zones if they've been explicitly set if (!ranges[i].zonesSetFromCV) { for (int j = 0; j < ranges[i].zones.size(); j++) { const PaceZoneInfo &zi = ranges[i].zones[j]; strzones += QString("%1,%2,%3\n").arg(zi.name).arg(zi.desc).arg(zi.lo); } strzones += QString("\n"); } #else if(ranges.size() <= 1) { strzones += QString("FROM BEGIN UNTIL END, CV=%1:").arg(cv); } else if (i == 0) { strzones += QString("FROM BEGIN UNTIL %1, CV=%2:").arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cv); } else if (i == ranges.size() - 1) { strzones += QString("FROM %1 UNTIL END, CV=%2:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cv); } else { strzones += QString("FROM %1 UNTIL %2, CV=%3:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cv); } strzones += QString("\n"); for (int j = 0; j < ranges[i].zones.size(); j++) { const PaceZoneInfo &zi = ranges[i].zones[j]; if (ranges[i].zones[j].hi == INT_MAX) { strzones += QString("%1,%2,%3,MAX\n").arg(zi.name).arg(zi.desc).arg(zi.lo); } else { strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.hi); } } strzones += QString("\n"); #endif } QFile file(home.canonicalPath() + "/pace.zones"); if (file.open(QFile::WriteOnly)) { QTextStream stream(&file); stream << strzones; file.close(); } }
/* * Copyright (c) 2006 Sean C. Rhea ([email protected]) * * 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 <QMessageBox> #include "Zones.h" #include "Colors.h" #include "Settings.h" #include "TimeUtils.h" #include <QtGui> #include <QtAlgorithms> #include <qcolor.h> #include <assert.h> #include <cmath> // the infinity endpoints are indicated with extreme date ranges // but not zero dates so we can edit and compare them static const QDate date_zero(1900, 01, 01); static const QDate date_infinity(9999,12,31); // initialize default static zone parameters void Zones::initializeZoneParameters() { static int initial_zone_default[] = { 0, 55, 75, 90, 105, 120, 150 }; static const QString initial_zone_default_desc[] = { tr("Active Recovery"), tr("Endurance"), tr("Tempo"), tr("Threshold"), tr("VO2Max"), tr("Anaerobic"), tr("Neuromuscular") }; static const char *initial_zone_default_name[] = { "Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7" }; static int initial_nzones_default = sizeof(initial_zone_default) / sizeof(initial_zone_default[0]); scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_desc.clear(); scheme.zone_default_name.clear(); scheme.nzones_default = 0; scheme.nzones_default = initial_nzones_default; for (int z = 0; z < scheme.nzones_default; z++) { scheme.zone_default.append(initial_zone_default[z]); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(QString(initial_zone_default_name[z])); scheme.zone_default_desc.append(QString(initial_zone_default_desc[z])); } } // read zone file, allowing for zones with or without end dates bool Zones::read(QFile &file) { defaults_from_user = false; scheme.zone_default.clear(); scheme.zone_default_is_pct.clear(); scheme.zone_default_name.clear(); scheme.zone_default_desc.clear(); scheme.nzones_default = 0; ranges.clear(); // set up possible warning dialog warning = QString(); int warning_lines = 0; const int max_warning_lines = 100; int defaultwprime = 20000; // default to 20kJ int defaultpmax = 1000; // macro to append lines to the warning #define append_to_warning(s) \ if (warning_lines < max_warning_lines) { \ warning.append(s); } \ else if (warning_lines == max_warning_lines) { \ warning.append("...\n"); } \ warning_lines++; // read using text mode takes care of end-lines if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err = "can't open file"; return false; } QTextStream fileStream(&file); QRegExp commentrx("\\s*#.*$"); QRegExp blankrx("^[ \t]*$"); QRegExp rangerx[] = { QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s*([,:]?\\s*(FTP|CP)\\s*=\\s*(\\d+))?" // optional {CP/FTP = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive), QRegExp("^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s+(?:until|to|-)\\s+" // until "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?" // end date "\\s*:?,?\\s*((FTP|CP)\\s*=\\s*(\\d+))?" // optional {CP/FTP = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive) }; QRegExp ftpx("^FTP=(\\d+)$"); QRegExp wprimerx("^W'=(\\d+)$"); QRegExp pmaxx("^Pmax=(\\d+)$"); QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*" "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+|MAX)\\s*(%?)\\s*)?$", Qt::CaseInsensitive); QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$", Qt::CaseInsensitive); int lineno = 0; // the current range in the file // ZoneRange *range = NULL; bool in_range = false; QDate begin = date_zero, end = date_infinity; int cp=0; int ftp=0; int wprime=0; int pmax=0; QList<ZoneInfo> zoneInfos; // true if zone defaults are found in the file (then we need to write them) bool zones_are_defaults = false; while (!fileStream.atEnd() ) { ++lineno; QString line = fileStream.readLine(); int pos = commentrx.indexIn(line, 0); if (pos != -1) line = line.left(pos); if (blankrx.indexIn(line, 0) == 0) goto next_line; // check for default zone range definition (may be followed by zone definitions) if (zonedefaultsx.indexIn(line, 0) != -1) { zones_are_defaults = true; // defaults are allowed only at the beginning of the file if (ranges.size()) { err = "Zone defaults must be specified at head of power.zones file"; return false; } // only one set of defaults is allowed if (scheme.nzones_default) { err = "Only one set of zone defaults may be specified in power.zones file"; return false; } goto next_line; } // check for range specification (may be followed by zone definitions) for (int r=0; r<2; r++) { if (rangerx[r].indexIn(line, 0) != -1) { if (in_range) { // if zones are empty, then generate them ZoneRange range(begin, end, cp, ftp ? ftp : cp, wprime ? wprime : defaultwprime, pmax ? pmax : defaultpmax); range.zones = zoneInfos; if (range.zones.empty()) { if (range.cp > 0) { setZonesFromCP(range); } else { err = tr("line %1: read new range without reading " "any zones for previous one").arg(lineno); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } in_range = true; zones_are_defaults = false; zoneInfos.clear(); // process the beginning date if (rangerx[r].cap(1) == "BEGIN") { begin = date_zero; } else { begin = QDate(rangerx[r].cap(2).toInt(), rangerx[r].cap(3).toInt(), rangerx[r].cap(4).toInt()); } // process an end date, if any, else it is null if (rangerx[r].cap(5) == "END") { end = date_infinity; } else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) { end = QDate(rangerx[r].cap(6).toInt(), rangerx[r].cap(7).toInt(), rangerx[r].cap(8).toInt()); } else { end = QDate(); } // set up the range, capturing CP if it's specified // range = new ZoneRange(begin, end); int nCP = (r ? 11 : 7); if (rangerx[r].captureCount() == (nCP)) cp = rangerx[r].cap(nCP).toInt(); else cp = 0; // bleck goto next_line; } } // check for FTP if (ftpx.indexIn(line, 0) != -1) { if (!in_range) qDebug()<<"ignoring errant FTP= in power.zones"; else { ftp = ftpx.cap(1).toInt(); // ok its stored, so if it is in kJ upscale // if it is zero as never set, then use default if (!ftp) { ftp = cp; } } } // check for w' if (wprimerx.indexIn(line, 0) != -1) { if (!in_range) qDebug()<<"ignoring errant W'= in power.zones"; else { wprime = wprimerx.cap(1).toInt(); // ok its stored, so if it is in kJ upscale // if it is zero as never set, then use default if (wprime) { if (wprime < 1000) wprime *= 1000; // JOULES not kJ defaultwprime = wprime; } else { wprime = defaultwprime; } } } // check for Pmax if (pmaxx.indexIn(line, 0) != -1) { if (!in_range) qDebug()<<"ignoring errant Pmax= in power.zones"; else { pmax = pmaxx.cap(1).toInt(); } } // check for zone definition if (zonerx.indexIn(line, 0) != -1) { if (!(in_range || zones_are_defaults)) { err = tr("line %1: read zone without " "preceding date range").arg(lineno); file.close(); return false; } int lo = zonerx.cap(3).toInt(); // allow for zone specified as % of CP bool lo_is_pct = false; if (zonerx.cap(4) == "%") { if (zones_are_defaults) { lo_is_pct = true; } else if (cp > 0) { lo = int(lo * cp / 100); } else { err = tr("attempt to set zone based on % of " "CP without setting CP in line number %1.\n").arg(lineno); file.close(); return false; } } int hi; // if this is not a zone defaults specification, process possible hi end of zones if (zones_are_defaults || zonerx.cap(5).isEmpty()) { hi = -1; // signal an undefined number } else if (zonerx.cap(5) == "MAX") { hi = INT_MAX; } else { hi = zonerx.cap(5).toInt(); // allow for zone specified as % of CP if (zonerx.cap(5) == "%") { if (cp > 0) { hi = int(hi * cp / 100); } else { err = tr("attempt to set zone based on % of CP " "without setting CP in line number %1.\n"). arg(lineno); file.close(); return false; } } } if (zones_are_defaults) { scheme.nzones_default++; scheme.zone_default_is_pct.append(lo_is_pct); scheme.zone_default.append(lo); scheme.zone_default_name.append(zonerx.cap(1)); scheme.zone_default_desc.append(zonerx.cap(2)); defaults_from_user = true; } else { ZoneInfo zone(zonerx.cap(1), zonerx.cap(2), lo, hi); zoneInfos.append(zone); } } next_line: {} } if (in_range) { ZoneRange range(begin, end, cp, ftp ? ftp : cp, wprime ? wprime : defaultwprime, pmax ? pmax : defaultpmax); range.zones = zoneInfos; if (range.zones.empty()) { if (range.cp > 0) { setZonesFromCP(range); } else { err = tr("file ended without reading any zones for last range"); file.close(); return false; } } else { qSort(range.zones); } ranges.append(range); } file.close(); // sort the ranges qSort(ranges); // set the default zones if not in file if (!scheme.nzones_default) { // do we have a zone which is explicitly set? for (int i=0; i<ranges.count(); i++) { if (ranges[i].zonesSetFromCP == false) { // set the defaults using this one! scheme.nzones_default = ranges[i].zones.count(); for (int j=0; j<scheme.nzones_default; j++) { scheme.zone_default.append(((double)ranges[i].zones[j].lo / (double)ranges[i].cp) * 100.00); scheme.zone_default_is_pct.append(true); scheme.zone_default_name.append(ranges[i].zones[j].name); scheme.zone_default_desc.append(ranges[i].zones[j].desc); } } } // still not set then reset to defaults as usual if (!scheme.nzones_default) initializeZoneParameters(); } // resolve undefined endpoints in ranges and zones for (int nr = 0; nr < ranges.size(); nr++) { // clean up gaps or overlaps in zone ranges if (ranges[nr].end.isNull()) { ranges[nr].end = (nr < ranges.size() - 1) ? ranges[nr + 1].begin : date_infinity; } else if ((nr < ranges.size() - 1) && (ranges[nr + 1].begin != ranges[nr].end)) { append_to_warning(tr("Setting end date of range %1 " "to start date of range %2.\n"). arg(nr + 1). arg(nr + 2) ); ranges[nr].end = ranges[nr + 1].begin; } else if ((nr == ranges.size() - 1) && (ranges[nr].end < QDate::currentDate())) { append_to_warning(tr("Extending final range %1 to infinite " "to include present date.\n").arg(nr + 1)); ranges[nr].end = date_infinity; } if (ranges[nr].cp <= 0) { err = tr("CP must be greater than zero in zone " "range %1 of power.zones").arg(nr + 1); return false; } if (ranges[nr].zones.size()) { // check that the first zone starts with zero // ranges[nr].zones[0].lo = 0; // there is no reason we should enforce this // resolve zone end powers for (int nz = 0; nz < ranges[nr].zones.size(); nz++) { if (ranges[nr].zones[nz].hi == -1) { ranges[nr].zones[nz].hi = (nz < ranges[nr].zones.size() - 1) ? ranges[nr].zones[nz + 1].lo : INT_MAX; } else if ((nz < ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi != ranges[nr].zones[nz + 1].lo)) { if (abs(ranges[nr].zones[nz].hi - ranges[nr].zones[nz + 1].lo) > 4) { append_to_warning(tr("Range %1: matching top of zone %2 " "(%3) to bottom of zone %4 (%5).\n"). arg(nr + 1). arg(ranges[nr].zones[nz].name). arg(ranges[nr].zones[nz].hi). arg(ranges[nr].zones[nz + 1].name). arg(ranges[nr].zones[nz + 1].lo) ); } ranges[nr].zones[nz].hi = ranges[nr].zones[nz + 1].lo; } else if ((nz == ranges[nr].zones.size() - 1) && (ranges[nr].zones[nz].hi < INT_MAX)) { append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n"). arg(nr + 1). arg(ranges[nr].zones[nz].name). arg(ranges[nr].zones[nz].hi) ); ranges[nr].zones[nz].hi = INT_MAX; } } } } // mark zones as modified so pages which depend on zones can be updated modificationTime = QDateTime::currentDateTime(); return true; } // note empty dates are treated as automatic matches for begin or // end of range int Zones::whichRange(const QDate &date) const { for (int rnum = 0; rnum < ranges.size(); ++rnum) { const ZoneRange &range = ranges[rnum]; if (((date >= range.begin) || (range.begin.isNull())) && ((date < range.end) || (range.end.isNull()))) return rnum; } return -1; } int Zones::numZones(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum].zones.size(); } int Zones::whichZone(int rnum, double value) const { assert(rnum < ranges.size()); const ZoneRange &range = ranges[rnum]; for (int j = 0; j < range.zones.size(); ++j) { const ZoneInfo &info = range.zones[j]; // note: the "end" of range is actually in the next zone if ((value >= info.lo) && (value < info.hi)) return j; } // if we got here either it is negative, nan, inf or way high return -1; } void Zones::zoneInfo(int rnum, int znum, QString &name, QString &description, int &low, int &high) const { assert(rnum < ranges.size()); const ZoneRange &range = ranges[rnum]; assert(znum < range.zones.size()); const ZoneInfo &zone = range.zones[znum]; name = zone.name; description = zone.desc; low = zone.lo; high = zone.hi; } int Zones::getCP(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum].cp; } int Zones::getFTP(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum].ftp; } int Zones::getWprime(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum].wprime; } int Zones::getPmax(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum].pmax; } void Zones::setCP(int rnum, int cp) { ranges[rnum].cp = cp; modificationTime = QDateTime::currentDateTime(); } void Zones::setFTP(int rnum, int ftp) { ranges[rnum].ftp = ftp; modificationTime = QDateTime::currentDateTime(); } void Zones::setWprime(int rnum, int wprime) { ranges[rnum].wprime = wprime; modificationTime = QDateTime::currentDateTime(); } void Zones::setPmax(int rnum, int pmax) { ranges[rnum].pmax = pmax; modificationTime = QDateTime::currentDateTime(); } // generate a list of zones from CP int Zones::lowsFromCP(QList <int> *lows, int cp) const { lows->clear(); for (int z = 0; z < scheme.nzones_default; z++) { lows->append(scheme.zone_default_is_pct[z] ? scheme.zone_default[z] * cp / 100 : scheme.zone_default[z]); } return scheme.nzones_default; } // access the zone name QString Zones::getDefaultZoneName(int z) const { return scheme.zone_default_name[z]; } // access the zone description QString Zones::getDefaultZoneDesc(int z) const { return scheme.zone_default_desc[z]; } // set the zones from the CP value (the cp variable) void Zones::setZonesFromCP(ZoneRange &range) { range.zones.clear(); if (scheme.nzones_default == 0) initializeZoneParameters(); for (int i = 0; i < scheme.nzones_default; i++) { int lo = scheme.zone_default_is_pct[i] ? scheme.zone_default[i] * range.cp / 100 : scheme.zone_default[i]; int hi = lo; ZoneInfo zone(scheme.zone_default_name[i], scheme.zone_default_desc[i], lo, hi); range.zones.append(zone); } // sort the zones (some may be pct, others absolute, so zones need to be sorted, // rather than the defaults qSort(range.zones); // set zone end dates for (int i = 0; i < range.zones.size(); i++) { range.zones[i].hi = (i < scheme.nzones_default - 1) ? range.zones[i + 1].lo : INT_MAX; } // mark that the zones were set from CP, so if zones are subsequently // written, only CP is saved range.zonesSetFromCP = true; } void Zones::setZonesFromCP(int rnum) { assert((rnum >= 0) && (rnum < ranges.size())); setZonesFromCP(ranges[rnum]); } // return the list of starting values of zones for a given range QList <int> Zones::getZoneLows(int rnum) const { if (rnum >= ranges.size()) return QList <int>(); const ZoneRange &range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range.zones.size(); i++) { return_values.append(ranges[rnum].zones[i].lo); } return return_values; } // return the list of ending values of zones for a given range QList <int> Zones::getZoneHighs(int rnum) const { if (rnum >= ranges.size()) return QList <int>(); const ZoneRange &range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range.zones.size(); i++) { return_values.append(ranges[rnum].zones[i].hi); } return return_values; } // return the list of zone names QList <QString> Zones::getZoneNames(int rnum) const { if (rnum >= ranges.size()) return QList <QString>(); const ZoneRange &range = ranges[rnum]; QList <QString> return_values; for (int i = 0; i < range.zones.size(); i++) { return_values.append(ranges[rnum].zones[i].name); } return return_values; } QString Zones::summarize(int rnum, QVector<double> &time_in_zone, QColor color) const { assert(rnum < ranges.size()); const ZoneRange &range = ranges[rnum]; assert(time_in_zone.size() == range.zones.size()); QString summary; if(range.cp > 0) { summary += "<table align=\"center\" width=\"70%\" border=\"0\">"; summary += "<tr><td align=\"center\">"; summary += tr("Critical Power (watts): %1").arg(range.cp); summary += "</td></tr></table>"; } summary += "<table align=\"center\" width=\"70%\" "; summary += "border=\"0\">"; summary += "<tr>"; summary += tr("<td align=\"center\">Zone</td>"); summary += tr("<td align=\"center\">Description</td>"); summary += tr("<td align=\"center\">Low (watts)</td>"); summary += tr("<td align=\"center\">High (watts)</td>"); summary += tr("<td align=\"center\">Time</td>"); summary += tr("<td align=\"center\">%</td>"); summary += "</tr>"; double duration = 0; foreach(double v, time_in_zone) { duration += v; } for (int zone = 0; zone < time_in_zone.size(); ++zone) { if (time_in_zone[zone] > 0.0) { QString name, desc; int lo, hi; zoneInfo(rnum, zone, name, desc, lo, hi); if (zone % 2 == 0) summary += "<tr bgcolor='" + color.name() + "'>"; else summary += "<tr>"; summary += QString("<td align=\"center\">%1</td>").arg(name); summary += QString("<td align=\"center\">%1</td>").arg(desc); summary += QString("<td align=\"center\">%1</td>").arg(lo); if (hi == INT_MAX) summary += "<td align=\"center\">MAX</td>"; else summary += QString("<td align=\"center\">%1</td>").arg(hi); summary += QString("<td align=\"center\">%1</td>") .arg(time_to_string((unsigned) round(time_in_zone[zone]))); summary += QString("<td align=\"center\">%1</td>") .arg((double)time_in_zone[zone]/duration * 100, 0, 'f', 0); summary += "</tr>"; } } summary += "</table>"; return summary; } #define USE_SHORT_POWER_ZONES_FORMAT true /* whether a less redundent format should be used */ void Zones::write(QDir home) { QString strzones; // always write the defaults (config pane can adjust) strzones += QString("DEFAULTS:\n"); for (int z = 0; z < scheme.nzones_default; z++) { strzones += QString("%1,%2,%3%4\n"). arg(scheme.zone_default_name[z]). arg(scheme.zone_default_desc[z]). arg(scheme.zone_default[z]). arg(scheme.zone_default_is_pct[z] ? "%" : ""); } strzones += QString("\n"); for (int i = 0; i < ranges.size(); i++) { int cp = getCP(i); int ftp = getFTP(i); int wprime = getWprime(i); int pmax = getPmax(i); // print header for range // note this explicitly sets the first and last ranges such that all time is spanned #if USE_SHORT_POWER_ZONES_FORMAT // note: BEGIN is not needed anymore // since it becomes Jan 01 1900 strzones += QString("%1: CP=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp); strzones += QString("\n"); // wite out the FTP value strzones += QString("FTP=%1\n").arg(ftp); // wite out the W' value strzones += QString("W'=%1\n").arg(wprime); // wite out the Pmax value strzones += QString("Pmax=%1\n").arg(pmax); // step through and print the zones if they've been explicitly set if (!ranges[i].zonesSetFromCP) { for (int j = 0; j < ranges[i].zones.size(); j++) { const ZoneInfo &zi = ranges[i].zones[j]; strzones += QString("%1,%2,%3\n").arg(zi.name).arg(zi.desc).arg(zi.lo); } strzones += QString("\n"); } #else if(ranges.size() <= 1) { strzones += QString("FROM BEGIN UNTIL END, CP=%1:").arg(cp); } else if (i == 0) { strzones += QString("FROM BEGIN UNTIL %1, CP=%2:").arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp); } else if (i == ranges.size() - 1) { strzones += QString("FROM %1 UNTIL END, CP=%2:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp); } else { strzones += QString("FROM %1 UNTIL %2, CP=%3:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp); } strzones += QString("\n"); for (int j = 0; j < ranges[i].zones.size(); j++) { const ZoneInfo &zi = ranges[i].zones[j]; if (ranges[i].zones[j].hi == INT_MAX) { strzones += QString("%1,%2,%3,MAX\n").arg(zi.name).arg(zi.desc).arg(zi.lo); } else { strzones += QString("%1,%2,%3,%4\n").arg(zi.name).arg(zi.desc).arg(zi.lo).arg(zi.hi); } } strzones += QString("\n"); #endif } QFile file(home.canonicalPath() + "/power.zones"); if (file.open(QFile::WriteOnly)) { QTextStream stream(&file); stream << strzones; file.close(); } else { QMessageBox msgBox; msgBox.setIcon(QMessageBox::Critical); msgBox.setText(tr("Problem Saving Power Zones")); msgBox.setInformativeText(tr("File: %1 cannot be opened for 'Writing'. Please check file properties.").arg(home.canonicalPath() + "/power.zones")); msgBox.exec(); return; } }
/* * Copyright (c) 2006 Sean C. Rhea ([email protected]) * * 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 <QMessageBox> #include "Zones.h" #include "TimeUtils.h" #include <QtGui> #include <QtAlgorithms> #include <qcolor.h> #include <assert.h> #include <math.h> static QList <int> zone_default; static QList <bool> zone_default_is_pct; static QList <QString> zone_default_name; static QList <QString> zone_default_desc; static int nzones_default = 0; // the infinity endpoints are indicated with empty dates static const QDate date_zero = QDate::QDate(); static const QDate date_infinity = QDate::QDate(); // functions used for sorting zones and ranges bool Zones::zone_default_index_lessthan(int i1, int i2) { return ( zone_default[i1] * (zone_default_is_pct[i1] ? 250 : 1) < zone_default[i2] * (zone_default_is_pct[i2] ? 250 : 1) ); } bool Zones::zoneptr_lessthan(ZoneInfo *z1, ZoneInfo *z2) { return ( (z1->lo < z2->lo) || ((z1->lo == z2->lo) && (z1->hi < z2->hi)) ); } bool Zones::rangeptr_lessthan(ZoneRange *r1, ZoneRange *r2) { return ( ( (! r2->begin.isNull()) && ( r1->begin.isNull() || r1->begin < r2->begin ) ) || ((r1->begin == r2->begin) && (! r1->end.isNull()) && ( r2->end.isNull() || r1->end < r2->end ) ) ); } // initialize default static zone parameters void Zones::initializeZoneParameters() { static int initial_zone_default[] = { 0, 55, 75, 90, 105, 120, 150 }; static const char *initial_zone_default_desc[] = { "Active Recovery", "Endurance", "Tempo", "Threshold", "VO2Max", "Anaerobic", "Neuromuscular" }; static const char *initial_zone_default_name[] = { "Z1", "Z2", "Z3", "Z4", "Z5", "Z6", "Z7" }; static int initial_nzones_default = sizeof(initial_zone_default) / sizeof(initial_zone_default[0]); zone_default.clear(); zone_default_is_pct.clear(); zone_default_desc.clear(); zone_default_name.clear(); nzones_default = 0; nzones_default = initial_nzones_default; for (int z = 0; z < nzones_default; z ++) { zone_default.append(initial_zone_default[z]); zone_default_is_pct.append(true); zone_default_name.append(QString(initial_zone_default_name[z])); zone_default_desc.append(QString(initial_zone_default_desc[z])); } fprintf( stderr, "%d default zones set:\n", nzones_default ); } // read zone file, allowing for zones with or without end dates bool Zones::read(QFile &file) { defaults_from_user = false; zone_default.clear(); zone_default_is_pct.clear(); zone_default_name.clear(); zone_default_desc.clear(); nzones_default = 0; // set up possible warning dialog warning = QString(); int warning_lines = 0; const int max_warning_lines = 100; // macro to append lines to the warning #define append_to_warning(s) \ if (warning_lines < max_warning_lines) \ warning.append(s); \ else if (warning_lines == max_warning_lines) \ warning.append("...\n"); \ warning_lines ++; // read using text mode takes care of end-lines if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err = "can't open file"; return false; } QTextStream fileStream(&file); QRegExp commentrx("\\s*#.*$"); QRegExp blankrx("^[ \t]*$"); QRegExp rangerx[] = { QRegExp( "^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s*([,:]?\\s*(FTP|CP)\\s*=\\s*(\\d+))?" // optional {CP/FTP = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive ), QRegExp( "^\\s*(?:from\\s+)?" // optional "from" "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|BEGIN)" // begin date "\\s+(?:until|to|-)\\s+" // until "((\\d\\d\\d\\d)[-/](\\d{1,2})[-/](\\d{1,2})|END)?" // end date "\\s*:?,?\\s*((FTP|CP)\\s*=\\s*(\\d+))?" // optional {CP/FTP = integer (optional %)} "\\s*:?\\s*$", // optional : Qt::CaseInsensitive ) }; QRegExp zonerx("^\\s*([^ ,][^,]*),\\s*([^ ,][^,]*),\\s*" "(\\d+)\\s*(%?)\\s*(?:,\\s*(\\d+|MAX)\\s*(%?)\\s*)?$", Qt::CaseInsensitive ); QRegExp zonedefaultsx("^\\s*(?:zone)?\\s*defaults?\\s*:?\\s*$", Qt::CaseInsensitive ); int lineno = 0; // the current range in the file ZoneRange *range = NULL; // true if zone defaults are found in the file (then we need to write them) bool zones_are_defaults = false; while (! fileStream.atEnd() ) { ++lineno; QString line = fileStream.readLine(); fprintf(stderr, "line %d: \"%s\"\n", lineno, line.toAscii().constData()); int pos = commentrx.indexIn(line, 0); if (pos != -1) { fprintf(stderr, "line %d: blank\n", lineno); line = line.left(pos); } if (blankrx.indexIn(line, 0) == 0) { goto next_line; } // check for default zone range definition (may be followed by zone definitions) if (zonedefaultsx.indexIn(line, 0) != -1) { fprintf(stderr, "line %d: zone defaults specification identified\n", lineno); zones_are_defaults = true; // defaults are allowed only at the beginning of the file if (ranges.size()) { err = "Zone defaults must be specified at head of power.zones file"; return false; } // only one set of defaults is allowed if (nzones_default) { err = "Only one set of zone defaults may be specified in power.zones file"; return false; } goto next_line; } // check for range specification (may be followed by zone definitions) for (int r = 0; r < 2; r++) { if (rangerx[r].indexIn(line, 0) != -1) { zones_are_defaults = false; fprintf(stderr, "line %d: matched range: %s to %s\n", lineno, rangerx[r].cap(1).toAscii().constData(), rangerx[r].cap(5).toAscii().constData()); QDate begin, end; // process the beginning date if (rangerx[r].cap(1) == "BEGIN") begin = date_zero; else { begin = QDate(rangerx[r].cap(2).toInt(), rangerx[r].cap(3).toInt(), rangerx[r].cap(4).toInt() ); } // process an end date, if any, else it is null if (rangerx[r].cap(5) == "END") end = date_infinity; else if (rangerx[r].cap(6).toInt() || rangerx[r].cap(7).toInt() || rangerx[r].cap(8).toInt()) { end = QDate(rangerx[r].cap(6).toInt(), rangerx[r].cap(7).toInt(), rangerx[r].cap(8).toInt() ); fprintf(stderr, "end date = %s\n", end.toString().toAscii().constData()); } else { end = QDate(); fprintf(stderr, "no end date\n"); } if (range) { // if zones are empty, then generate them if (range->zones.empty()) { fprintf(stderr, "create zones from cp = %d\n", range->cp); if (range->cp > 0) setZonesFromCP(range); else { err = tr("line %1: read new range without reading " "any zones for previous one").arg(lineno); file.close(); return false; } } // else sort them else { fprintf(stderr, "Sorting zones for range %d\n", ranges.size() + 1); qSort(range->zones.begin(), range->zones.end(), zoneptr_lessthan); } ranges.append(range); } // set up the range, capturing CP if it's specified range = new ZoneRange(begin, end); int nCP = (r ? 11 : 7); if (rangerx[r].numCaptures() == (nCP)) { range->cp = rangerx[r].cap(nCP).toInt(); fprintf(stderr, "setting CP = %d\n", range->cp); } goto next_line; } } // check for zone definition if (zonerx.indexIn(line, 0) != -1) { if (! (range || zones_are_defaults)) { err = tr("line %1: read zone without " "preceeding date range").arg(lineno); file.close(); return false; } int lo = zonerx.cap(3).toInt(); // allow for zone specified as % of CP bool lo_is_pct = false; if (zonerx.cap(4) == "%") if (zones_are_defaults) lo_is_pct = true; else if (range->cp > 0) lo = int(lo * range->cp / 100); else { err = tr("attempt to set zone based on % of " "CP without setting CP in line number %1.\n"). arg(lineno); file.close(); return false; } int hi; // if this is not a zone defaults specification, process possible hi end of zones if (zones_are_defaults || zonerx.cap(5).isEmpty()) hi = -1; // signal an undefined number else if (zonerx.cap(5) == "MAX") hi = INT_MAX; else { hi = zonerx.cap(5).toInt(); // allow for zone specified as % of CP if (zonerx.cap(5) == "%") if (range->cp > 0) hi = int(hi * range->cp / 100); else { err = tr("attempt to set zone based on % of CP " "without setting CP in line number %1.\n"). arg(lineno); file.close(); return false; } } if (zones_are_defaults) { nzones_default ++; zone_default_is_pct.append(lo_is_pct); zone_default.append(lo); zone_default_name.append(zonerx.cap(1)); zone_default_desc.append(zonerx.cap(2)); fprintf(stderr, "line %d: zone default #%d found: \"%s\" (%s): %d%s\n", lineno, nzones_default, zone_default_name[nzones_default - 1].toAscii().constData(), zone_default_desc[nzones_default - 1].toAscii().constData(), zone_default[nzones_default - 1], zone_default_is_pct[nzones_default - 1] ? "%" : "" ); defaults_from_user = true; } else { ZoneInfo *zone = new ZoneInfo(zonerx.cap(1), zonerx.cap(2), lo, hi); fprintf(stderr, "line %d: matched zones: " "\"%s\", \"%s\", %s, %s\n", lineno, zonerx.cap(1).toAscii().constData(), zonerx.cap(2).toAscii().constData(), zonerx.cap(3).isEmpty() ? "null" : zonerx.cap(3).toAscii().constData(), zonerx.cap(4).isEmpty() ? "null" : zonerx.cap(4).toAscii().constData() ); range->zones.append(zone); } } next_line: {} } if (range) { if (range->zones.empty()) { fprintf(stderr, "empty zones found: cp = %d\n", range->cp); if (range->cp > 0) setZonesFromCP(range); else { err = tr("file ended without reading any zones for last range"); file.close(); return false; } } else { fprintf(stderr, "Sorting zones for final range %d\n", ranges.size()); qSort(range->zones.begin(), range->zones.end(), zoneptr_lessthan); } ranges.append(range); } file.close(); // sort the ranges qSort(ranges.begin(), ranges.end(), rangeptr_lessthan); // sort the zone defaults, as best we can (may not be right if there's // a mix of % and absolute ranges) if (nzones_default) { fprintf(stderr, "Sorting zone defaults...\n"); QVector <int> zone_default_index(nzones_default); for (int i = 0; i < nzones_default; i++) zone_default_index[i] = i; qSort(zone_default_index.begin(), zone_default_index.end(), zone_default_index_lessthan); QVector <int> zone_default_new(nzones_default); QVector <bool> zone_default_is_pct_new(nzones_default); QVector <QString> zone_default_name_new(nzones_default); QVector <QString> zone_default_desc_new(nzones_default); for (int i = 0; i < nzones_default; i++) { zone_default_new[i] = zone_default[zone_default_index[i]]; zone_default_is_pct_new[i] = zone_default_is_pct[zone_default_index[i]]; zone_default_name_new[i] = zone_default_name[zone_default_index[i]]; zone_default_desc_new[i] = zone_default_desc[zone_default_index[i]]; } for (int i = 0; i < nzones_default; i++) { zone_default[i] = zone_default_new[i]; zone_default_is_pct[i] = zone_default_is_pct_new[i]; zone_default_name[i] = zone_default_name_new[i]; zone_default_desc[i] = zone_default_desc_new[i]; } } // resolve undefined endpoints in ranges and zones for (int nr = 0; nr < ranges.size(); nr ++) { // clean up gaps or overlaps in zone ranges if (ranges[nr]->end.isNull()) ranges[nr]->end = (nr < ranges.size() - 1) ? ranges[nr + 1]->begin : date_infinity; else if ((nr < ranges.size() - 1) && (ranges[nr + 1]->begin != ranges[nr]->end) ) { append_to_warning(tr("Setting end date of range %1 " "to start date of range %2.\n"). arg(nr + 1). arg(nr + 2) ); ranges[nr]->end = ranges[nr + 1]->begin; } else if ((nr == ranges.size() - 1) && (ranges[nr]->end < QDate::currentDate()) ) { append_to_warning(tr("Extending final range %1 to infinite " "to include present date.\n").arg(nr + 1)); ranges[nr]->end = date_infinity; } if (ranges[nr]->zones.size()) { // check that the first zone starts with zero ranges[nr]->zones[0]->lo = 0; // resolve zone end powers for (int nz = 0; nz < ranges[nr]->zones.size(); nz ++) if (ranges[nr]->zones[nz]->hi == -1) ranges[nr]->zones[nz]->hi = (nz < ranges[nr]->zones.size() - 1) ? ranges[nr]->zones[nz + 1]->lo : INT_MAX; else if ((nz < ranges[nr]->zones.size() - 1) && (ranges[nr]->zones[nz]->hi != ranges[nr]->zones[nz + 1]->lo) ) { if (abs(ranges[nr]->zones[nz]->hi - ranges[nr]->zones[nz + 1]->lo) > 4) append_to_warning(tr("Range %1: matching top of zone %2 " "(%3) to bottom of zone %4 (%5).\n"). arg(nr + 1). arg(ranges[nr]->zones[nz]->name). arg(ranges[nr]->zones[nz]->hi). arg(ranges[nr]->zones[nz + 1]->name). arg(ranges[nr]->zones[nz + 1]->lo) ); ranges[nr]->zones[nz]->hi = ranges[nr]->zones[nz + 1]->lo; } else if ((nz == ranges[nr]->zones.size() - 1) && (ranges[nr]->zones[nz]->hi < INT_MAX) ) { append_to_warning(tr("Range %1: setting top of zone %2 from %3 to MAX.\n"). arg(nr + 1). arg(ranges[nr]->zones[nz]->name). arg(ranges[nr]->zones[nz]->hi) ); ranges[nr]->zones[nz]->hi = INT_MAX; } } fprintf(stderr, "sorted range %d: from %s to %s with %s zones\n", nr + 1, ranges[nr]->begin.isNull() ? "BEGIN" : ranges[nr]->begin.toString().toAscii().constData(), ranges[nr]->end.isNull() ? "END" : ranges[nr]->end.toString().toAscii().constData(), ranges[nr]->zonesSetFromCP ? "calculated" : "specified" ); } // mark zones as modified so pages which depend on zones can be updated modificationTime = QDateTime::currentDateTime(); return true; } // note empty dates are treated as automatic matches for begin or // end of range int Zones::whichRange(const QDate &date) const { int rnum = 0; QListIterator<ZoneRange*> i(ranges); while (i.hasNext()) { ZoneRange *range = i.next(); if (((date >= range->begin) || (range->begin.isNull())) && ((date < range->end) || (range->end.isNull())) ) return rnum; ++rnum; } return -1; } int Zones::numZones(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum]->zones.size(); } int Zones::whichZone(int rnum, double value) const { assert(rnum < ranges.size()); ZoneRange *range = ranges[rnum]; for (int j = 0; j < range->zones.size(); ++j) { ZoneInfo *info = range->zones[j]; // note: the "end" of range is actually in the next zone if ((value >= info->lo) && (value < info->hi)) return j; } return -1; } void Zones::zoneInfo(int rnum, int znum, QString &name, QString &description, int &low, int &high) const { assert(rnum < ranges.size()); ZoneRange *range = ranges[rnum]; assert(znum < range->zones.size()); ZoneInfo *zone = range->zones[znum]; name = zone->name; description = zone->desc; low = zone->lo; high = zone->hi; } int Zones::getCP(int rnum) const { assert(rnum < ranges.size()); return ranges[rnum]->cp; } void Zones::setCP(int rnum, int cp) { ranges[rnum]->cp = cp; modificationTime = QDateTime::currentDateTime(); } // generate a list of zones from CP int Zones::lowsFromCP(QList <int> *lows, int cp) { if (nzones_default == 0) initializeZoneParameters(); lows->clear(); for (int z = 0; z < nzones_default; z++) lows->append(zone_default_is_pct[z] ? zone_default[z] * cp / 100 : zone_default[z]); return nzones_default; } // access the zone name QString Zones::getDefaultZoneName(int z) { return zone_default_name[z]; } // access the zone description QString Zones::getDefaultZoneDesc(int z) { return zone_default_desc[z]; } // set the zones from the CP value (the cp variable) void Zones::setZonesFromCP(ZoneRange *range) { range->zones.clear(); if (nzones_default == 0) initializeZoneParameters(); for (int i = 0; i < nzones_default; i++) { int lo = zone_default_is_pct[i] ? zone_default[i] * range->cp / 100 : zone_default[i]; int hi = lo; ZoneInfo *zone = new ZoneInfo(zone_default_name[i], zone_default_desc[i], lo, hi); range->zones.append(zone); } // sort the zones (some may be pct, others absolute, so zones need to be sorted, // rather than the defaults qSort(range->zones.begin(), range->zones.end(), zoneptr_lessthan); // set zone end dates for (int i = 0; i < range->zones.size(); i ++) range->zones[i]->hi = (i < nzones_default - 1) ? range->zones[i + 1]->lo : INT_MAX; // mark that the zones were set from CP, so if zones are subsequently // written, only CP is saved range->zonesSetFromCP = true; } void Zones::setZonesFromCP(int rnum) { assert((rnum >= 0) && (rnum < ranges.size())); setZonesFromCP(ranges[rnum]); } // return the list of starting values of zones for a given range QList <int> Zones::getZoneLows(int rnum) { if (rnum >= ranges.size()) return QList <int>::QList(); ZoneRange *range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range->zones.size(); i ++) return_values.append(ranges[rnum]->zones[i]->lo); return return_values; } // return the list of ending values of zones for a given range QList <int> Zones::getZoneHighs(int rnum) { if (rnum >= ranges.size()) return QList <int>::QList(); ZoneRange *range = ranges[rnum]; QList <int> return_values; for (int i = 0; i < range->zones.size(); i ++) return_values.append(ranges[rnum]->zones[i]->hi); return return_values; } // return the list of zone names QList <QString> Zones::getZoneNames(int rnum) { if (rnum >= ranges.size()) return QList <QString>::QList(); ZoneRange *range = ranges[rnum]; QList <QString> return_values; for (int i = 0; i < range->zones.size(); i ++) return_values.append(ranges[rnum]->zones[i]->name); return return_values; } QString Zones::summarize(int rnum, QVector<double> &time_in_zone) const { assert(rnum < ranges.size()); ZoneRange *range = ranges[rnum]; assert(time_in_zone.size() == range->zones.size()); QString summary; if(range->cp > 0){ summary += "<table align=\"center\" width=\"70%\" border=\"0\">"; summary += "<tr><td align=\"center\">"; summary += tr("Critical Power: %1").arg(range->cp); summary += "</td></tr></table>"; } summary += "<table align=\"center\" width=\"70%\" "; summary += "border=\"0\">"; summary += "<tr>"; summary += "<td align=\"center\">Zone</td>"; summary += "<td align=\"center\">Description</td>"; summary += "<td align=\"center\">Low</td>"; summary += "<td align=\"center\">High</td>"; summary += "<td align=\"center\">Time</td>"; summary += "</tr>"; QColor color = QApplication::palette().alternateBase().color(); color = QColor::fromHsv(color.hue(), color.saturation() * 2, color.value()); for (int zone = 0; zone < time_in_zone.size(); ++zone) { if (time_in_zone[zone] > 0.0) { QString name, desc; int lo, hi; zoneInfo(rnum, zone, name, desc, lo, hi); if (zone % 2 == 0) summary += "<tr bgcolor='" + color.name() + "'>"; else summary += "<tr>"; summary += QString("<td align=\"center\">%1</td>").arg(name); summary += QString("<td align=\"center\">%1</td>").arg(desc); summary += QString("<td align=\"center\">%1</td>").arg(lo); if (hi == INT_MAX) summary += "<td align=\"center\">MAX</td>"; else summary += QString("<td align=\"center\">%1</td>").arg(hi); summary += QString("<td align=\"center\">%1</td>") .arg(time_to_string((unsigned) round(time_in_zone[zone]))); summary += "</tr>"; } } summary += "</table>"; return summary; } #define USE_SHORT_POWER_ZONES_FORMAT true /* whether a less redundent format should be used */ void Zones::write(QDir home) { QString strzones; // write the defaults if they were specified by the user if (defaults_from_user) { strzones += QString("DEFAULTS:\n"); for (int z = 0 ; z < nzones_default; z ++) strzones += QString("%1,%2,%3%4\n"). arg(zone_default_name[z]). arg(zone_default_desc[z]). arg(zone_default[z]). arg(zone_default_is_pct[z]?"%":""); strzones += QString("\n"); } for (int i = 0; i < ranges.size(); i++) { int cp = getCP(i); // print header for range // note this explicitly sets the first and last ranges such that all time is spanned #if USE_SHORT_POWER_ZONES_FORMAT if (i == 0) strzones += QString("BEGIN: CP=%1").arg(cp); else strzones += QString("%1: CP=%2").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp); strzones += QString("\n"); // step through and print the zones if they've been explicitly set if (! ranges[i]->zonesSetFromCP) { for (int j = 0; j < ranges[i]->zones.size(); j ++) strzones += QString("%1,%2,%3\n").arg(ranges[i]->zones[j]->name).arg(ranges[i]->zones[j]->desc).arg(ranges[i]->zones[j]->lo); strzones += QString("\n"); } #else if(ranges.size() <= 1) strzones += QString("FROM BEGIN UNTIL END, CP=%1:").arg(cp); else if (i == 0) strzones += QString("FROM BEGIN UNTIL %1, CP=%2:").arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp); else if (i == ranges.size() - 1) strzones += QString("FROM %1 UNTIL END, CP=%2:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(cp); else strzones += QString("FROM %1 UNTIL %2, CP=%3:").arg(getStartDate(i).toString("yyyy/MM/dd")).arg(getEndDate(i).toString("yyyy/MM/dd")).arg(cp); strzones += QString("\n"); for (int j = 0; j < ranges[i]->zones.size(); j ++) if (ranges[i]->zones[j]->hi == INT_MAX) strzones += QString("%1,%2,%3,MAX\n").arg(ranges[i]->zones[j]->name).arg(ranges[i]->zones[j]->desc).arg(ranges[i]->zones[j]->lo); else strzones += QString("%1,%2,%3,%4\n").arg(ranges[i]->zones[j]->name).arg(ranges[i]->zones[j]->desc).arg(ranges[i]->zones[j]->lo).arg(ranges[i]->zones[j]->hi); strzones += QString("\n"); #endif } QFile file(home.absolutePath() + "/power.zones"); if (file.open(QFile::WriteOnly)) { QTextStream stream(&file); stream << strzones; file.close(); } }