Adopt use of Seconds data-type

This commit is contained in:
Rene Saarsoo 2020-09-22 17:36:12 +03:00
parent 78a1bed1f2
commit 45467a434f
11 changed files with 93 additions and 46 deletions

View File

@ -1,4 +1,5 @@
import { IntervalLabelTokenValue } from "./parser/tokenizer"; import { IntervalLabelTokenValue } from "./parser/tokenizer";
import { Seconds } from "./types";
export type Workout = { export type Workout = {
name: string; name: string;
@ -9,13 +10,13 @@ export type Workout = {
export type Interval = { export type Interval = {
type: IntervalLabelTokenValue; type: IntervalLabelTokenValue;
duration: number; duration: Seconds;
intensity: { from: number; to: number }; intensity: { from: number; to: number };
cadence?: number; cadence?: number;
comments: Comment[]; comments: Comment[];
}; };
export type Comment = { export type Comment = {
offset: number; offset: Seconds;
text: string; text: string;
}; };

View File

@ -17,7 +17,7 @@ const generateRangeInterval = (
[tagName]: [ [tagName]: [
{ {
_attr: { _attr: {
Duration: duration, Duration: duration.value,
PowerLow: intensity.from, PowerLow: intensity.from,
PowerHigh: intensity.from, PowerHigh: intensity.from,
...(cadence ? { Cadence: cadence } : {}), ...(cadence ? { Cadence: cadence } : {}),
@ -33,7 +33,7 @@ const generateSteadyStateInterval = ({ duration, intensity, cadence, comments }:
SteadyState: [ SteadyState: [
{ {
_attr: { _attr: {
Duration: duration, Duration: duration.value,
Power: intensity.from, Power: intensity.from,
...(cadence ? { Cadence: cadence } : {}), ...(cadence ? { Cadence: cadence } : {}),
}, },

View File

@ -62,7 +62,9 @@ Rest: 5:00 45%
Object { Object {
"cadence": undefined, "cadence": undefined,
"comments": Array [], "comments": Array [],
"duration": 300, "duration": Seconds {
"value": 300,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.5, "to": 0.5,
@ -72,7 +74,9 @@ Rest: 5:00 45%
Object { Object {
"cadence": 90, "cadence": 90,
"comments": Array [], "comments": Array [],
"duration": 600, "duration": Seconds {
"value": 600,
},
"intensity": Object { "intensity": Object {
"from": 0.8, "from": 0.8,
"to": 0.8, "to": 0.8,
@ -82,7 +86,9 @@ Rest: 5:00 45%
Object { Object {
"cadence": undefined, "cadence": undefined,
"comments": Array [], "comments": Array [],
"duration": 300, "duration": Seconds {
"value": 300,
},
"intensity": Object { "intensity": Object {
"from": 0.45, "from": 0.45,
"to": 0.45, "to": 0.45,
@ -106,7 +112,9 @@ Cooldown: 5:30 70%..45%
Object { Object {
"cadence": 100, "cadence": 100,
"comments": Array [], "comments": Array [],
"duration": 330, "duration": Seconds {
"value": 330,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.8, "to": 0.8,
@ -116,7 +124,9 @@ Cooldown: 5:30 70%..45%
Object { Object {
"cadence": undefined, "cadence": undefined,
"comments": Array [], "comments": Array [],
"duration": 330, "duration": Seconds {
"value": 330,
},
"intensity": Object { "intensity": Object {
"from": 0.7, "from": 0.7,
"to": 0.45, "to": 0.45,
@ -146,7 +156,9 @@ Cooldown: 5:30 70%..45%
Object { Object {
"cadence": undefined, "cadence": undefined,
"comments": Array [], "comments": Array [],
"duration": 10, "duration": Seconds {
"value": 10,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.5, "to": 0.5,
@ -158,7 +170,9 @@ Cooldown: 5:30 70%..45%
Object { Object {
"cadence": 100, "cadence": 100,
"comments": Array [], "comments": Array [],
"duration": 10, "duration": Seconds {
"value": 10,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.5, "to": 0.5,
@ -170,7 +184,9 @@ Cooldown: 5:30 70%..45%
Object { Object {
"cadence": 100, "cadence": 100,
"comments": Array [], "comments": Array [],
"duration": 10, "duration": Seconds {
"value": 10,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.5, "to": 0.5,
@ -185,7 +201,9 @@ Cooldown: 5:30 70%..45%
Object { Object {
"cadence": 100, "cadence": 100,
"comments": Array [], "comments": Array [],
"duration": 10, "duration": Seconds {
"value": 10,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.5, "to": 0.5,
@ -197,7 +215,9 @@ Cooldown: 5:30 70%..45%
Object { Object {
"cadence": 100, "cadence": 100,
"comments": Array [], "comments": Array [],
"duration": 10, "duration": Seconds {
"value": 10,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.5, "to": 0.5,
@ -208,14 +228,14 @@ Cooldown: 5:30 70%..45%
}); });
it("parses correct duration formats", () => { it("parses correct duration formats", () => {
expect(parseInterval("Interval: 0:10 50%").duration).toEqual(10); expect(parseInterval("Interval: 0:10 50%").duration.value).toEqual(10);
expect(parseInterval("Interval: 00:10 50%").duration).toEqual(10); expect(parseInterval("Interval: 00:10 50%").duration.value).toEqual(10);
expect(parseInterval("Interval: 0:00:10 50%").duration).toEqual(10); expect(parseInterval("Interval: 0:00:10 50%").duration.value).toEqual(10);
expect(parseInterval("Interval: 0:02:05 50%").duration).toEqual(125); expect(parseInterval("Interval: 0:02:05 50%").duration.value).toEqual(125);
expect(parseInterval("Interval: 1:00:00 50%").duration).toEqual(3600); expect(parseInterval("Interval: 1:00:00 50%").duration.value).toEqual(3600);
expect(parseInterval("Interval: 1:00:0 50%").duration).toEqual(3600); expect(parseInterval("Interval: 1:00:0 50%").duration.value).toEqual(3600);
expect(parseInterval("Interval: 1:0:0 50%").duration).toEqual(3600); expect(parseInterval("Interval: 1:0:0 50%").duration.value).toEqual(3600);
expect(parseInterval("Interval: 10:00:00 50%").duration).toEqual(36000); expect(parseInterval("Interval: 10:00:00 50%").duration.value).toEqual(36000);
}); });
it("throws error for incorrect duration formats", () => { it("throws error for incorrect duration formats", () => {
@ -277,27 +297,39 @@ Rest: 5:00 50%
"cadence": undefined, "cadence": undefined,
"comments": Array [ "comments": Array [
Object { Object {
"offset": 0, "offset": Seconds {
"value": 0,
},
"text": "Find your rythm.", "text": "Find your rythm.",
}, },
Object { Object {
"offset": 60, "offset": Seconds {
"value": 60,
},
"text": "Try to settle in for the effort", "text": "Try to settle in for the effort",
}, },
Object { Object {
"offset": 300, "offset": Seconds {
"value": 300,
},
"text": "Half way through", "text": "Half way through",
}, },
Object { Object {
"offset": 540, "offset": Seconds {
"value": 540,
},
"text": "Almost there", "text": "Almost there",
}, },
Object { Object {
"offset": 570, "offset": Seconds {
"value": 570,
},
"text": "Final push. YOU GOT IT!", "text": "Final push. YOU GOT IT!",
}, },
], ],
"duration": 600, "duration": Seconds {
"value": 600,
},
"intensity": Object { "intensity": Object {
"from": 0.9, "from": 0.9,
"to": 0.9, "to": 0.9,
@ -308,15 +340,21 @@ Rest: 5:00 50%
"cadence": undefined, "cadence": undefined,
"comments": Array [ "comments": Array [
Object { Object {
"offset": 0, "offset": Seconds {
"value": 0,
},
"text": "Great effort!", "text": "Great effort!",
}, },
Object { Object {
"offset": 30, "offset": Seconds {
"value": 30,
},
"text": "Cool down well after all of this.", "text": "Cool down well after all of this.",
}, },
], ],
"duration": 300, "duration": Seconds {
"value": 300,
},
"intensity": Object { "intensity": Object {
"from": 0.5, "from": 0.5,
"to": 0.5, "to": 0.5,

View File

@ -1,4 +1,5 @@
import { Interval, Workout, Comment } from "../ast"; import { Interval, Workout, Comment } from "../ast";
import { Seconds } from "../types";
import { ParseError } from "./ParseError"; import { ParseError } from "./ParseError";
import { isIntervalLabelTokenValue, SourceLocation, Token } from "./tokenizer"; import { isIntervalLabelTokenValue, SourceLocation, Token } from "./tokenizer";
@ -68,7 +69,7 @@ const parseIntervalComments = (tokens: Token[]): [Comment[], Token[]] => {
throw new ParseError(`Expected [comment text] instead got ${tokenToString(text)}`, text?.loc || offset.loc); throw new ParseError(`Expected [comment text] instead got ${tokenToString(text)}`, text?.loc || offset.loc);
} }
comments.push({ comments.push({
offset: offset.value, offset: new Seconds(offset.value),
text: text.value, text: text.value,
}); });
tokens = rest; tokens = rest;
@ -90,7 +91,7 @@ const parseIntervalParams = (tokens: Token[], loc: SourceLocation): [IntervalDat
while (tokens[0]) { while (tokens[0]) {
const token = tokens[0]; const token = tokens[0];
if (token.type === "duration") { if (token.type === "duration") {
data.duration = token.value; data.duration = new Seconds(token.value);
tokens.shift(); tokens.shift();
} else if (token.type === "cadence") { } else if (token.type === "cadence") {
data.cadence = token.value; data.cadence = token.value;

View File

@ -12,7 +12,7 @@ export const stats = ({ intervals }: Workout): string => {
const normIntensity = normalizedIntensity(intervals); const normIntensity = normalizedIntensity(intervals);
return ` return `
Total duration: ${(duration / 60).toFixed()} minutes Total duration: ${(duration.value / 60).toFixed()} minutes
Average intensity: ${(avgIntensity * 100).toFixed()}% Average intensity: ${(avgIntensity * 100).toFixed()}%
Normalized intensity: ${(normIntensity * 100).toFixed()}% Normalized intensity: ${(normIntensity * 100).toFixed()}%

View File

@ -5,9 +5,9 @@ import { Interval } from "../ast";
const intervalToIntensities = ({ duration, intensity }: Interval): number[] => { const intervalToIntensities = ({ duration, intensity }: Interval): number[] => {
const seconds = []; const seconds = [];
const { from, to } = intensity; const { from, to } = intensity;
for (let i = 0; i < duration; i++) { for (let i = 0; i < duration.value; i++) {
// Intensity in a single second // Intensity in a single second
seconds.push(from + (to - from) * (i / duration)); seconds.push(from + (to - from) * (i / duration.value));
} }
return seconds; return seconds;
}; };

View File

@ -4,7 +4,7 @@ import { average } from "./average";
import { intervalsToIntensities } from "./intervalsToIntensities"; import { intervalsToIntensities } from "./intervalsToIntensities";
// Starting at the beginning of the data, calculate 30-second rolling average // Starting at the beginning of the data, calculate 30-second rolling average
const windowSize = 30; const windowSize = 30; // equals to nr of seconds, but also to nr of entries in intensities array
const rollingAverages = (intensities: number[]): number[] => { const rollingAverages = (intensities: number[]): number[] => {
if (intensities.length < windowSize) { if (intensities.length < windowSize) {
throw new Error(`Workout must be at least ${windowSize} seconds long`); throw new Error(`Workout must be at least ${windowSize} seconds long`);

View File

@ -1,4 +1,5 @@
import { pluck, sum } from "ramda"; import { pluck, sum } from "ramda";
import { Interval } from "../ast"; import { Interval } from "../ast";
import { Seconds } from "../types";
export const totalDuration = (intervals: Interval[]) => sum(pluck("duration", intervals)); export const totalDuration = (intervals: Interval[]): Seconds => new Seconds(sum(pluck("duration.value", intervals)));

View File

@ -1,4 +1,5 @@
import { Interval } from "../ast"; import { Interval } from "../ast";
import { Seconds } from "../types";
// Training Stress Score formula from Training and Racing with a Power Meter: // Training Stress Score formula from Training and Racing with a Power Meter:
// //
@ -8,15 +9,15 @@ import { Interval } from "../ast";
// W - power in watts // W - power in watts
// IF - intensity factor (power / FTP) // IF - intensity factor (power / FTP)
const steadyTss = (duration: number, intensity: number): number => { const steadyTss = (duration: Seconds, intensity: number): number => {
return ((duration * intensity * intensity) / 3600) * 100; return ((duration.value * intensity * intensity) / 3600) * 100;
}; };
const rangeTss = (duration: number, from: number, to: number): number => { const rangeTss = (duration: Seconds, from: number, to: number): number => {
let score = 0; let score = 0;
const step = 1; const step = new Seconds(1);
for (let i = 0; i < duration; i += step) { for (let i = 0; i < duration.value; i += step.value) {
const intensity = from + (to - from) * (i / duration); const intensity = from + (to - from) * (i / duration.value);
score += steadyTss(step, intensity); score += steadyTss(step, intensity);
} }
return score; return score;

View File

@ -1,3 +1,5 @@
import { Seconds } from "../types";
// Training Stress Score formula from Training and Racing with a Power Meter: // Training Stress Score formula from Training and Racing with a Power Meter:
// //
// TSS = (s * W * IF) / (FTP * 3600) * 100 // TSS = (s * W * IF) / (FTP * 3600) * 100
@ -11,6 +13,6 @@
// TSS = (s * (FTP * IF) * IF) / (FTP * 3600) * 100 // TSS = (s * (FTP * IF) * IF) / (FTP * 3600) * 100
// TSS = (s * IF * IF) / 3600 * 100 // TSS = (s * IF * IF) / 3600 * 100
export const tss2 = (duration: number, intensity: number): number => { export const tss2 = (duration: Seconds, intensity: number): number => {
return ((duration * intensity * intensity) / 3600) * 100; return ((duration.value * intensity * intensity) / 3600) * 100;
}; };

3
src/types.ts Normal file
View File

@ -0,0 +1,3 @@
export class Seconds {
constructor(readonly value: number) {}
}