Implement FreeRide intervals
This commit is contained in:
parent
eef96cce92
commit
ac126f36da
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
## TODO
|
||||
|
||||
- FreeRide blocks
|
||||
- Repeats (and nested repeats)
|
||||
- Unsupported params: message duration & y-position
|
||||
- More restricted syntax for text (with quotes)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
Author: Zwift
|
||||
Name: FTP Test (shorter)
|
||||
Description:
|
||||
The short variation of the standard FTP test starts off with a short warmup,
|
||||
a quick leg opening ramp, and a 5 minute hard effort to get the legs pumping.
|
||||
After a brief rest it's time to give it your all and go as hard as you can for 20 solid minutes.
|
||||
Pace yourself and try to go as hard as you can sustain for the entire 20 minutes -
|
||||
you will be scored on the final 20 minute segment.
|
||||
|
||||
Upon saving your ride, you will be notified if your FTP improved.
|
||||
|
||||
Warmup: 5:00 30%..70%
|
||||
@ 00:20 Welcome to the FTP test
|
||||
@ 00:35 Time to get warmed up and get your cadence up to 90-100rpm
|
||||
|
||||
Interval: 00:20 90%
|
||||
Interval: 00:20 110%
|
||||
Interval: 00:20 130%
|
||||
|
||||
Interval: 03:00 60%
|
||||
Interval: 03:00 110%
|
||||
Interval: 02:00 120%
|
||||
|
||||
Rest: 06:00 55%
|
||||
@ 00:10 In 6 minutes the FTP test begins
|
||||
@ 05:00 Prepare for your max 20 minute effort
|
||||
|
||||
FreeRide: 20:00
|
||||
@ 00:10 Bring your power up to what you think you can hold for 20 minutes
|
||||
@ 05:00 How ya doin?
|
||||
@ 10:00 Half way done! If it's feeling easy now might be time to add 10 watts.
|
||||
@ 15:00 5 minutes left!
|
||||
@ 16:00 4 minutes left. Increase your power if you're feeling strong.
|
||||
@ 17:00 3 minutes left
|
||||
@ 18:00 2 minutes left
|
||||
@ 19:00 1 minute left. Go all in!
|
||||
@ 19:30 30 sec. Go for it.
|
||||
@ 19:50 10 seconds.
|
||||
|
||||
Cooldown: 5:00 50%..30%
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<workout_file>
|
||||
<author>Zwift</author>
|
||||
<name>FTP Test (shorter)</name>
|
||||
<description>The short variation of the standard FTP test starts off with a short warmup, a quick leg opening ramp, and a 5 minute hard effort to get the legs pumping. After a brief rest it's time to give it your all and go as hard as you can for 20 solid minutes. Pace yourself and try to go as hard as you can sustain for the entire 20 minutes - you will be scored on the final 20 minute segment.
|
||||
|
||||
Upon saving your ride, you will be notified if your FTP improved.</description>
|
||||
<sportType>bike</sportType>
|
||||
<tags/>
|
||||
<workout>
|
||||
<Warmup Duration="300" PowerLow="0.30000001" PowerHigh="0.69999999">
|
||||
<textevent timeoffset="20" message="Welcome to the FTP test"/>
|
||||
<textevent timeoffset="35" message="Time to get warmed up and"/>
|
||||
<textevent timeoffset="35" message="get your cadence up to 90-100rpm" y="270"/>
|
||||
</Warmup>
|
||||
<SteadyState Duration="20" Power="0.89999998"/>
|
||||
<SteadyState Duration="20" Power="1.1"/>
|
||||
<SteadyState Duration="20" Power="1.3"/>
|
||||
<SteadyState Duration="180.00002" Power="0.60000002"/>
|
||||
<SteadyState Duration="180.00002" Power="1.1"/>
|
||||
<SteadyState Duration="120.00001" Power="1.2"/>
|
||||
<SteadyState Duration="360.00003" Power="0.55000001">
|
||||
<textevent timeoffset="10" message="In 6 minutes the FTP test begins"/>
|
||||
<textevent timeoffset="580" message="Prepare for your max 20 minute effort" duration="19"/>
|
||||
</SteadyState>
|
||||
<FreeRide Duration="1200" FlatRoad="1">
|
||||
<textevent timeoffset="10" message="Bring your power up to what you think" duration="20"/>
|
||||
<textevent timeoffset="10" message="you can hold for 20 minutes" duration="20" y="270"/>
|
||||
<textevent timeoffset="300" message="How ya doin?"/>
|
||||
<textevent timeoffset="600" message="Half way done! If it's feeling easy"/>
|
||||
<textevent timeoffset="600" message="now might be time to add 10 watts." y="270"/>
|
||||
<textevent timeoffset="900.00006" message="5 minutes left!"/>
|
||||
<textevent timeoffset="960.00006" message="4 minutes left. Increase your"/>
|
||||
<textevent timeoffset="960.00006" message="power if you're feeling strong." y="270"/>
|
||||
<textevent timeoffset="1020.0001" message="3 minutes left"/>
|
||||
<textevent timeoffset="1080" message="2 minutes left"/>
|
||||
<textevent timeoffset="1140" message="1 minute left. Go all in!"/>
|
||||
<textevent timeoffset="1170" message="30 sec. Go for it."/>
|
||||
<textevent timeoffset="1190" message="10 seconds."/>
|
||||
</FreeRide>
|
||||
<Cooldown Duration="300" PowerLow="0.5" PowerHigh="0.30000001"/>
|
||||
</workout>
|
||||
</workout_file>
|
||||
|
|
@ -29,3 +29,17 @@ export class IntensityRange {
|
|||
return this._end;
|
||||
}
|
||||
}
|
||||
|
||||
export class FreeIntensity {
|
||||
get value() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
get start() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
get end() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { IntervalType } from "./parser/tokenizer";
|
||||
import { Duration } from "./Duration";
|
||||
import { Intensity, IntensityRange } from "./Intensity";
|
||||
import { FreeIntensity, Intensity, IntensityRange } from "./Intensity";
|
||||
|
||||
export type Workout = {
|
||||
name: string;
|
||||
|
|
@ -12,7 +12,7 @@ export type Workout = {
|
|||
export type Interval = {
|
||||
type: IntervalType;
|
||||
duration: Duration;
|
||||
intensity: Intensity | IntensityRange;
|
||||
intensity: Intensity | IntensityRange | FreeIntensity;
|
||||
cadence?: number;
|
||||
comments: Comment[];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as xml from "xml";
|
||||
import { Interval, Workout, Comment } from "./ast";
|
||||
import { detectRepeats, RepeatedInterval } from "./detectRepeats";
|
||||
import { FreeIntensity } from "./Intensity";
|
||||
|
||||
// Zwift Workout XML generator
|
||||
|
||||
|
|
@ -44,6 +45,19 @@ const generateSteadyStateInterval = ({ duration, intensity, cadence, comments }:
|
|||
};
|
||||
};
|
||||
|
||||
const generateFreeRideInterval = ({ duration, comments }: Interval): xml.XmlObject => {
|
||||
return {
|
||||
FreeRide: [
|
||||
{
|
||||
_attr: {
|
||||
Duration: duration.seconds,
|
||||
},
|
||||
},
|
||||
...generateTextEvents(comments),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const generateRepeatInterval = (repInterval: RepeatedInterval): xml.XmlObject => {
|
||||
const [on, off] = repInterval.intervals;
|
||||
return {
|
||||
|
|
@ -76,6 +90,8 @@ const generateInterval = (interval: Interval | RepeatedInterval): xml.XmlObject
|
|||
return generateRangeInterval("Warmup", interval);
|
||||
} else if (intensity.start > intensity.end) {
|
||||
return generateRangeInterval("Cooldown", interval);
|
||||
} else if (intensity instanceof FreeIntensity) {
|
||||
return generateFreeRideInterval(interval);
|
||||
} else {
|
||||
return generateSteadyStateInterval(interval);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -207,6 +207,28 @@ Cooldown: 5:30 70%..45%
|
|||
`);
|
||||
});
|
||||
|
||||
it("parses free-ride intervals", () => {
|
||||
expect(
|
||||
parse(`
|
||||
Name: My Workout
|
||||
|
||||
FreeRide: 5:00
|
||||
`).intervals,
|
||||
).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"cadence": undefined,
|
||||
"comments": Array [],
|
||||
"duration": Duration {
|
||||
"seconds": 300,
|
||||
},
|
||||
"intensity": FreeIntensity {},
|
||||
"type": "FreeRide",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
const parseInterval = (interval: string) => parse(`Name: My Workout\n${interval}`).intervals[0];
|
||||
|
||||
it("requires duration and power parameters to be specified", () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { Interval, Workout, Comment } from "../ast";
|
||||
import { Duration } from "../Duration";
|
||||
import { Intensity, IntensityRange } from "../Intensity";
|
||||
import { FreeIntensity, Intensity, IntensityRange } from "../Intensity";
|
||||
import { ParseError } from "./ParseError";
|
||||
import { SourceLocation, Token } from "./tokenizer";
|
||||
import { IntervalType, SourceLocation, Token } from "./tokenizer";
|
||||
|
||||
type Header = Partial<Omit<Workout, "intervals">>;
|
||||
|
||||
|
|
@ -78,41 +78,44 @@ const parseIntervalComments = (tokens: Token[]): [Comment[], Token[]] => {
|
|||
return [comments, tokens];
|
||||
};
|
||||
|
||||
type IntervalData = Omit<Interval, "type">;
|
||||
|
||||
const parseIntervalParams = (tokens: Token[], loc: SourceLocation): [IntervalData, Token[]] => {
|
||||
const data: Partial<IntervalData> = {};
|
||||
const parseIntervalParams = (type: IntervalType, tokens: Token[], loc: SourceLocation): [Interval, Token[]] => {
|
||||
let duration;
|
||||
let cadence;
|
||||
let intensity;
|
||||
|
||||
while (tokens[0]) {
|
||||
const token = tokens[0];
|
||||
if (token.type === "duration") {
|
||||
data.duration = new Duration(token.value);
|
||||
duration = new Duration(token.value);
|
||||
tokens.shift();
|
||||
} else if (token.type === "cadence") {
|
||||
data.cadence = token.value;
|
||||
cadence = token.value;
|
||||
tokens.shift();
|
||||
} else if (token.type === "intensity") {
|
||||
data.intensity = new Intensity(token.value);
|
||||
intensity = new Intensity(token.value);
|
||||
tokens.shift();
|
||||
} else if (token.type === "intensity-range") {
|
||||
data.intensity = new IntensityRange(token.value[0], token.value[1]);
|
||||
intensity = new IntensityRange(token.value[0], token.value[1]);
|
||||
tokens.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!("duration" in data)) {
|
||||
if (!duration) {
|
||||
throw new ParseError("Duration not specified", loc);
|
||||
}
|
||||
if (!("intensity" in data)) {
|
||||
if (!intensity) {
|
||||
if (type === "FreeRide") {
|
||||
intensity = new FreeIntensity();
|
||||
} else {
|
||||
throw new ParseError("Power not specified", loc);
|
||||
}
|
||||
}
|
||||
|
||||
const [comments, rest] = parseIntervalComments(tokens);
|
||||
data.comments = comments;
|
||||
|
||||
return [data as IntervalData, rest];
|
||||
return [{ type, duration, intensity, cadence, comments }, rest];
|
||||
};
|
||||
|
||||
const parseIntervals = (tokens: Token[]): Interval[] => {
|
||||
|
|
@ -121,14 +124,8 @@ const parseIntervals = (tokens: Token[]): Interval[] => {
|
|||
while (tokens[0]) {
|
||||
const token = tokens.shift() as Token;
|
||||
if (token.type === "interval") {
|
||||
const [{ duration, intensity, cadence, comments }, rest] = parseIntervalParams(tokens, token.loc);
|
||||
intervals.push({
|
||||
type: token.value,
|
||||
duration,
|
||||
intensity,
|
||||
cadence,
|
||||
comments,
|
||||
});
|
||||
const [interval, rest] = parseIntervalParams(token.value, tokens, token.loc);
|
||||
intervals.push(interval);
|
||||
tokens = rest;
|
||||
} else {
|
||||
throw new ParseError(`Unexpected token ${tokenToString(token)}`, token.loc);
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { ParseError } from "./ParseError";
|
||||
|
||||
export type HeaderType = "Name" | "Author" | "Description";
|
||||
export type IntervalType = "Warmup" | "Rest" | "Interval" | "Cooldown";
|
||||
export type IntervalType = "Warmup" | "Rest" | "Interval" | "Cooldown" | "FreeRide";
|
||||
|
||||
const isHeaderType = (value: string): value is HeaderType => {
|
||||
return ["Name", "Author", "Description"].includes(value);
|
||||
};
|
||||
const isIntervalType = (value: string): value is IntervalType => {
|
||||
return ["Warmup", "Rest", "Interval", "Cooldown"].includes(value);
|
||||
return ["Warmup", "Rest", "Interval", "Cooldown", "FreeRide"].includes(value);
|
||||
};
|
||||
|
||||
// 0-based row and column indexes. First line is 0th.
|
||||
|
|
|
|||
|
|
@ -78,6 +78,69 @@ exports[`Generate ZWO examples/darth-vader.txt 1`] = `
|
|||
</workout_file>"
|
||||
`;
|
||||
|
||||
exports[`Generate ZWO examples/ftp-test.txt 1`] = `
|
||||
"<workout_file>
|
||||
<name>FTP Test (shorter)</name>
|
||||
<author>Zwift</author>
|
||||
<description>The short variation of the standard FTP test starts off with a short warmup,
|
||||
a quick leg opening ramp, and a 5 minute hard effort to get the legs pumping.
|
||||
After a brief rest it's time to give it your all and go as hard as you can for 20 solid minutes.
|
||||
Pace yourself and try to go as hard as you can sustain for the entire 20 minutes -
|
||||
you will be scored on the final 20 minute segment.
|
||||
|
||||
Upon saving your ride, you will be notified if your FTP improved.</description>
|
||||
<sportType>bike</sportType>
|
||||
<Warmup Duration=\\"300\\" PowerLow=\\"0.3\\" PowerHigh=\\"0.7\\">
|
||||
<textevent timeoffset=\\"20\\" message=\\"Welcome to the FTP test\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"35\\" message=\\"Time to get warmed up and get your cadence up to 90-100rpm\\">
|
||||
</textevent>
|
||||
</Warmup>
|
||||
<SteadyState Duration=\\"20\\" Power=\\"0.9\\">
|
||||
</SteadyState>
|
||||
<SteadyState Duration=\\"20\\" Power=\\"1.1\\">
|
||||
</SteadyState>
|
||||
<SteadyState Duration=\\"20\\" Power=\\"1.3\\">
|
||||
</SteadyState>
|
||||
<SteadyState Duration=\\"180\\" Power=\\"0.6\\">
|
||||
</SteadyState>
|
||||
<SteadyState Duration=\\"180\\" Power=\\"1.1\\">
|
||||
</SteadyState>
|
||||
<SteadyState Duration=\\"120\\" Power=\\"1.2\\">
|
||||
</SteadyState>
|
||||
<SteadyState Duration=\\"360\\" Power=\\"0.55\\">
|
||||
<textevent timeoffset=\\"10\\" message=\\"In 6 minutes the FTP test begins\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"300\\" message=\\"Prepare for your max 20 minute effort\\">
|
||||
</textevent>
|
||||
</SteadyState>
|
||||
<FreeRide Duration=\\"1200\\">
|
||||
<textevent timeoffset=\\"10\\" message=\\"Bring your power up to what you think you can hold for 20 minutes\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"300\\" message=\\"How ya doin?\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"600\\" message=\\"Half way done! If it's feeling easy now might be time to add 10 watts.\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"900\\" message=\\"5 minutes left!\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"960\\" message=\\"4 minutes left. Increase your power if you're feeling strong.\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"1020\\" message=\\"3 minutes left\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"1080\\" message=\\"2 minutes left\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"1140\\" message=\\"1 minute left. Go all in!\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"1170\\" message=\\"30 sec. Go for it.\\">
|
||||
</textevent>
|
||||
<textevent timeoffset=\\"1190\\" message=\\"10 seconds.\\">
|
||||
</textevent>
|
||||
</FreeRide>
|
||||
<Cooldown Duration=\\"300\\" PowerLow=\\"0.5\\" PowerHigh=\\"0.3\\">
|
||||
</Cooldown>
|
||||
</workout_file>"
|
||||
`;
|
||||
|
||||
exports[`Generate ZWO examples/halvfems.txt 1`] = `
|
||||
"<workout_file>
|
||||
<name>Halvfems</name>
|
||||
|
|
@ -205,6 +268,26 @@ Zone Distribution:
|
|||
"
|
||||
`;
|
||||
|
||||
exports[`Generate stats examples/ftp-test.txt 1`] = `
|
||||
"
|
||||
Total duration: 45 minutes
|
||||
|
||||
Average intensity: 36%
|
||||
Normalized intensity: 71%
|
||||
|
||||
TSS #1: 21
|
||||
TSS #2: 37
|
||||
|
||||
Zone Distribution:
|
||||
35 min - Z1: Recovery
|
||||
4 min - Z2: Endurance
|
||||
0 min - Z3: Tempo
|
||||
0 min - Z4: Threshold
|
||||
3 min - Z5: VO2 Max
|
||||
2 min - Z6: Anaerobic
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`Generate stats examples/halvfems.txt 1`] = `
|
||||
"
|
||||
Total duration: 62 minutes
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ const createZwo = (filename: string) => generateZwo(parse(fs.readFileSync(filena
|
|||
const filenames = [
|
||||
"examples/comments.txt",
|
||||
"examples/darth-vader.txt",
|
||||
"examples/ftp-test.txt",
|
||||
"examples/halvfems.txt",
|
||||
"examples/threshold-pushing.txt",
|
||||
];
|
||||
|
|
|
|||
Loading…
Reference in New Issue