diff --git a/src/utils/stopwatch.ts b/src/utils/stopwatch.ts new file mode 100644 index 0000000..ad0e9a4 --- /dev/null +++ b/src/utils/stopwatch.ts @@ -0,0 +1,68 @@ +interface StartCallback { + (currentDate: Date, stopwatch: Stopwatch): void; +} + +interface StopCallback { + (elapsedMilliseconds: number, currentDate: Date, stopwatch: Stopwatch): void; +} + +export default class Stopwatch { + #initialDate = 0; + #isRunning = false; + #elapsedMilliseconds = 0; + #onStart: StartCallback = null; + #onStop: StopCallback = null; + + public constructor(onStart?: StartCallback, onStop?: StopCallback) { + this.#onStart = onStart; + this.#onStop = onStop; + } + + public get elapsedMilliseconds(): number { + return this.#isRunning + ? (this.#elapsedMilliseconds + new Date().valueOf() - this.#initialDate) + : this.#elapsedMilliseconds; + } + + public get isRunning(): boolean { + return this.#isRunning; + } + + public start(): boolean { + if (!this.#isRunning) { + const currentDate = new Date(); + this.#initialDate = currentDate.valueOf(); + this.#isRunning = true; + this.#onStart?.(currentDate, this); + return true; + } + return false; + } + + public stop(): boolean { + if (this.#isRunning) { + const currentDate = new Date(); + this.#elapsedMilliseconds += currentDate.valueOf() - this.#initialDate; + this.#isRunning = false; + this.#onStop?.(this.#elapsedMilliseconds, currentDate, this); + return true; + } + return false; + } + + public reset(): void { + this.stop(); + this.#elapsedMilliseconds = 0; + } + + public restart(): void { + this.reset(); + this.start(); + } + + public static startNew(onStart?: StartCallback, onStop?: StopCallback): Stopwatch { + const stopwatch = new Stopwatch(onStart, onStop); + stopwatch.start(); + return stopwatch; + } +} diff --git a/test/stopwatch.test.ts b/test/stopwatch.test.ts new file mode 100644 index 0000000..e80a1b9 --- /dev/null +++ b/test/stopwatch.test.ts @@ -0,0 +1,59 @@ +import { describe, test, expect } from "@jest/globals"; +import sleep from "../src/utils/sleep"; +import Stopwatch from "../src/utils/stopwatch"; + +describe("Stopwatch", () => { + test("base functionality of Stopwatch works", async () => { + const stopwatch = new Stopwatch(); + expect(stopwatch.isRunning).toBe(false); + expect(stopwatch.elapsedMilliseconds).toBe(0); + expect(stopwatch.start()).toBe(true); + await sleep(100); + expect(stopwatch.start()).toBe(false); + expect(stopwatch.stop()).toBe(true); + expect(stopwatch.stop()).toBe(false); + expect(stopwatch.elapsedMilliseconds).toBeGreaterThan(50); + expect(stopwatch.elapsedMilliseconds).toBeLessThan(200); + stopwatch.reset(); + expect(stopwatch.elapsedMilliseconds).toBe(0); + }); + + test("Stopwatch executes callbacks on start and end", async () => { + let started = 0; + let stopped = 0; + let ms = 0; + + const stopwatch = new Stopwatch(() => ++started, elapsedMilliseconds => { + ++stopped; + ms = elapsedMilliseconds; + }); + + expect(stopwatch.isRunning).toBe(false); + expect(stopwatch.elapsedMilliseconds).toBe(0); + + expect(stopwatch.start()).toBe(true); + expect(started).toBe(1); + expect(stopped).toBe(0); + + await sleep(100); + + expect(stopwatch.start()).toBe(false); + expect(started).toBe(1); + expect(stopped).toBe(0); + + expect(stopwatch.stop()).toBe(true); + expect(started).toBe(1); + expect(stopped).toBe(1); + + expect(stopwatch.stop()).toBe(false); + expect(started).toBe(1); + expect(stopped).toBe(1); + + expect(stopwatch.elapsedMilliseconds).toBeGreaterThan(50); + expect(stopwatch.elapsedMilliseconds).toBeLessThan(200); + expect(stopwatch.elapsedMilliseconds).toBe(ms); + + stopwatch.reset(); + expect(stopwatch.elapsedMilliseconds).toBe(0); + }); +});