Skip to content

Commit 48bd522

Browse files
feat(data-structures/unstable): add RollingCounter (#7028)
1 parent 5f3572a commit 48bd522

File tree

3 files changed

+437
-0
lines changed

3 files changed

+437
-0
lines changed

data_structures/deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"./comparators": "./comparators.ts",
1111
"./red-black-tree": "./red_black_tree.ts",
1212
"./unstable-2d-array": "./unstable_2d_array.ts",
13+
"./unstable-rolling-counter": "./unstable_rolling_counter.ts",
1314
"./unstable-deque": "./unstable_deque.ts"
1415
}
1516
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright 2018-2026 the Deno authors. MIT license.
2+
// This module is browser compatible.
3+
4+
/**
5+
* A fixed-size rolling counter.
6+
*
7+
* The counter splits a window into a fixed number of segments, each tracking
8+
* a count. It has no built-in clock. Call
9+
* {@linkcode RollingCounter.prototype.rotate | `rotate`} to advance the
10+
* window on your own schedule.
11+
*
12+
* The class is iterable and yields segment counts from oldest to newest.
13+
*
14+
* @experimental **UNSTABLE**: New API, yet to be vetted.
15+
*
16+
* @example Usage
17+
* ```ts
18+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
19+
* import { assertEquals } from "@std/assert";
20+
*
21+
* // 3 segments, all starting at zero
22+
* const counter = new RollingCounter(3);
23+
*
24+
* // Add 5 to the current (newest) segment
25+
* counter.increment(5);
26+
* assertEquals(counter.total, 5);
27+
* assertEquals([...counter], [0, 0, 5]);
28+
*
29+
* // Advance the window, then add 3 to the new segment
30+
* counter.rotate();
31+
* counter.increment(3);
32+
* assertEquals(counter.total, 8);
33+
* assertEquals([...counter], [0, 5, 3]);
34+
*
35+
* // Bulk-rotate 2 steps: evicts the two oldest (0 and 5)
36+
* counter.rotate(2);
37+
* assertEquals(counter.total, 3);
38+
* assertEquals([...counter], [3, 0, 0]);
39+
* ```
40+
*/
41+
export class RollingCounter {
42+
#segments: number[];
43+
#cursor: number;
44+
#total: number;
45+
46+
/**
47+
* Creates a counter with the given number of segments, all starting at zero.
48+
*
49+
* @param segmentCount The number of segments. Must be a positive integer.
50+
*/
51+
constructor(segmentCount: number) {
52+
if (
53+
!Number.isInteger(segmentCount) || segmentCount < 1
54+
) {
55+
throw new RangeError(
56+
`Cannot create RollingCounter: segmentCount must be a positive integer, got ${segmentCount}`,
57+
);
58+
}
59+
this.#segments = new Array<number>(segmentCount).fill(0);
60+
this.#cursor = segmentCount - 1;
61+
this.#total = 0;
62+
}
63+
64+
/**
65+
* Adds `n` to the current segment.
66+
*
67+
* @param n The amount to add. Defaults to `1`. Must be a non-negative integer.
68+
* @returns The new total across all segments.
69+
*
70+
* @example Usage
71+
* ```ts
72+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
73+
* import { assertEquals } from "@std/assert";
74+
*
75+
* const counter = new RollingCounter(3);
76+
* const total = counter.increment(5);
77+
* assertEquals(total, 5);
78+
* ```
79+
*/
80+
increment(n: number = 1): number {
81+
if (!Number.isInteger(n) || n < 0) {
82+
throw new RangeError(
83+
`Cannot increment RollingCounter: n must be a non-negative integer, got ${n}`,
84+
);
85+
}
86+
this.#segments[this.#cursor]! += n;
87+
this.#total += n;
88+
return this.#total;
89+
}
90+
91+
/**
92+
* Advances the window by `steps`, dropping the oldest segments. If `steps`
93+
* is at least {@linkcode segmentCount}, all segments are cleared.
94+
*
95+
* @param steps How many steps to advance. Defaults to `1`. Must be a
96+
* non-negative integer.
97+
* @returns The sum of the counts that were removed.
98+
*
99+
* @example Usage
100+
* ```ts
101+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
102+
* import { assertEquals } from "@std/assert";
103+
*
104+
* const counter = new RollingCounter(2);
105+
* counter.increment(10);
106+
* counter.rotate();
107+
* const evicted = counter.rotate();
108+
* assertEquals(evicted, 10);
109+
* ```
110+
*
111+
* @example Bulk rotation
112+
* ```ts
113+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
114+
* import { assertEquals } from "@std/assert";
115+
*
116+
* const counter = new RollingCounter(3);
117+
* counter.increment(5);
118+
* counter.rotate();
119+
* counter.increment(3);
120+
*
121+
* const evicted = counter.rotate(2);
122+
* assertEquals(evicted, 5);
123+
* assertEquals(counter.total, 3);
124+
* ```
125+
*/
126+
rotate(steps: number = 1): number {
127+
if (!Number.isInteger(steps) || steps < 0) {
128+
throw new RangeError(
129+
`Cannot rotate RollingCounter: steps must be a non-negative integer, got ${steps}`,
130+
);
131+
}
132+
const len = this.#segments.length;
133+
if (steps >= len) {
134+
const evicted = this.#total;
135+
this.#segments.fill(0);
136+
this.#cursor = (this.#cursor + steps) % len;
137+
this.#total = 0;
138+
return evicted;
139+
}
140+
let evicted = 0;
141+
for (let i = 0; i < steps; i++) {
142+
this.#cursor = (this.#cursor + 1) % len;
143+
evicted += this.#segments[this.#cursor]!;
144+
this.#segments[this.#cursor] = 0;
145+
}
146+
this.#total -= evicted;
147+
return evicted;
148+
}
149+
150+
/**
151+
* The sum of all segment counts.
152+
*
153+
* @returns The sum of all segment counts.
154+
*
155+
* @example Usage
156+
* ```ts
157+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
158+
* import { assertEquals } from "@std/assert";
159+
*
160+
* const counter = new RollingCounter(3);
161+
* counter.increment(5);
162+
* counter.rotate();
163+
* counter.increment(3);
164+
* assertEquals(counter.total, 8);
165+
* ```
166+
*/
167+
get total(): number {
168+
return this.#total;
169+
}
170+
171+
/**
172+
* The number of segments in the window.
173+
*
174+
* @returns The number of segments.
175+
*
176+
* @example Usage
177+
* ```ts
178+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
179+
* import { assertEquals } from "@std/assert";
180+
*
181+
* const counter = new RollingCounter(5);
182+
* assertEquals(counter.segmentCount, 5);
183+
* ```
184+
*/
185+
get segmentCount(): number {
186+
return this.#segments.length;
187+
}
188+
189+
/**
190+
* Resets all segments to zero, as if the counter were just created.
191+
*
192+
* @example Usage
193+
* ```ts
194+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
195+
* import { assertEquals } from "@std/assert";
196+
*
197+
* const counter = new RollingCounter(3);
198+
* counter.increment(10);
199+
* counter.clear();
200+
* assertEquals(counter.total, 0);
201+
* ```
202+
*/
203+
clear(): void {
204+
this.#segments.fill(0);
205+
this.#cursor = this.#segments.length - 1;
206+
this.#total = 0;
207+
}
208+
209+
/**
210+
* Yields segment counts from oldest to newest.
211+
*
212+
* @returns An iterator over segment counts.
213+
*
214+
* @example Usage
215+
* ```ts
216+
* import { RollingCounter } from "@std/data-structures/unstable-rolling-counter";
217+
* import { assertEquals } from "@std/assert";
218+
*
219+
* const counter = new RollingCounter(3);
220+
* counter.increment(5);
221+
* counter.rotate();
222+
* counter.increment(3);
223+
* assertEquals([...counter], [0, 5, 3]);
224+
* ```
225+
*/
226+
*[Symbol.iterator](): IterableIterator<number> {
227+
const len = this.#segments.length;
228+
for (let i = 1; i <= len; i++) {
229+
yield this.#segments[(this.#cursor + i) % len]!;
230+
}
231+
}
232+
}

0 commit comments

Comments
 (0)