/* * SPDX-FileCopyrightText: Dora "cat" * SPDX-License-Identifier: MPL-2.0 * * This Source Code Form is subject to the terms of the Mozilla Public License, * v. 2.0. If a copy of the MPL was not distributed with this file, You can * obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef GUARD_TOURMALINE_DUALKEYMAP_H #define GUARD_TOURMALINE_DUALKEYMAP_H #include "../Systems/Logging.hpp" #include "Hashing.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Tourmaline::Containers { template class DualkeyMap { public: using QueryResult = std::pair, std::reference_wrapper>, Value &>; using Entry = std::tuple; DualkeyMap() { hashList.reserve(baseReservation); } ~DualkeyMap() { // I'm sure there is a better way to do this for (DualkeyHash *hash : hashList) { if (hash != nullptr) [[likely]] { delete hash; } } } Entry Insert(AKey firstKey, BKey secondKey, Value value) { DualkeyHash *hash = new DualkeyHash(std::move(firstKey), std::move(secondKey), std::move(value)); if (graveyard.empty()) { hashList.push_back(hash); } else { hashList[graveyard.top()] = hash; graveyard.pop(); } return {hash->firstKey, hash->secondKey, hash->value}; } std::size_t Remove(std::optional firstKey, std::optional secondKey) { bool isFirstKeyGiven = firstKey.has_value(); bool isSecondKeyGiven = secondKey.has_value(); if (!isFirstKeyGiven && !isSecondKeyGiven) [[unlikely]] { Systems::Logging::Log( "Failed to Delete! DualkeyMap::Delete require at least 1 " "key to be given! Terminating", "Dualkey Map", Systems::Logging::LogLevel::Critical); } std::size_t firstKeyHash = isFirstKeyGiven ? std::hash{}(firstKey.value()) : 0; std::size_t secondKeyHash = isSecondKeyGiven ? std::hash{}(secondKey.value()) : 0; std::size_t index = 0, amountDeleted = 0; uint8_t stateOfIndexing = isFirstKeyGiven + (isSecondKeyGiven << 1); for (DualkeyHash *hash : hashList) { // Tombstone if (hash == nullptr) [[unlikely]] { continue; } switch (stateOfIndexing) { case 1: // Only first key is given if (firstKeyHash == hash->firstKeyHash && firstKey.value() == hash->firstKey) { delete hash; hashList[index] = nullptr; graveyard.push(index); ++amountDeleted; } break; case 2: // Only second key is given if (secondKeyHash == hash->secondKeyHash && secondKey.value() == hash->secondKey) { delete hash; hashList[index] = nullptr; graveyard.push(index); ++amountDeleted; } break; case 3: if (firstKeyHash == hash->firstKeyHash && secondKeyHash == hash->secondKeyHash && firstKey.value() == hash->firstKey && secondKey.value() == hash->secondKey) { delete hash; hashList[index] = nullptr; graveyard.push(index); return 1; } break; } ++index; } return amountDeleted; } [[nodiscard("Discarding an expensive query!")]] std::vector Query(std::optional firstKey, std::optional secondKey) { bool isFirstKeyGiven = firstKey.has_value(); bool isSecondKeyGiven = secondKey.has_value(); if (!isFirstKeyGiven && !isSecondKeyGiven) [[unlikely]] { Systems::Logging::Log( "Failed to Query! DualkeyMap::Query require at least 1 " "key to be given! Terminating", "Dualkey Map", Systems::Logging::LogLevel::Critical); } std::size_t firstKeyHash = isFirstKeyGiven ? std::hash{}(firstKey.value()) : 0; std::size_t secondKeyHash = isSecondKeyGiven ? std::hash{}(secondKey.value()) : 0; std::vector finishedQuery{}; uint8_t stateOfIndexing = isFirstKeyGiven + (isSecondKeyGiven << 1); // Putting hash checks first to benefit from short circuits for (DualkeyHash *hash : hashList) { // Tombstone if (hash == nullptr) [[unlikely]] { continue; } switch (stateOfIndexing) { case 1: // Only first key is given if (firstKeyHash == hash->firstKeyHash && firstKey.value() == hash->firstKey) { finishedQuery.emplace_back(std::cref(hash->secondKey), hash->value); } continue; case 2: // Only second key is given if (secondKeyHash == hash->secondKeyHash && secondKey.value() == hash->secondKey) { finishedQuery.emplace_back(std::cref(hash->firstKey), hash->value); } continue; case 3: // Both are given if (firstKeyHash == hash->firstKeyHash && secondKeyHash == hash->secondKeyHash && firstKey.value() == hash->firstKey && secondKey.value() == hash->secondKey) { finishedQuery.emplace_back(std::monostate{}, hash->value); break; } continue; } break; } return finishedQuery; } template requires(std::same_as || std::same_as) [[nodiscard("Discarding a very expensive query!")]] int QueryWithAll(const Key (&keys)[keyCount]) { constexpr bool searchingInFirstKey = std::is_same_v; // I really can't wait for C++26 contracts if constexpr (keyCount == 0) { Systems::Logging::Log("Failed to Query! QueryWithAll require at least 2 " "key to be given, zero was given! Terminating", "Dualkey Map", Systems::Logging::LogLevel::Critical); } // Hoping this never ever gets triggered :sigh: if constexpr (keyCount == 1) { Systems::Logging::Log("QueryWithAll should not be used for single key " "entry! Please use Query for this instead.", "Dualkey Map", Systems::Logging::LogLevel::Error); } // While we don't necessary need the hashes, // it just helps us tremendously benefit from short circuit checks std::array keyHashes; for (uint64_t index = 0; index < keyCount; index++) { keyHashes[index] = std::hash{}(keys[index]); } std::vector, keyCount>> queryResults; uint64_t hashToCompare; Key *keyToCompare; std::conditional_t resultKey; for (DualkeyHash *hash : hashList) { // The hell of doing 2 conditions with similar logics in // the same logical block if constexpr (searchingInFirstKey) { hashToCompare = hash->firstKeyHash; keyToCompare = const_cast(&hash->firstKey); resultKey = const_cast(&hash->secondKey); } else { hashToCompare = hash->secondKeyHash; keyToCompare = const_cast(&hash->secondKey); resultKey = const_cast(&hash->firstKey); } // The code above was done to make this code more uniform for (uint64_t index = 0; index < keyCount; index++) { if (keyHashes[index] == hashToCompare && keys[index] == *keyToCompare) { bool doesExist = false; for (auto &queryRecord : queryResults) { if (*queryRecord.resultKey == *resultKey) { queryRecord.valueQueryResults[index] = &hash->value; ++queryRecord.howManyFound; doesExist = true; break; } } if (doesExist) { break; } // Since the result record is not present // we have to make it queryResults.emplace_back(); auto &newRecord = queryResults.back(); newRecord.resultKey = resultKey; newRecord.valueQueryResults[index] = &hash->value; } } } for (const auto &queryRecord : queryResults) { Systems::Logging::Log( std::format("Opposite = {}, found = {}", reinterpret_cast(queryRecord.resultKey), queryRecord.howManyFound), "DKM", Systems::Logging::LogLevel::Info, queryRecord.howManyFound == keyCount); } return 0; } void Scan(std::function scanFunction) { for (DualkeyHash *hash : hashList) { if (hash == nullptr) { continue; } if (scanFunction(hash->firstKeyHash, hash->secondKeyHash, hash->value)) { return; } } } void Scan(std::function scanFunction) { for (DualkeyHash *hash : hashList) { if (hash == nullptr) { continue; } if (scanFunction(hash->firstKey, hash->secondKey, hash->value)) { return; } } } [[nodiscard]] std::size_t Count() { return hashList.size() - graveyard.size(); } // No copying due to the container expected to be the sole // owner of the data DualkeyMap(const DualkeyMap &) = delete; DualkeyMap &operator=(const DualkeyMap &) = delete; private: template struct MultiQueryResult { OppositeKey *resultKey = nullptr; std::size_t howManyFound = 1; std::array valueQueryResults; }; struct DualkeyHash { DualkeyHash(AKey &&firstKey, BKey &&secondKey, Value &&value) : firstKey(std::move(firstKey)), secondKey(std::move(secondKey)), firstKeyHash(std::hash{}(this->firstKey)), secondKeyHash(std::hash{}(this->secondKey)), value(std::move(value)) {} const AKey firstKey; const BKey secondKey; const std::size_t firstKeyHash; const std::size_t secondKeyHash; mutable Value value; }; // It makes more sense to store the individual hash std::vector hashList; std::stack graveyard; }; } // namespace Tourmaline::Containers #endif