Detect repeated intervals: initial implementation

This commit is contained in:
Rene Saarsoo 2020-09-24 16:57:08 +03:00
parent ba88317fb9
commit 5c462d545f
2 changed files with 142 additions and 0 deletions

85
src/detectRepeats.test.ts Normal file
View File

@ -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);
});
});

57
src/detectRepeats.ts Normal file
View File

@ -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;
};