Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ install_manifest.txt
*.swp
*.pyc
*.cbp
build/
_codeql_build_dir/
_codeql_detected_source_root
91 changes: 76 additions & 15 deletions src/Exclusion.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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)
{
Expand All @@ -96,6 +115,7 @@ std::vector <std::string> Exclusion::tokens () const
// and every block within
// exc day on <date> --> yields single day range
// exc day off <date> --> yields single day range
// exc day on/off <date> (period>0) --> yields day ranges every period days
//
std::vector <Range> Exclusion::ranges (const Range& range) const
{
Expand All @@ -109,11 +129,52 @@ std::vector <Range> 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 <int64_t> (_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)
Expand Down
1 change: 1 addition & 0 deletions src/Exclusion.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Exclusion
private:
std::vector <std::string> _tokens {};
bool _additive {false};
int _period {0};
};

#endif
40 changes: 39 additions & 1 deletion test/exclusion.t.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
////////////////////////////////////////////////////////////////////////////////
int main (int, char**)
{
UnitTest t (261);
UnitTest t (282);

try
{
Expand Down Expand Up @@ -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)
Expand Down