/* * 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 "../Concepts.hpp" #include "../Systems/Logging.hpp" #include "ContainerOptions.hpp" #include "Hashmap.hpp" #include "Corrade/Containers/Array.h" #include #include #include #include #include #include #include #include #include #include #include #include namespace Tourmaline::Containers { template class DualkeyMap { public: // Return Types template requires Concepts::Either struct MultiQueryResult { // Having to use pointers here over references was not fun // but it was for greater good const OppositeKey *oppositeKey; Corrade::Containers::Array valueQueryResults; std::size_t howManyFound = 1; }; using QueryResult = std::pair, std::reference_wrapper>, Value &>; using Entry = std::tuple; // Construct/Destruct DualkeyMap() { hashList.reserve(Options.baseReservation); } ~DualkeyMap() { // I'm sure there is a better way to do this for (DualkeyHash *hash : hashList) { if (hash != nullptr) [[likely]] { delete hash; } } } // No copying due to the container expected to be the sole // owner of the data DualkeyMap(const DualkeyMap &) = delete; DualkeyMap &operator=(const DualkeyMap &) = delete; // Public controls 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: // Both given 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]] std::size_t Count() { return hashList.size() - graveyard.size(); } // Queries [[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 Concepts::Either [[nodiscard("Discarding a very expensive query!")]] std::vector> QueryWithAll(const Corrade::Containers::Array &keys) { std::vector> queryResult = queryWithMany(keys); std::erase_if(queryResult, [keyCount = keys.size()]( const MultiQueryResult &queryRecord) { return queryRecord.howManyFound != keyCount; }); return queryResult; } 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; } } } private: // Interal data structures 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; }; // Actual data std::vector hashList; std::stack graveyard; // Interal querying template > inline std::vector> queryWithMany(const Corrade::Containers::Array &keys) { constexpr bool searchingInFirstKey = std::is_same_v; std::size_t keyCount = keys.size(); // I really can't wait for C++26 contracts if (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 (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 Corrade::Containers::Array keyHashes{keyCount}; for (uint64_t index = 0; index < keyCount; index++) { keyHashes[index] = std::hash{}(keys[index]); } uint64_t hashToCompare; Key *keyToCompare; OppositeKey *oppositeKey; Containers::Hashmap, {8.0f, 0.01f, 2.5f, 2048, 8}> // Aggressive hashmap :o queryResults; for (DualkeyHash *hash : hashList) { // Tombstone if (hash == nullptr) { continue; } // 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); oppositeKey = const_cast(&hash->secondKey); } else { hashToCompare = hash->secondKeyHash; keyToCompare = const_cast(&hash->secondKey); oppositeKey = 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) { if (queryResults.Has(*oppositeKey)) [[likely]] { auto &entry = queryResults.Get(*oppositeKey); entry.valueQueryResults[index] = &hash->value; ++entry.howManyFound; break; } queryResults .Insert( *oppositeKey, {oppositeKey, Corrade::Containers::Array{keyCount}}) .valueQueryResults[index] = &hash->value; } } } return queryResults.ExtractValuesToArray(); } }; } // namespace Tourmaline::Containers #endif