Compare commits
8 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
da62a0f5c6 | |
|
|
1a0d26f4ca | |
|
|
c9ab304460 | |
|
|
e32b65926f | |
|
|
e49a1738ec | |
|
|
29cdd41966 | |
|
|
6bbdea1908 | |
|
|
cec3c57477 |
|
|
@ -117,7 +117,6 @@ console.log(stats(workout));
|
||||||
- More restricted syntax for text (with quotes)
|
- More restricted syntax for text (with quotes)
|
||||||
- Concatenate similar intervals
|
- Concatenate similar intervals
|
||||||
- Distinguish between terrain-sensitive and insensitive free-ride.
|
- Distinguish between terrain-sensitive and insensitive free-ride.
|
||||||
- Use `<Ramp>` in addition to `<Warmup>` and `<Cooldown>`
|
|
||||||
|
|
||||||
[zwift]: https://zwift.com/
|
[zwift]: https://zwift.com/
|
||||||
[zwofactory]: https://zwofactory.com/
|
[zwofactory]: https://zwofactory.com/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
Name: Ramps
|
||||||
|
Author: R.Saarsoo
|
||||||
|
Description:
|
||||||
|
Various kinds of ramp intervals.
|
||||||
|
|
||||||
|
Ramp: 5:00 40%..75%
|
||||||
|
|
||||||
|
Ramp: 10:00 80%..90%
|
||||||
|
Ramp: 10:00 90%..80%
|
||||||
|
|
||||||
|
Warmup: 10:00 80%..90%
|
||||||
|
Cooldown: 10:00 90%..80%
|
||||||
|
|
||||||
|
Ramp: 5:00 75%..40%
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "zwiftout",
|
"name": "zwiftout",
|
||||||
"version": "2.0.0",
|
"version": "2.3.0",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
"description": "Zwift workout generator command line tool and library",
|
"description": "Zwift workout generator command line tool and library",
|
||||||
"author": "Rene Saarsoo <github@triin.net>",
|
"author": "Rene Saarsoo <github@triin.net>",
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"types": "dist/index.d.ts",
|
"types": "dist/index.d.ts",
|
||||||
"bin": {
|
"bin": {
|
||||||
"zwiftout": "./bin/zwiftout.js"
|
"zwiftout": "bin/zwiftout.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1",
|
"argparse": "^2.0.1",
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,26 @@ export class RangeIntensity implements Intensity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RangeIntensityEnd implements Intensity {
|
||||||
|
constructor(private _end: number) {}
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this._end;
|
||||||
|
}
|
||||||
|
|
||||||
|
get start(): number {
|
||||||
|
throw new Error("RangeIntensityEnd has no start");
|
||||||
|
}
|
||||||
|
|
||||||
|
get end() {
|
||||||
|
return this._end;
|
||||||
|
}
|
||||||
|
|
||||||
|
get zone() {
|
||||||
|
return intensityValueToZoneType(this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class FreeIntensity implements Intensity {
|
export class FreeIntensity implements Intensity {
|
||||||
get value() {
|
get value() {
|
||||||
// To match Zwift, which gives 64 TSS for 1h of freeride.
|
// To match Zwift, which gives 64 TSS for 1h of freeride.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ const generateTextEvents = (comments: Comment[]): xml.XmlObject[] => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateRangeInterval = (
|
const generateRangeInterval = (
|
||||||
tagName: "Warmup" | "Cooldown",
|
tagName: "Warmup" | "Cooldown" | "Ramp",
|
||||||
{ duration, intensity, cadence, comments }: Interval,
|
{ duration, intensity, cadence, comments }: Interval,
|
||||||
): xml.XmlObject => {
|
): xml.XmlObject => {
|
||||||
return {
|
return {
|
||||||
|
|
@ -79,16 +79,22 @@ const generateRepeatInterval = (repInterval: RepeatedInterval): xml.XmlObject =>
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateInterval = (interval: Interval | RepeatedInterval): xml.XmlObject => {
|
const generateInterval = (
|
||||||
|
interval: Interval | RepeatedInterval,
|
||||||
|
index: number,
|
||||||
|
allIntervals: (Interval | RepeatedInterval)[],
|
||||||
|
): xml.XmlObject => {
|
||||||
if (interval.type === "repeat") {
|
if (interval.type === "repeat") {
|
||||||
return generateRepeatInterval(interval);
|
return generateRepeatInterval(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { intensity } = interval;
|
const { intensity } = interval;
|
||||||
if (intensity.start < intensity.end) {
|
if (index === 0 && intensity.start < intensity.end) {
|
||||||
return generateRangeInterval("Warmup", interval);
|
return generateRangeInterval("Warmup", interval);
|
||||||
} else if (intensity.start > intensity.end) {
|
} else if (index === allIntervals.length - 1 && intensity.start > intensity.end) {
|
||||||
return generateRangeInterval("Cooldown", interval);
|
return generateRangeInterval("Cooldown", interval);
|
||||||
|
} else if (intensity.start !== intensity.end) {
|
||||||
|
return generateRangeInterval("Ramp", interval);
|
||||||
} else if (intensity.zone === "free") {
|
} else if (intensity.zone === "free") {
|
||||||
return generateFreeRideInterval(interval);
|
return generateFreeRideInterval(interval);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,10 @@ export const parseCliOptions = (): CliOptions => {
|
||||||
default: false,
|
default: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
argParser.add_argument("file", { nargs: 1 });
|
argParser.add_argument("file", {
|
||||||
|
nargs: "?",
|
||||||
|
default: 0, // Default to reading STDIN
|
||||||
|
});
|
||||||
|
|
||||||
// As we only allow one file as input,
|
return argParser.parse_args();
|
||||||
// convert filenames array to just a single string.
|
|
||||||
const { file, ...rest } = argParser.parse_args();
|
|
||||||
return {
|
|
||||||
file: file[0],
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { Interval, Workout } from "../ast";
|
||||||
|
import { FreeIntensity, RangeIntensity, RangeIntensityEnd } from "../Intensity";
|
||||||
|
|
||||||
|
const fillIntensities = (prevInterval: Interval, interval: Interval): Interval => {
|
||||||
|
if (!(interval.intensity instanceof RangeIntensityEnd)) {
|
||||||
|
return interval;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevInterval.intensity instanceof FreeIntensity) {
|
||||||
|
throw new Error("range-intensity-end interval can't be after free-intensity interval");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...interval,
|
||||||
|
intensity: new RangeIntensity(prevInterval.intensity.end, interval.intensity.end),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Given: [1, 2, 3, 4]
|
||||||
|
// Returns: [[1,2], [2,3], [3,4]]
|
||||||
|
const pairs = <T>(arr: T[]): T[][] => {
|
||||||
|
const result: T[][] = [];
|
||||||
|
for (let i = 1; i < arr.length; i++) {
|
||||||
|
result.push([arr[i - 1], arr[i]]);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fillIntensitiesInIntervals = (intervals: Interval[]): Interval[] => {
|
||||||
|
if (intervals.length <= 1) {
|
||||||
|
if (intervals.length === 1 && intervals[0].intensity instanceof RangeIntensityEnd) {
|
||||||
|
throw new Error("range-intensity-end interval can't be the first interval");
|
||||||
|
}
|
||||||
|
return intervals;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [intervals[0], ...pairs(intervals).map(([prev, curr]) => fillIntensities(prev, curr))];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fillRangeIntensities = (workout: Workout): Workout => {
|
||||||
|
return {
|
||||||
|
...workout,
|
||||||
|
intervals: fillIntensitiesInIntervals(workout.intervals),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Workout } from "../ast";
|
import { Workout } from "../ast";
|
||||||
|
import { fillRangeIntensities } from "./fillRangeIntensities";
|
||||||
import { parseTokens } from "./parser";
|
import { parseTokens } from "./parser";
|
||||||
import { tokenize } from "./tokenizer";
|
import { tokenize } from "./tokenizer";
|
||||||
import { validate } from "./validate";
|
import { validate } from "./validate";
|
||||||
|
|
||||||
export const parse = (source: string): Workout => validate(parseTokens(tokenize(source)));
|
export const parse = (source: string): Workout => validate(fillRangeIntensities(parseTokens(tokenize(source))));
|
||||||
|
|
|
||||||
|
|
@ -231,6 +231,7 @@ Name: My Workout
|
||||||
|
|
||||||
Warmup: 5:30 50%..80% 100rpm
|
Warmup: 5:30 50%..80% 100rpm
|
||||||
Cooldown: 5:30 70%..45%
|
Cooldown: 5:30 70%..45%
|
||||||
|
Ramp: 5:30 90%..100%
|
||||||
`).intervals,
|
`).intervals,
|
||||||
).toMatchInlineSnapshot(`
|
).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
|
|
@ -258,10 +259,147 @@ Cooldown: 5:30 70%..45%
|
||||||
},
|
},
|
||||||
"type": "Cooldown",
|
"type": "Cooldown",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"cadence": undefined,
|
||||||
|
"comments": Array [],
|
||||||
|
"duration": Duration {
|
||||||
|
"seconds": 330,
|
||||||
|
},
|
||||||
|
"intensity": RangeIntensity {
|
||||||
|
"_end": 1,
|
||||||
|
"_start": 0.9,
|
||||||
|
},
|
||||||
|
"type": "Ramp",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("parses end-of-range intervals after power-range interval", () => {
|
||||||
|
expect(
|
||||||
|
parse(`
|
||||||
|
Name: My Workout
|
||||||
|
|
||||||
|
Ramp: 5:00 50%..75%
|
||||||
|
Ramp: 5:00 ..100%
|
||||||
|
Ramp: 5:00 ..50%
|
||||||
|
`).intervals,
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"cadence": undefined,
|
||||||
|
"comments": Array [],
|
||||||
|
"duration": Duration {
|
||||||
|
"seconds": 300,
|
||||||
|
},
|
||||||
|
"intensity": RangeIntensity {
|
||||||
|
"_end": 0.75,
|
||||||
|
"_start": 0.5,
|
||||||
|
},
|
||||||
|
"type": "Ramp",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"cadence": undefined,
|
||||||
|
"comments": Array [],
|
||||||
|
"duration": Duration {
|
||||||
|
"seconds": 300,
|
||||||
|
},
|
||||||
|
"intensity": RangeIntensity {
|
||||||
|
"_end": 1,
|
||||||
|
"_start": 0.75,
|
||||||
|
},
|
||||||
|
"type": "Ramp",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"cadence": undefined,
|
||||||
|
"comments": Array [],
|
||||||
|
"duration": Duration {
|
||||||
|
"seconds": 300,
|
||||||
|
},
|
||||||
|
"intensity": RangeIntensity {
|
||||||
|
"_end": 0.5,
|
||||||
|
"_start": 1,
|
||||||
|
},
|
||||||
|
"type": "Ramp",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses end-of-range intervals after normal interval", () => {
|
||||||
|
expect(
|
||||||
|
parse(`
|
||||||
|
Name: My Workout
|
||||||
|
|
||||||
|
Interval: 5:00 75%
|
||||||
|
Ramp: 5:00 ..100%
|
||||||
|
Ramp: 5:00 ..50%
|
||||||
|
`).intervals,
|
||||||
|
).toMatchInlineSnapshot(`
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"cadence": undefined,
|
||||||
|
"comments": Array [],
|
||||||
|
"duration": Duration {
|
||||||
|
"seconds": 300,
|
||||||
|
},
|
||||||
|
"intensity": ConstantIntensity {
|
||||||
|
"_value": 0.75,
|
||||||
|
},
|
||||||
|
"type": "Interval",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"cadence": undefined,
|
||||||
|
"comments": Array [],
|
||||||
|
"duration": Duration {
|
||||||
|
"seconds": 300,
|
||||||
|
},
|
||||||
|
"intensity": RangeIntensity {
|
||||||
|
"_end": 1,
|
||||||
|
"_start": 0.75,
|
||||||
|
},
|
||||||
|
"type": "Ramp",
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"cadence": undefined,
|
||||||
|
"comments": Array [],
|
||||||
|
"duration": Duration {
|
||||||
|
"seconds": 300,
|
||||||
|
},
|
||||||
|
"intensity": RangeIntensity {
|
||||||
|
"_end": 0.5,
|
||||||
|
"_start": 1,
|
||||||
|
},
|
||||||
|
"type": "Ramp",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws error when end-of-range intervals after free-ride interval", () => {
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
parse(`
|
||||||
|
Name: My Workout
|
||||||
|
|
||||||
|
FreeRide: 5:00
|
||||||
|
Ramp: 5:00 ..100%
|
||||||
|
Ramp: 5:00 ..50%
|
||||||
|
`).intervals,
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"range-intensity-end interval can't be after free-intensity interval"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws error when end-of-range intervals is the first interval", () => {
|
||||||
|
expect(
|
||||||
|
() =>
|
||||||
|
parse(`
|
||||||
|
Name: My Workout
|
||||||
|
|
||||||
|
Ramp: 5:00 ..50%
|
||||||
|
`).intervals,
|
||||||
|
).toThrowErrorMatchingInlineSnapshot(`"range-intensity-end interval can't be the first interval"`);
|
||||||
|
});
|
||||||
|
|
||||||
it("parses free-ride intervals", () => {
|
it("parses free-ride intervals", () => {
|
||||||
expect(
|
expect(
|
||||||
parse(`
|
parse(`
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { last } from "ramda";
|
import { last } from "ramda";
|
||||||
import { Interval, Workout, Comment } from "../ast";
|
import { Interval, Workout, Comment } from "../ast";
|
||||||
import { Duration } from "../Duration";
|
import { Duration } from "../Duration";
|
||||||
import { ConstantIntensity, FreeIntensity, RangeIntensity } from "../Intensity";
|
import { ConstantIntensity, FreeIntensity, RangeIntensity, RangeIntensityEnd } from "../Intensity";
|
||||||
import { ParseError } from "./ParseError";
|
import { ParseError } from "./ParseError";
|
||||||
import { IntervalType, OffsetToken, SourceLocation, Token } from "./tokenizer";
|
import { IntervalType, OffsetToken, SourceLocation, Token } from "./tokenizer";
|
||||||
|
|
||||||
|
|
@ -158,6 +158,9 @@ const parseIntervalParams = (type: IntervalType, tokens: Token[], loc: SourceLoc
|
||||||
} else if (token.type === "intensity-range") {
|
} else if (token.type === "intensity-range") {
|
||||||
intensity = new RangeIntensity(token.value[0], token.value[1]);
|
intensity = new RangeIntensity(token.value[0], token.value[1]);
|
||||||
tokens.shift();
|
tokens.shift();
|
||||||
|
} else if (token.type === "intensity-range-end") {
|
||||||
|
intensity = new RangeIntensityEnd(token.value);
|
||||||
|
tokens.shift();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
import { ParseError } from "./ParseError";
|
import { ParseError } from "./ParseError";
|
||||||
|
|
||||||
export type HeaderType = "Name" | "Author" | "Description" | "Tags";
|
export type HeaderType = "Name" | "Author" | "Description" | "Tags";
|
||||||
export type IntervalType = "Warmup" | "Rest" | "Interval" | "Cooldown" | "FreeRide";
|
export type IntervalType = "Warmup" | "Rest" | "Interval" | "Cooldown" | "FreeRide" | "Ramp";
|
||||||
|
|
||||||
const isHeaderType = (value: string): value is HeaderType => {
|
const isHeaderType = (value: string): value is HeaderType => {
|
||||||
return ["Name", "Author", "Description", "Tags"].includes(value);
|
return ["Name", "Author", "Description", "Tags"].includes(value);
|
||||||
};
|
};
|
||||||
const isIntervalType = (value: string): value is IntervalType => {
|
const isIntervalType = (value: string): value is IntervalType => {
|
||||||
return ["Warmup", "Rest", "Interval", "Cooldown", "FreeRide"].includes(value);
|
return ["Warmup", "Rest", "Interval", "Cooldown", "FreeRide", "Ramp"].includes(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 0-based row and column indexes. First line is 0th.
|
// 0-based row and column indexes. First line is 0th.
|
||||||
|
|
@ -47,6 +47,11 @@ export type RangeIntensityToken = {
|
||||||
value: [number, number];
|
value: [number, number];
|
||||||
loc: SourceLocation;
|
loc: SourceLocation;
|
||||||
};
|
};
|
||||||
|
export type RangeIntensityEndToken = {
|
||||||
|
type: "intensity-range-end";
|
||||||
|
value: number;
|
||||||
|
loc: SourceLocation;
|
||||||
|
};
|
||||||
export type CommentStartToken = {
|
export type CommentStartToken = {
|
||||||
type: "comment-start";
|
type: "comment-start";
|
||||||
value?: undefined;
|
value?: undefined;
|
||||||
|
|
@ -59,6 +64,7 @@ export type Token =
|
||||||
| NumberToken
|
| NumberToken
|
||||||
| OffsetToken
|
| OffsetToken
|
||||||
| RangeIntensityToken
|
| RangeIntensityToken
|
||||||
|
| RangeIntensityEndToken
|
||||||
| CommentStartToken;
|
| CommentStartToken;
|
||||||
|
|
||||||
const toInteger = (str: string): number => {
|
const toInteger = (str: string): number => {
|
||||||
|
|
@ -81,10 +87,14 @@ const tokenizeValueParam = (text: string, loc: SourceLocation): Token => {
|
||||||
if (/^[0-9]+rpm$/.test(text)) {
|
if (/^[0-9]+rpm$/.test(text)) {
|
||||||
return { type: "cadence", value: toInteger(text), loc };
|
return { type: "cadence", value: toInteger(text), loc };
|
||||||
}
|
}
|
||||||
if (/^[0-9]+%..[0-9]+%$/.test(text)) {
|
if (/^[0-9]+%\.\.[0-9]+%$/.test(text)) {
|
||||||
const [from, to] = text.split("..").map(toInteger).map(toFraction);
|
const [from, to] = text.split("..").map(toInteger).map(toFraction);
|
||||||
return { type: "intensity-range", value: [from, to], loc };
|
return { type: "intensity-range", value: [from, to], loc };
|
||||||
}
|
}
|
||||||
|
if (/^\.\.[0-9]+%$/.test(text)) {
|
||||||
|
const [, /* _ */ to] = text.split("..").map(toInteger).map(toFraction);
|
||||||
|
return { type: "intensity-range-end", value: to, loc };
|
||||||
|
}
|
||||||
if (/^[0-9]+%$/.test(text)) {
|
if (/^[0-9]+%$/.test(text)) {
|
||||||
return { type: "intensity", value: toFraction(toInteger(text)), loc };
|
return { type: "intensity", value: toFraction(toInteger(text)), loc };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,31 @@ The workouts are alphabetically ordered from easiest to hardest, so enjoy the mi
|
||||||
</workout_file>"
|
</workout_file>"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Generate ZWO examples/ramps.txt 1`] = `
|
||||||
|
"<workout_file>
|
||||||
|
<name>Ramps</name>
|
||||||
|
<author>R.Saarsoo</author>
|
||||||
|
<description>Various kinds of ramp intervals.</description>
|
||||||
|
<tags>
|
||||||
|
</tags>
|
||||||
|
<sportType>bike</sportType>
|
||||||
|
<workout>
|
||||||
|
<Warmup Duration=\\"300\\" PowerLow=\\"0.4\\" PowerHigh=\\"0.75\\">
|
||||||
|
</Warmup>
|
||||||
|
<Ramp Duration=\\"600\\" PowerLow=\\"0.8\\" PowerHigh=\\"0.9\\">
|
||||||
|
</Ramp>
|
||||||
|
<Ramp Duration=\\"600\\" PowerLow=\\"0.9\\" PowerHigh=\\"0.8\\">
|
||||||
|
</Ramp>
|
||||||
|
<Ramp Duration=\\"600\\" PowerLow=\\"0.8\\" PowerHigh=\\"0.9\\">
|
||||||
|
</Ramp>
|
||||||
|
<Ramp Duration=\\"600\\" PowerLow=\\"0.9\\" PowerHigh=\\"0.8\\">
|
||||||
|
</Ramp>
|
||||||
|
<Cooldown Duration=\\"300\\" PowerLow=\\"0.75\\" PowerHigh=\\"0.4\\">
|
||||||
|
</Cooldown>
|
||||||
|
</workout>
|
||||||
|
</workout_file>"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Generate ZWO examples/threshold-pushing.txt 1`] = `
|
exports[`Generate ZWO examples/threshold-pushing.txt 1`] = `
|
||||||
"<workout_file>
|
"<workout_file>
|
||||||
<name>Threshold pushing</name>
|
<name>Threshold pushing</name>
|
||||||
|
|
@ -338,6 +363,27 @@ Zone Distribution:
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`Generate stats examples/ramps.txt 1`] = `
|
||||||
|
"
|
||||||
|
Total duration: 50 minutes
|
||||||
|
|
||||||
|
Average intensity: 80%
|
||||||
|
Normalized intensity: 82%
|
||||||
|
|
||||||
|
TSS: 56
|
||||||
|
XP: 300
|
||||||
|
|
||||||
|
Zone Distribution:
|
||||||
|
6 min - Z1: Recovery
|
||||||
|
4 min - Z2: Endurance
|
||||||
|
40 min - Z3: Tempo
|
||||||
|
0 min - Z4: Threshold
|
||||||
|
0 min - Z5: VO2 Max
|
||||||
|
0 min - Z6: Anaerobic
|
||||||
|
0 min - Freeride
|
||||||
|
"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`Generate stats examples/threshold-pushing.txt 1`] = `
|
exports[`Generate stats examples/threshold-pushing.txt 1`] = `
|
||||||
"
|
"
|
||||||
Total duration: 79 minutes
|
Total duration: 79 minutes
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const filenames = [
|
||||||
"examples/ftp-test.txt",
|
"examples/ftp-test.txt",
|
||||||
"examples/halvfems.txt",
|
"examples/halvfems.txt",
|
||||||
"examples/threshold-pushing.txt",
|
"examples/threshold-pushing.txt",
|
||||||
|
"examples/ramps.txt",
|
||||||
];
|
];
|
||||||
|
|
||||||
describe("Generate ZWO", () => {
|
describe("Generate ZWO", () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue