You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Merge branch 'main' into chore/code-review/argsort-alias-doc
Resolves the CHANGELOG conflict: main's PartialSort.TopK overlap-guard
entry and this branch's RadixSort.ArgSort doc entry were both added at
the head of the Unreleased "Fixed" section. Both are kept.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CHANGELOG.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,6 +6,13 @@ All notable changes to Celerity are documented here. This project follows [Keep
6
6
7
7
### Added
8
8
9
+
-**`SegmentTree<T, TMonoid>`** in `Celerity.Collections` — range aggregates over any **associative** fold, with point update and range query both `O(log n)`. Answers the range min / max / gcd / bitwise questions `FenwickTree<T>` structurally cannot, since a Fenwick range is the *difference* of two prefix folds and so needs an inverse. Against the `O(n)` scan the BCL leaves you with: **14.8x** at 100k elements on interleaved update + range-min, **81x** on a query batch, **1.4x** at 1k. Range *updates* are not supported. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
10
+
-**`IMonoid<T>`** with `SumMonoid<T>`, `MinMonoid<T>`, `MaxMonoid<T>`, `BitwiseAndMonoid<T>` and `BitwiseOrMonoid<T>` — taken as a `struct` type parameter, like the hashers, so `Combine` inlines. `MinMonoid` / `MaxMonoid` document a finite-values domain for floating-point `T`. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
11
+
-`SegmentTreeTests`, `SegmentTreeDifferentialTests` and `MonoidTests`, including an exhaustive length × range sweep under a **non-commutative** fold — the only kind that can catch the `2n` layout mis-ordering a combine. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
12
+
- Cross-collection rows in `ClearNoOpVersionTests` (a third fixed-length exception, alongside `BitSet` and `FenwickTree`), `EnumeratorInvalidationAndClearCoverageTests` and `OversizedSourceAndResidualGuardTests`. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
13
+
- A `SegmentTree``Celerity.Fuzz` target and Native AOT smoke coverage over three monoid instantiations. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
14
+
-`SegmentTreeBenchmark` in the CI-tracked suite, with its dashboard cards and landing-page ship card. Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
15
+
- API-reference and README sections for `SegmentTree` and `IMonoid`, correcting `FenwickTree`'s docs, which said a segment tree was "not shipped". Closes [#348](https://github.com/marius-bughiu/Celerity/issues/348).
9
16
-**`Celerity.Sorting`** — a new package with the non-comparison sorts the BCL has no path for: `RadixSort` (LSD radix over `uint` / `int` / `ulong` / `long` / `float` / `double`, in keys-only, key+payload and `ArgSort` forms, **stable**), `CountingSort` (bounded `byte` / `ushort` / declared-`[min, max]``int` ranges, **stable**) and `PartialSort` (`O(n)` introselect plus an `O(n log k)``TopK`). `Array.Sort` is contractually in-place while radix needs `O(n)` scratch, so this is a gap the BCL cannot close. `SortWithScratch` overloads make `RadixSort` / `CountingSort` allocation-free; `PartialSort` allocates nothing in any form. Depends only on `Celerity.Primitives`. Closes [#309](https://github.com/marius-bughiu/Celerity/issues/309).
10
17
- Crossovers are documented rather than claimed away: `RadixSort` loses below a few hundred elements, `CountingSort` loses once the key range approaches the element count, and `PartialSort` beats LINQ on allocation rather than asymptotics. `RadixSort` orders `NaN` by sign bit and `-0.0` before `+0.0`, where `Array.Sort` moves all NaNs to the front. Closes [#309](https://github.com/marius-bughiu/Celerity/issues/309).
11
18
- Dedicated and differential tests, three `Celerity.Fuzz` targets reconciled against `Array.Sort`, Native AOT smoke coverage, `RadixSortBenchmark` / `CountingSortBenchmark` / `PartialSortBenchmark` in the CI-tracked suite with their dashboard cards, `docs/api/sorting.md`, and README entries. Closes [#309](https://github.com/marius-bughiu/Celerity/issues/309).
@@ -20,6 +27,7 @@ All notable changes to Celerity are documented here. This project follows [Keep
20
27
21
28
### Fixed
22
29
30
+
-`PartialSort.TopK` now throws `ArgumentException` when its `destination` overlaps its `source`, instead of silently returning a wrong answer and writing to the source it documents as untouched. Disjoint slices of one array are still accepted, matching `RadixSort` and `CountingSort`.
23
31
-`RadixSort.ArgSort`'s XML docs promised an `ArgumentException` when `indices` shares storage with `keys`, but only the `int`-keyed overload can throw it — the aliasing check is a same-element-type test by design. The doc now says which overload it covers, and why the rest treat a reinterpreted alias as out of contract. Documentation only.
24
32
- Eight documentation links pointed at anchors that do not exist: seven `CeleritySet` / `SwissSet` references in `docs/api/collections.md` and one in `CHANGELOG.md`. GitHub deletes `<`, `>` and `,` from a heading without substituting a separator, so `CeleritySet<T, THasher>` anchors as `#celeritysett-thasher`, not the `#celerityset-t-thasher` everyone writes. Closes [#339](https://github.com/marius-bughiu/Celerity/issues/339).
Both take their ordering as a **struct**`IComparer<T>` type parameter (`DefaultComparer<T>` by default), exactly as the hashers are struct type parameters, so the comparison inlines instead of costing a virtual call per key inspected inside a node.
102
102
103
-
**Prefix sums**
103
+
**Range aggregates**
104
104
105
105
-`FenwickTree<T>` — a **Binary Indexed Tree** over a fixed-length numeric sequence (`where T : struct, INumber<T>`): **point update** and **prefix / range sum** both in `O(log n)`, in one flat array with no per-node overhead. The prefix-sum structure the BCL lacks — running aggregates, rank / order-statistics counters, cumulative-frequency tables — where a plain array is `O(n)` per query (recompute the slice) *or*`O(n)` per update (fix the suffix). Wins precisely when updates and partial-sum queries interleave.
106
+
-`SegmentTree<T, TMonoid>` — range aggregates over an arbitrary **associative** fold: **point update** and **range query** both in `O(log n)`, in one flat array of `2n` cells. The half of the range-query space a Fenwick tree cannot reach — its query is the *difference* of two prefix folds, so it needs an inverse, while a segment tree stores each node's fold outright. That puts range **min**, **max**, **gcd**, bitwise **and**/**or** and any monoid you write in reach. The BCL has no range-aggregate structure at all, so the baseline is a plain array scanned per query. `SumMonoid` / `MinMonoid` / `MaxMonoid` / `BitwiseAndMonoid` / `BitwiseOrMonoid` ship built in, as struct type parameters so the fold inlines; non-commutative folds are safe, since the query preserves index order.
<summary><b>Range min / max / any associative fold with live updates</b> — SegmentTree</summary>
561
+
562
+
`SegmentTree<T, TMonoid>` answers the aggregate of any half-open range under an arbitrary **associative** fold, with **point updates** and **range queries** both in `O(log n)`. It is the half of the range-query space `FenwickTree<T>` cannot reach: a Fenwick range query is the *difference* of two prefix sums, so the operation must have an inverse — minimum has none. The fold arrives as a **struct** type parameter, exactly like the hashers, so `Combine` inlines instead of costing a virtual call per level.
563
+
564
+
```csharp
565
+
// A live order book: the cheapest ask in any price band, while prices keep moving.
// Any monoid works. Write a struct with an Identity and an associative Combine:
575
+
publicreadonlystructGcdMonoid : IMonoid<uint>
576
+
{
577
+
publicuintIdentity=>0; // gcd(0, a) == a
578
+
publicuintCombine(uintleft, uintright)
579
+
{
580
+
while (right!=0) (left, right) = (right, left%right);
581
+
returnleft;
582
+
}
583
+
}
584
+
```
585
+
586
+
</details>
587
+
558
588
<details>
559
589
<summary><b>Construct from an existing collection</b></summary>
560
590
@@ -623,6 +653,7 @@ Each type buys a different tradeoff. Find your workload below; if it isn't here,
623
653
|**Look a string key up from a `ReadOnlySpan<char>`** you already hold (route dispatch, header lookup, parse-then-map) without allocating a `string` per probe | span overloads on `FrozenCelerityDictionary` / `FrozenCeleritySet` / `CelerityDictionary<string, …>` / `CeleritySet<string, …>` / `Trie<TValue>`|`TryGetValue(ReadOnlySpan<char>, …)` / `ContainsKey` / `Contains` probe the table directly, deleting the `new string(span)` allocation and copy per lookup. Available whenever the hasher implements `ISpanHashProvider` — every built-in `String*Hasher` does. Same results as the `string` overloads (ordinal comparison); an empty span means `""`, never the `null` key. See [span-keyed lookups](docs/api/collections.md#span-keyed-lookups). |
624
654
| **Sorted keys** — you need the entries in comparer order, or the ordered questions a hash table cannot answer: smallest / largest key, "first key at or after *x*", "every key in `[a, b)`" (time-series by timestamp, order books, LSM-style memtables, sweep-line events, interval endpoints) | `BTreeDictionary<TKey, TValue>` / `BTreeSet<T>` | B-tree with up to 31 keys per node in flat arrays: a lookup visits `log₃₂(n)` nodes instead of chasing `log₂(n)` pointers (~4 cache misses instead of ~20 at `n = 1M`), an in-order walk streams contiguous arrays rather than successor pointers, and allocation is one node per 31 entries instead of one object per entry. The BCL has no B-tree: `SortedDictionary<,>` / `SortedSet<>` are red-black trees, `SortedList<,>` is `O(n)` per middle insert, and `OrderedDictionary<,>` (.NET 9) is *insertion*-ordered, not sorted. Wins on the **interleaved insert + lookup + range-scan** load; for a few dozen entries a `SortedList<,>` is hard to beat, and if you never need order a hash table answers in `O(1)`. |
625
655
|**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. |
656
+
| **Range min / max / gcd / bit-mask over a sequence you keep mutating** — sliding-window extrema over a live history, "cheapest offer in this price band" over an order book, per-window capability masks, or any other **associative fold with no inverse** | `SegmentTree<T, TMonoid>` | Point update and range query both `O(log n)`, in one flat array of `2n` cells. `FenwickTree<T>` cannot answer these at all: it computes a range as the *difference* of two prefix folds, so the operation must be invertible. Five folds ship (`Sum` / `Min` / `Max` / `BitwiseAnd` / `BitwiseOr`) and any associative one you write is a field-free struct; non-commutative folds are safe. The BCL has no range-aggregate structure, so the alternative is an `O(n)` scan per query — **14.8× faster** on interleaved update + range-min at 100k, **81×** on a query batch, but only **1.4×** at 1k, where scanning a contiguous array is cache-friendly. If the fold is addition use `FenwickTree<T>` (half the memory); if the sequence never changes after build, a sparse table or a prefix array answers in `O(1)`. Range *updates* are not supported — that needs lazy propagation, a different contract. |
626
657
| **Set algebra over two lists you already hold in sorted order** — intersect / union / diff sorted ID, row-id or posting lists, or just ask how many values they share (inverted indexes, cohort intersection, join-key pre-filters) | `SortedSpan.Intersect` / `Union` / `Except` / `IntersectCount` / `Overlaps` (in `Celerity.Primitives`) | Not a collection — static set algebra over spans. A two-cursor merge exploits the ordering the data already has, so it touches each element once and writes into caller-owned memory: **4.2× faster than `HashSet<int>` at 1M × 1M and 0 bytes allocated against 17.9 MB**, and **257× faster** on the asymmetric 1k × 10M shape where it gallops. `IntersectCount` / `Overlaps` need no buffer at all. ⚠️ **Both spans must be sorted ascending** — unsorted input silently returns a wrong answer. If your data is not already sorted, sorting it first to use this is usually a loss; reach for a set instead. See [sorted-span set algebra](docs/api/utilities.md#sortedspan-sorted-span-set-algebra). |
627
658
| 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. |
0 commit comments