//////////////////////////////////////////////////////////////////////////////// // // Copyright 2015 - 2016, Paul Beckingham, Federico Hernandez. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included // in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. // // http://www.opensource.org/licenses/mit-license.php // //////////////////////////////////////////////////////////////////////////////// #include #include #include #include #include /* #include // TODO Remove. */ //////////////////////////////////////////////////////////////////////////////// // A filter is just another interval, containing start, end and tags. // // Supported interval forms: // ["from"] ["to"|"-" ] // ["from"] "for" // ["before"|"after" ] // "ago" // Interval getFilter (const CLI& cli) { Interval filter; std::string start; std::string end; std::string duration; std::vector args; for (auto& arg : cli._args) { if (arg.hasTag ("BINARY") || arg.hasTag ("CMD") || arg.hasTag ("EXT")) continue; if (arg.hasTag ("FILTER")) { auto canonical = arg.attribute ("canonical"); auto raw = arg.attribute ("raw"); if (arg.hasTag ("HINT")) { if (expandIntervalHint (canonical, start, end)) { args.push_back (""); args.push_back ("-"); args.push_back (""); } // Hints that are not expandable to a date range are ignored. } else if (arg._lextype == Lexer::Type::date) { if (start == "") start = raw; else if (end == "") end = raw; args.push_back (""); } else if (arg._lextype == Lexer::Type::duration) { if (duration == "") duration = raw; args.push_back (""); } else if (arg.hasTag ("KEYWORD")) { args.push_back (raw); } else { filter.tag (raw); } } } Range range; // if (args.size () == 1 && args[0] == "") { range.start = Datetime (start); range.end = Datetime (); } // from else if (args.size () == 2 && args[0] == "from" && args[1] == "") { range.start = Datetime (start); range.end = Datetime (); } // to/- else if (args.size () == 3 && args[0] == "" && (args[1] == "to" || args[1] == "-") && args[2] == "") { range.start = Datetime (start); range.end = Datetime (end); } // from/since to/- else if (args.size () == 4 && args[0] == "from" && args[1] == "" && (args[2] == "to" || args[2] == "-") && args[3] == "") { range.start = Datetime (start); range.end = Datetime (end); } // for else if (args.size () == 3 && args[0] == "" && args[1] == "for" && args[2] == "") { range.start = Datetime (start); range.end = Datetime (start) + Duration (duration).toTime_t (); } // from/since for else if (args.size () == 4 && (args[0] == "from" || args[0] == "since") && args[1] == "" && args[2] == "for" && args[3] == "") { range.start = Datetime (start); range.end = Datetime (start) + Duration (duration).toTime_t (); } // before else if (args.size () == 3 && args[0] == "" && args[1] == "before" && args[2] == "") { range.start = Datetime (start) - Duration (duration).toTime_t (); range.end = Datetime (start); } // after else if (args.size () == 3 && args[0] == "" && args[1] == "after" && args[2] == "") { range.start = Datetime (start); range.end = Datetime (start) + Duration (duration).toTime_t (); } // ago else if (args.size () == 2 && args[0] == "" && args[1] == "ago") { range.start = Datetime () - Duration (duration).toTime_t (); range.end = Datetime (); } // else if (args.size () == 1 && args[0] == "") { range.start = Datetime () - Duration (duration).toTime_t (); range.end = Datetime (); } // Unrecognized date range construct. else if (args.size ()) { throw std::string ("Unrecognized date range: '") + join (" ", args) + "'."; } filter.range = range; /* std::cout << "# getFilter:\n"; std::cout << "# " << filter.dump () << "\n"; */ return filter; } //////////////////////////////////////////////////////////////////////////////// // Read rules and extract all holiday definitions. Create a Range for each // one that spans from midnight to midnight. std::vector getHolidays (const Rules& rules) { std::vector results; for (auto& holiday : rules.all ("holidays.")) { auto lastDot = holiday.rfind ('.'); if (lastDot != std::string::npos) { Range r; Datetime d (holiday.substr (lastDot + 1), "Y_M_D"); r.start = d; ++d; r.end = d; results.push_back (r); } } /* std::cout << "# getHolidays:\n"; for (auto& h : results) std::cout << "# " << h.dump () << "\n"; */ return results; } //////////////////////////////////////////////////////////////////////////////// // [1] Read holiday definitions from the rules, extract their dates and create // a set of Range from them. // [2] For 'exc day ...' exclusions, separate into daysOn and daysOff sets, // based on whether the exclusion is additive. // [3] Treat daysOff as additional holidays. // [4] Subtract daysOn from the set of holidays. // [5] Take all the 'exc ...' exclusions and expand them into // concrete ranges within the overall range, adding them to the results. // // The result is the complete set of untrackable time that lies within the // input range. This will be a set of nights, weekends, holidays and lunchtimes. std::vector getAllExclusions ( const Rules& rules, const Range& range) { // Start with the set of all holidays, intersected with range. std::vector results; results = addRanges (range, results, getHolidays (rules)); auto exclusions = getExclusions (rules); // Find exclusions 'exc day on ' and remove from holidays. // Find exlcusions 'exc day off ' and add to holidays. std::vector daysOn; std::vector daysOff; for (auto& exclusion : exclusions) { if (exclusion.tokens ()[1] == "day") { if (exclusion.additive ()) for (auto& r : exclusion.ranges (range)) daysOn.push_back (r); else for (auto& r : exclusion.ranges (range)) daysOff.push_back (r); } } // daysOff are combined with existing holidays. results = addRanges (range, results, daysOff); // daysOn are subtracted from the existing holidays. results = subtractRanges (range, results, daysOn); // Expand all exclusions that are not 'exc day ...' into excluded ranges that // overlage with range. std::vector exclusionRanges; for (auto& exclusion : exclusions) if (exclusion.tokens ()[1] != "day") for (auto& r : exclusion.ranges (range)) exclusionRanges.push_back (r); /* auto all = addRanges (range, results, exclusionRanges); std::cout << "# getAllExclusions:\n"; for (auto& r : all) std::cout << "# " << r.dump () << "\n"; return all; */ return addRanges (range, results, exclusionRanges); } //////////////////////////////////////////////////////////////////////////////// std::vector getExclusions (const Rules& rules) { // Add exclusions from configuration. std::vector all; for (auto& name : rules.all ("exclusions.")) all.push_back (Exclusion (lowerCase (name), rules.get (name))); /* std::cout << "# getExclusions:\n"; for (auto& e : all) std::cout << "# " << e.dump () << "\n"; */ return all; } //////////////////////////////////////////////////////////////////////////////// std::vector getAllInclusions (Database& database) { std::vector all; for (auto& line : database.allLines ()) { Interval i; i.initialize (line); all.push_back (i); } /* std::cout << "# getAllInclusions:\n"; for (auto& i : all) std::cout << "# " << i.dump () << "\n"; */ return all; } //////////////////////////////////////////////////////////////////////////////// std::vector subset ( const Interval& filter, const std::vector & intervals) { std::vector all; for (auto& interval : intervals) if (matchesFilter (interval, filter)) all.push_back (interval); /* std::cout << "# subset (filter intervals):\n"; for (auto& i : all) std::cout << "# " << i.dump () << "\n"; */ return all; } //////////////////////////////////////////////////////////////////////////////// std::vector subset ( const Range& range, const std::vector & ranges) { std::vector all; for (auto& r : ranges) if (range.overlap (r)) all.push_back (r); /* std::cout << "# subset (ranges):\n"; for (auto& r : all) std::cout << "# " << r.dump () << "\n"; */ return all; } //////////////////////////////////////////////////////////////////////////////// std::vector subset ( const Range& range, const std::vector & intervals) { std::vector all; for (auto& interval : intervals) if (range.overlap (interval.range)) all.push_back (interval); /* std::cout << "# subset (intervals):\n"; for (auto& i : all) std::cout << "# " << i.dump () << "\n"; */ return all; } //////////////////////////////////////////////////////////////////////////////// std::vector collapse ( const Interval& interval, const std::vector & exclusions) { std::vector all; // Start with a single range from the interval, from which to subtract. std::vector pieces {interval.range}; for (auto& exclusion : exclusions) { std::vector split_pieces; for (auto& piece : pieces) { // If the exclusion is entirely within the piece, then collapse. if (exclusion.start > piece.start && (exclusion.end < piece.end || piece.end.toEpoch () == 0)) { for (auto& smaller_piece : piece.subtract (exclusion)) split_pieces.push_back (smaller_piece); } // If the exclusion merely overlap the piece, do nothing. This is because // tracked time start and end is not clipped, but recorded faithfully. else { split_pieces.push_back (piece); } } pieces = split_pieces; } // Return all the fragments as clipped intervals. for (auto& piece : pieces) all.push_back (clip (interval, piece)); /* std::cout << "# results:\n"; for (auto& i : all) std::cout << "# " << i.dump () << "\n"; */ return all; } //////////////////////////////////////////////////////////////////////////////// // Subset both ranges and additions by limits, and combine. std::vector addRanges ( const Range& limits, const std::vector & ranges, const std::vector & additions) { std::vector results; for (auto& range : ranges) if (limits.overlap (range)) results.push_back (range); for (auto& addition : additions) if (limits.overlap (addition)) results.push_back (addition); /* std::cout << "# addRange:\n"; for (auto& result : results) std::cout << "# " << result.dump () << "\n"; */ return results; } //////////////////////////////////////////////////////////////////////////////// // Subtract a set of Range from another set of Range, all within a defined // range. std::vector subtractRanges ( const Range& limits, const std::vector & ranges, const std::vector & subtractions) { if (! subtractions.size ()) return ranges; std::vector results; for (auto& r1 : ranges) for (auto& r2 : subtractions) for (auto& r3 : r1.subtract (r2)) results.push_back (limits.intersect (r3)); /* std::cout << "# addRange:\n"; for (auto& result : results) std::cout << "# " << result.dump () << "\n"; */ return results; } //////////////////////////////////////////////////////////////////////////////// // From a set of intervals, find the earliest start and the latest end, and // return these in a Range. Range outerRange (const std::vector & intervals) { Range outer; for (auto& interval : intervals) { if (interval.range.start < outer.start || outer.start.toEpoch () == 0) outer.start = interval.range.start; // Deliberately mixed start/end. if (interval.range.start > outer.end) outer.end = interval.range.start; if (interval.range.end > outer.end) outer.end = interval.range.end; if (! interval.range.ended ()) outer.end = Datetime (); } /* std::cout << "# outerRange " << outer.dump () << "\n"; */ return outer; } //////////////////////////////////////////////////////////////////////////////// // An interval matches a filter interval if the start/end overlaps, and all // filter interval tags are found in the interval. // // [1] interval.range.end.toEpoch () == 0 // [2] interval.range.end > filter.range.start // [3] filter.range.end.toEpoch () == 0 // [4] interval.range.start < filter.range.end // // Match: (1 || 2) && (3 || 4) // filter closed [--------) 1 2 3 4 5 6 result // -------------------------------------------------------------------------- // A [--------) . . 0 0 0 1 0 0 0 // B [--------) . 0 1 0 1 1 1 1 // C . [----) . 0 1 0 1 1 1 1 // D . [--------) 0 1 0 1 1 1 1 // E . . [--------) 0 1 0 0 1 0 0 // F [-------------) 0 1 0 1 1 1 1 // G [... . 1 0 0 1 1 1 1 // H . [... . 1 0 0 1 1 1 1 // I . . [... 1 0 0 0 1 0 0 // // // // filter open [... 1 2 3 4 5 6 result // -------------------------------------------------------------------------- // A [--------) . 0 0 1 0 0 1 0 // B [--------) 0 1 1 0 1 1 1 // C . [----) 0 1 1 0 1 1 1 // D . [--------) 0 1 1 0 1 1 1 // E . [--------) 0 1 1 0 1 1 1 // F [-------------) 0 1 1 0 1 1 1 // G [... 1 0 1 0 1 1 1 // H . [... 1 0 1 0 1 1 1 // I . [... 1 0 1 0 1 1 1 // bool matchesFilter (const Interval& interval, const Interval& filter) { if ((filter.range.start.toEpoch () == 0 && filter.range.end.toEpoch () == 0) || ((interval.range.end.toEpoch () == 0 || interval.range.end > filter.range.start) && (filter.range.end.toEpoch () == 0 || interval.range.start < filter.range.end))) { for (auto& tag : filter.tags ()) if (! interval.hasTag (tag)) return false; return true; } return false; } //////////////////////////////////////////////////////////////////////////////// // Take an interval and clip it to the range. Interval clip (const Interval& interval, const Range& range) { if (! range.started ()) return interval; Interval clipped {interval}; clipped.range = clipped.range.intersect (range); return clipped; } //////////////////////////////////////////////////////////////////////////////// std::vector getTrackedIntervals ( Database& database, const Rules& rules, Interval& filter) { auto inclusions = getAllInclusions (database); // Exclusions are only usable within a range, so if no filter range exists, // determine the outermost range of the inclusions, ie: // // [earliest start, latest end) // // Avoid assigning a zero-width range - leave it unstarted instead. if (! filter.range.started ()) { auto outer = outerRange (inclusions); if (outer.total ()) filter.range = outer; } // Get the set of expanded exclusions that overlap the range defined by the // timeline. If no range is defined, derive it from the set of all data. auto exclusions = getAllExclusions (rules, filter.range); std::vector intervals; for (auto& inclusion : subset (filter, inclusions)) for (auto& interval : collapse (clip (inclusion, filter.range), exclusions)) intervals.push_back (interval); return intervals; } //////////////////////////////////////////////////////////////////////////////// // Untracked time is that which is not excluded, and not filled. Gaps. std::vector getUntrackedRanges ( const Rules& rules) { std::vector gaps; // Get the set of expanded exclusions that overlap the range defined by the // timeline. If no range is defined, derive it from the set of all data. /* auto exclusions = getExcludedRanges (rules); */ // TODO subtract all exclusions // TODO subtract all inclusions return gaps; } //////////////////////////////////////////////////////////////////////////////// Interval getLatestInterval (Database& database) { Interval i; auto lastLine = database.lastLine (); if (lastLine != "") i.initialize (lastLine); return i; } ////////////////////////////////////////////////////////////////////////////////