310 lines
7.4 KiB
Go
310 lines
7.4 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"
|
|
bolt "go.etcd.io/bbolt"
|
|
)
|
|
|
|
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)
|
|
|
|
db, err := bolt.Open(dbPath, 0600, nil)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error opening database: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
defer db.Close()
|
|
wr, err := logbook.NewWorkoutRepository(db)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error opening workout repository: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
mr, err := logbook.NewMeasurementRepository(db)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error opening measurement repository: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
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]))
|
|
}
|
|
}
|
|
}
|
|
//a, b := review.FitPowerLaw(weights, reps, date, 14.0)
|
|
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(time.Now(), float64(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)
|
|
}
|
|
}
|
|
|