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
31 changes: 19 additions & 12 deletions cmd/bodyweights.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ type monthAvg struct {

var (
bodyweightsListSinceFlag string
bodyweightsListUntilFlag string
bodyweightsStatsSinceFlag string
bodyweightsStatsUntilFlag string
)

var bodyweightsCmd = &cobra.Command{
Expand All @@ -36,7 +38,7 @@ var bodyweightsListCmd = &cobra.Command{
Use: "list",
Short: "List recorded bodyweights",
RunE: func(cmd *cobra.Command, args []string) error {
entries, err := loadBodyweightEntries(bodyweightsListSinceFlag)
entries, err := loadBodyweightEntries(bodyweightsListSinceFlag, bodyweightsListUntilFlag)
if err != nil {
return err
}
Expand All @@ -56,7 +58,7 @@ var bodyweightsStatsCmd = &cobra.Command{
Use: "stats",
Short: "Show bodyweight statistics",
RunE: func(cmd *cobra.Command, args []string) error {
entries, err := loadBodyweightEntries(bodyweightsStatsSinceFlag)
entries, err := loadBodyweightEntries(bodyweightsStatsSinceFlag, bodyweightsStatsUntilFlag)
if err != nil {
return err
}
Expand All @@ -74,24 +76,26 @@ func init() {
bodyweightsCmd.AddCommand(bodyweightsListCmd)
bodyweightsCmd.AddCommand(bodyweightsStatsCmd)

bodyweightsListCmd.Flags().StringVar(&bodyweightsListSinceFlag, "since", "", "Filter entries on or after date (e.g. 2025-01-01, 30d, 4w, 6m, 1y)")
bodyweightsStatsCmd.Flags().StringVar(&bodyweightsStatsSinceFlag, "since", "", "Filter entries on or after date (e.g. 2025-01-01, 30d, 4w, 6m, 1y)")
bodyweightsListCmd.Flags().StringVar(&bodyweightsListSinceFlag, "since", "", "Filter entries on or after date (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
bodyweightsListCmd.Flags().StringVar(&bodyweightsListUntilFlag, "until", "", "Filter entries through date, inclusive (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
bodyweightsStatsCmd.Flags().StringVar(&bodyweightsStatsSinceFlag, "since", "", "Filter entries on or after date (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
bodyweightsStatsCmd.Flags().StringVar(&bodyweightsStatsUntilFlag, "until", "", "Filter entries through date, inclusive (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
}

func loadBodyweightEntries(sinceFlag string) ([]bodyweightEntry, error) {
func loadBodyweightEntries(sinceFlag, untilFlag string) ([]bodyweightEntry, error) {
c := client.New()
var posts []Post
if err := c.Query("post.getMyPosts", nil, &posts); err != nil {
return nil, err
}

var since time.Time
if sinceFlag != "" {
parsed, err := parseSince(sinceFlag)
if err != nil {
return nil, err
}
since = parsed
since, err := parseDateValue(sinceFlag)
if err != nil {
return nil, err
}
until, err := parseUntilValue(untilFlag)
if err != nil {
return nil, err
}

entries := make([]bodyweightEntry, 0, len(posts))
Expand All @@ -109,6 +113,9 @@ func loadBodyweightEntries(sinceFlag string) ([]bodyweightEntry, error) {
if !since.IsZero() && date.Before(since) {
continue
}
if !until.IsZero() && !date.Before(until) {
continue
}

entries = append(entries, bodyweightEntry{
date: date,
Expand Down
25 changes: 11 additions & 14 deletions cmd/stats.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

var (
statsSinceFlag string
statsUntilFlag string
statsExerciseFlag string
statsJSONFlag bool
statsDetailFlag bool
Expand Down Expand Up @@ -47,20 +48,15 @@ var statsCmd = &cobra.Command{
if err := c.Query("post.getMyPosts", nil, &posts); err != nil {
return err
}
if statsSinceFlag != "" {
since, err := parseSince(statsSinceFlag)
if err != nil {
return err
}
var filtered []Post
for _, p := range posts {
t, err := time.Parse(time.RFC3339Nano, p.StartedAt)
if err != nil || !t.Before(since) {
filtered = append(filtered, p)
}
}
posts = filtered
since, err := parseDateValue(statsSinceFlag)
if err != nil {
return err
}
until, err := parseUntilValue(statsUntilFlag)
if err != nil {
return err
}
posts = filterByWindow(posts, since, until)
if statsExerciseFlag != "" {
posts = filterExercises(posts, statsExerciseFlag)
}
Expand Down Expand Up @@ -89,7 +85,8 @@ var statsCmd = &cobra.Command{

func init() {
workoutsCmd.AddCommand(statsCmd)
statsCmd.Flags().StringVar(&statsSinceFlag, "since", "", "Filter workouts on or after date (e.g. 2025-01-01, 30d, 4w, 6m, 1y)")
statsCmd.Flags().StringVar(&statsSinceFlag, "since", "", "Filter workouts on or after date (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
statsCmd.Flags().StringVar(&statsUntilFlag, "until", "", "Filter workouts through date, inclusive (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
statsCmd.Flags().StringVar(&statsExerciseFlag, "exercise", "", "Filter to exercises matching this name (word-prefix match)")
statsCmd.Flags().BoolVar(&statsJSONFlag, "json", false, "Output as JSON")
statsCmd.Flags().BoolVar(&statsDetailFlag, "detail", false, "Show per-session breakdown")
Expand Down
116 changes: 76 additions & 40 deletions cmd/workouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type Post struct {

var listJSONFlag bool
var listSinceFlag string
var listUntilFlag string
var listExerciseFlag string

var listCmd = &cobra.Command{
Expand All @@ -61,20 +62,15 @@ var listCmd = &cobra.Command{
if err := c.Query("post.getMyPosts", nil, &posts); err != nil {
return err
}
if listSinceFlag != "" {
since, err := parseSince(listSinceFlag)
if err != nil {
return err
}
var filtered []Post
for _, p := range posts {
t, err := time.Parse(time.RFC3339Nano, p.StartedAt)
if err != nil || !t.Before(since) {
filtered = append(filtered, p)
}
}
posts = filtered
since, err := parseDateValue(listSinceFlag)
if err != nil {
return err
}
until, err := parseUntilValue(listUntilFlag)
if err != nil {
return err
}
posts = filterByWindow(posts, since, until)
if listExerciseFlag != "" {
posts = filterExercises(posts, listExerciseFlag)
}
Expand All @@ -85,32 +81,82 @@ var listCmd = &cobra.Command{
},
}

func parseSince(s string) (time.Time, error) {
// Try absolute date first
// parseDateValue parses --since / --until / show argument values.
// Accepted forms: "today", "yesterday", absolute YYYY-MM-DD, or relative
// Nd/Nw/Nm/Ny. Returns local midnight for the target day; empty string
// yields the zero time.
func parseDateValue(s string) (time.Time, error) {
if s == "" {
return time.Time{}, nil
}
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)

switch strings.ToLower(s) {
case "today":
return today, nil
case "yesterday":
return today.AddDate(0, 0, -1), nil
}
if t, err := time.ParseInLocation("2006-01-02", s, time.Local); err == nil {
return t, nil
}
// Try relative: e.g. 30d, 4w, 6m, 1y
if len(s) < 2 {
return time.Time{}, fmt.Errorf("invalid --since value: %q", s)
return time.Time{}, fmt.Errorf("invalid date %q (use YYYY-MM-DD, today, yesterday, or Nd/Nw/Nm/Ny)", s)
}
n := 0
if _, err := fmt.Sscanf(s[:len(s)-1], "%d", &n); err != nil {
return time.Time{}, fmt.Errorf("invalid --since value: %q", s)
return time.Time{}, fmt.Errorf("invalid date %q (use YYYY-MM-DD, today, yesterday, or Nd/Nw/Nm/Ny)", s)
}
now := time.Now()
switch s[len(s)-1] {
case 'd':
return now.AddDate(0, 0, -n), nil
return today.AddDate(0, 0, -n), nil
case 'w':
return now.AddDate(0, 0, -n*7), nil
return today.AddDate(0, 0, -n*7), nil
case 'm':
return now.AddDate(0, -n, 0), nil
return today.AddDate(0, -n, 0), nil
case 'y':
return now.AddDate(-n, 0, 0), nil
return today.AddDate(-n, 0, 0), nil
default:
return time.Time{}, fmt.Errorf("invalid --since unit %q: use d, w, m, or y", string(s[len(s)-1]))
return time.Time{}, fmt.Errorf("invalid date unit %q: use d, w, m, or y", string(s[len(s)-1]))
}
}

// parseUntilValue resolves --until to the exclusive upper bound of a half-open
// window. The user-supplied date names a calendar day they expect to be
// included, so we add 24h to the parsed start-of-day. Empty string yields the
// zero time, which callers treat as "no upper bound".
func parseUntilValue(s string) (time.Time, error) {
t, err := parseDateValue(s)
if err != nil || t.IsZero() {
return t, err
}
return t.AddDate(0, 0, 1), nil
}

// filterByWindow keeps posts with StartedAt in [since, until). Either bound
// being zero disables that side. RFC3339Nano parse failures are kept (the
// user can decide what to do with malformed timestamps downstream).
func filterByWindow(posts []Post, since, until time.Time) []Post {
if since.IsZero() && until.IsZero() {
return posts
}
out := posts[:0]
for _, p := range posts {
t, err := time.Parse(time.RFC3339Nano, p.StartedAt)
if err != nil {
out = append(out, p)
continue
}
if !since.IsZero() && t.Before(since) {
continue
}
if !until.IsZero() && !t.Before(until) {
continue
}
out = append(out, p)
}
return out
}

var showJSONFlag bool
Expand All @@ -120,10 +166,13 @@ var showCmd = &cobra.Command{
Short: "Show workout(s) for a given date (e.g. 2025-03-08, today, yesterday)",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
target, err := parseDate(args[0])
target, err := parseDateValue(args[0])
if err != nil {
return err
}
if target.IsZero() {
return fmt.Errorf("date argument is required")
}

c := client.New()
var posts []Post
Expand Down Expand Up @@ -154,26 +203,13 @@ var showCmd = &cobra.Command{
},
}

func parseDate(s string) (time.Time, error) {
now := time.Now()
switch strings.ToLower(s) {
case "today":
return now, nil
case "yesterday":
return now.AddDate(0, 0, -1), nil
}
if t, err := time.ParseInLocation("2006-01-02", s, time.Local); err == nil {
return t, nil
}
return time.Time{}, fmt.Errorf("invalid date: %q (use YYYY-MM-DD, today, or yesterday)", s)
}

func init() {
workoutsCmd.AddCommand(listCmd)
workoutsCmd.AddCommand(showCmd)
showCmd.Flags().BoolVar(&showJSONFlag, "json", false, "Output as JSON")
listCmd.Flags().BoolVar(&listJSONFlag, "json", false, "Output as JSON instead of fitdown")
listCmd.Flags().StringVar(&listSinceFlag, "since", "", "Filter workouts on or after date (e.g. 2025-01-01, 30d, 4w, 6m, 1y)")
listCmd.Flags().StringVar(&listSinceFlag, "since", "", "Filter workouts on or after date (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
listCmd.Flags().StringVar(&listUntilFlag, "until", "", "Filter workouts through date, inclusive (today, yesterday, YYYY-MM-DD, or Nd/Nw/Nm/Ny)")
listCmd.Flags().StringVar(&listExerciseFlag, "exercise", "", "Filter to exercises matching this name (word-prefix match)")
}

Expand Down
Loading