146 lines
4.8 KiB
C++
146 lines
4.8 KiB
C++
#include "core/model/slot_map.h"
|
|
|
|
#include <algorithm>
|
|
|
|
#include "core/json/json.h"
|
|
|
|
// slot_map implementation (extracted from bank_book, Q-W1 T4-05).
|
|
//
|
|
// The invariant: entries_ is kept sorted ascending by slot, one id per slot, one
|
|
// slot per id. Every mutator restores it; queries assume it. serialize rides the
|
|
// shared core/json emit helpers — the emitted fragment is byte-identical to the
|
|
// pre-extraction bank_book writer.
|
|
|
|
namespace reasampler::model {
|
|
|
|
void SlotMap::sortBySlot() {
|
|
std::stable_sort(entries_.begin(), entries_.end(),
|
|
[](const Entry& a, const Entry& b) { return a.slot < b.slot; });
|
|
}
|
|
|
|
int SlotMap::slotOf(const std::string& id) const {
|
|
for (const auto& e : entries_)
|
|
if (e.id == id) return e.slot;
|
|
return -1;
|
|
}
|
|
|
|
std::string SlotMap::idAt(int slot) const {
|
|
for (const auto& e : entries_)
|
|
if (e.slot == slot) return e.id;
|
|
return {};
|
|
}
|
|
|
|
int SlotMap::maxSlot() const {
|
|
int m = -1;
|
|
for (const auto& e : entries_)
|
|
if (e.slot > m) m = e.slot;
|
|
return m;
|
|
}
|
|
|
|
std::vector<std::string> SlotMap::orderedIds() const {
|
|
// entries_ is sorted ascending by slot, so a straight walk is display order.
|
|
std::vector<std::string> out;
|
|
out.reserve(entries_.size());
|
|
for (const auto& e : entries_) out.push_back(e.id);
|
|
return out;
|
|
}
|
|
|
|
void SlotMap::append(const std::string& id) {
|
|
if (id.empty()) return;
|
|
remove(id); // an existing id is re-appended, not left in place
|
|
entries_.push_back(Entry{id, maxSlot() + 1}); // next free slot after the last occupied
|
|
sortBySlot();
|
|
}
|
|
|
|
bool SlotMap::remove(const std::string& id) {
|
|
for (auto it = entries_.begin(); it != entries_.end(); ++it) {
|
|
if (it->id == id) {
|
|
entries_.erase(it); // leaves the slot empty — no re-pack
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool SlotMap::reorder(const std::string& id, int targetSlot) {
|
|
if (slotOf(id) < 0) return false; // not mapped -> no mutation
|
|
if (targetSlot < 0) targetSlot = 0;
|
|
if (slotOf(id) == targetSlot) return false; // already there — true no-op
|
|
|
|
// Detach the moving id first so the occupancy test below sees the post-move world.
|
|
remove(id);
|
|
|
|
const bool occupied = !idAt(targetSlot).empty();
|
|
if (occupied) {
|
|
// Insert-before-and-shift: every occupant at slot >= targetSlot shifts up by one,
|
|
// preserving relative order and interior gaps above the target. The moving id then
|
|
// takes targetSlot cleanly.
|
|
for (auto& e : entries_)
|
|
if (e.slot >= targetSlot) ++e.slot;
|
|
}
|
|
entries_.push_back(Entry{id, targetSlot});
|
|
sortBySlot();
|
|
return true;
|
|
}
|
|
|
|
void SlotMap::resetDense(const std::vector<std::string>& ids) {
|
|
entries_.clear();
|
|
int slot = 0;
|
|
for (const auto& id : ids) {
|
|
if (id.empty()) continue;
|
|
if (slotOf(id) >= 0) continue; // skip a duplicate id (one slot per id)
|
|
entries_.push_back(Entry{id, slot++});
|
|
}
|
|
// Already ascending by construction; no sort needed.
|
|
}
|
|
|
|
void SlotMap::reconcile(const std::vector<std::string>& liveIds) {
|
|
// Drop markers whose sample left the index.
|
|
entries_.erase(
|
|
std::remove_if(entries_.begin(), entries_.end(),
|
|
[&](const Entry& e) {
|
|
return std::find(liveIds.begin(), liveIds.end(), e.id) ==
|
|
liveIds.end();
|
|
}),
|
|
entries_.end());
|
|
// Append live ids that have no mapping yet (out-of-band index growth), in liveIds
|
|
// order, each to the next free slot after the current frontier.
|
|
for (const auto& id : liveIds)
|
|
if (slotOf(id) < 0) append(id);
|
|
sortBySlot();
|
|
}
|
|
|
|
bool SlotMap::operator==(const SlotMap& o) const {
|
|
return entries_ == o.entries_;
|
|
}
|
|
|
|
SlotMap SlotMap::fromEntries(const std::vector<std::pair<std::string, int>>& pairs) {
|
|
SlotMap m;
|
|
for (const auto& [id, slot] : pairs) {
|
|
if (id.empty() || slot < 0) continue; // drop malformed pair
|
|
if (m.slotOf(id) >= 0) continue; // duplicate id: first wins
|
|
if (!m.idAt(slot).empty()) continue; // slot taken: never double-occupy
|
|
m.entries_.push_back(Entry{id, slot});
|
|
}
|
|
m.sortBySlot();
|
|
return m;
|
|
}
|
|
|
|
std::string SlotMap::serialize() const {
|
|
// Array of {id, slot} objects in ascending slot order (entries_ is kept sorted).
|
|
// json::Writer + numToStr are the same emit path the pre-extraction writer used,
|
|
// so the fragment is byte-identical.
|
|
std::string out;
|
|
out += '[';
|
|
for (std::size_t i = 0; i < entries_.size(); ++i) {
|
|
if (i) out += ',';
|
|
json::Writer e(out);
|
|
e.keyStr("id", entries_[i].id);
|
|
e.keyRaw("slot", json::numToStr(entries_[i].slot));
|
|
}
|
|
out += ']';
|
|
return out;
|
|
}
|
|
|
|
} // namespace reasampler::model
|