101 lines
3.0 KiB
TypeScript
101 lines
3.0 KiB
TypeScript
import { eqProps, flatten, zip } from "ramda";
|
|
import { Interval, Comment } from "./ast";
|
|
import { Duration } from "./Duration";
|
|
|
|
export type RepeatedInterval = {
|
|
type: "repeat";
|
|
times: number;
|
|
intervals: Interval[];
|
|
comments: Comment[];
|
|
};
|
|
|
|
// All fields besides comments must equal
|
|
const equalIntervals = (a: Interval, b: Interval): boolean =>
|
|
eqProps("type", a, b) && eqProps("duration", a, b) && eqProps("intensity", a, b) && eqProps("cadence", a, b);
|
|
|
|
const equalIntervalArrays = (as: Interval[], bs: Interval[]): boolean =>
|
|
zip(as, bs).every(([a, b]) => equalIntervals(a, b));
|
|
|
|
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 (equalIntervalArrays(reference, possibleRepeat)) {
|
|
repeats++;
|
|
} else {
|
|
return repeats;
|
|
}
|
|
}
|
|
return repeats;
|
|
};
|
|
|
|
const offsetComments = (interval: Interval, baseOffset: Duration): Comment[] => {
|
|
return interval.comments.map(({ offset, ...rest }) => ({
|
|
offset: baseOffset.add(offset),
|
|
...rest,
|
|
}));
|
|
};
|
|
|
|
const collectComments = (intervals: Interval[]): Comment[] => {
|
|
let previousIntervalsDuration = new Duration(0);
|
|
return flatten(
|
|
intervals.map((interval) => {
|
|
const comments = offsetComments(interval, previousIntervalsDuration);
|
|
previousIntervalsDuration = previousIntervalsDuration.add(interval.duration);
|
|
return comments;
|
|
}),
|
|
);
|
|
};
|
|
|
|
const stripComments = (intervals: Interval[]): Interval[] => {
|
|
return intervals.map(({ comments, ...rest }) => ({ comments: [], ...rest }));
|
|
};
|
|
|
|
const extractRepeatedInterval = (intervals: Interval[], i: number): RepeatedInterval | undefined => {
|
|
const reference = intervals.slice(i, i + windowSize);
|
|
const repeats = countRepetitions(reference, intervals, i);
|
|
if (repeats === 1) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
type: "repeat",
|
|
times: repeats,
|
|
intervals: stripComments(reference),
|
|
comments: collectComments(intervals.slice(i, i + windowSize * repeats)),
|
|
};
|
|
};
|
|
|
|
const isRangeInterval = ({ intensity }: Interval): boolean => intensity.start !== intensity.end;
|
|
|
|
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 repeatedInterval = extractRepeatedInterval(intervals, i);
|
|
if (repeatedInterval) {
|
|
processed.push(repeatedInterval);
|
|
i += repeatedInterval.times * windowSize;
|
|
} else {
|
|
processed.push(intervals[i]);
|
|
i++;
|
|
}
|
|
}
|
|
|
|
return processed;
|
|
};
|