Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ All notable changes to Celerity are documented here. This project follows [Keep

### Added

- **`IDictionary<TKey, TValue?>` on the mutable dictionaries** — the nine hash-table and dense dictionaries now implement the mutable BCL interface alongside `IReadOnlyDictionary<,>`, so they drop into any API taking `IDictionary<,>`. `Keys` / `Values` widen to read-only `ICollection<T>` views, and `Contains` / `Remove` over a `KeyValuePair<,>` match on the pair — both as `Dictionary<,>` does. Additive: no existing public signature changed. Closes [#307](https://github.com/marius-bughiu/Celerity/issues/307).
- A public `CopyTo(KeyValuePair<TKey, TValue?>[], int)` on those dictionaries, plus `Contains` / `CopyTo` / `IsReadOnly` on their `Keys` / `Values` views. Closes [#307](https://github.com/marius-bughiu/Celerity/issues/307).
- `DictionaryInterfaceTests` — a cross-collection suite pinning the interface contract against a `Dictionary<,>` oracle, one row per dictionary, with matching Native AOT smoke coverage. Closes [#307](https://github.com/marius-bughiu/Celerity/issues/307).
- API-reference and README sections for the new interface, including `EnumMap`'s bounded-key caveat. Closes [#307](https://github.com/marius-bughiu/Celerity/issues/307).
- **`CompressedIntSet`** in `Celerity.Collections` — an exact, compressed set of 32-bit integers for the huge-and-sparse shape `BitSet`, `SparseSet` and `IntSet` do not serve. Each 65,536-value chunk is stored as a sorted array or a bitmap by density (plus an opt-in run-length form for clustered data that `Optimize()` and `AddRange` produce), so set algebra works chunk-at-a-time instead of one hash probe per element: at 1M values over a 100M universe it intersects ~9x faster and unions ~11x faster than `HashSet<int>`, in ~9x less memory. Implements `ISet<int>` and `IReadOnlySet<int>`, plus `AddRange`, `Optimize`, `IntersectCount`, `Cardinality` and `MemoryUsageInBytes`; enumeration is in ascending order. There is **no portable Roaring format** — Celerity ships no serializers — so this is an in-process structure, not Lucene / Druid / Spark interop. Closes [#310](https://github.com/marius-bughiu/Celerity/issues/310).
- `CompressedIntSetBenchmark` in the CI-tracked suite and the matching **CompressedIntSet** dashboard card, plus API-reference and README docs, dedicated and cross-collection tests, a `Celerity.Fuzz` target, and Native AOT smoke coverage. Closes [#310](https://github.com/marius-bughiu/Celerity/issues/310).
- **`RankSelectBitVector`** in `Celerity.Collections` — an immutable succinct index over a dense bit vector that answers `Rank(i)` (set bits below a position) in `O(1)` and `Select(k)` (position of the `k`-th set bit) in `O(log n)`, filling a BCL gap: .NET ships no rank or select anywhere, so the alternative is a hand-rolled `O(i/64)` popcount loop. Builds from a `BitSet`, packed `ulong[]`, or a list of set positions; `Rank0`, `TrySelect`, `IndexSizeInBytes`, and `ToBitSet` round out the surface. The index costs 25% over the bits and is **build-once** — any mutation requires an `O(n/64)` rebuild, so a vector that keeps changing should stay a `BitSet`. Closes [#312](https://github.com/marius-bughiu/Celerity/issues/312).
Expand All @@ -28,6 +32,8 @@ All notable changes to Celerity are documented here. This project follows [Keep

### Fixed

- A `PooledCelerityDictionary` `Keys` / `Values` view captured before `Dispose()` reported a stale `Count` and copied from returned arrays instead of throwing. Every view member that reads dictionary state now reports `ObjectDisposedException`, matching the rest of the type. Closes [#307](https://github.com/marius-bughiu/Celerity/issues/307).

- `Deque<T>.Clear()` no longer invalidates active enumerators when the deque is already empty. It was the only count-based collection where a `Clear()` that removed nothing tore down live enumerators, so a defensive clear mid-enumeration threw. Clearing a populated deque still invalidates them, as before. Closes [#333](https://github.com/marius-bughiu/Celerity/issues/333).
- The no-op-`Clear()` contract is now pinned across the whole collection family by a new cross-collection test suite, so it cannot drift again. Closes [#333](https://github.com/marius-bughiu/Celerity/issues/333).
- **A breaking API change could ship silently.** `dotnet pack` now validates every package against its last published version across all three TFMs and fails on any break, so a removed or narrowed public member can no longer reach NuGet.org with CI green. It runs on every PR, not just at release, and intentional breaks are recorded in a reviewed suppression file. CI-only; no consumer-visible behaviour change. Closes [#315](https://github.com/marius-bughiu/Celerity/issues/315).
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ Both take their ordering as a **struct** `IComparer<T>` type parameter (`Default
- `CountMinSketch<T, THasher>` — **probabilistic** frequency estimator: estimates per-element counts from a fixed grid, **never underestimating** (overestimate bounded by `epsilon · TotalCount`). Mergeable.
- `TopKSketch<T, THasher>` — **probabilistic** top-k / heavy-hitters sketch (Space-Saving): reports a stream's most frequent elements from a fixed `k` monitors in `O(k)` memory, **never underestimating** and never missing a hitter above `TotalCount / k`.

All dictionaries implement `IReadOnlyDictionary<TKey, TValue?>` and ship allocation-free struct enumerators, `Keys` / `Values` views, and an `IEnumerable<KeyValuePair<TKey, TValue>>` constructor. The hash-table collections store `default(TKey)` (zero / `null`) out-of-band so it never collides with the empty-slot sentinel; `SmallDictionary` stores it inline.
The mutable dictionaries implement **both** `IDictionary<TKey, TValue?>` and `IReadOnlyDictionary<TKey, TValue?>`, so they drop into an existing API taking either BCL interface; the immutable `FrozenCelerityDictionary` and the prefix-tree `Trie<TValue>` implement the read-only one only. All of them ship allocation-free struct enumerators, `Keys` / `Values` views, and an `IEnumerable<KeyValuePair<TKey, TValue>>` constructor. The hash-table collections store `default(TKey)` (zero / `null`) out-of-band so it never collides with the empty-slot sentinel; `SmallDictionary` stores it inline.

## Quick start

Expand Down Expand Up @@ -621,7 +621,7 @@ Each type buys a different tradeoff. Find your workload below; if it isn't here,
| **Prefix / range sums over a sequence you keep mutating** — running aggregates, rank / order-statistics counters (inversions, "how many ≤ x seen"), cumulative-frequency tables | `FenwickTree<T>` | Binary Indexed Tree (`T : INumber<T>`): **point update** and **prefix / range sum** both `O(log n)`, in one array with no per-node overhead. The BCL has no prefix-sum structure; a plain array forces `O(n)` per query (recompute the slice) *or* `O(n)` per update (fix the suffix). Wins precisely when updates and partial-sum queries interleave. If the data is immutable after build, a one-shot precomputed prefix-sum array answers in `O(1)` with less code; if you only update and never query a partial sum, a raw array is simpler. |
| Need a stable iteration order or multi-threaded access | `BTreeDictionary<,>` / `BTreeSet<>` for sorted order, `Trie<TValue>` for ordered string keys; BCL `ConcurrentDictionary<,>` for concurrency | Celerity is single-threaded, and the **hash-based** collections leave iteration order unspecified. The ordered collections do promise order by contract: the B-trees iterate in comparer order, `Trie<TValue>` in ascending ordinal key order. |

**Celerity is not the right answer when** you need concurrent access (use `ConcurrentDictionary<,>` or your own lock — Celerity is single-threaded), or a guaranteed iteration order from the **hash-based** collections (those dictionaries expose `IReadOnlyDictionary<,>` and those sets `ISet<>`, and neither promises an order across versions). When you do need ordered iteration, reach for the ordered collections instead: `BTreeDictionary<,>` / `BTreeSet<>` iterate in comparer order and support bounds and range scans, and `Trie<TValue>` gives ascending ordinal order over string keys. `BTreeDictionary<,>` also implements the **mutable** `IDictionary<,>` interface, and `BTreeSet<>` implements `ISet<>`.
**Celerity is not the right answer when** you need concurrent access (use `ConcurrentDictionary<,>` or your own lock — Celerity is single-threaded), or a guaranteed iteration order from the **hash-based** collections (they implement `IDictionary<,>` / `IReadOnlyDictionary<,>` and `ISet<>`, but none promises an order across versions). When you do need ordered iteration, reach for the ordered collections instead: `BTreeDictionary<,>` / `BTreeSet<>` iterate in comparer order and support bounds and range scans, and `Trie<TValue>` gives ascending ordinal order over string keys. Interface support is no longer a reason to choose one over another — every mutable dictionary implements `IDictionary<,>` and every mutable set implements `ISet<>`.

## Choosing a hasher

Expand Down Expand Up @@ -770,7 +770,7 @@ Celerity is **Native AOT and trimming compatible** — no reflection, runtime co

## API at a glance

The dictionaries mirror the parts of `Dictionary<TKey, TValue>` most callers reach for: indexer get/set, `ContainsKey`, `TryGetValue`, `Add`, `TryAdd`, `Remove` (both overloads), `Clear`, `EnsureCapacity` / `TrimExcess`, `Count`, `Keys`, `Values`, `GetEnumerator()`. The string-keyed types additionally take a `ReadOnlySpan<char>` on `TryGetValue` / `ContainsKey` / `Contains`, so a caller holding a slice of a buffer never allocates a `string` to probe. They implement `IReadOnlyDictionary<TKey, TValue?>` and accept an `IEnumerable<KeyValuePair<TKey, TValue>>` at construction. The sets expose `Add`, `TryAdd`, `Contains`, `Remove`, `Clear`, `EnsureCapacity` / `TrimExcess`, `Count`, and a struct enumerator. `EnsureCapacity(n)` pre-grows the table once for a known-size bulk insert (no incremental rehashes); `TrimExcess()` rehashes back down to fit `Count`. The zero / `default(TKey)` key (or element) is stored out-of-band so it never collides with the empty-slot sentinel.
The dictionaries mirror the parts of `Dictionary<TKey, TValue>` most callers reach for: indexer get/set, `ContainsKey`, `TryGetValue`, `Add`, `TryAdd`, `Remove` (both overloads), `Clear`, `EnsureCapacity` / `TrimExcess`, `Count`, `Keys`, `Values`, `GetEnumerator()`. The string-keyed types additionally take a `ReadOnlySpan<char>` on `TryGetValue` / `ContainsKey` / `Contains`, so a caller holding a slice of a buffer never allocates a `string` to probe. They implement `IDictionary<TKey, TValue?>` and `IReadOnlyDictionary<TKey, TValue?>` — the `Keys` / `Values` views widen to read-only `ICollection<T>`s through the mutable interface, whose mutators throw `NotSupportedException` exactly as `Dictionary<,>.KeyCollection` does — and accept an `IEnumerable<KeyValuePair<TKey, TValue>>` at construction. The sets expose `Add`, `TryAdd`, `Contains`, `Remove`, `Clear`, `EnsureCapacity` / `TrimExcess`, `Count`, and a struct enumerator. `EnsureCapacity(n)` pre-grows the table once for a known-size bulk insert (no incremental rehashes); `TrimExcess()` rehashes back down to fit `Count`. The zero / `default(TKey)` key (or element) is stored out-of-band so it never collides with the empty-slot sentinel.

Full constructors, signatures, exceptions, and per-type examples: **[API reference](docs/README.md)**.

Expand Down
Loading
Loading