Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 59 additions & 15 deletions cmd/routines.go
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,17 @@ func renderOnePreset(w io.Writer, p Preset, headingMarker string) error {
name = fmt.Sprintf("%s (%s)", name, *ex.ExerciseNotes)
}
fmt.Fprintln(w, name)
// An exercise added to a routine but never given target sets/reps
// arrives as one (or more) all-zero placeholder sets. Rendering them
// as `0@0` / `0@+0` / `0:00` looks like nonsense data; suppress the
// set lines but keep the exercise name so the LLM-agent reader still
// sees that the exercise is part of the routine.
if allSetsAreZero(ex.SetsData) {
continue
}
var lines []string
for _, s := range ex.SetsData {
lines = append(lines, fitdownSetLine(ex.ExerciseTypes, s))
lines = append(lines, fitdownSetLine(ex.ExerciseTypes, s.InputOne, s.InputTwo))
}
// Compress consecutive identical lines into Nx... notation.
for i := 0; i < len(lines); {
Expand All @@ -273,30 +281,66 @@ func renderOnePreset(w io.Writer, p Preset, headingMarker string) error {
return nil
}

// fitdownSetLine is split out of workouts.printFitdown so routines can render
// the same notation without duplicating the type switch. Keeping it here
// rather than in workouts.go keeps the workouts file untouched, at the cost
// of a tiny bit of duplication of the WR/BR/AB/WD/DD/ND mapping.
func fitdownSetLine(exTypes string, s PresetSetData) string {
// allSetsAreZero reports whether every set in the slice has both inputs == 0.
// Used to detect placeholder sets in routines (an exercise added but never
// given a target) and an empty `ND` time entry in either renderer.
func allSetsAreZero(sets []PresetSetData) bool {
if len(sets) == 0 {
return true
}
for _, s := range sets {
a, _ := s.InputOne.Float64()
b, _ := s.InputTwo.Float64()
if a != 0 || b != 0 {
return false
}
}
return true
}

// fitdownSetLine renders one set line in fitdown notation. Shared by the
// workouts and routines renderers — both receive the same `exerciseTypes`
// tag from Liftoff and emit identical notation. The fitdown spec only
// documents `${reps}@${poundage}` and `Nx` compression; everything else
// (WR/BR/AB/WD/DD/ND mapping, the `+`/`-` weight prefixes, the bodyweight
// simplification below) is our extension.
//
// Simplification: when a WR/BR/AB set's weight component is 0 — i.e. a
// bodyweight rep with no extra load or assistance — we drop the `@…`
// suffix and emit just the rep count. "5" is clearer than "5@+0".
func fitdownSetLine(exTypes string, inputOne, inputTwo json.Number) string {
weightZero := func() bool {
v, _ := inputOne.Float64()
return v == 0
}
switch exTypes {
case "WR":
return fmt.Sprintf("%s@%s", s.InputTwo, s.InputOne)
if weightZero() {
return fmt.Sprintf("%s", inputTwo)
}
return fmt.Sprintf("%s@%s", inputTwo, inputOne)
case "AB":
return fmt.Sprintf("%s@-%s", s.InputTwo, s.InputOne)
if weightZero() {
return fmt.Sprintf("%s", inputTwo)
}
return fmt.Sprintf("%s@-%s", inputTwo, inputOne)
case "BR":
return fmt.Sprintf("%s@+%s", s.InputTwo, s.InputOne)
if weightZero() {
return fmt.Sprintf("%s", inputTwo)
}
return fmt.Sprintf("%s@+%s", inputTwo, inputOne)
case "WD":
km, _ := s.InputTwo.Float64()
return fmt.Sprintf("%slb %.3fmi", s.InputOne, km/1.60934)
km, _ := inputTwo.Float64()
return fmt.Sprintf("%slb %.3fmi", inputOne, km/1.60934)
case "DD":
secs, _ := s.InputTwo.Int64()
km, _ := s.InputOne.Float64()
secs, _ := inputTwo.Int64()
km, _ := inputOne.Float64()
return fmt.Sprintf("%.2fmi %d:%02d", km/1.60934, secs/60, secs%60)
case "ND":
secs, _ := s.InputTwo.Int64()
secs, _ := inputTwo.Int64()
return fmt.Sprintf("%d:%02d", secs/60, secs%60)
default:
return fmt.Sprintf("[%s] %s %s", exTypes, s.InputOne, s.InputTwo)
return fmt.Sprintf("[%s] %s %s", exTypes, inputOne, inputTwo)
}
}

Expand Down
43 changes: 43 additions & 0 deletions cmd/routines_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,49 @@ func TestRenderOnePreset_ShowUsesH1(t *testing.T) {
}
}

func TestRenderOnePreset_AllZeroSetsSuppressed(t *testing.T) {
// An exercise added to a routine but never given target sets/reps
// arrives as one or more zero/zero placeholder sets. The exercise
// name should still appear (the LLM-agent reader cares that it's
// part of the routine) but the bogus "0@0" / "0:00" set lines must
// not.
p := Preset{
Name: "Free Weights",
ExerciseData: []PresetExerciseData{
{
ExerciseName: "Bench Press",
ExerciseTypes: "WR",
SetsData: []PresetSetData{{InputOne: json.Number("100"), InputTwo: json.Number("5")}},
},
{
ExerciseName: "Placeholder",
ExerciseTypes: "WR",
SetsData: []PresetSetData{{InputOne: json.Number("0"), InputTwo: json.Number("0")}},
},
{
ExerciseName: "Sci-fit Upper Body",
ExerciseTypes: "ND",
SetsData: []PresetSetData{{InputOne: json.Number("0"), InputTwo: json.Number("0")}},
},
},
}
var buf bytes.Buffer
renderOnePreset(&buf, p, "##")
out := buf.String()
if !strings.Contains(out, "Bench Press\n5@100") {
t.Errorf("real exercise should render normally; got:\n%s", out)
}
if !strings.Contains(out, "Placeholder") {
t.Errorf("placeholder exercise name should still appear; got:\n%s", out)
}
if strings.Contains(out, "0@0") {
t.Errorf("zero/zero WR set should be suppressed; got:\n%s", out)
}
if strings.Contains(out, "0:00") {
t.Errorf("zero ND duration set should be suppressed; got:\n%s", out)
}
}

func TestPickPreset(t *testing.T) {
resp := &presetsResponse{
PresetsWithoutFolder: []Preset{
Expand Down
23 changes: 1 addition & 22 deletions cmd/workouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,28 +308,7 @@ func renderWorkoutsFitdown(w io.Writer, posts []Post) error {

var lines []string
for _, s := range ex.SetsData {
var line string
switch ex.ExerciseTypes {
case "WR":
line = fmt.Sprintf("%s@%s", s.InputTwo, s.InputOne)
case "AB":
line = fmt.Sprintf("%s@-%s", s.InputTwo, s.InputOne)
case "BR":
line = fmt.Sprintf("%s@+%s", s.InputTwo, s.InputOne)
case "WD":
km, _ := s.InputTwo.Float64()
line = fmt.Sprintf("%slb %.3fmi", s.InputOne, km/1.60934)
case "DD":
secs, _ := s.InputTwo.Int64()
km, _ := s.InputOne.Float64()
line = fmt.Sprintf("%.2fmi %d:%02d", km/1.60934, secs/60, secs%60)
case "ND":
secs, _ := s.InputTwo.Int64()
line = fmt.Sprintf("%d:%02d", secs/60, secs%60)
default:
line = fmt.Sprintf("[%s] %s %s", ex.ExerciseTypes, s.InputOne, s.InputTwo)
}
lines = append(lines, line)
lines = append(lines, fitdownSetLine(ex.ExerciseTypes, s.InputOne, s.InputTwo))
}

// Compress consecutive identical lines into Nx... notation
Expand Down
31 changes: 20 additions & 11 deletions cmd/workouts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,28 @@ func TestRenderWorkoutsFitdown_ExerciseNoteInParens(t *testing.T) {
}
}

// Set-line notation hasn't changed in this PR but is the load-bearing
// piece routines share with workouts (via the fitdownSetLine duplicate).
// One representative case per ExerciseType so a regression in either
// renderer surfaces in CI.
// Representative cases per ExerciseType so a regression in the shared
// fitdownSetLine surfaces in CI. The bodyweight cases (BR/AB with zero
// weight component) verify the @+0/@-0 simplification — the spec is silent
// on these so the rule lives in code, not docs.
func TestRenderWorkoutsFitdown_SetNotation(t *testing.T) {
cases := []struct {
name string
exType string
one, two string
want string
notWant string // optional negative assertion
}{
{"WR", "100", "5", "5@100"}, // weight/reps
{"BR", "0", "10", "10@+0"}, // bodyweight reps
{"AB", "20", "10", "10@-20"}, // assisted bodyweight (minus)
{"ND", "0", "495", "8:15"}, // no-data duration
{"WR weighted", "WR", "100", "5", "5@100", ""},
{"WR zero weight simplifies", "WR", "0", "5", "\n5\n", "5@0"},
{"BR with extra weight", "BR", "12", "5", "5@+12", ""},
{"BR zero added simplifies", "BR", "0", "10", "\n10\n", "10@+0"},
{"AB with assistance", "AB", "20", "10", "10@-20", ""},
{"AB zero assistance simplifies", "AB", "0", "10", "\n10\n", "10@-0"},
{"ND duration", "ND", "0", "495", "8:15", ""},
}
for _, c := range cases {
t.Run(c.exType, func(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
w := bareWorkout()
w.ExerciseData[0].ExerciseTypes = c.exType
w.ExerciseData[0].SetsData[0] = SetData{
Expand All @@ -91,8 +96,12 @@ func TestRenderWorkoutsFitdown_SetNotation(t *testing.T) {
}
var buf bytes.Buffer
renderWorkoutsFitdown(&buf, []Post{w})
if !strings.Contains(buf.String(), c.want) {
t.Errorf("%s: expected %q in output; got:\n%s", c.exType, c.want, buf.String())
out := buf.String()
if !strings.Contains(out, c.want) {
t.Errorf("expected %q in output; got:\n%s", c.want, out)
}
if c.notWant != "" && strings.Contains(out, c.notWant) {
t.Errorf("did NOT want %q in output (should have been simplified); got:\n%s", c.notWant, out)
}
})
}
Expand Down
Loading