From 5c462d545f1196cf57b7b895d1fe450652169b89 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Thu, 24 Sep 2020 16:57:08 +0300 Subject: [PATCH] Detect repeated intervals: initial implementation --- src/detectRepeats.test.ts | 85 +++++++++++++++++++++++++++++++++++++++ src/detectRepeats.ts | 57 ++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/detectRepeats.test.ts create mode 100644 src/detectRepeats.ts diff --git a/src/detectRepeats.test.ts b/src/detectRepeats.test.ts new file mode 100644 index 0000000..56f6d55 --- /dev/null +++ b/src/detectRepeats.test.ts @@ -0,0 +1,85 @@ +import { Interval } from "./ast"; +import { detectRepeats } from "./detectRepeats"; +import { Seconds } from "./types"; + +describe("detectRepeats()", () => { + it("does nothing with empty array", () => { + expect(detectRepeats([])).toEqual([]); + }); + + it("does nothing when no interval repeats", () => { + const intervals: Interval[] = [ + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Interval", duration: new Seconds(60), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Interval", duration: new Seconds(30), intensity: { from: 1.2, to: 1.2 }, comments: [] }, + { type: "Cooldown", duration: new Seconds(60), intensity: { from: 1, to: 0.5 }, comments: [] }, + ]; + expect(detectRepeats(intervals)).toEqual(intervals); + }); + + it("detects whole workout consisting of repetitions", () => { + const intervals: Interval[] = [ + { type: "Interval", duration: new Seconds(120), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Interval", duration: new Seconds(120), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Interval", duration: new Seconds(120), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Interval", duration: new Seconds(120), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + ]; + expect(detectRepeats(intervals)).toEqual([ + { + times: 4, + intervals: [ + { type: "Interval", duration: new Seconds(120), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + ], + }, + ]); + }); + + it("detects repetitions in the middle of workout", () => { + const intervals: Interval[] = [ + { type: "Warmup", duration: new Seconds(60), intensity: { from: 0.5, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(120), intensity: { from: 0.2, to: 0.2 }, comments: [] }, + { type: "Interval", duration: new Seconds(60), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Interval", duration: new Seconds(60), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Interval", duration: new Seconds(60), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Interval", duration: new Seconds(60), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + { type: "Rest", duration: new Seconds(120), intensity: { from: 0.2, to: 0.2 }, comments: [] }, + { type: "Cooldown", duration: new Seconds(60), intensity: { from: 1, to: 0.5 }, comments: [] }, + ]; + expect(detectRepeats(intervals)).toEqual([ + { type: "Warmup", duration: new Seconds(60), intensity: { from: 0.5, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(120), intensity: { from: 0.2, to: 0.2 }, comments: [] }, + { + times: 4, + intervals: [ + { type: "Interval", duration: new Seconds(60), intensity: { from: 1, to: 1 }, comments: [] }, + { type: "Rest", duration: new Seconds(60), intensity: { from: 0.5, to: 0.5 }, comments: [] }, + ], + }, + { type: "Rest", duration: new Seconds(120), intensity: { from: 0.2, to: 0.2 }, comments: [] }, + { type: "Cooldown", duration: new Seconds(60), intensity: { from: 1, to: 0.5 }, comments: [] }, + ]); + }); + + it("does not consider warmup/cooldown-range intervals to be repeatable", () => { + const intervals: Interval[] = [ + { type: "Warmup", duration: new Seconds(60), intensity: { from: 0.1, to: 1 }, comments: [] }, + { type: "Cooldown", duration: new Seconds(120), intensity: { from: 1, to: 0.5 }, comments: [] }, + { type: "Warmup", duration: new Seconds(60), intensity: { from: 0.1, to: 1 }, comments: [] }, + { type: "Cooldown", duration: new Seconds(120), intensity: { from: 1, to: 0.5 }, comments: [] }, + { type: "Warmup", duration: new Seconds(60), intensity: { from: 0.1, to: 1 }, comments: [] }, + { type: "Cooldown", duration: new Seconds(120), intensity: { from: 1, to: 0.5 }, comments: [] }, + { type: "Warmup", duration: new Seconds(60), intensity: { from: 0.1, to: 1 }, comments: [] }, + { type: "Cooldown", duration: new Seconds(120), intensity: { from: 1, to: 0.5 }, comments: [] }, + ]; + expect(detectRepeats(intervals)).toEqual(intervals); + }); +}); diff --git a/src/detectRepeats.ts b/src/detectRepeats.ts new file mode 100644 index 0000000..34e3740 --- /dev/null +++ b/src/detectRepeats.ts @@ -0,0 +1,57 @@ +import { equals } from "ramda"; +import { Interval } from "./ast"; + +export type RepeatedInterval = { + times: number; + intervals: Interval[]; +}; + +const windowSize = 2; + +const countRepetitions = (reference: Interval[], intervals: Interval[], startIndex: number): number => { + let repeats = 1; + while (startIndex + repeats * windowSize < intervals.length) { + const from = startIndex + repeats * windowSize; + const possibleRepeat = intervals.slice(from, from + windowSize); + if (equals(reference, possibleRepeat)) { + repeats++; + } else { + return repeats; + } + } + return repeats; +}; + +const isRangeInterval = (interval: Interval): boolean => interval.intensity.from !== interval.intensity.to; + +export const detectRepeats = (intervals: Interval[]): (Interval | RepeatedInterval)[] => { + if (intervals.length < windowSize) { + return intervals; + } + + const processed: (Interval | RepeatedInterval)[] = []; + let i = 0; + while (i < intervals.length) { + // Ignore warmup/cooldown-range intervals + if (isRangeInterval(intervals[i])) { + processed.push(intervals[i]); + i++; + continue; + } + + const reference = intervals.slice(i, i + windowSize); + const repeats = countRepetitions(reference, intervals, i); + if (repeats > 1) { + processed.push({ + times: repeats, + intervals: reference, + }); + i += repeats * windowSize; + } else { + processed.push(intervals[i]); + i++; + } + } + + return processed; +};