diff --git a/Framework/Kernel/CMakeLists.txt b/Framework/Kernel/CMakeLists.txt index 49be5e3f5b3f..635d3fed4a49 100644 --- a/Framework/Kernel/CMakeLists.txt +++ b/Framework/Kernel/CMakeLists.txt @@ -1,5 +1,4 @@ set(SRC_FILES - src/SetValueWhenProperty.cpp src/ANN_complete.cpp src/ArrayBoundedValidator.cpp src/ArrayLengthValidator.cpp @@ -95,6 +94,7 @@ set(SRC_FILES src/ReadLock.cpp src/RebinParamsValidator.cpp src/RegexStrings.cpp + src/SetValueWhenProperty.cpp src/SingletonHolder.cpp src/SobolSequence.cpp src/StartsWithValidator.cpp @@ -107,6 +107,7 @@ set(SRC_FILES src/ThreadPool.cpp src/ThreadPoolRunnable.cpp src/ThreadSafeLogStream.cpp + src/TimeROI.cpp src/TimeSeriesProperty.cpp src/TimeSplitter.cpp src/Timer.cpp @@ -139,7 +140,6 @@ set(SRC_UNITY_IGNORE_FILES ) set(INC_FILES - inc/MantidKernel/SetValueWhenProperty.h inc/MantidKernel/ANN/ANN.h inc/MantidKernel/ANN/ANNperf.h inc/MantidKernel/ANN/ANNx.h @@ -265,6 +265,7 @@ set(INC_FILES inc/MantidKernel/RebinParamsValidator.h inc/MantidKernel/RegexStrings.h inc/MantidKernel/RegistrationHelper.h + inc/MantidKernel/SetValueWhenProperty.h inc/MantidKernel/SingletonHolder.h inc/MantidKernel/SobolSequence.h inc/MantidKernel/SpecialCoordinateSystem.h @@ -282,6 +283,7 @@ set(INC_FILES inc/MantidKernel/ThreadSafeLogStream.h inc/MantidKernel/ThreadScheduler.h inc/MantidKernel/ThreadSchedulerMutexes.h + inc/MantidKernel/TimeROI.h inc/MantidKernel/TimeSeriesProperty.h inc/MantidKernel/TimeSplitter.h inc/MantidKernel/Timer.h @@ -426,6 +428,7 @@ set(TEST_FILES ThreadPoolTest.h ThreadSchedulerMutexesTest.h ThreadSchedulerTest.h + TimeROITest.h TimeSeriesPropertyTest.h TimeSplitterTest.h TimerTest.h diff --git a/Framework/Kernel/inc/MantidKernel/TimeROI.h b/Framework/Kernel/inc/MantidKernel/TimeROI.h new file mode 100644 index 000000000000..e829dce9376b --- /dev/null +++ b/Framework/Kernel/inc/MantidKernel/TimeROI.h @@ -0,0 +1,55 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2022 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +#pragma once + +#include "MantidKernel/DllConfig.h" +#include "MantidKernel/TimeSeriesProperty.h" + +namespace Mantid { +namespace Kernel { + +namespace { +// const std::string NAME{"roi"}; +} + +/** TimeROI : Object that holds information about when the time measurement was active. + */ +class MANTID_KERNEL_DLL TimeROI { +public: + TimeROI(); + TimeROI(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime); + double durationInSeconds() const; + double durationInSeconds(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime) const; + std::size_t numBoundaries() const; + bool empty() const; + void addROI(const std::string &startTime, const std::string &stopTime); + void addROI(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime); + void addROI(const std::time_t &startTime, const std::time_t &stopTime); + void addMask(const std::string &startTime, const std::string &stopTime); + void addMask(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime); + void addMask(const std::time_t &startTime, const std::time_t &stopTime); + bool valueAtTime(const Types::Core::DateAndTime &time) const; + void update_union(const TimeROI &other); + void update_intersection(const TimeROI &other); + void removeRedundantEntries(); + bool operator==(const TimeROI &other) const; + void debugPrint() const; + +private: + std::vector getAllTimes(const TimeROI &other); + void replaceValues(const std::vector ×, const std::vector &values); + bool isCompletelyInROI(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime) const; + /** + * @brief m_roi private member that holds most of the information + * + * This handles the details of the ROI and guarantees that views into the underlying structure is sorted by time. + */ + TimeSeriesProperty m_roi; +}; + +} // namespace Kernel +} // namespace Mantid diff --git a/Framework/Kernel/src/TimeROI.cpp b/Framework/Kernel/src/TimeROI.cpp new file mode 100644 index 000000000000..fb8f05465d8c --- /dev/null +++ b/Framework/Kernel/src/TimeROI.cpp @@ -0,0 +1,376 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2022 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + + +#include // TODO REMOVE +#include + +#include "MantidKernel/Logger.h" +#include "MantidKernel/TimeROI.h" + +namespace Mantid { +namespace Kernel { + +using Mantid::Types::Core::DateAndTime; + +namespace { +/// static Logger definition +Logger g_log("TimeROI"); +/// the underlying property needs a name +const std::string NAME{"Kernel_TimeROI"}; + +const bool ROI_USE{true}; +const bool ROI_IGNORE{false}; + +/// @throws std::runtime_error if not in increasing order +void assert_increasing(const DateAndTime &startTime, const DateAndTime &stopTime) { + if (!bool(startTime < stopTime)) { + std::stringstream msg; + msg << startTime << " and " << stopTime << " are not in increasing order"; + throw std::runtime_error(msg.str()); + } +} + +/** + * This returns true if the first value is USE, the last value is IGNORE, and all the others alternate. + * The assumption is that if the values meet this criteria, there is no reason to try to reduce them because they are + * already unique and minimal number of values. + */ +bool valuesAreAlternating(const std::vector &values) { + const auto NUM_VALUES = values.size(); + // should be an even number of values + if (NUM_VALUES % 2 != 0) + return false; + + // the values must start with use and end with ignore + if ((values.front() == ROI_IGNORE) || (values.back() == ROI_USE)) + return false; + + // even entries should be use and odd should be ignore + for (size_t i = 0; i < NUM_VALUES; ++i) { + if (i % 2 == 0) { + if (values[i] == ROI_IGNORE) // even entries should be USE + return false; + } else { + if (values[i] == ROI_USE) { // odd entries should be IGNORE + return false; + } + } + } + return true; +} +} // namespace + +TimeROI::TimeROI() : m_roi{NAME} {} + +TimeROI::TimeROI(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime) : m_roi{NAME} { + this->addROI(startTime, stopTime); +} + +void TimeROI::addROI(const std::string &startTime, const std::string &stopTime) { + this->addROI(DateAndTime(startTime), DateAndTime(stopTime)); +} + +/** + * Add new region as a union. + */ +void TimeROI::addROI(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime) { + assert_increasing(startTime, stopTime); + if ((this->empty()) || (startTime > m_roi.lastTime()) || (stopTime < m_roi.firstTime())) { + // add in the new region + m_roi.addValue(startTime, ROI_USE); + m_roi.addValue(stopTime, ROI_IGNORE); + } else if (this->isCompletelyInROI(startTime, stopTime)) { + g_log.debug("TimeROI::addROI is already accounted for and being ignored"); + } else { + g_log.debug("TimeROI::addROI using union method"); + // add as an union with this + this->update_union(TimeROI(startTime, stopTime)); + } +} + +void TimeROI::addROI(const std::time_t &startTime, const std::time_t &stopTime) { + this->addROI(DateAndTime(startTime), DateAndTime(stopTime)); +} + +void TimeROI::addMask(const std::string &startTime, const std::string &stopTime) { + this->addMask(DateAndTime(startTime), DateAndTime(stopTime)); +} + +/** + * Remove a region that is already in use. + * + * This subtracts the intersection which means adding a mask to an empty area does nothing. This may leave redundant + * values in the ROI + */ +void TimeROI::addMask(const Types::Core::DateAndTime &startTime, const Types::Core::DateAndTime &stopTime) { + assert_increasing(startTime, stopTime); + + if (this->empty()) { + g_log.debug("TimeROI::addMask to an empty object is ignored"); + } else if ((startTime > m_roi.lastTime()) || (stopTime < m_roi.firstTime())) { + g_log.debug("TimeROI::addMask to ignored region"); + } else if ((startTime <= m_roi.firstTime()) && (stopTime >= m_roi.lastTime())) { + // the mask includes everything so remove all current values + this->m_roi.clear(); + } else if (isCompletelyInROI(startTime, stopTime)) { + g_log.debug("TimeROI::addMask cutting notch in existing ROI"); + // cutting a notch in an existing ROI + m_roi.addValue(startTime, ROI_IGNORE); + m_roi.addValue(stopTime, ROI_USE); + } else { + g_log.debug("TimeROI::addMask using intersection method"); + // create an ROI that is full possible range minus the mask then intersect it + TimeROI temp(std::min(m_roi.firstTime(), startTime), std::max(m_roi.lastTime(), stopTime)); + temp.m_roi.addValue(startTime, ROI_IGNORE); + temp.m_roi.addValue(stopTime, ROI_USE); + + this->update_intersection(temp); + } +} + +void TimeROI::addMask(const std::time_t &startTime, const std::time_t &stopTime) { + this->addMask(DateAndTime(startTime), DateAndTime(stopTime)); +} + +/** + * This method returns true if the entire region between startTime and stopTime is inside an existing ROI. + * If part of the supplied region is not covered this returns false. + */ +bool TimeROI::isCompletelyInROI(const Types::Core::DateAndTime &startTime, + const Types::Core::DateAndTime &stopTime) const { + // check if the region is in the overall window at all + if ((startTime > m_roi.lastTime()) || (stopTime < m_roi.firstTime())) + return false; + + // since the ROI should be alternating "use" and "ignore", see if the start and stop are within a single region + const auto × = m_roi.timesAsVector(); + const auto iterStart = std::lower_bound(times.cbegin(), times.cend(), startTime); + const auto iterStop = std::lower_bound(iterStart, times.cend(), stopTime); + // too far apart + if (std::distance(iterStart, iterStop) > 0) + return false; + + // the value at the start time should be "use" + return this->valueAtTime(startTime); +} + +/** + * This returns whether the time should be "used" (rather than ignored). + * Anything outside of the region of interest is ignored. + * + * The value is, essentially, whatever it was at the last recorded time before or equal to the one requested. + */ +bool TimeROI::valueAtTime(const DateAndTime &time) const { + if (this->empty() || time < m_roi.firstTime()) { + return ROI_IGNORE; + } else { + return m_roi.getSingleValue(time); + } +} + +/// get a list of all unique times. order is not guaranteed +std::vector TimeROI::getAllTimes(const TimeROI &other) { + + std::set times_set; + const auto times_lft = this->m_roi.timesAsVector(); + for (const auto time : times_lft) + times_set.insert(time); + const auto times_rgt = other.m_roi.timesAsVector(); + for (const auto time : times_rgt) + times_set.insert(time); + + // copy into the vector + std::vector times_all; + times_all.assign(times_set.begin(), times_set.end()); + + return times_all; +} + +void TimeROI::replaceValues(const std::vector ×, const std::vector &values) { + if (times.size() != values.size()) { + std::stringstream msg; + msg << "Times and Values are different size: " << times.size() << " != " << values.size(); + throw std::runtime_error(msg.str()); + } + + // remove all current values + this->m_roi.clear(); + + // see if everything to add is "IGNORE" + bool set_values = std::any_of(values.cbegin(), values.cend(), [](bool value) { return value; }); + + // set the values if there are any use regions + if (set_values) { + this->m_roi.addValues(times, values); + } +} + +/** + * Updates the TimeROI values with the union with another TimeROI. + * See https://en.wikipedia.org/wiki/Union_(set_theory) for more details + * + * This will remove redundant entries as a side-effect. + */ +void TimeROI::update_union(const TimeROI &other) { + // exit early if the two TimeROI are identical + if (*this == other) + return; + + // get rid of redundant entries before starting + this->removeRedundantEntries(); + // get a list of all unique times + std::vector times_all = getAllTimes(other); + + // calculate what values to add + std::vector additional_values(times_all.size()); + std::transform(times_all.begin(), times_all.end(), additional_values.begin(), [this, other](const DateAndTime &time) { + return bool(this->valueAtTime(time) || other.valueAtTime(time)); + }); + + // remove old values and replace with new ones + this->replaceValues(times_all, additional_values); + this->removeRedundantEntries(); +} + +/** + * Updates the TimeROI values with the intersection with another TimeROI. + * See https://en.wikipedia.org/wiki/Intersection for more details + * + * This will remove redundant entries as a side-effect. + */ +void TimeROI::update_intersection(const TimeROI &other) { + // exit early if the two TimeROI are identical + if (*this == other) + return; + + // get rid of redundant entries before starting + this->removeRedundantEntries(); + + // get a list of all unique times + std::vector times_all = getAllTimes(other); + + // calculate what values to add + std::vector additional_values(times_all.size()); + std::transform(times_all.begin(), times_all.end(), additional_values.begin(), [this, other](const DateAndTime &time) { + return bool(this->valueAtTime(time) && other.valueAtTime(time)); + }); + + // remove old values and replace with new ones + this->replaceValues(times_all, additional_values); + this->removeRedundantEntries(); +} + +/** + * Remove time/value pairs that are not necessary to describe the TimeROI + * - Sort the times/values + * - Remove values that are not needed (e.g. IGNORE followed by IGNORE) + * - Remove values that are overridden. Overridden values are ones where a new value was added at the same time, the + * last one added will be used. + */ +void TimeROI::removeRedundantEntries() { + if (this->numBoundaries() < 2) { + return; // nothing to do with zero or one elements + } + + // when an individual time has multiple values, use the last value added + m_roi.eliminateDuplicates(); + + // get a copy of the current roi + const auto values_old = m_roi.valuesAsVector(); + if (valuesAreAlternating(values_old)) { + // there is nothing more to do + return; + } + const auto times_old = m_roi.timesAsVector(); + const auto ORIG_SIZE = values_old.size(); + + // create new vector to put result into + std::vector values_new; + std::vector times_new; + + // skip ahead to first time that isn't ignore + // since before being in the ROI means ignore + std::size_t index_old = 0; + while (values_old[index_old] == ROI_IGNORE) { + index_old++; + } + // add the current location which will always start with use + values_new.push_back(ROI_USE); + times_new.push_back(times_old[index_old]); + index_old++; // advance past location just added + + // copy in values that aren't the same as the ones before them + for (; index_old < ORIG_SIZE; ++index_old) { + if (values_old[index_old] != values_old[index_old - 1]) { + values_new.push_back(values_old[index_old]); + times_new.push_back(times_old[index_old]); + } + } + + // update the member value if anything has changed + if (values_new.size() != ORIG_SIZE) + m_roi.replaceValues(times_new, values_new); +} + +bool TimeROI::operator==(const TimeROI &other) const { return this->m_roi == other.m_roi; } + +void TimeROI::debugPrint() const { + const auto values = m_roi.valuesAsVector(); + const auto times = m_roi.timesAsVector(); + for (std::size_t i = 0; i < values.size(); ++i) { + std::cout << i << ": " << times[i] << ", " << values[i] << std::endl; + } +} + +/** + * Duration of the whole TimeROI + */ +double TimeROI::durationInSeconds() const { + const auto ROI_SIZE = this->numBoundaries(); + if (ROI_SIZE == 0) { + return 0.; + } else if (m_roi.lastValue() == ROI_USE) { + return std::numeric_limits::infinity(); + } else { + const std::vector &values = m_roi.valuesAsVector(); + const std::vector × = m_roi.timesAsVectorSeconds(); + double total{0.}; + for (std::size_t i = 0; i < ROI_SIZE - 1; ++i) { + if (values[i]) + total += (times[i + 1] - times[i]); + } + + return total; + } +} + +/** + * Duration of the TimeROI between startTime and stopTime + */ +double TimeROI::durationInSeconds(const Types::Core::DateAndTime &startTime, + const Types::Core::DateAndTime &stopTime) const { + assert_increasing(startTime, stopTime); + if (stopTime <= m_roi.firstTime()) { // asking before ROI + return 0.; + } else if (startTime >= m_roi.lastTime()) { // asking after ROI + return 0.; + } else if ((startTime <= m_roi.firstTime()) && (stopTime >= m_roi.lastTime())) { // full range of ROI + return this->durationInSeconds(); + } else { // do the calculation + // the time requested is an intersection of start/stop time and this object + TimeROI temp{startTime, stopTime}; + temp.update_intersection(*this); + return temp.durationInSeconds(); + } +} + +std::size_t TimeROI::numBoundaries() const { return static_cast(m_roi.size()); } + +bool TimeROI::empty() const { return bool(this->numBoundaries() == 0); } + +} // namespace Kernel +} // namespace Mantid diff --git a/Framework/Kernel/src/TimeSeriesProperty.cpp b/Framework/Kernel/src/TimeSeriesProperty.cpp index 5660946e3d1f..0c9b27bb993a 100644 --- a/Framework/Kernel/src/TimeSeriesProperty.cpp +++ b/Framework/Kernel/src/TimeSeriesProperty.cpp @@ -1901,8 +1901,9 @@ template void TimeSeriesProperty::eliminateDuplicates() { countSize(); // 3. Finish - g_log.warning() << "Log " << this->name() << " has " << numremoved << " entries removed due to duplicated time. " - << "\n"; + if (numremoved > 0) + g_log.notice() << "Log " << this->name() << " has " << numremoved << " entries removed due to duplicated time. " + << "\n"; } /* diff --git a/Framework/Kernel/test/TimeROITest.h b/Framework/Kernel/test/TimeROITest.h new file mode 100644 index 000000000000..24acf13a5ff0 --- /dev/null +++ b/Framework/Kernel/test/TimeROITest.h @@ -0,0 +1,279 @@ +// Mantid Repository : https://github.com/mantidproject/mantid +// +// Copyright © 2022 ISIS Rutherford Appleton Laboratory UKRI, +// NScD Oak Ridge National Laboratory, European Spallation Source, +// Institut Laue - Langevin & CSNS, Institute of High Energy Physics, CAS +// SPDX - License - Identifier: GPL - 3.0 + +#pragma once + +#include + +#include "MantidKernel/TimeROI.h" + +using Mantid::Kernel::TimeROI; +using Mantid::Types::Core::DateAndTime; + +constexpr double ONE_DAY_DURATION{24 * 3600}; + +const std::string DECEMBER_START("2022-12-01T00:01"); +const std::string DECEMBER_STOP("2023-01-01T00:01"); +const TimeROI DECEMBER{DateAndTime(DECEMBER_START), DateAndTime(DECEMBER_STOP)}; + +const std::string HANUKKAH_START("2022-12-19T00:01"); +const std::string HANUKKAH_STOP("2022-12-26T00:01"); +constexpr double HANUKKAH_DURATION{7. * ONE_DAY_DURATION}; + +const std::string CHRISTMAS_START("2022-12-25T00:01"); +const std::string CHRISTMAS_STOP("2022-12-26T00:01"); // same as HANUKKAH_STOP +const TimeROI CHRISTMAS{CHRISTMAS_START, CHRISTMAS_STOP}; + +const std::string NEW_YEARS_START("2022-12-31T00:01"); +const std::string NEW_YEARS_STOP("2023-01-01T00:01"); + +class TimeROITest : public CxxTest::TestSuite { +public: + // This pair of boilerplate methods prevent the suite being created statically + // This means the constructor isn't called when running other tests + static TimeROITest *createSuite() { return new TimeROITest(); } + static void destroySuite(TimeROITest *suite) { delete suite; } + + void test_emptyROI() { + TimeROI value; + TS_ASSERT_EQUALS(value.durationInSeconds(), 0.); + TS_ASSERT(value.empty()); + TS_ASSERT_EQUALS(value.numBoundaries(), 0); + value.removeRedundantEntries(); + } + + void test_badRegions() { + TimeROI value; + TS_ASSERT_THROWS(value.addROI(NEW_YEARS_STOP, NEW_YEARS_START), const std::runtime_error &); + TS_ASSERT_EQUALS(value.numBoundaries(), 0); + TS_ASSERT_THROWS(value.addMask(NEW_YEARS_STOP, NEW_YEARS_START), const std::runtime_error &); + TS_ASSERT_EQUALS(value.numBoundaries(), 0); + } + + void test_durations() { + TimeROI value{HANUKKAH_START, HANUKKAH_STOP}; + + // verify the full duration + TS_ASSERT_EQUALS(value.durationInSeconds(), HANUKKAH_DURATION); + TS_ASSERT_EQUALS(value.durationInSeconds(HANUKKAH_START, HANUKKAH_STOP), HANUKKAH_DURATION); + + // window parameter order matters + TS_ASSERT_THROWS(value.durationInSeconds(HANUKKAH_STOP, HANUKKAH_START), const std::runtime_error &); + + // window entirely outside of TimeROI gives zero + TS_ASSERT_EQUALS(value.durationInSeconds(DECEMBER_START, HANUKKAH_START), 0.); + TS_ASSERT_EQUALS(value.durationInSeconds(HANUKKAH_STOP, NEW_YEARS_STOP), 0.); + + // from the beginning + TS_ASSERT_EQUALS(value.durationInSeconds(DECEMBER_START, CHRISTMAS_START) / ONE_DAY_DURATION, 6.); + TS_ASSERT_EQUALS(value.durationInSeconds(HANUKKAH_START, CHRISTMAS_START) / ONE_DAY_DURATION, 6.); + + // past the end + TS_ASSERT_EQUALS(value.durationInSeconds(CHRISTMAS_START, HANUKKAH_STOP) / ONE_DAY_DURATION, 1.); + TS_ASSERT_EQUALS(value.durationInSeconds(CHRISTMAS_START, NEW_YEARS_STOP) / ONE_DAY_DURATION, 1.); + } + + void test_sortedROI() { + TimeROI value; + // add Hanukkah + value.addROI(HANUKKAH_START, HANUKKAH_STOP); + TS_ASSERT_EQUALS(value.durationInSeconds(), HANUKKAH_DURATION); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + TS_ASSERT(!value.empty()); + + // add New Year's eve + value.addROI(NEW_YEARS_START, NEW_YEARS_STOP); + TS_ASSERT_EQUALS(value.durationInSeconds(), HANUKKAH_DURATION + ONE_DAY_DURATION); + TS_ASSERT_EQUALS(value.numBoundaries(), 4); + + // add Christmas - fully contained in existing TimeROI + value.addROI(CHRISTMAS_START, CHRISTMAS_STOP); + TS_ASSERT_EQUALS(value.durationInSeconds(), HANUKKAH_DURATION + ONE_DAY_DURATION); + TS_ASSERT_EQUALS(value.numBoundaries(), 4); + + // get rid of entries that have no effect + value.removeRedundantEntries(); + TS_ASSERT_EQUALS(value.numBoundaries(), 4); + } + + void test_addOverlapping() { + const DateAndTime ONE("2023-01-01T00:01"); + const DateAndTime TWO("2023-01-02T00:01"); + const DateAndTime THREE("2023-01-03T00:01"); + const DateAndTime FOUR("2023-01-04T00:01"); + const DateAndTime FIVE("2023-01-05T00:01"); + + TimeROI value{ONE, FOUR}; // 1-4 + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 3.); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + // extend one day past the end is 1-5 + value.addROI(THREE, FIVE); + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 4.); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + // add in time from the middle is still 1-5 + value.addROI(TWO, THREE); + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 4.); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + // now remove regions + value.addMask(TWO, THREE); // 1-2, 3-5 is left + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 3.); + TS_ASSERT_EQUALS(value.numBoundaries(), 4); + + value.addMask(TWO, FOUR); // 1-2, 4-5 is left + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 2.); + TS_ASSERT_EQUALS(value.numBoundaries(), 4); + + value.addMask(THREE, FIVE); // 1-2 is left + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 1.); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + // remove the rest + value.addMask(ONE, FOUR); + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 0.); + TS_ASSERT(value.empty()); + + // add back an ROI then remove parts until nothing is left + value.addROI(TWO, FIVE); // 2-5 + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 3.); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + value.addMask(ONE, THREE); // 3-5 + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 2.); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + value.addMask(ONE, FOUR); // 4-5 + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 1.); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + value.addMask(FOUR, FIVE); // empty + TS_ASSERT_EQUALS(value.durationInSeconds() / ONE_DAY_DURATION, 0.); + TS_ASSERT(value.empty()); + } + + void test_redundantValues() { + TimeROI value; + value.addROI(CHRISTMAS_START, CHRISTMAS_STOP); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + value.addROI(CHRISTMAS_START, CHRISTMAS_STOP); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + value.removeRedundantEntries(); + } + + void test_reversesortedROI() { + TimeROI value; + // add New Year's eve + value.addROI(DateAndTime(NEW_YEARS_START), DateAndTime(NEW_YEARS_STOP)); + TS_ASSERT_EQUALS(value.durationInSeconds(), ONE_DAY_DURATION); + TS_ASSERT_EQUALS(value.numBoundaries(), 2); + + // add Hanukkah + value.addROI(HANUKKAH_START, HANUKKAH_STOP); + TS_ASSERT_EQUALS(value.durationInSeconds(), ONE_DAY_DURATION + HANUKKAH_DURATION); + TS_ASSERT_EQUALS(value.numBoundaries(), 4); + } + + void test_onlyMask() { + TimeROI value; + // the result is empty + value.addMask(DateAndTime(NEW_YEARS_START), DateAndTime(NEW_YEARS_STOP)); + TS_ASSERT(value.empty()); + + // since it ends with "on" the duration is infinite + TS_ASSERT_EQUALS(value.durationInSeconds(), 0.); + value.removeRedundantEntries(); + TS_ASSERT(value.empty()); + } + + void test_overwrite() { + // mask first + TimeROI value1; + value1.addMask(DateAndTime(NEW_YEARS_START), DateAndTime(NEW_YEARS_STOP)); + // since it ends with "on" the duration is infinite + TS_ASSERT_EQUALS(value1.durationInSeconds(), 0.); + TS_ASSERT(value1.empty()); + + value1.addROI(DateAndTime(NEW_YEARS_START), DateAndTime(NEW_YEARS_STOP)); + TS_ASSERT_EQUALS(value1.durationInSeconds(), ONE_DAY_DURATION); + value1.removeRedundantEntries(); + TS_ASSERT_EQUALS(value1.numBoundaries(), 2); + + // roi first + TimeROI value2; + value2.addROI(DateAndTime(NEW_YEARS_START), DateAndTime(NEW_YEARS_STOP)); + TS_ASSERT_EQUALS(value2.durationInSeconds(), ONE_DAY_DURATION); + TS_ASSERT_EQUALS(value2.numBoundaries(), 2); + + value2.addMask(DateAndTime(NEW_YEARS_START), DateAndTime(NEW_YEARS_STOP)); + TS_ASSERT_EQUALS(value2.durationInSeconds(), 0.); + value2.removeRedundantEntries(); + TS_ASSERT_EQUALS(value2.numBoundaries(), 0); + } + + void test_valueAtTime() { + TS_ASSERT_EQUALS(DECEMBER.valueAtTime(DECEMBER_STOP), 0); + TS_ASSERT_EQUALS(DECEMBER.valueAtTime(CHRISTMAS_START), 1); + TS_ASSERT_EQUALS(DECEMBER.valueAtTime(DECEMBER_START), 1); + } + + void runIntersectionTest(const TimeROI &left, const TimeROI &right, const double exp_duration) { + // left intersecting with right + TimeROI one(left); + one.update_intersection(right); + TS_ASSERT_EQUALS(one.durationInSeconds() / ONE_DAY_DURATION, exp_duration / ONE_DAY_DURATION); + + // right intersecting with left + TimeROI two(right); + two.update_intersection(left); + TS_ASSERT_EQUALS(two.durationInSeconds() / ONE_DAY_DURATION, exp_duration / ONE_DAY_DURATION); + + // the values should be identical + TS_ASSERT_EQUALS(one, two); + } + + void test_intersection_same_date() { runIntersectionTest(CHRISTMAS, CHRISTMAS, CHRISTMAS.durationInSeconds()); } + + void test_intersection_full_overlap() { runIntersectionTest(DECEMBER, CHRISTMAS, CHRISTMAS.durationInSeconds()); } + + void test_intersection_partial_overlap() { + TimeROI left{HANUKKAH_START, NEW_YEARS_START}; + TimeROI right{HANUKKAH_STOP, NEW_YEARS_STOP}; + runIntersectionTest(left, right, 5. * ONE_DAY_DURATION); + } + + void test_intersection_no_overlap() { runIntersectionTest(CHRISTMAS, TimeROI{NEW_YEARS_START, NEW_YEARS_STOP}, 0.); } + + void runUnionTest(const TimeROI &left, const TimeROI &right, const double exp_duration) { + // left union with right + TimeROI one(left); + one.update_union(right); + TS_ASSERT_EQUALS(one.durationInSeconds() / ONE_DAY_DURATION, exp_duration / ONE_DAY_DURATION); + + // right union with left + TimeROI two(right); + two.update_union(left); + TS_ASSERT_EQUALS(two.durationInSeconds() / ONE_DAY_DURATION, exp_duration / ONE_DAY_DURATION); + + // the values should be identical + TS_ASSERT_EQUALS(one, two); + } + + void test_union_same_date() { runUnionTest(CHRISTMAS, CHRISTMAS, CHRISTMAS.durationInSeconds()); } + + void test_union_full_overlap() { runUnionTest(DECEMBER, CHRISTMAS, DECEMBER.durationInSeconds()); } + + void test_union_partial_overlap() { + TimeROI left{HANUKKAH_START, NEW_YEARS_START}; + TimeROI right{HANUKKAH_STOP, NEW_YEARS_STOP}; + runUnionTest(left, right, 13. * ONE_DAY_DURATION); + } + + void test_union_no_overlap() { + runUnionTest(CHRISTMAS, TimeROI{NEW_YEARS_START, NEW_YEARS_STOP}, 2. * ONE_DAY_DURATION); + } +};