diff --git a/.gitignore b/.gitignore index 6fa2f8f7..be64a71e 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ install_manifest.txt *.swp *.pyc *.cbp +build/ +_codeql_build_dir/ +_codeql_detected_source_root diff --git a/src/Exclusion.cpp b/src/Exclusion.cpp index 585ad8b4..eb6f645b 100644 --- a/src/Exclusion.cpp +++ b/src/Exclusion.cpp @@ -43,12 +43,16 @@ // -------------------------- ------------------------ // exclusions.days.2016_01_01 on // exclusions.days.2016_01_02 off +// exclusions.days.2018_01_05 off+14 // exclusions.friday <8:00 12:00-12:45 >17:30 // exclusions.monday <8:00 12:00-12:45 >17:30 // exclusions.thursday <8:00 12:00-12:45 >17:30 // exclusions.tuesday <8:00 12:00-12:45 >18:30 // exclusions.wednesday <8:00 12:00-13:30 >17:30 // +// The "off+N" / "on+N" value syntax for days entries defines a periodic +// exclusion recurring every N days starting from the given date. +// Exclusion::Exclusion (const std::string& name, const std::string& value) //void Exclusion::initialize (const std::string& line) { @@ -61,17 +65,32 @@ Exclusion::Exclusion (const std::string& name, const std::string& value) _tokens[0] == "exclusions") { if (_tokens.size () == 4 && - _tokens[1] == "days" && - _tokens[3] == "on") { - _additive = true; - return; - } - if (_tokens.size () == 4 && - _tokens[1] == "days" && - _tokens[3] == "off") + _tokens[1] == "days") { - _additive = false; - return; + auto& val = _tokens[3]; + + // Support periodic syntax: "on+N" or "off+N" + auto plus_pos = val.find ('+'); + if (plus_pos != std::string::npos) + { + auto period_str = val.substr (plus_pos + 1); + if (period_str.empty () || period_str.find_first_not_of ("0123456789") != std::string::npos) + throw format ("Invalid period in exclusion value: '{1}'.", value); + _period = std::stoi (period_str); + if (_period <= 0) + throw format ("Period in exclusion must be a positive integer: '{1}'.", value); + val = val.substr (0, plus_pos); + } + + if (val == "on") { + _additive = true; + return; + } + if (val == "off") + { + _additive = false; + return; + } } else if (Datetime::dayOfWeek (_tokens[1]) != -1) { @@ -96,6 +115,7 @@ std::vector Exclusion::tokens () const // and every block within // exc day on --> yields single day range // exc day off --> yields single day range +// exc day on/off (period>0) --> yields day ranges every period days // std::vector Exclusion::ranges (const Range& range) const { @@ -109,11 +129,52 @@ std::vector Exclusion::ranges (const Range& range) const auto day = _tokens[2]; std::replace (day.begin (), day.end (), '_', '-'); Datetime start (day); - Datetime end (start); - ++end; - Range all_day (start, end); - if (range.overlaps (all_day)) - results.push_back (all_day); + + if (_period > 0) + { + // Generate all occurrences within range that match start + k*period days. + const int64_t period_secs = static_cast (_period) * 86400; + auto startOfDay = [] (const Datetime& dt) + { + return Datetime (dt.year (), dt.month (), dt.day (), 0, 0, 0); + }; + + Datetime current = startOfDay (start); + + // Advance to first occurrence on or after range.start + if (current < range.start) + { + int64_t diff_secs = range.start.toEpoch () - current.toEpoch (); + int64_t periods = diff_secs / period_secs; + current += periods * period_secs; + // Normalize to start of day (handles DST edge cases) + current = startOfDay (current); + if (current < range.start) + { + current += period_secs; + current = startOfDay (current); + } + } + + while (current < range.end) + { + Datetime end = current; + ++end; + Range all_day (current, end); + if (range.overlaps (all_day)) + results.push_back (all_day); + current += period_secs; + current = startOfDay (current); + } + } + else + { + Datetime end (start); + ++end; + Range all_day (start, end); + if (range.overlaps (all_day)) + results.push_back (all_day); + } } else if ((dayOfWeek = Datetime::dayOfWeek (_tokens[1])) != -1) diff --git a/src/Exclusion.h b/src/Exclusion.h index bd12f2ed..8cd8bde1 100644 --- a/src/Exclusion.h +++ b/src/Exclusion.h @@ -47,6 +47,7 @@ class Exclusion private: std::vector _tokens {}; bool _additive {false}; + int _period {0}; }; #endif diff --git a/test/exclusion.t.cpp b/test/exclusion.t.cpp index 1b6e6146..4b231c53 100644 --- a/test/exclusion.t.cpp +++ b/test/exclusion.t.cpp @@ -32,7 +32,7 @@ //////////////////////////////////////////////////////////////////////////////// int main (int, char**) { - UnitTest t (261); + UnitTest t (282); try { @@ -391,6 +391,44 @@ int main (int, char**) t.is (ranges[0].end.toISOLocalExtended (), "2016-05-13T08:00:00", "Exclusion range[0].end() --> 2016-05-13T08:00:00"); t.is (ranges[1].start.toISOLocalExtended (), "2016-05-13T12:00:00", "Exclusion range[1].start() --> 2016-05-13T12:00:00"); t.is (ranges[1].end.toISOLocalExtended (), "2016-05-13T12:45:00", "Exclusion range[1].end() --> 2016-05-13T12:45:00"); + + // Periodic 'off+14': every 14 days starting from 2015-12-21. + // Occurrences within 2015-12-15 to 2016-01-15: 2015-12-21 and 2016-01-04. + Exclusion e12 ("exclusions.days.2015_12_21", "off+14"); + tokens = e12.tokens (); + t.ok (tokens.size () == 4, "Exclusion 'exclusions.days.2015_12_21 off+14' --> 4 tokens"); + t.is (tokens[0], "exclusions","Exclusion 'exclusions.days.2015_12_21 off+14' [0] --> 'exclusions'"); + t.is (tokens[1], "days", "Exclusion 'exclusions.days.2015_12_21 off+14' [1] --> 'days'"); + t.is (tokens[2], "2015_12_21","Exclusion 'exclusions.days.2015_12_21 off+14' [2] --> '2015_12_21'"); + t.is (tokens[3], "off", "Exclusion 'exclusions.days.2015_12_21 off+14' [3] --> 'off'"); + t.notok (e12.additive (), "Exclusion 'days off+14' --> !additive"); + + ranges = e12.ranges (r); + t.ok (ranges.size () == 2, "Exclusion periodic off+14 ranges --> [2]"); + t.is (ranges[0].start.toString ("Y-M-D"), "2015-12-21", "Exclusion periodic range[0].start() --> 2015-12-21"); + t.is (ranges[0].end.toString ("Y-M-D"), "2015-12-22", "Exclusion periodic range[0].end() --> 2015-12-22"); + t.is (ranges[1].start.toString ("Y-M-D"), "2016-01-04", "Exclusion periodic range[1].start() --> 2016-01-04"); + t.is (ranges[1].end.toString ("Y-M-D"), "2016-01-05", "Exclusion periodic range[1].end() --> 2016-01-05"); + + // Periodic 'off+7': start date before range.start. Verify first occurrence + // is the one that falls within the range. + // Start: 2015-12-14, period: 7 days. + // Occurrences in range: 2015-12-21, 2015-12-28, 2016-01-04, 2016-01-11. + Exclusion e13 ("exclusions.days.2015_12_14", "off+7"); + ranges = e13.ranges (r); + t.ok (ranges.size () == 4, "Exclusion periodic off+7 (before range) ranges --> [4]"); + t.is (ranges[0].start.toString ("Y-M-D"), "2015-12-21", "Exclusion periodic range[0].start() --> 2015-12-21"); + t.is (ranges[0].end.toString ("Y-M-D"), "2015-12-22", "Exclusion periodic range[0].end() --> 2015-12-22"); + t.is (ranges[1].start.toString ("Y-M-D"), "2015-12-28", "Exclusion periodic range[1].start() --> 2015-12-28"); + t.is (ranges[1].end.toString ("Y-M-D"), "2015-12-29", "Exclusion periodic range[1].end() --> 2015-12-29"); + t.is (ranges[2].start.toString ("Y-M-D"), "2016-01-04", "Exclusion periodic range[2].start() --> 2016-01-04"); + t.is (ranges[2].end.toString ("Y-M-D"), "2016-01-05", "Exclusion periodic range[2].end() --> 2016-01-05"); + t.is (ranges[3].start.toString ("Y-M-D"), "2016-01-11", "Exclusion periodic range[3].start() --> 2016-01-11"); + t.is (ranges[3].end.toString ("Y-M-D"), "2016-01-12", "Exclusion periodic range[3].end() --> 2016-01-12"); + + // Periodic 'on+14': verify additive flag is set correctly. + Exclusion e14 ("exclusions.days.2015_12_21", "on+14"); + t.ok (e14.additive (), "Exclusion 'days on+14' --> additive"); } catch (const std::string& e)