New meaning of negative offsets (breaking change)

Instead of always being relative to interval end,
negative offsets are now relative to next comment
(or the interval end, when there is no next comment).
This commit is contained in:
Rene Saarsoo 2021-03-02 23:32:05 +02:00
parent 574272602f
commit 93684069f0
2 changed files with 211 additions and 24 deletions

View File

@ -555,17 +555,13 @@ Rest: 5:00 50%
`); `);
}); });
it("parses intervals with negative comment offsets", () => { it("parses last comment with negative offset", () => {
expect( expect(
parse(` parse(`
Name: My Workout Name: My Workout
Interval: 10:00 90% Interval: 10:00 90%
@ 0:10 Find your rythm. @ 0:10 Find your rythm.
@ -0:10 Final push. YOU GOT IT! @ -0:10 Final push. YOU GOT IT!
Rest: 5:00 50%
@ -4:30 Great effort!
@ -2:00 Cool down well after all of this.
`), `),
).toMatchInlineSnapshot(` ).toMatchInlineSnapshot(`
Object { Object {
@ -604,37 +600,120 @@ Rest: 5:00 50%
}, },
"type": "Interval", "type": "Interval",
}, },
],
"name": "My Workout",
"tags": Array [],
}
`);
});
it("parses comment with negative offset before absolutely offset comment", () => {
expect(
parse(`
Name: My Workout
Interval: 1:00 90%
@ -0:10 Before last
@ 0:50 Last!
`),
).toMatchInlineSnapshot(`
Object {
"author": "",
"description": "",
"intervals": Array [
Object { Object {
"cadence": undefined, "cadence": undefined,
"comments": Array [ "comments": Array [
Object { Object {
"loc": Object { "loc": Object {
"col": 4, "col": 4,
"row": 7, "row": 3,
}, },
"offset": Duration { "offset": Duration {
"seconds": 30, "seconds": 40,
}, },
"text": "Great effort!", "text": "Before last",
}, },
Object { Object {
"loc": Object { "loc": Object {
"col": 4, "col": 4,
"row": 8, "row": 4,
}, },
"offset": Duration { "offset": Duration {
"seconds": 180, "seconds": 50,
}, },
"text": "Cool down well after all of this.", "text": "Last!",
}, },
], ],
"duration": Duration { "duration": Duration {
"seconds": 300, "seconds": 60,
}, },
"intensity": ConstantIntensity { "intensity": ConstantIntensity {
"_value": 0.5, "_value": 0.9,
}, },
"type": "Rest", "type": "Interval",
},
],
"name": "My Workout",
"tags": Array [],
}
`);
});
it("parses multiple comments with negative offsets in row", () => {
expect(
parse(`
Name: My Workout
Interval: 1:00 90%
@ -0:10 One more before last
@ -0:10 Before last
@ -0:10 Last!
`),
).toMatchInlineSnapshot(`
Object {
"author": "",
"description": "",
"intervals": Array [
Object {
"cadence": undefined,
"comments": Array [
Object {
"loc": Object {
"col": 4,
"row": 3,
},
"offset": Duration {
"seconds": 30,
},
"text": "One more before last",
},
Object {
"loc": Object {
"col": 4,
"row": 4,
},
"offset": Duration {
"seconds": 40,
},
"text": "Before last",
},
Object {
"loc": Object {
"col": 4,
"row": 5,
},
"offset": Duration {
"seconds": 50,
},
"text": "Last!",
},
],
"duration": Duration {
"seconds": 60,
},
"intensity": ConstantIntensity {
"_value": 0.9,
},
"type": "Interval",
}, },
], ],
"name": "My Workout", "name": "My Workout",
@ -780,6 +859,70 @@ Interval: 10:00 90%
`); `);
}); });
it("throws error when negative offset is followed by positive offset", () => {
expect(() =>
parse(`
Name: My Workout
Interval: 2:00 90%
@ -0:10 Comment 1
@ +0:30 Comment 2
@ 1:30 Comment 3
`),
).toThrowErrorMatchingInlineSnapshot(`"Negative offset followed by positive offset at line 5 char 5"`);
});
it("works fine when positive offset is followed by negative offset", () => {
expect(
parse(`
Name: My Workout
Interval: 1:00 90%
@ +0:10 Comment 1
@ -0:10 Comment 2
`),
).toMatchInlineSnapshot(`
Object {
"author": "",
"description": "",
"intervals": Array [
Object {
"cadence": undefined,
"comments": Array [
Object {
"loc": Object {
"col": 4,
"row": 3,
},
"offset": Duration {
"seconds": 10,
},
"text": "Comment 1",
},
Object {
"loc": Object {
"col": 4,
"row": 4,
},
"offset": Duration {
"seconds": 50,
},
"text": "Comment 2",
},
],
"duration": Duration {
"seconds": 60,
},
"intensity": ConstantIntensity {
"_value": 0.9,
},
"type": "Interval",
},
],
"name": "My Workout",
"tags": Array [],
}
`);
});
it("throws error when comment offset is outside of interval length", () => { it("throws error when comment offset is outside of interval length", () => {
expect(() => expect(() =>
parse(` parse(`

View File

@ -58,8 +58,14 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => {
return [header, tokens]; return [header, tokens];
}; };
type PartialComment = {
offsetToken: OffsetToken;
text: string;
loc: SourceLocation;
};
const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Comment[], Token[]] => { const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Comment[], Token[]] => {
const comments: Comment[] = []; const comments: PartialComment[] = [];
while (tokens[0]) { while (tokens[0]) {
const [start, offset, text, ...rest] = tokens; const [start, offset, text, ...rest] = tokens;
if (start.type === "comment-start") { if (start.type === "comment-start") {
@ -73,7 +79,7 @@ const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Co
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: absoluteOffset(offset, intervalDuration, last(comments)), offsetToken: offset,
text: text.value, text: text.value,
loc: offset.loc, loc: offset.loc,
}); });
@ -82,16 +88,54 @@ const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Co
break; break;
} }
} }
return [comments, tokens];
return [computeAbsoluteOffsets(comments, intervalDuration), tokens];
}; };
const absoluteOffset = (offset: OffsetToken, intervalDuration: Duration, previousComment?: Comment): Duration => { const computeAbsoluteOffsets = (partialComments: PartialComment[], intervalDuration: Duration): Comment[] => {
if (offset.kind === "relative-minus") { const comments: Comment[] = [];
return new Duration(intervalDuration.seconds - offset.value); for (let i = 0; i < partialComments.length; i++) {
} else if (offset.kind === "relative-plus" && previousComment) { const pComment = partialComments[i];
return new Duration(previousComment.offset.seconds + offset.value); const offsetToken = pComment.offsetToken;
} else {
return new Duration(offset.value); // Assume absolute offset by default
let offset: Duration = new Duration(offsetToken.value);
if (offsetToken.kind === "relative-plus") {
// Position relative to previous already-computed comment offset
const previousComment = last(comments);
if (previousComment) {
offset = new Duration(previousComment.offset.seconds + offset.seconds);
}
} else if (offsetToken.kind === "relative-minus") {
// Position relative to next comment or interval end
offset = new Duration(nextCommentOffset(partialComments, i, intervalDuration).seconds - offset.seconds);
}
comments.push({
offset,
loc: pComment.loc,
text: pComment.text,
});
}
return comments;
};
const nextCommentOffset = (partialComments: PartialComment[], i: number, intervalDuration: Duration): Duration => {
const nextComment = partialComments[i + 1];
if (!nextComment) {
return intervalDuration;
}
switch (nextComment.offsetToken.kind) {
case "relative-minus":
return new Duration(
nextCommentOffset(partialComments, i + 1, intervalDuration).seconds - nextComment.offsetToken.value,
);
case "relative-plus":
throw new ParseError("Negative offset followed by positive offset", nextComment.offsetToken.loc);
case "absolute":
default:
return new Duration(nextComment.offsetToken.value);
} }
}; };