logbook/cmd/lb/main.go

337 lines
8.0 KiB
Go

package main
import (
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"git.wow.st/gmp/logbook"
"git.wow.st/gmp/logbook/parser"
"git.wow.st/gmp/logbook/review"
)
func expandPath(path string) string {
if strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()
if err == nil {
return filepath.Join(home, path[2:])
}
}
return path
}
func usage() {
fmt.Fprintf(os.Stderr, `Usage:
lb [flags] <command> [args]
Commands:
import [file] Import workouts from DSL file (or stdin if no file)
list List all workouts in the database
list-measurements List all measurements in the database
performances [exercise] [variation] List past performances on specified exercise
predict date variable Predict the value of variable on date
delete [all][workouts][measurements] delete specified items
Flags:
-db <path> Path to BoltDB database file (default: ~/.config/logbook/logbook.db)
`)
}
func getPerformances(wr *logbook.WorkoutRepository, mr *logbook.MeasurementRepository) ([]review.Performance, error) {
pr := review.NewPerformanceRepository(wr, mr)
filter := func(p review.Performance) bool { return true }
switch flag.NArg() {
case 1:
case 2:
filter = func(p review.Performance) bool {
if p.Exercise == flag.Arg(1) {
return true
} else {
return false
}
}
case 3:
filter = func(p review.Performance) bool {
if p.Exercise == flag.Arg(1) && p.Type == flag.Arg(2) {
return true
} else {
return false
}
}
default:
usage()
os.Exit(1)
}
ps, err := pr.FindPerformances(filter)
return ps, err
}
func main() {
defaultDB := "~/.config/logbook/logbook.db"
var dbPath string
flag.StringVar(&dbPath, "db", defaultDB, "Path to BoltDB database file")
flag.Usage = usage
flag.Parse()
if flag.NArg() == 0 {
usage()
os.Exit(1)
}
dbPath = expandPath(dbPath)
wr, mr, err := logbook.NewRepositories(dbPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening repositories: %v\n", err)
os.Exit(1)
}
defer wr.Close()
defer mr.Close()
cmd := flag.Arg(0)
switch cmd {
case "import":
var input io.Reader
if flag.NArg() > 1 {
f, err := os.Open(flag.Arg(1))
if err != nil {
fmt.Fprintf(os.Stderr, "Error opening file: %v\n", err)
os.Exit(1)
}
defer f.Close()
input = f
} else {
input = os.Stdin
}
dslBytes, err := io.ReadAll(input)
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading input: %v\n", err)
os.Exit(1)
}
dslText := string(dslBytes)
workouts, measurements, err := parser.ParseDSL(dslText)
if err != nil {
fmt.Fprintf(os.Stderr, "Parse error: %v\n", err)
os.Exit(1)
}
if err := wr.AddWorkoutsFromIR(workouts); err != nil {
fmt.Fprintf(os.Stderr, "Database write error: %v\n", err)
os.Exit(1)
}
if err := mr.AddMeasurementsFromIR(measurements); err != nil {
fmt.Fprintf(os.Stderr, "Database write error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Successfully imported %d workout(s) and %d measurement(s).\n", len(workouts), len(measurements))
case "list":
workouts, _, err := wr.FindWorkouts(func(w *parser.Workout) bool { return true })
if err != nil {
fmt.Fprintf(os.Stderr, "Error reading workouts: %v\n", err)
os.Exit(1)
}
if len(workouts) == 0 {
fmt.Println("No workouts found.")
return
}
for _, w := range workouts {
dateStr := w.Date.Format("2 Jan 2006")
fmt.Printf("Workout: %s (%s) %d SetGroups.", dateStr, w.Type, len(w.SetGroups))
if w.Note != "" {
fmt.Printf(" \"%s\"", w.Note)
}
fmt.Println()
for _, sg := range w.SetGroups {
fmt.Printf("Setgroup: %s", sg.Exercise)
if sg.Type != "" {
fmt.Printf(" (%s)", sg.Type)
}
fmt.Printf(" %d planned sets.", len(sg.PlannedSets))
if sg.Note != "" {
fmt.Printf(" \"%s\"", sg.Note)
}
fmt.Println()
for _, s := range sg.PlannedSets {
fmt.Printf("Planned Set: %.0fx%d", s.Weight, s.Reps)
if s.Note != "" {
fmt.Printf(" (%s)", s.Note)
}
fmt.Println()
}
if len(sg.ActualSets) > 0 {
for _, s := range sg.ActualSets {
fmt.Printf("Actual Set: %.0fx%d (%d RIR)", s.Weight, s.Reps, s.RIR)
if s.Note != "" {
fmt.Printf(" (%s)", s.Note)
}
fmt.Println()
}
}
}
}
case "list-measurements":
ms, err := mr.FindAll()
if err != nil {
fmt.Fprintf(os.Stderr, "Error finding measurements: %v\n", err)
os.Exit(1)
}
for _,m := range(ms) {
dateStr := m.Date.Format("2 Jan 2006")
fmt.Printf("Measurement: %s %s = %0.1f\n", dateStr, m.Variable, m.Value)
}
case "performances":
ps, err := getPerformances(wr, mr)
if err != nil {
fmt.Printf("Error finding performance: %v\n", err)
os.Exit(1)
}
for _, p := range (ps) {
fmt.Printf("Performance: %v\n", p)
}
case "estimate": // show rep chart for an exercise
ps, err := getPerformances(wr, mr)
if err != nil {
fmt.Printf("Error finding performance: %v\n", err)
os.Exit(1)
}
var weights, reps []float64
var dates []time.Time
isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type)
for _, p := range (ps) {
for i, w := range (p.Weights) {
if isbw {
w = w + p.Bodyweight
}
if p.RIR[i] == 0 {
weights = append(weights, w)
reps = append(reps, float64(p.Reps[i]))
dates = append(dates, p.Times[i].UTC())
fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i]))
}
}
}
est := review.NewEstimator(review.WithHalfLife(14.0))
err = est.Fit(weights, reps, dates)
if err != nil {
fmt.Printf("Error estimating performance: %v\n", err)
os.Exit(1)
}
for i := 1; i<=10; i++ {
adj := 0.0
if isbw {
adj = ps[len(ps)-1].Bodyweight
}
fmt.Printf("%d: %0.0f\n", i, est.EstimateMaxWeight(float64(i)) - adj)
}
case "weekly-estimate":
ps, err := getPerformances(wr, mr)
if err != nil {
fmt.Printf("Error finding performance: %v\n", err)
os.Exit(1)
}
var weights, reps []float64
var dates []time.Time
isbw := review.IsBodyweight(ps[0].Exercise, ps[0].Type)
for _, p := range (ps) {
for i, w := range (p.Weights) {
if isbw {
w = w + p.Bodyweight
}
if p.RIR[i] == 0 {
weights = append(weights, w)
reps = append(reps, float64(p.Reps[i]))
dates = append(dates, p.Times[i].UTC())
fmt.Printf("Set: weight %0.0f, reps %0.0f\n", w, float64(p.Reps[i]))
}
}
}
var adj float64
if isbw {
adj = ps[len(ps)-1].Bodyweight
}
est := review.NewEstimator(review.WithHalfLife(14.0))
err = est.Fit(weights, reps, dates)
if err != nil {
fmt.Printf("Error estimating performance: %v\n", err)
os.Exit(1)
}
times, vals := est.TimelineEstimate1RM(7*24*time.Hour)
for i, t := range times {
fmt.Printf("%s: %0.0f\n", t.Format("2006-01-02"), vals[i] - adj)
}
case "predict":
if flag.NArg() != 3 {
usage()
os.Exit(1)
}
dateStr := flag.Arg(1)
date, err := time.Parse("2006-01-02", dateStr)
if err != nil {
fmt.Printf("Invalid date: %v", err)
os.Exit(1)
}
variable := flag.Arg(2)
fmt.Printf("Predicting %s for date %s\n", variable, date.Format("2006-01-02"))
p, err := mr.Predict(variable, date, 0.75)
if err != nil {
fmt.Printf("Prediction error: %v\n", err)
os.Exit(1)
}
fmt.Printf("%f\n",p)
case "delete":
if flag.NArg() != 2 {
usage()
os.Exit(1)
}
switch flag.Arg(1) {
case "workouts":
_, ids, err := wr.FindWorkouts(func(*parser.Workout) bool { return true })
if err != nil {
fmt.Printf("Error finding workouts: %v\n", err)
os.Exit(1)
}
for _, id := range(ids) {
err = wr.DeleteWorkout(id)
if err != nil {
fmt.Printf("Error deleting workout: %v\n", err)
os.Exit(1)
}
}
fmt.Printf("%d workouts deleted.\n", len(ids))
case "measurements":
ms, err := mr.FindAll()
if err != nil {
fmt.Printf("Error finding measurements: %v\n", err)
os.Exit(1)
}
for _, m := range(ms) {
err = mr.Delete(m.ID)
if err != nil {
fmt.Printf("Error deleting workout: %v\n", err)
os.Exit(1)
}
}
fmt.Printf("%d measurements deleted.\n", len(ms))
}
default:
usage()
os.Exit(1)
}
}