From 93684069f0dd975e9fd8a17818e7be3e57917545 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Tue, 2 Mar 2021 23:32:05 +0200 Subject: [PATCH] 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). --- src/parser/parser.test.ts | 171 ++++++++++++++++++++++++++++++++++---- src/parser/parser.ts | 64 +++++++++++--- 2 files changed, 211 insertions(+), 24 deletions(-) diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index 9bf5265..d0a3692 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -555,17 +555,13 @@ Rest: 5:00 50% `); }); - it("parses intervals with negative comment offsets", () => { + it("parses last comment with negative offset", () => { expect( parse(` Name: My Workout Interval: 10:00 90% @ 0:10 Find your rythm. @ -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(` Object { @@ -604,37 +600,120 @@ Rest: 5:00 50% }, "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 { "cadence": undefined, "comments": Array [ Object { "loc": Object { "col": 4, - "row": 7, + "row": 3, }, "offset": Duration { - "seconds": 30, + "seconds": 40, }, - "text": "Great effort!", + "text": "Before last", }, Object { "loc": Object { "col": 4, - "row": 8, + "row": 4, }, "offset": Duration { - "seconds": 180, + "seconds": 50, }, - "text": "Cool down well after all of this.", + "text": "Last!", }, ], "duration": Duration { - "seconds": 300, + "seconds": 60, }, "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", @@ -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", () => { expect(() => parse(` diff --git a/src/parser/parser.ts b/src/parser/parser.ts index 34954e0..bb886ed 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -58,8 +58,14 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => { return [header, tokens]; }; +type PartialComment = { + offsetToken: OffsetToken; + text: string; + loc: SourceLocation; +}; + const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Comment[], Token[]] => { - const comments: Comment[] = []; + const comments: PartialComment[] = []; while (tokens[0]) { const [start, offset, text, ...rest] = tokens; 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); } comments.push({ - offset: absoluteOffset(offset, intervalDuration, last(comments)), + offsetToken: offset, text: text.value, loc: offset.loc, }); @@ -82,16 +88,54 @@ const parseIntervalComments = (tokens: Token[], intervalDuration: Duration): [Co break; } } - return [comments, tokens]; + + return [computeAbsoluteOffsets(comments, intervalDuration), tokens]; }; -const absoluteOffset = (offset: OffsetToken, intervalDuration: Duration, previousComment?: Comment): Duration => { - if (offset.kind === "relative-minus") { - return new Duration(intervalDuration.seconds - offset.value); - } else if (offset.kind === "relative-plus" && previousComment) { - return new Duration(previousComment.offset.seconds + offset.value); - } else { - return new Duration(offset.value); +const computeAbsoluteOffsets = (partialComments: PartialComment[], intervalDuration: Duration): Comment[] => { + const comments: Comment[] = []; + for (let i = 0; i < partialComments.length; i++) { + const pComment = partialComments[i]; + const offsetToken = pComment.offsetToken; + + // 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); } };