diff --git a/CMakeLists.txt b/CMakeLists.txt index d8336be..5073cc5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -120,6 +120,7 @@ set(SOURCE_FILES src/logging.cpp src/Timed.cpp src/TimeRange.cpp + src/Timeline.cpp ) add_executable(rhubarb ${SOURCE_FILES}) target_link_libraries(rhubarb ${Boost_LIBRARIES} cppFormat sphinxbase pocketSphinx) @@ -129,7 +130,11 @@ target_compile_options(rhubarb PUBLIC ${enableWarningsFlags}) #include_directories("${gtest_SOURCE_DIR}/include") set(TEST_FILES tests/stringToolsTests.cpp + tests/TimelineTests.cpp src/stringTools.cpp + src/Timeline.cpp + src/TimeRange.cpp + src/centiseconds.cpp ) add_executable(runTests ${TEST_FILES}) target_link_libraries(runTests gtest gmock gmock_main) diff --git a/src/TimeRange.cpp b/src/TimeRange.cpp index 00b09f1..98b075f 100644 --- a/src/TimeRange.cpp +++ b/src/TimeRange.cpp @@ -1,21 +1,45 @@ #include "TimeRange.h" #include +#include -TimeRange::TimeRange(centiseconds start, centiseconds end) : +using time_type = TimeRange::time_type; + +TimeRange::TimeRange(time_type start, time_type end) : start(start), end(end) { - if (start > end) throw std::invalid_argument("start must not be less than end."); + if (start > end) throw std::invalid_argument("Start must not be less than end."); } -centiseconds TimeRange::getStart() const { +time_type TimeRange::getStart() const { return start; } -centiseconds TimeRange::getEnd() const { +time_type TimeRange::getEnd() const { return end; } -centiseconds TimeRange::getLength() const { +time_type TimeRange::getLength() const { return end - start; } + +void TimeRange::resize(const TimeRange& newRange) { + start = newRange.start; + end = newRange.end; +} + +void TimeRange::resize(time_type start, time_type end) { + resize(TimeRange(start, end)); +} + +bool TimeRange::operator==(const TimeRange& rhs) const { + return start == rhs.start && end == rhs.end; +} + +bool TimeRange::operator!=(const TimeRange& rhs) const { + return !operator==(rhs); +} + +std::ostream& operator<<(std::ostream& stream, const TimeRange& timeRange) { + return stream << "TimeRange(" << timeRange.getStart() << ", " << timeRange.getEnd() << ")"; +} diff --git a/src/TimeRange.h b/src/TimeRange.h index c05aced..a0f546a 100644 --- a/src/TimeRange.h +++ b/src/TimeRange.h @@ -3,11 +3,26 @@ class TimeRange { public: - TimeRange(centiseconds start, centiseconds end); - centiseconds getStart() const; - centiseconds getEnd() const; - centiseconds getLength() const; + using time_type = centiseconds; + TimeRange(time_type start, time_type end); + TimeRange(const TimeRange&) = default; + TimeRange(TimeRange&&) = default; + + TimeRange& operator=(const TimeRange&) = default; + TimeRange& operator=(TimeRange&&) = default; + + time_type getStart() const; + time_type getEnd() const; + time_type getLength() const; + + void resize(const TimeRange& newRange); + void resize(time_type start, time_type end); + + bool operator==(const TimeRange& rhs) const; + bool operator!=(const TimeRange& rhs) const; private: - const centiseconds start, end; + time_type start, end; }; + +std::ostream& operator<<(std::ostream& stream, const TimeRange& timeRange); diff --git a/src/Timed.h b/src/Timed.h index 62372ba..0358b96 100644 --- a/src/Timed.h +++ b/src/Timed.h @@ -1,19 +1,52 @@ #pragma once #include +#include template class Timed : public TimeRange { public: - Timed(centiseconds start, centiseconds end, TValue value) : + Timed(time_type start, time_type end, TValue value) : TimeRange(start, end), value(value) {} + Timed(TimeRange timeRange, TValue value) : + TimeRange(timeRange), + value(value) + {} + + Timed(const Timed&) = default; + Timed(Timed&&) = default; + + Timed& operator=(const Timed&) = default; + Timed& operator=(Timed&&) = default; + + TValue& getValue() { + return value; + } + const TValue& getValue() const { return value; } + void setValue(TValue value) { + this->value = value; + } + + bool operator==(const Timed& rhs) const { + return TimeRange::operator==(rhs) && value == rhs.value; + } + + bool operator!=(const Timed& rhs) const { + return !operator==(rhs); + } + private: - const TValue value; + TValue value; }; + +template +std::ostream& operator<<(std::ostream& stream, const Timed& timedValue) { + return stream << "Timed(" << timedValue.getStart() << ", " << timedValue.getEnd() << ", " << timedValue.getValue() << ")"; +} diff --git a/src/Timeline.cpp b/src/Timeline.cpp new file mode 100644 index 0000000..6e59b2c --- /dev/null +++ b/src/Timeline.cpp @@ -0,0 +1 @@ +#include "Timeline.h" diff --git a/src/Timeline.h b/src/Timeline.h new file mode 100644 index 0000000..1358378 --- /dev/null +++ b/src/Timeline.h @@ -0,0 +1,250 @@ +#pragma once +#include "Timed.h" +#include +#include + +template +class Timeline { +public: + using time_type = TimeRange::time_type; + +private: + struct compare { + bool operator()(const Timed& lhs, const Timed& rhs) const { + return lhs.getStart() < rhs.getStart(); + } + bool operator()(const time_type& lhs, const Timed& rhs) const { + return lhs < rhs.getStart(); + } + using is_transparent = int; + }; + +public: + using set_type = std::set, compare>; + using const_iterator = typename set_type::const_iterator; + using iterator = const_iterator; + using reverse_iterator = typename set_type::reverse_iterator; + using size_type = size_t; + using value_type = Timed; + + class reference { + public: + using const_reference = const T&; + + operator const_reference() const { + return timeline.get(time).getValue(); + } + + reference& operator=(const T& value) { + timeline.set(time, time + time_type(1), value); + return *this; + } + + private: + friend class Timeline; + + reference(Timeline& timeline, time_type time) : + timeline(timeline), + time(time) + {} + + Timeline& timeline; + time_type time; + }; + + explicit Timeline(const Timed timedValue) : + elements(), + range(timedValue) + { + if (timedValue.getLength() != time_type::zero()) { + elements.insert(timedValue); + } + }; + + explicit Timeline(const TimeRange& timeRange, const T& value = T()) : + Timeline(Timed(timeRange, value)) + { } + + Timeline(time_type start, time_type end, const T& value = T()) : + Timeline(Timed(start, end, value)) + {} + + template + Timeline(InputIterator first, InputIterator last, const T& value = T()) : + Timeline(getRange(first, last), value) + { + for (auto it = first; it != last; ++it) { + set(*it); + } + } + + explicit Timeline(std::initializer_list> initializerList, const T& value = T()) : + Timeline(initializerList.begin(), initializerList.end(), value) + {} + + bool empty() const { + return elements.empty(); + } + + size_type size() const { + return elements.size(); + } + + const TimeRange& getRange() const { + return range; + } + + iterator begin() const { + return elements.begin(); + } + + iterator end() const { + return elements.end(); + } + + reverse_iterator rbegin() const { + return elements.rbegin(); + } + + reverse_iterator rend() const { + return elements.rend(); + } + + iterator find(time_type time) const { + if (time < range.getStart() || time >= range.getEnd()) { + return elements.end(); + } + + iterator it = elements.upper_bound(time); + --it; + return it; + } + + const Timed& get(time_type time) const { + iterator it = find(time); + if (it == elements.end()) { + throw std::invalid_argument("Argument out of range."); + } + return *it; + } + + iterator set(Timed timedValue) { + // Make sure the timed value overlaps with our range + if (timedValue.getEnd() <= range.getStart() || timedValue.getStart() >= range.getEnd()) { + return elements.end(); + } + + // Make sure the timed value is not empty + if (timedValue.getLength() == time_type::zero()) { + return elements.end(); + } + + // Trim the timed value to our range + timedValue.resize( + std::max(timedValue.getStart(), range.getStart()), + std::min(timedValue.getEnd(), range.getEnd())); + + // Extend the timed value if it touches elements with equal value + bool isFlushLeft = timedValue.getStart() == range.getStart(); + if (!isFlushLeft) { + iterator elementBefore = find(timedValue.getStart() - time_type(1)); + if (elementBefore->getValue() == timedValue.getValue()) { + timedValue.resize(elementBefore->getStart(), timedValue.getEnd()); + } + } + bool isFlushRight = timedValue.getEnd() == range.getEnd(); + if (!isFlushRight) { + iterator elementAfter = find(timedValue.getEnd()); + if (elementAfter->getValue() == timedValue.getValue()) { + timedValue.resize(timedValue.getStart(), elementAfter->getEnd()); + } + } + + // Split overlapping elements + splitAt(timedValue.getStart()); + splitAt(timedValue.getEnd()); + + // Erase overlapping elements + elements.erase(find(timedValue.getStart()), find(timedValue.getEnd())); + + // Add timed value + return elements.insert(timedValue).first; + } + + iterator set(const TimeRange& timeRange, const T& value) { + return set(Timed(timeRange, value)); + } + + iterator set(time_type start, time_type end, const T& value) { + return set(Timed(start, end, value)); + } + + reference operator[](time_type time) { + if (time < range.getStart() || time >= range.getEnd()) { + throw std::invalid_argument("Argument out of range."); + } + return reference(*this, time); + } + + // ReSharper disable once CppConstValueFunctionReturnType + const reference operator[](time_type time) const { + return reference(*this, time); + } + + Timeline(const Timeline&) = default; + Timeline(Timeline&&) = default; + Timeline& operator=(const Timeline&) = default; + Timeline& operator=(Timeline&&) = default; + + bool operator==(const Timeline& rhs) const { + return range == rhs.range && elements == rhs.elements; + } + + bool operator!=(const Timeline& rhs) const { + return !operator==(rhs); + } + +private: + template + static TimeRange getRange(InputIterator first, InputIterator last) { + if (first == last) { + return TimeRange(time_type::zero(), time_type::zero()); + } + + time_type start = time_type::max(); + time_type end = time_type::min(); + for (auto it = first; it != last; ++it) { + start = std::min(start, it->getStart()); + end = std::max(end, it->getEnd()); + } + return TimeRange(start, end); + } + + void splitAt(time_type splitTime) { + if (splitTime == range.getStart() || splitTime == range.getEnd()) return; + + iterator elementBefore = find(splitTime - time_type(1)); + iterator elementAfter = find(splitTime); + if (elementBefore != elementAfter) return; + + Timed tmp = *elementBefore; + elements.erase(elementBefore); + elements.insert(Timed(tmp.getStart(), splitTime, tmp.getValue())); + elements.insert(Timed(splitTime, tmp.getEnd(), tmp.getValue())); + } + + set_type elements; + TimeRange range; +}; + +template +std::ostream& operator<<(std::ostream& stream, const Timeline& timeline) { + stream << "Timeline{"; + bool isFirst = true; + for (auto element : timeline) { + if (!isFirst) stream << ", "; + isFirst = false; + stream << element; + } + return stream << "}"; +} diff --git a/src/centiseconds.cpp b/src/centiseconds.cpp index ce14bd7..9e7d978 100644 --- a/src/centiseconds.cpp +++ b/src/centiseconds.cpp @@ -1,4 +1,3 @@ -#include #include #include #include "Centiseconds.h" @@ -6,4 +5,3 @@ std::ostream& operator <<(std::ostream& stream, const centiseconds cs) { return stream << cs.count() << "cs"; } - diff --git a/tests/TimelineTests.cpp b/tests/TimelineTests.cpp new file mode 100644 index 0000000..a38e912 --- /dev/null +++ b/tests/TimelineTests.cpp @@ -0,0 +1,239 @@ +#include +#include "Timeline.h" +#include +#include + +using namespace testing; +using cs = centiseconds; +using std::vector; + +TEST(Timeline, constructors_initializeState) { + EXPECT_THAT( + Timeline(Timed(cs(10), cs(30), 42)), + ElementsAre(Timed(cs(10), cs(30), 42)) + ); + EXPECT_THAT( + Timeline(TimeRange(cs(10), cs(30)), 42), + ElementsAre(Timed(cs(10), cs(30), 42)) + ); + EXPECT_THAT( + Timeline(cs(10), cs(30), 42), + ElementsAre(Timed(cs(10), cs(30), 42)) + ); + auto args = { + Timed(cs(-10), cs(30), 1), + Timed(cs(10), cs(40), 2), + Timed(cs(50), cs(60), 3) + }; + auto expected = { + Timed(cs(-10), cs(10), 1), + Timed(cs(10), cs(40), 2), + Timed(cs(40), cs(50), 42), + Timed(cs(50), cs(60), 3) + }; + EXPECT_THAT( + Timeline(args.begin(), args.end(), 42), + ElementsAreArray(expected) + ); + EXPECT_THAT( + Timeline(args, 42), + ElementsAreArray(expected) + ); +} + +TEST(Timeline, constructors_throwForInvalidArgs) { + EXPECT_THROW( + Timeline(cs(10), cs(9)), + std::invalid_argument + ); +} + +TEST(Timeline, empty) { + Timeline empty1{}; + EXPECT_TRUE(empty1.empty()); + EXPECT_THAT(empty1, IsEmpty()); + + Timeline empty2(cs(1), cs(1)); + EXPECT_TRUE(empty2.empty()); + EXPECT_THAT(empty2, IsEmpty()); + + Timeline nonEmpty(cs(1), cs(2)); + EXPECT_FALSE(nonEmpty.empty()); + EXPECT_THAT(nonEmpty, Not(IsEmpty())); +} + +TEST(Timeline, size) { + Timeline empty1{}; + EXPECT_EQ(0, empty1.size()); + EXPECT_THAT(empty1, SizeIs(0)); + + Timeline empty2(cs(1), cs(1)); + EXPECT_EQ(0, empty2.size()); + EXPECT_THAT(empty2, SizeIs(0)); + + Timeline size1(cs(1), cs(10)); + EXPECT_EQ(1, size1.size()); + EXPECT_THAT(size1, SizeIs(1)); + + Timeline size2{Timed(cs(-10), cs(10), 1), Timed(cs(10), cs(11), 5)}; + EXPECT_EQ(2, size2.size()); + EXPECT_THAT(size2, SizeIs(2)); +} + +TEST(Timeline, getRange) { + Timeline empty1{}; + EXPECT_EQ(TimeRange(cs(0), cs(0)), empty1.getRange()); + + Timeline empty2(cs(1), cs(1)); + EXPECT_EQ(TimeRange(cs(1), cs(1)), empty2.getRange()); + + Timeline nonEmpty1(cs(1), cs(10)); + EXPECT_EQ(TimeRange(cs(1), cs(10)), nonEmpty1.getRange()); + + Timeline nonEmpty2{ Timed(cs(-10), cs(10), 1), Timed(cs(10), cs(11), 5) }; + EXPECT_EQ(TimeRange(cs(-10), cs(11)), nonEmpty2.getRange()); +} + +TEST(Timeline, iterators) { + Timeline timeline{ Timed(cs(-5), cs(0), 10), Timed(cs(5), cs(15), 9) }; + auto expected = { Timed(cs(-5), cs(0), 10), Timed(cs(0), cs(5), 0), Timed(cs(5), cs(15), 9) }; + EXPECT_THAT(timeline, ElementsAreArray(expected)); + + vector> reversedActual; + std::copy(timeline.rbegin(), timeline.rend(), back_inserter(reversedActual)); + vector> reversedExpected; + std::reverse_copy(expected.begin(), expected.end(), back_inserter(reversedExpected)); + EXPECT_THAT(reversedActual, ElementsAreArray(reversedExpected)); +} + +TEST(Timeline, find) { + vector> elements = { + Timed(cs(1), cs(2), 1), // #0 + Timed(cs(2), cs(4), 2), // #1 + Timed(cs(4), cs(5), 3) // #2 + }; + Timeline timeline(elements.begin(), elements.end()); + EXPECT_EQ(timeline.end(), timeline.find(cs(-1))); + EXPECT_EQ(timeline.end(), timeline.find(cs(0))); + EXPECT_EQ(elements[0], *timeline.find(cs(1))); + EXPECT_EQ(elements[1], *timeline.find(cs(2))); + EXPECT_EQ(elements[1], *timeline.find(cs(3))); + EXPECT_EQ(elements[2], *timeline.find(cs(4))); + EXPECT_EQ(timeline.end(), timeline.find(cs(5))); +} + +TEST(Timeline, get) { + vector> elements = { + Timed(cs(1), cs(2), 1), // #0 + Timed(cs(2), cs(4), 2), // #1 + Timed(cs(4), cs(5), 3) // #2 + }; + Timeline timeline(elements.begin(), elements.end()); + EXPECT_THROW(timeline.get(cs(-1)), std::invalid_argument); + EXPECT_THROW(timeline.get(cs(0)), std::invalid_argument); + EXPECT_EQ(elements[0], timeline.get(cs(1))); + EXPECT_EQ(elements[1], timeline.get(cs(2))); + EXPECT_EQ(elements[1], timeline.get(cs(3))); + EXPECT_EQ(elements[2], timeline.get(cs(4))); + EXPECT_THROW(timeline.get(cs(5)), std::invalid_argument); +} + +void testSetter(std::function&, Timeline&)> set) { + const Timed initial(cs(0), cs(10), 42); + Timeline timeline(initial); + vector expectedValues(10, 42); + auto newElements = { + Timed(cs(1), cs(2), 4), + Timed(cs(3), cs(6), 4), + Timed(cs(7), cs(9), 5), + Timed(cs(9), cs(10), 6), + Timed(cs(2), cs(3), 4), + Timed(cs(0), cs(1), 7), + Timed(cs(-10), cs(1), 8), + Timed(cs(-10), cs(0), 9), + Timed(cs(-10), cs(-1), 10), + Timed(cs(9), cs(20), 11), + Timed(cs(10), cs(20), 12), + Timed(cs(11), cs(20), 13), + Timed(cs(4), cs(6), 14), + Timed(cs(4), cs(6), 15), + Timed(cs(8), cs(10), 15), + Timed(cs(6), cs(8), 15), + Timed(cs(6), cs(8), 16) + }; + for (const auto& newElement : newElements) { + // Set element in timeline + set(newElement, timeline); + + // Update expected value for every index + cs elementStart = max(newElement.getStart(), cs(0)); + cs elementEnd = min(newElement.getEnd(), cs(10)); + for (cs t = elementStart; t < elementEnd; ++t) { + expectedValues[t.count()] = newElement.getValue(); + } + + // Check timeline via indexer + for (cs t = cs(0); t < cs(10); ++t) { + EXPECT_EQ(expectedValues[t.count()], timeline[t]); + } + + // Check timeline via iterators + int lastValue = std::numeric_limits::min(); + for (const auto& element : timeline) { + // No element shound have zero-length + EXPECT_LT(cs(0), element.getLength()); + + // No two adjacent elements should have the same value; they should have been merged + EXPECT_NE(lastValue, element.getValue()); + lastValue = element.getValue(); + + // Element should match expected values + for (cs t = element.getStart(); t < element.getEnd(); ++t) { + EXPECT_EQ(expectedValues[t.count()], element.getValue()); + } + } + } +} + +TEST(Timeline, set) { + testSetter([](const Timed& element, Timeline& timeline) { + timeline.set(element); + }); + testSetter([](const Timed& element, Timeline& timeline) { + timeline.set(element, element.getValue()); + }); + testSetter([](const Timed& element, Timeline& timeline) { + timeline.set(element.getStart(), element.getEnd(), element.getValue()); + }); +} + +TEST(Timeline, indexer_set) { + testSetter([](const Timed& element, Timeline& timeline) { + for (cs t = element.getStart(); t < element.getEnd(); ++t) { + if (t >= timeline.getRange().getStart() && t < timeline.getRange().getEnd()) { + timeline[t] = element.getValue(); + } else { + EXPECT_THROW(timeline[t] = element.getValue(), std::invalid_argument); + } + } + }); +} + +TEST(Timeline, equality) { + vector> timelines = { + Timeline{}, + Timeline(cs(1), cs(1)), + Timeline(cs(1), cs(2)), + Timeline(cs(-10), cs(0)) + }; + + for (size_t i = 0; i < timelines.size(); ++i) { + for (size_t j = 0; j < timelines.size(); ++j) { + if (i == j) { + EXPECT_EQ(timelines[i], Timeline(timelines[j])); + } else { + EXPECT_NE(timelines[i], timelines[j]); + } + } + } +} \ No newline at end of file