Skip to content

Commit 9bc1436

Browse files
feat: add --format flag to bodyweights stats command
Fixes contract violation where `bodyweights stats` rejected `--format` while every other export subcommand accepted it. Adds `--format markdown|json` flag and a JSON output shape (current, high, low, change, ratePerMonth, plateau, months) mirroring the markdown stats. Closes #25 Co-authored-by: Darrell <[email protected]>
1 parent a30bec5 commit 9bc1436

1 file changed

Lines changed: 101 additions & 6 deletions

File tree

cmd/bodyweights.go

Lines changed: 101 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,12 @@ type monthAvg struct {
2323
}
2424

2525
var (
26-
bodyweightsListSinceFlag string
27-
bodyweightsListUntilFlag string
28-
bodyweightsListFormatFlag string
29-
bodyweightsStatsSinceFlag string
30-
bodyweightsStatsUntilFlag string
26+
bodyweightsListSinceFlag string
27+
bodyweightsListUntilFlag string
28+
bodyweightsListFormatFlag string
29+
bodyweightsStatsSinceFlag string
30+
bodyweightsStatsUntilFlag string
31+
bodyweightsStatsFormatFlag string
3132
)
3233

3334
var bodyweightsCmd = &cobra.Command{
@@ -42,6 +43,27 @@ type bodyweightJSON struct {
4243
Weight float64 `json:"weight"`
4344
}
4445

46+
type bodyweightStatsJSON struct {
47+
Current float64 `json:"current"`
48+
High float64 `json:"high"`
49+
Low float64 `json:"low"`
50+
Change float64 `json:"change"`
51+
RatePerMonth *float64 `json:"ratePerMonth,omitempty"`
52+
Plateau *plateauJSON `json:"plateau,omitempty"`
53+
Months []monthAvgJSON `json:"months"`
54+
}
55+
56+
type plateauJSON struct {
57+
Since string `json:"since"`
58+
Avg float64 `json:"avg"`
59+
Stddev float64 `json:"stddev"`
60+
}
61+
62+
type monthAvgJSON struct {
63+
Month string `json:"month"`
64+
Avg float64 `json:"avg"`
65+
}
66+
4567
var bodyweightsListCmd = &cobra.Command{
4668
Use: "list",
4769
Short: "List recorded bodyweights",
@@ -80,15 +102,24 @@ var bodyweightsStatsCmd = &cobra.Command{
80102
Use: "stats",
81103
Short: "Show bodyweight statistics",
82104
RunE: func(cmd *cobra.Command, args []string) error {
105+
format, err := validateFormat(bodyweightsStatsFormatFlag)
106+
if err != nil {
107+
return err
108+
}
83109
entries, err := loadBodyweightEntries(bodyweightsStatsSinceFlag, bodyweightsStatsUntilFlag)
84110
if err != nil {
85111
return err
86112
}
87113
if len(entries) == 0 {
114+
if format == "json" {
115+
return printJSON(bodyweightStatsJSON{Months: []monthAvgJSON{}})
116+
}
88117
fmt.Println("No bodyweights found.")
89118
return nil
90119
}
91-
120+
if format == "json" {
121+
return printJSON(buildBodyweightStatsJSON(entries))
122+
}
92123
printBodyweightStats(entries)
93124
return nil
94125
},
@@ -103,6 +134,7 @@ func init() {
103134
bodyweightsListCmd.Flags().StringVar(&bodyweightsListFormatFlag, "format", "markdown", "Output format: markdown (default) or json")
104135
bodyweightsStatsCmd.Flags().StringVar(&bodyweightsStatsSinceFlag, "since", "", "Filter entries on or after date (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
105136
bodyweightsStatsCmd.Flags().StringVar(&bodyweightsStatsUntilFlag, "until", "", "Filter entries through date, inclusive (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
137+
bodyweightsStatsCmd.Flags().StringVar(&bodyweightsStatsFormatFlag, "format", "markdown", "Output format: markdown (default) or json")
106138
}
107139

108140
func loadBodyweightEntries(sinceFlag, untilFlag string) ([]bodyweightEntry, error) {
@@ -211,6 +243,69 @@ func printBodyweightStats(entries []bodyweightEntry) {
211243
}
212244
}
213245

246+
func buildBodyweightStatsJSON(entries []bodyweightEntry) bodyweightStatsJSON {
247+
first := entries[0]
248+
last := entries[len(entries)-1]
249+
minWeight := entries[0].weight
250+
maxWeight := entries[0].weight
251+
for _, entry := range entries {
252+
if entry.weight < minWeight {
253+
minWeight = entry.weight
254+
}
255+
if entry.weight > maxWeight {
256+
maxWeight = entry.weight
257+
}
258+
}
259+
260+
change := last.weight - first.weight
261+
months := last.date.Sub(first.date).Hours() / 24 / 30.44
262+
263+
stats := bodyweightStatsJSON{
264+
Current: last.weight,
265+
High: maxWeight,
266+
Low: minWeight,
267+
Change: change,
268+
}
269+
270+
if months > 0.5 {
271+
rate := change / months
272+
stats.RatePerMonth = &rate
273+
}
274+
275+
monthAvgs := monthlyBodyweightAverages(entries)
276+
277+
if len(monthAvgs) >= 3 {
278+
tail := monthAvgs
279+
if len(tail) > 6 {
280+
tail = tail[len(tail)-6:]
281+
}
282+
mean := 0.0
283+
for _, m := range tail {
284+
mean += m.avg
285+
}
286+
mean /= float64(len(tail))
287+
variance := 0.0
288+
for _, m := range tail {
289+
variance += (m.avg - mean) * (m.avg - mean)
290+
}
291+
stddev := math.Sqrt(variance / float64(len(tail)-1))
292+
if stddev < 2.0 {
293+
stats.Plateau = &plateauJSON{
294+
Since: monthNameFromKey(tail[0].month),
295+
Avg: mean,
296+
Stddev: stddev,
297+
}
298+
}
299+
}
300+
301+
stats.Months = make([]monthAvgJSON, len(monthAvgs))
302+
for i, m := range monthAvgs {
303+
stats.Months[i] = monthAvgJSON{Month: m.month, Avg: m.avg}
304+
}
305+
306+
return stats
307+
}
308+
214309
func monthlyBodyweightAverages(entries []bodyweightEntry) []monthAvg {
215310
monthMap := map[string][]float64{}
216311
for _, entry := range entries {

0 commit comments

Comments
 (0)