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
22 changes: 22 additions & 0 deletions cmd/bodyweights.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,35 @@ func loadBodyweightEntries(sinceFlag, untilFlag string) ([]bodyweightEntry, erro
})
}

entries = dedupeByDay(entries)

sort.Slice(entries, func(i, j int) bool {
return entries[i].date.Before(entries[j].date)
})

return entries, nil
}

// dedupeByDay collapses entries to one per local calendar day. Bodyweight is
// read off each workout post, so logging two workouts on the same day yields
// two entries for that date (#30). When a day has more than one measurement,
// keep the latest one (by StartedAt timestamp). Order is not guaranteed; the
// caller sorts afterward.
func dedupeByDay(entries []bodyweightEntry) []bodyweightEntry {
latest := make(map[string]bodyweightEntry, len(entries))
for _, e := range entries {
key := e.date.Format("2006-01-02")
if cur, ok := latest[key]; !ok || e.date.After(cur.date) {
latest[key] = e
}
}
out := make([]bodyweightEntry, 0, len(latest))
for _, e := range latest {
out = append(out, e)
}
return out
}

func printBodyweightStats(entries []bodyweightEntry) {
first := entries[0]
last := entries[len(entries)-1]
Expand Down
51 changes: 51 additions & 0 deletions cmd/bodyweights_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cmd

import (
"sort"
"testing"
"time"
)

// Two workouts on the same calendar day must collapse to a single
// bodyweight row, keeping the latest measurement that day. (#30)
func TestDedupeByDay_KeepsLatestPerDay(t *testing.T) {
mk := func(ts string, w float64) bodyweightEntry {
d, err := time.Parse(time.RFC3339, ts)
if err != nil {
t.Fatalf("bad test timestamp %q: %v", ts, err)
}
return bodyweightEntry{date: d, weight: w}
}

in := []bodyweightEntry{
mk("2026-05-02T07:00:00Z", 175.0), // morning workout
mk("2026-05-02T18:00:00Z", 177.0), // evening workout, same day
mk("2026-05-03T08:00:00Z", 176.0),
}

out := dedupeByDay(in)
sort.Slice(out, func(i, j int) bool { return out[i].date.Before(out[j].date) })

if len(out) != 2 {
t.Fatalf("expected 2 rows after dedupe (one per day), got %d: %+v", len(out), out)
}
if got := out[0].date.Format("2006-01-02"); got != "2026-05-02" {
t.Errorf("first row date = %s, want 2026-05-02", got)
}
if out[0].weight != 177.0 {
t.Errorf("same-day collision should keep the latest measurement (177.0), got %v", out[0].weight)
}
if out[1].weight != 176.0 {
t.Errorf("second day weight = %v, want 176.0", out[1].weight)
}
}

// A single entry per day passes through unchanged.
func TestDedupeByDay_NoDuplicates(t *testing.T) {
d1, _ := time.Parse(time.RFC3339, "2026-01-01T10:00:00Z")
d2, _ := time.Parse(time.RFC3339, "2026-01-02T10:00:00Z")
in := []bodyweightEntry{{date: d1, weight: 180}, {date: d2, weight: 181}}
if got := len(dedupeByDay(in)); got != 2 {
t.Errorf("expected 2 rows, got %d", got)
}
}
Loading