RollingCounter

import { RollingCounter } from "jsr:@ende/data-structures/unstable-rolling-counter";

class ยท experimental

class RollingCounter implements Iterable<number>, ReadonlyRollingCounter

A fixed-size rolling counter.

The counter splits a window into a fixed number of segments, each tracking a count. It has no built-in clock. Call rotate to advance the window on your own schedule.

The class is iterable and yields segment counts from oldest to newest.

Method Time complexity
increment(n) Constant
rotate(steps) Linear in min(steps, segment count)
current Constant
total Constant
segmentCount Constant
at(index) Constant
clear() Linear in the number of segments
toArray() Linear in the number of segments
toJSON() Linear in the number of segments
[Symbol.iterator] Linear in the number of segments
RollingCounter() Linear in the number of segments
RollingCounter.from(source) Linear in the number of segments

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

// 3 segments, all starting at zero
const counter = new RollingCounter(3);

// Add 5 to the current (newest) segment
counter.increment(5);
assertEquals(counter.total, 5);
assertEquals([...counter], [0, 0, 5]);

// Advance the window, then add 3 to the new segment
counter.rotate();
counter.increment(3);
assertEquals(counter.total, 8);
assertEquals([...counter], [0, 5, 3]);

// Bulk-rotate 2 steps: evicts the two oldest (0 and 5)
counter.rotate(2);
assertEquals(counter.total, 3);
assertEquals([...counter], [3, 0, 0]);

Constructors

constructor

new RollingCounter(segmentCount: number)

experimental

Creates a counter with the given number of segments, all starting at zero.

segmentCount: number

The number of segments. Must be a positive integer.

Static methods

from

static from(source: RollingCounter | RollingCounterSnapshot): RollingCounter

experimental

Creates a counter from an existing RollingCounter or from a snapshot previously obtained via toJSON. The snapshot's segments array defines both the number of segments and their initial values, ordered oldest to newest (matching iteration order). The last element is the current (newest) segment.

When given a RollingCounter, returns an independent clone with a canonical internal layout: segments laid out oldest-to-newest with the current segment at the last position. Mutating the clone does not affect the source.

source: RollingCounter | RollingCounterSnapshot

A RollingCounter to clone, or a snapshot previously obtained from toJSON.

Returns

A new RollingCounter with the given state.

Example: Round-trip serialization

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const original = new RollingCounter(3);
original.increment(5);
original.rotate();
original.increment(3);

const snapshot = original.toJSON();
const restored = RollingCounter.from(snapshot);

assertEquals([...restored], [...original]);
assertEquals(restored.total, original.total);
assertEquals(restored.segmentCount, original.segmentCount);

Example: Cloning from a counter

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const original = new RollingCounter(3);
original.increment(5);
original.rotate();
original.increment(3);

const clone = RollingCounter.from(original);
clone.rotate();
clone.increment(7);

assertEquals([...original], [0, 5, 3]);
assertEquals([...clone], [5, 3, 7]);

Properties

[Symbol.toStringTag]

readonly [Symbol.toStringTag]: "RollingCounter"

experimental

A string tag for the class, used by Object.prototype.toString().

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
assertEquals(
  Object.prototype.toString.call(counter),
  "[object RollingCounter]",
);

Methods

increment

increment(n: number = 1): number

experimental

Adds n to the current segment.

n: number = 1

The amount to add. Defaults to 1. Must be a non-negative integer.

Returns

The new total across all segments.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
const total = counter.increment(5);
assertEquals(total, 5);

rotate

rotate(steps: number = 1): number

experimental

Advances the window by steps, dropping the oldest segments. If steps is at least segmentCount, all segments are cleared.

steps: number = 1

How many steps to advance. Defaults to 1. Must be a non-negative integer.

Returns

The sum of the counts that were removed.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(2);
counter.increment(10);
counter.rotate();
const evicted = counter.rotate();
assertEquals(evicted, 10);

Example: Bulk rotation

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(5);
counter.rotate();
counter.increment(3);

const evicted = counter.rotate(2);
assertEquals(evicted, 5);
assertEquals(counter.total, 3);

current

get current(): number

experimental

The count in the current (newest) segment.

Returns

The count in the current segment.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(5);
assertEquals(counter.current, 5);

counter.rotate();
assertEquals(counter.current, 0);

counter.increment(3);
assertEquals(counter.current, 3);

total

get total(): number

experimental

The sum of all segment counts.

Returns

The sum of all segment counts.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(5);
counter.rotate();
counter.increment(3);
assertEquals(counter.total, 8);

segmentCount

get segmentCount(): number

experimental

The number of segments in the window.

Returns

The number of segments.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(5);
assertEquals(counter.segmentCount, 5);

at

at(index: number): number | undefined

experimental

Returns the segment count at index, where index 0 is the oldest segment and index segmentCount - 1 is the current (newest) segment. Negative indices count from the newest end (at(-1) is the current segment, same as current). Returns undefined for out-of-range indices.

index: number

The zero-based index in oldest-to-newest order. Negative values count from the newest end.

Returns

The count at the index, or undefined if out of range.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(5);
counter.rotate();
counter.increment(3);

assertEquals(counter.at(0), 0);
assertEquals(counter.at(1), 5);
assertEquals(counter.at(2), 3);
assertEquals(counter.at(-1), 3);
assertEquals(counter.at(99), undefined);

clear

clear(): void

experimental

Resets all segments to zero, as if the counter were just created.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(10);
counter.clear();
assertEquals(counter.total, 0);

toArray

toArray(): number[]

experimental

Returns a fresh array of segment counts ordered from oldest to newest (matching iteration order). The returned array is owned by the caller; mutating it does not affect the counter.

Returns

A new array of segment counts in oldest-to-newest order.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(5);
counter.rotate();
counter.increment(3);

assertEquals(counter.toArray(), [0, 5, 3]);

toJSON

toJSON(): RollingCounterSnapshot

experimental

Returns a serializable snapshot of the counter state. The segments array is ordered oldest to newest (matching iteration order), so the last element is the current segment.

The snapshot is compatible with JSON.stringify and can be restored with RollingCounter.from.

Returns

A plain-object snapshot of the counter.

Example: JSON round-trip

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(5);

const json = JSON.stringify(counter);
const restored = RollingCounter.from(JSON.parse(json));
assertEquals(restored.total, 5);

[Symbol.iterator]

[Symbol.iterator](): IterableIterator<number>

experimental

Yields segment counts from oldest to newest.

Returns

An iterator over segment counts.

Example: Usage

import { RollingCounter } from "@ende/data-structures/unstable-rolling-counter";
import { assertEquals } from "@ende/assert";

const counter = new RollingCounter(3);
counter.increment(5);
counter.rotate();
counter.increment(3);
assertEquals([...counter], [0, 5, 3]);

Source