diff --git a/cpp/Makefile b/cpp/Makefile index 62b1dc7..acb41f3 100644 --- a/cpp/Makefile +++ b/cpp/Makefile @@ -9,7 +9,7 @@ build: cmake cmake --build ${BUILD_DIR} test: build - ctest --test-dir ${BUILD_DIR} + ctest --output-on-failure --test-dir ${BUILD_DIR} clean: rm -rf ${BUILD_DIR}/* \ No newline at end of file diff --git a/cpp/src/HashBiMap.h b/cpp/src/HashBiMap.h new file mode 100644 index 0000000..8012ad9 --- /dev/null +++ b/cpp/src/HashBiMap.h @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include + +/** + * @brief A bidirectional map that allows unique key-value pairs. + * + * This class provides a bidirectional map where each key is associated with a + * unique value and each value is associated with a unique key. It supports + * insertion, deletion, and lookup operations in both directions. + * + * This class does NOT allow key<->value mappings to be updated. Once a + * key-value pair is inserted, it cannot be changed. + * + * @tparam K Type of the keys. + * @tparam V Type of the values. + */ +template class HashBiMap { +private: + std::unordered_map forwardMap; + std::unordered_map reverseMap; + +public: + // puts a key-value pair + void put(const K &key, const V &value) { + if (forwardMap.find(key) != forwardMap.end() || + reverseMap.find(value) != reverseMap.end()) { + throw std::invalid_argument( + "Duplicate key or value not allowed in HashBiMap"); + } + forwardMap[key] = value; + reverseMap[value] = key; + } + + // Removes a key-value pair by key + void removeByKey(const K &key) { + auto it = forwardMap.find(key); + if (it != forwardMap.end()) { + V value = it->second; + forwardMap.erase(it); + reverseMap.erase(value); + } + } + + // Removes a key-value pair by value + void removeByValue(const V &value) { + auto it = reverseMap.find(value); + if (it != reverseMap.end()) { + K key = it->second; + reverseMap.erase(it); + forwardMap.erase(key); + } + } + + // Retrieves the value associated with a key + V getByKey(const K &key) const { + auto it = forwardMap.find(key); + if (it == forwardMap.end()) { + throw std::out_of_range("Key not found"); + } + return it->second; + } + + // Retrieves the key associated with a value + K getByValue(const V &value) const { + auto it = reverseMap.find(value); + if (it == reverseMap.end()) { + throw std::out_of_range("Value not found"); + } + return it->second; + } + + // Checks if the map contains a given key + bool containsKey(const K &key) const { + return forwardMap.find(key) != forwardMap.end(); + } + + // Checks if the map contains a given value + bool containsValue(const V &value) const { + return reverseMap.find(value) != reverseMap.end(); + } + + size_t getSize() const { return forwardMap.size(); } + + // Clears the entire map + void clear() { + forwardMap.clear(); + reverseMap.clear(); + } + + /** + * @brief Saves the HashBiMap to a file. + * + * The format of this file will be "['key1','key2','key3',...]" where the + * index position of the key is the value of the key in the map. For example + * if the HashBiMap is {"foo" -> 2, "baz" -> 0, "bar" -> 1}, the file will + * contain "['baz','bar','foo']". + * + * Therefore the assumptions are that + * - the map values are contiguous integers from [0,size_of_index] + * + * @param bimap The HashBiMap to save. + * @param filename The name of the file to save the map to. + */ + void saveNamesMappingToFile(const std::string &filename) { + std::vector keys(getSize()); + for (const auto &pair : forwardMap) { + keys[pair.second] = pair.first; + } + + std::ofstream outFile(filename); + if (!outFile) { + throw std::runtime_error("Unable to open file for writing"); + } + + outFile << "["; + for (size_t i = 0; i < keys.size(); ++i) { + outFile << "'" << keys[i] << "'"; + if (i < keys.size() - 1) { + outFile << ","; + } + } + outFile << "]"; + outFile.close(); + } + + /** + * @brief Loads the HashBiMap from a file. + * + * The format of this file should be "['key1','key2','key3',...]" where the + * index position of the key is the value of the key in the map. + * + * @param filename The name of the file to load the map from. + * @return The loaded HashBiMap. + */ + static HashBiMap + loadNamesMappingFromFile(const std::string &filename) { + std::ifstream inFile(filename); + if (!inFile) { + throw std::runtime_error("Unable to open file for reading"); + } + + std::string content; + std::getline(inFile, content); + inFile.close(); + + if (content.front() != '[' || content.back() != ']') { + throw std::runtime_error("Invalid file format"); + } + + content = + content.substr(1, content.size() - 2); // Remove the square brackets + + std::vector keys; + std::stringstream ss(content); + std::string item; + while (std::getline(ss, item, ',')) { + item.erase(std::remove(item.begin(), item.end(), '\''), item.end()); + keys.push_back(item); + } + + HashBiMap bimap; + for (size_t i = 0; i < keys.size(); ++i) { + bimap.put(keys[i], i); + } + + return bimap; + } +}; diff --git a/cpp/test/CMakeLists.txt b/cpp/test/CMakeLists.txt index 1606f1a..ece11ed 100644 --- a/cpp/test/CMakeLists.txt +++ b/cpp/test/CMakeLists.txt @@ -1,5 +1,5 @@ # List the test source files -set(TEST_FILES test_main.cpp doctest_setup.cpp) # Add any test files here +set(TEST_FILES test_main.cpp test_HashBiMap.cpp doctest_setup.cpp) # Add any test files here # Create an executable for the tests add_executable(VoyagerTests ${TEST_FILES}) diff --git a/cpp/test/test_HashBiMap.cpp b/cpp/test/test_HashBiMap.cpp new file mode 100644 index 0000000..64e56a6 --- /dev/null +++ b/cpp/test/test_HashBiMap.cpp @@ -0,0 +1,98 @@ +#include "HashBiMap.h" +#include "doctest.h" + +TEST_CASE("HashBiMap: put and getByKey") { + HashBiMap map; + map.put("one", 1); + map.put("two", 2); + + REQUIRE(map.getByKey("one") == 1); + REQUIRE(map.getByKey("two") == 2); + REQUIRE(map.getSize() == 2); + + REQUIRE_THROWS_AS(map.put("one", 1), std::invalid_argument); + // throw exception if key is already present + REQUIRE_THROWS_AS(map.put("one", 99), std::invalid_argument); + // throw exception if value is already present + REQUIRE_THROWS_AS(map.put("foo", 1), std::invalid_argument); +} + +TEST_CASE("HashBiMap: put and getByValue") { + HashBiMap map; + map.put("one", 1); + map.put("two", 2); + + REQUIRE(map.getByValue(1) == "one"); + REQUIRE(map.getByValue(2) == "two"); +} + +TEST_CASE("HashBiMap: removeByKey") { + HashBiMap map; + map.put("one", 1); + map.put("two", 2); + + map.removeByKey("one"); + REQUIRE(!map.containsKey("one")); + REQUIRE(!map.containsValue(1)); + REQUIRE(map.getSize() == 1); +} + +TEST_CASE("HashBiMap: removeByValue") { + HashBiMap map; + map.put("one", 1); + map.put("two", 2); + + map.removeByValue(1); + REQUIRE(!map.containsKey("one")); + REQUIRE(!map.containsValue(1)); + REQUIRE(map.getSize() == 1); +} + +TEST_CASE("HashBiMap: clear") { + HashBiMap map; + map.put("one", 1); + map.put("two", 2); + + map.clear(); + REQUIRE(map.getSize() == 0); + REQUIRE(!map.containsKey("one")); + REQUIRE(!map.containsValue(2)); +} + +TEST_CASE("HashBiMap: containsKey and containsValue") { + HashBiMap map; + map.put("one", 1); + map.put("two", 2); + + REQUIRE(map.containsKey("one")); + REQUIRE(map.containsValue(1)); + REQUIRE(map.containsKey("two")); + REQUIRE(map.containsValue(2)); +} + +TEST_CASE("Save HashBiMap to file and load from file") { + HashBiMap map; + map.put("two", 2); + map.put("zero", 0); + map.put("one", 1); + + map.saveNamesMappingToFile("test_HashBiMap.txt"); + + // Only one line is expected in the file + std::ifstream file("test_HashBiMap.txt"); + std::string fileContents; + std::getline(file, fileContents); + file.close(); + std::cout << fileContents << std::endl; + + REQUIRE(fileContents == "['zero','one','two']"); + + HashBiMap loadedMap = + HashBiMap::loadNamesMappingFromFile( + "test_HashBiMap.txt"); + + REQUIRE(loadedMap.getSize() == 3); + REQUIRE(loadedMap.getByKey("zero") == 0); + REQUIRE(loadedMap.getByKey("one") == 1); + REQUIRE(loadedMap.getByKey("two") == 2); +}