diff --git a/core/include/cubos/core/memory/unordered_bimap.hpp b/core/include/cubos/core/memory/unordered_bimap.hpp new file mode 100644 index 0000000000..b33bedc093 --- /dev/null +++ b/core/include/cubos/core/memory/unordered_bimap.hpp @@ -0,0 +1,164 @@ +/// @file +/// @brief Class @ref cubos::core::memory::UnorderedBimap. +/// @ingroup core-memory + +#pragma once + +#include + +namespace cubos::core::memory +{ + /// @brief A bidirectional hash table. + /// @tparam L Left type. + /// @tparam R Right type. + /// @tparam LHash Hash functor type for @p L. + /// @tparam RHash Hash functor type for @p R. + /// @ingroup core-memory + template , typename RHash = std::hash> + class UnorderedBimap final + { + public: + using Iterator = std::unordered_map::const_iterator; + + /// @brief Adds a new entry to the map. + /// @note If any of the values already exists, the old entries with them are removed. + /// @param left Left value. + /// @param right Right value. + void add(L left, R right) + { + auto leftIt = mLeftToRight.find(left); + if (leftIt != mLeftToRight.end()) + { + mRightToLeft.erase(leftIt->second); + } + + auto rightIt = mRightToLeft.find(right); + if (rightIt != mRightToLeft.end()) + { + mLeftToRight.erase(rightIt->second); + } + + mLeftToRight.insert_or_assign(left, right); + mRightToLeft.insert_or_assign(right, left); + } + + /// @brief Removes the entry associated to the given left value. + /// @param left Left value. + /// @return Whether the entry was removed. + bool removeLeft(const L& left) + { + auto it = mLeftToRight.find(left); + if (it == mLeftToRight.end()) + { + return false; + } + + mRightToLeft.erase(it->second); + mLeftToRight.erase(it); + return true; + } + + /// @brief Removes the entry associated to the given right value. + /// @param right Right value. + /// @return Whether the entry was removed. + bool removeRight(const R& right) + { + auto it = mRightToLeft.find(right); + if (it == mRightToLeft.end()) + { + return false; + } + + mLeftToRight.erase(it->second); + mRightToLeft.erase(it); + return true; + } + + /// @brief Checks if the map has the given entry. + /// @param left Left value. + /// @param right Right value. + /// @return Whether the map has the entry. + bool has(const L& left, const R& right) const + { + auto it = mLeftToRight.find(left); + if (it == mLeftToRight.end()) + { + return false; + } + return it->second == right; + } + + /// @brief Checks if the map contains the given left value. + /// @param left Left value. + /// @return Whether the map contains the value. + bool hasLeft(const L& left) const + { + return mLeftToRight.find(left) != mLeftToRight.end(); + } + + /// @brief Checks if the map contains the given right value. + /// @param right Right value. + /// @return Whether the map contains the value. + bool hasRight(const R& right) const + { + return mRightToLeft.find(right) != mRightToLeft.end(); + } + + /// @brief Gets the right value associated to the given left value. + /// @note Aborts if the left value isn't stored. + /// @param left Left value. + /// @return Right value. + const R& getRight(const L& left) const + { + return mLeftToRight.at(left); + } + + /// @brief Gets the left value associated to the given right value. + /// @note Aborts if the right value isn't stored. + /// @param right Right value. + /// @return Left value. + const L& getLeft(const R& right) const + { + return mRightToLeft.at(right); + } + + /// @brief Clears the map. + void clear() + { + mLeftToRight.clear(); + mRightToLeft.clear(); + } + + /// @brief Gets the number of entries in the map. + /// @return Entry count. + std::size_t size() const + { + return mLeftToRight.size(); + } + + /// @brief Checks if the map is empty. + /// @return Whether the map is empty. + bool empty() const + { + return mLeftToRight.empty(); + } + + /// @brief Gets an iterator to the beginning of the map. + /// @return Iterator. + Iterator begin() const + { + return mLeftToRight.begin(); + } + + /// @brief Gets an iterator to the end of the map. + /// @return Iterator. + Iterator end() const + { + return mLeftToRight.end(); + } + + private: + std::unordered_map mLeftToRight; + std::unordered_map mRightToLeft; + }; +} // namespace cubos::core::memory diff --git a/core/tests/CMakeLists.txt b/core/tests/CMakeLists.txt index dd4768957d..21bac6d6e8 100644 --- a/core/tests/CMakeLists.txt +++ b/core/tests/CMakeLists.txt @@ -22,6 +22,8 @@ add_executable( data/fs/file_system.cpp data/context.cpp + memory/unordered_bimap.cpp + ecs/registry.cpp ecs/world.cpp ecs/query.cpp diff --git a/core/tests/memory/unordered_bimap.cpp b/core/tests/memory/unordered_bimap.cpp new file mode 100644 index 0000000000..338e033648 --- /dev/null +++ b/core/tests/memory/unordered_bimap.cpp @@ -0,0 +1,74 @@ +#include + +#include + +TEST_CASE("memory::UnorderedBimap") +{ + using cubos::core::memory::UnorderedBimap; + + UnorderedBimap bimap{}; + + SUBCASE("just initialized") + { + } + + SUBCASE("had entries") + { + bimap.add(1, 'a'); + + CHECK_FALSE(bimap.empty()); + CHECK(bimap.size() == 1); + + CHECK(bimap.has(1, 'a')); + CHECK(bimap.hasLeft(1)); + CHECK(bimap.hasRight('a')); + CHECK(bimap.getLeft('a') == 1); + CHECK(bimap.getRight(1) == 'a'); + + CHECK(bimap.begin() != bimap.end()); + CHECK(bimap.begin()->first == 1); + CHECK(bimap.begin()->second == 'a'); + CHECK(++bimap.begin() == bimap.end()); + + bimap.add(2, 'b'); + + CHECK(bimap.size() == 2); + CHECK(bimap.has(1, 'a')); + CHECK(bimap.has(2, 'b')); + + bimap.add(1, 'b'); + + CHECK(bimap.size() == 1); + CHECK(bimap.has(1, 'b')); + CHECK_FALSE(bimap.has(1, 'a')); + CHECK_FALSE(bimap.has(2, 'b')); + + SUBCASE("remove left") + { + CHECK(bimap.removeLeft(1)); + } + + SUBCASE("remove right") + { + CHECK(bimap.removeRight('b')); + } + + SUBCASE("clear") + { + bimap.clear(); + } + } + + // Shouldn't contain anything + CHECK(bimap.empty()); + CHECK(bimap.size() == 0); + + CHECK_FALSE(bimap.has(1, 'a')); + CHECK_FALSE(bimap.hasLeft(1)); + CHECK_FALSE(bimap.hasRight('a')); + + CHECK(bimap.begin() == bimap.end()); + + CHECK_FALSE(bimap.removeLeft(1)); + CHECK_FALSE(bimap.removeRight('a')); +}