diff --git a/src/include/interval-map/interval-map.hh b/src/include/interval-map/interval-map.hh index ce1d9dd..ab3fb44 100644 --- a/src/include/interval-map/interval-map.hh +++ b/src/include/interval-map/interval-map.hh @@ -2,6 +2,9 @@ #include +// Testing class forward declaration +class IntervalMapTest; + namespace amby { template class interval_map { @@ -9,17 +12,48 @@ public: interval_map(V const& init) : init_(init) {} void assign(K const& begin, K const& end, V const& val) { - // TODO: implement + if (!(begin < end)) + return; + + auto it = underlying_.upper_bound(end); + auto const end_val = at_upper_bound(it); + + bool insert_begin = !(val == init_); + + while (it != underlying_.begin()) { + it = std::prev(it); + auto begin_found = it->first < begin; + if (begin_found) { + insert_begin = !(val == it->second); + break; + } + if (it != underlying_.end()) + // Account for up-coming `std::prev` with `++` + underlying_.erase(it++); + } + + if (insert_begin) + it = underlying_.insert(it, {begin, val}); + // Get the proper upper-bound for `end` + it = (it == underlying_.end()) ? it : std::next(it); + if (!(at_upper_bound(it) == end_val)) + underlying_.insert(it, {end, end_val}); } V const& operator[](K const& key) const { - auto it = underlying_.upper_bound(key); + return at_upper_bound(underlying_.upper_bound(key)); + } + + // Used in testing + friend class ::IntervalMapTest; + +private: + V const& at_upper_bound(std::map::const_iterator it) const { if (it == underlying_.begin()) return init_; return std::prev(it)->second; } -private: V init_; std::map underlying_{}; }; diff --git a/tests/unit/model.hh b/tests/unit/model.hh new file mode 100644 index 0000000..9acb19f --- /dev/null +++ b/tests/unit/model.hh @@ -0,0 +1,35 @@ +#pragma once + +#include + +template struct Range { + K begin; + K end; + V val; +}; + +// Same behaviour as interval_map, but implementation is trivally correct +template class Model { +public: + Model(V const& init) : init_(init) {} + + void assign(K const& begin, K const& end, V const& val) { + if (!(begin < end)) + return; + ranges_.emplace_back(begin, end, val); + } + + V const& operator[](K const& key) const { + for (auto it = ranges_.rbegin(); it != ranges_.rend(); ++it) { + if (key < it->begin) + continue; + if (it->end <= key) + continue; + return it->val; + } + return init_; + } + + V init_; + std::vector> ranges_{}; +}; diff --git a/tests/unit/unit_test.cc b/tests/unit/unit_test.cc index 288c811..c429d29 100644 --- a/tests/unit/unit_test.cc +++ b/tests/unit/unit_test.cc @@ -2,8 +2,12 @@ #include +#include +#include #include +#include "model.hh" + template class KeyInterface { public: explicit KeyInterface(T val) : underlying_(val) {} @@ -58,7 +62,82 @@ static_assert(std::is_same_v< ValueInterface, decltype(std::numeric_limits>::lowest())>); -TEST(interval_map, minimal_interface) { +class IntervalMapTest : public testing::Test { +protected: + using key_type = char; + using value_type = int; + using map_type = amby::interval_map; + using model_type = Model; + + map_type map{0}; + model_type model{0}; + + void SetUp() override { + map = map_type{0}; + model = model_type{0}; + } + + void TearDown() override { + check(); + } + + void assign(key_type const& begin, key_type const& end, + value_type const& val) { + map.assign(begin, end, val); + model.assign(begin, end, val); + } + + void check() const { + SCOPED_TRACE(stringify_map()); + SCOPED_TRACE(stringify_operations()); + check_ranges(); + check_canonicity(); + } + + std::string stringify_map() const { + std::ostringstream out; + out << "map: "; + for (const auto& [key, val] : map.underlying_) + out << "[" << +key << ": " << +val << "]"; + return out.str(); + } + + std::string stringify_operations() const { + std::ostringstream out; + out << "ops: "; + for (const auto& [start, end, val] : model.ranges_) + out << "[" << +start << ":" << +end << " => " << +val << "]"; + return out.str(); + } + + // Compare against the fake 'Model' implementation + void check_ranges() const { + auto i = std::numeric_limits::min(); + for (; i < std::numeric_limits::max(); ++i) { + ASSERT_EQ(map[i], model[i]) << "(i: " << +i << ")"; + } + ASSERT_EQ(map[i], model[i]) << "(i: " << +i << ")"; + }; + + void check_canonicity() const { + // Consecutive map entries must not contain the same value + for (auto it = map.underlying_.begin(); it != map.underlying_.end(); + ++it) { + const auto next = std::next(it, 1); + if (next == map.underlying_.end()) + break; + EXPECT_NE(it->second, next->second); + } + + // The first entry must not contain the initial value + if (const auto it = map.underlying_.begin(); + it != map.underlying_.end()) { + EXPECT_NE(it->second, map.init_); + } + } +}; + +TEST_F(IntervalMapTest, minimal_interface) { using Key = KeyInterface; using Value = ValueInterface; @@ -66,3 +145,121 @@ TEST(interval_map, minimal_interface) { ASSERT_EQ(map[Key(0)], Value(0)); map.assign(Key(0), Key(1), Value(1)); } + +TEST_F(IntervalMapTest, no_insertion) {} + +TEST_F(IntervalMapTest, insert_begin_equal_end) { + assign(0, 0, 1); +} + +TEST_F(IntervalMapTest, insert_begin_bigger_than_end) { + assign(1, 0, 1); +} + +TEST_F(IntervalMapTest, insert_one_range) { + assign(std::numeric_limits::min(), 0, 1); +} + +TEST_F(IntervalMapTest, insert_non_overlapping_ranges) { + assign(std::numeric_limits::min(), 0, 1); + assign(10, std::numeric_limits::max(), 2); +} + +TEST_F(IntervalMapTest, insert_up_to_max) { + assign(std::numeric_limits::min(), + std::numeric_limits::max(), 1); +} + +TEST_F(IntervalMapTest, insert_range_right_after) { + assign(0, 10, 1); + assign(10, 20, 1); +} + +TEST_F(IntervalMapTest, insert_range_right_before) { + assign(10, 20, 1); + assign(0, 10, 1); +} + +TEST_F(IntervalMapTest, insert_range_middle) { + assign(0, 10, 1); + assign(20, 30, 1); + assign(10, 20, 1); +} + +TEST_F(IntervalMapTest, insert_range_inside_another) { + assign(0, 20, 1); + assign(5, 15, 2); +} + +TEST_F(IntervalMapTest, insert_range_around_another) { + assign(5, 15, 2); + assign(0, 20, 1); +} + +TEST_F(IntervalMapTest, insert_range_overlaps_many) { + assign(0, 10, 1); + assign(10, 20, 2); + assign(20, 30, 3); + assign(30, 40, 4); + assign(40, 50, 5); + assign(0, 50, -1); +} + +TEST_F(IntervalMapTest, insert_range_overlaps_many_init_value) { + assign(0, 10, 1); + assign(10, 20, 2); + assign(20, 30, 3); + assign(30, 40, 4); + assign(40, 50, 5); + assign(0, 50, 0); +} + +TEST_F(IntervalMapTest, insert_range_overlaps_many_oversize) { + assign(0, 10, 1); + assign(10, 20, 2); + assign(20, 30, 3); + assign(30, 40, 4); + assign(40, 50, 5); + assign(-10, 60, -1); +} + +TEST_F(IntervalMapTest, fuzzing_001) { + assign(-50, 20, 1); + assign(40, 80, 2); + assign(-100, -10, 3); +} + +TEST_F(IntervalMapTest, fuzzing_002) { + assign(-100, 90, 1); + assign(0, 120, 2); + assign(-60, 60, 3); +} + +TEST_F(IntervalMapTest, fuzzing_003) { + assign(-80, 70, 1); + assign(-50, 40, 2); + assign(-40, 20, 3); + assign(-110, -10, 4); +} + +TEST_F(IntervalMapTest, randomized_test) { + auto const seed = []() { + std::random_device r; + return r(); + }(); + SCOPED_TRACE(seed); + auto random = std::mt19937_64(seed); + + auto keys = std::uniform_int_distribution( + std::numeric_limits::min(), + std::numeric_limits::max()); + auto values = std::uniform_int_distribution(0, 10); + + for (auto i = 0; i < 1000; ++i) { + auto const start = keys(random); + auto const end = keys(random); + auto const value = values(random); + assign(start, end, value); + check(); + } +}