diff --git a/frontend/sync-client/src/utils/min-covered.test.ts b/frontend/sync-client/src/utils/min-covered.test.ts new file mode 100644 index 00000000..f53c8cdd --- /dev/null +++ b/frontend/sync-client/src/utils/min-covered.test.ts @@ -0,0 +1,48 @@ +import { CoveredValues } from "./min-covered"; + +describe("CoveredValues", () => { + test("should initialize with the given min value", () => { + const covered = new CoveredValues(5); + expect(covered.min).toBe(5); + }); + + test("should add values greater than min", () => { + const covered = new CoveredValues(0); + covered.add(3); + expect(covered.min).toBe(0); + covered.add(1); + expect(covered.min).toBe(1); + covered.add(4); + expect(covered.min).toBe(1); + covered.add(2); + expect(covered.min).toBe(4); + }); + + test("should ignore duplicate values", () => { + const covered = new CoveredValues(0); + covered.add(3); + covered.add(3); + covered.add(3); + expect(covered.min).toBe(0); + covered.add(1); + covered.add(2); + expect(covered.min).toBe(3); + }); + + test("should handle multiple consecutive values", () => { + const covered = new CoveredValues(132); + for (let i = 250; i > 132; i--) { + expect(covered.min).toBe(132); + covered.add(i); + } + expect(covered.min).toBe(250); + }); + + test("should handle adding values lower than current min", () => { + const covered = new CoveredValues(5); + covered.add(3); + expect(covered.min).toBe(5); + covered.add(6); + expect(covered.min).toBe(6); + }); +}); diff --git a/frontend/sync-client/src/utils/min-covered.ts b/frontend/sync-client/src/utils/min-covered.ts new file mode 100644 index 00000000..5bdf3ec8 --- /dev/null +++ b/frontend/sync-client/src/utils/min-covered.ts @@ -0,0 +1,51 @@ +/** + * A class that tracks the minimum covered value in a sequence of numbers. + * It keeps track of a minimum value based on the seen values. + * + * It expects integers slightly out of order and makes sure that the value of `min` is + * always the minimum of the seen values. This is done with bounded memory usage. + * + * @example + * ```typescript + * const covered = new CoveredValues(0); + * covered.add(2); // seenValues = [2], min = 0 + * covered.add(1); // seenValues = [], min = 2 + * covered.min; // returns 2 + * ``` + */ +export class CoveredValues { + private seenValues: number[] = []; + + public constructor(private minValue: number) {} + + public add(value: number): void { + if (value < this.minValue) { + return; + } + + let i = 0; + while (i < this.seenValues.length && this.seenValues[i] < value) { + i++; + } + + if (i === this.seenValues.length) { + this.seenValues.push(value); + } else if (this.seenValues[i] === value) { + return; + } else { + this.seenValues.splice(i, 0, value); + } + + while ( + this.seenValues.length > 0 && + this.seenValues[0] === this.minValue + 1 + ) { + this.seenValues.shift(); + this.minValue++; + } + } + + public get min(): number { + return this.minValue; + } +}