purge history since March 2016
This commit is contained in:
commit
30e1bc8250
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
diet.txt
|
||||
weight.txt
|
||||
go.sum
|
||||
.hdiet
|
||||
weightplot.png
|
||||
hdiet
|
||||
.gitdist
|
||||
|
18
License
Normal file
18
License
Normal file
|
@ -0,0 +1,18 @@
|
|||
hdiet: Adaptation of the Hacker's Diet computer tools by John Walker.
|
||||
Copyright (C) 2016 Alexander Weinhold
|
||||
|
||||
Unless explicitly stated otherwise, the following applies:
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
56
Readme.md
Normal file
56
Readme.md
Normal file
|
@ -0,0 +1,56 @@
|
|||
hdiet
|
||||
========
|
||||
|
||||
hdiet is an adaptation of the Hacker's Diet computer tools for weight tracking. You can find releases at [releases.gutmet.org](https://releases.gutmet.org) or build it yourself.
|
||||
|
||||
The Hacker's Diet
|
||||
=================
|
||||
|
||||
"The Hacker's Diet" is an ingenious [book](https://www.fourmilab.ch/hackdiet/) by John Walker which applies a systems and signals view to bodyweight. If you haven't already, go read it.
|
||||
|
||||
Walker provides excellent online tools for tracking and Excel-/Palm-based tools for offline tracking. I'm not comfortable with either, though, and as it [turns out](https://www.fourmilab.ch/hackdiet/e4/pencilpaper.html), the calculations needed for trend calculation are simple.
|
||||
|
||||
build
|
||||
=====
|
||||
|
||||
You need go v1.9 or higher. If you have go module support enabled, check the repository out wherever and run <i>go build hdiet.go</i> - otherwise use go get.
|
||||
|
||||
usage
|
||||
=====
|
||||
|
||||
Get a release or build hdiet yourself, then put it somewhere in your [path](https://en.wikipedia.org/wiki/PATH_(variable))
|
||||
|
||||
hdiet has the following commands:
|
||||
|
||||
* init: initialize the directory (create an empty file '.hdiet' as marker for the program)
|
||||
* log: write today's weight to the log file
|
||||
* show: plot weight to image and display
|
||||
* calcdiet: calculate a planned diet
|
||||
|
||||
Weights are logged as plain text, each line consisting of YYYY-MM-DD tab WEIGHT. The same holds for a calculated diet.
|
||||
|
||||
The trend is displayed in red, the actual weight in blue. Red is what you should be looking at.
|
||||
|
||||
|
||||
quick start
|
||||
===========
|
||||
|
||||
```
|
||||
cd /path/to/your/log/folder
|
||||
hdiet init
|
||||
hdiet log 82.7
|
||||
```
|
||||
|
||||
You will get a warning that you need at least 2 values the first time you log.
|
||||
|
||||
Example
|
||||
=======
|
||||
|
||||
Example of a filled weight log:
|
||||
|
||||
![](../plain/weight_example.png)
|
||||
|
||||
|
||||
Example of a trend graph without calculated diet:
|
||||
|
||||
![](../plain/plot_example.png)
|
8
go.mod
Normal file
8
go.mod
Normal file
|
@ -0,0 +1,8 @@
|
|||
module git.gutmet.org/hdiet.git
|
||||
|
||||
require (
|
||||
git.gutmet.org/go-chart.git v3.0.1+incompatible // indirect
|
||||
git.gutmet.org/golang-freetype.git v2.0.0+incompatible // indirect
|
||||
git.gutmet.org/golang-image.git v2.0.0+incompatible // indirect
|
||||
git.gutmet.org/goutil.git v0.0.0-20181209084938-d1f435a43662 // indirect
|
||||
)
|
327
hdiet.go
Normal file
327
hdiet.go
Normal file
|
@ -0,0 +1,327 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"git.gutmet.org/go-chart.git"
|
||||
"git.gutmet.org/go-chart.git/drawing"
|
||||
"git.gutmet.org/goutil.git"
|
||||
"math"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
initflag = ".hdiet"
|
||||
logfile = "weight.txt"
|
||||
dietfile = "diet.txt"
|
||||
dateLayout = "2006-01-02"
|
||||
imgfile = "weightplot.png"
|
||||
width = 1600
|
||||
height = 900
|
||||
)
|
||||
|
||||
type DateFlag struct {
|
||||
date *time.Time
|
||||
}
|
||||
|
||||
func (d DateFlag) String() string {
|
||||
if d.date == nil {
|
||||
return ""
|
||||
} else {
|
||||
return time.Time(*d.date).Format(dateLayout)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DateFlag) Set(s string) error {
|
||||
if d == nil {
|
||||
return errors.New("func (d *DateFlag) Set(s string): d is nil")
|
||||
}
|
||||
val, err := time.Parse(dateLayout, s)
|
||||
if err == nil {
|
||||
d.date = &val
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func isInitialized() bool {
|
||||
return goutil.PathExists(initflag)
|
||||
}
|
||||
|
||||
func errNotInitialized() error {
|
||||
dir, err := os.Getwd()
|
||||
if err == nil {
|
||||
err = errors.New(dir + ": " + "is not an initialized hdiet directory")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func initHdiet(args []string) error {
|
||||
if !isInitialized() {
|
||||
return goutil.WriteFile(initflag, "")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func log(args []string) error {
|
||||
if !isInitialized() {
|
||||
return errNotInitialized()
|
||||
}
|
||||
if len(args) != 1 {
|
||||
return errors.New("log: Please provide weight as argument (floating point number)")
|
||||
}
|
||||
weight, err := strconv.ParseFloat(args[0], 64)
|
||||
if err != nil {
|
||||
return errors.New("log: " + err.Error())
|
||||
}
|
||||
date := time.Now().Format(dateLayout)
|
||||
err = goutil.AppendToFile(logfile, fmt.Sprintf("%s\t%.1f\n", date, weight))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return show(showFlags{})
|
||||
}
|
||||
|
||||
type point struct {
|
||||
date time.Time
|
||||
weight float64
|
||||
trend float64
|
||||
}
|
||||
|
||||
type dateInterval struct {
|
||||
min time.Time
|
||||
max time.Time
|
||||
}
|
||||
|
||||
type yRange struct {
|
||||
min float64
|
||||
max float64
|
||||
}
|
||||
|
||||
func process(file string) ([]point, error) {
|
||||
weights, err := goutil.ReadFile(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lines := strings.Split(weights, "\n")
|
||||
trend := 0.0
|
||||
s := []point{}
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
splitline := strings.Split(line, "\t")
|
||||
if len(splitline) < 2 {
|
||||
return nil, errors.New("process - invalid line in weight file: " + line)
|
||||
}
|
||||
date, _ := time.Parse(dateLayout, splitline[0])
|
||||
weight, _ := strconv.ParseFloat(splitline[1], 64)
|
||||
if trend == 0.0 {
|
||||
trend = weight
|
||||
} else {
|
||||
trend = math.Floor((trend+0.1*(weight-trend))*100) / 100
|
||||
}
|
||||
s = append(s, point{date, weight, trend})
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
type showFlags struct {
|
||||
from DateFlag
|
||||
to DateFlag
|
||||
}
|
||||
|
||||
func show(args showFlags) error {
|
||||
points, err := process(logfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dietpoints, _ := process(dietfile)
|
||||
var to time.Time
|
||||
var from time.Time
|
||||
if args.to.date != nil {
|
||||
to = *args.to.date
|
||||
} else {
|
||||
to = time.Now()
|
||||
}
|
||||
if args.from.date != nil {
|
||||
from = *args.from.date
|
||||
} else {
|
||||
from = to.AddDate(0, 0, -31)
|
||||
}
|
||||
|
||||
fromTo := dateInterval{from, to}
|
||||
s1 := timeSeries("weight", chart.ColorCyan)
|
||||
yRange1 := addPointsGetRange(&s1, points, fromTo, func(p point) float64 { return p.weight })
|
||||
s2 := timeSeries("trend", drawing.Color{R: 255, A: 255})
|
||||
yRange2 := addPointsGetRange(&s2, points, fromTo, func(p point) float64 { return p.trend })
|
||||
s3 := timeSeries("diet", drawing.Color{R: 255, G: 211, A: 255})
|
||||
yRange3 := yRange2
|
||||
if dietpoints != nil {
|
||||
yRange3 = addPointsGetRange(&s3, dietpoints, fromTo, func(p point) float64 { return p.weight })
|
||||
}
|
||||
YRange := yRange{math.Min(math.Min(yRange1.min, yRange2.min), yRange3.min), math.Max(math.Max(yRange1.max, yRange2.max), yRange3.max)}
|
||||
YRange.min = math.Floor(YRange.min) - 1.0
|
||||
YRange.max = math.Ceil(YRange.max) + 1.0
|
||||
graph := graph(s1, s2, s3, YRange)
|
||||
err = writeGraphToFile(graph)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return openGraphFile()
|
||||
}
|
||||
|
||||
func showCommand() (goutil.CommandFlagsInit, goutil.CommandFunc) {
|
||||
sf := showFlags{}
|
||||
flagsInit := func(s *flag.FlagSet) {
|
||||
s.Var(&sf.from, "from", "YYYY-MM-DD (optional)")
|
||||
s.Var(&sf.to, "to", "YYYY-MM-DD (optional)")
|
||||
}
|
||||
return flagsInit, func([]string) error { return show(sf) }
|
||||
}
|
||||
|
||||
func writeDietFile(from time.Time, initial float64, goal float64, change float64) error {
|
||||
date := from
|
||||
weight := initial
|
||||
s := "#Date\tTarget\n"
|
||||
s += fmt.Sprintf("%s\t%.1f\n", date.Format(dateLayout), weight)
|
||||
for weight > goal {
|
||||
date = date.AddDate(0, 0, 1)
|
||||
weight += change / 7.0
|
||||
s += fmt.Sprintf("%s\t%.3f\n", date.Format(dateLayout), weight)
|
||||
}
|
||||
err := goutil.WriteFile(dietfile, s)
|
||||
if err == nil {
|
||||
fmt.Println("Wrote to " + dietfile)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func diet([]string) error {
|
||||
if !isInitialized() {
|
||||
return errNotInitialized()
|
||||
}
|
||||
fromStr, err := goutil.AskFor("From? (YYYY-MM-DD) [today]")
|
||||
var from time.Time
|
||||
if fromStr == "" {
|
||||
from = time.Now()
|
||||
} else {
|
||||
from, err = time.Parse(dateLayout, fromStr)
|
||||
}
|
||||
initial, err := parseFloat(askFor(err, "Initial weight?"))
|
||||
goal, err := parseFloat(askFor(err, "Goal weight?"))
|
||||
change, err := parseFloat(askFor(err, "Change per week?"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if change == 0 {
|
||||
return errors.New("change is zero")
|
||||
}
|
||||
if (initial >= goal && change > 0) || (initial <= goal && change < 0) {
|
||||
change *= -1
|
||||
}
|
||||
return writeDietFile(from, initial, goal, change)
|
||||
}
|
||||
|
||||
func main() {
|
||||
commands := []goutil.Command{
|
||||
goutil.NewCommand("init", initHdiet, "init an hdiet folder"),
|
||||
goutil.NewCommand("log", log, "log today's weight (given as argument)"),
|
||||
goutil.NewCommandWithFlags("show", showCommand, "show the trend curve (today's or [from] [to])"),
|
||||
goutil.NewCommand("calcdiet", diet, "calculate a diet curve"),
|
||||
}
|
||||
err := goutil.Execute(commands)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(-1)
|
||||
}
|
||||
}
|
||||
|
||||
/*****************************/
|
||||
|
||||
func addPointsGetRange(s *chart.TimeSeries, points []point, di dateInterval, project func(point) float64) yRange {
|
||||
YRange := yRange{math.MaxFloat64, -math.MaxFloat64}
|
||||
for _, p := range points {
|
||||
if (p.date.After(di.min) || p.date.Equal(di.min)) && (p.date.Before(di.max) || p.date.Equal(di.max)) && !math.IsNaN(project(p)) {
|
||||
s.XValues = append(s.XValues, p.date)
|
||||
yVal := project(p)
|
||||
s.YValues = append(s.YValues, yVal)
|
||||
if yVal < YRange.min {
|
||||
YRange.min = yVal
|
||||
}
|
||||
if yVal > YRange.max {
|
||||
YRange.max = yVal
|
||||
}
|
||||
}
|
||||
}
|
||||
return YRange
|
||||
}
|
||||
|
||||
func timeSeries(name string, color drawing.Color) chart.TimeSeries {
|
||||
return chart.TimeSeries{Name: name, Style: chart.Style{Show: true, StrokeColor: color}}
|
||||
}
|
||||
|
||||
func graph(s1 chart.TimeSeries, s2 chart.TimeSeries, s3 chart.TimeSeries, YRange yRange) chart.Chart {
|
||||
graph := chart.Chart{
|
||||
Width: width,
|
||||
Height: height,
|
||||
XAxis: chart.XAxis{
|
||||
Name: "date",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
ValueFormatter: chart.TimeDateValueFormatter,
|
||||
},
|
||||
YAxis: chart.YAxis{
|
||||
Name: "kg",
|
||||
NameStyle: chart.StyleShow(),
|
||||
Style: chart.StyleShow(),
|
||||
Range: &chart.ContinuousRange{
|
||||
Min: YRange.min,
|
||||
Max: YRange.max,
|
||||
},
|
||||
},
|
||||
Background: chart.Style{
|
||||
Padding: chart.Box{
|
||||
Top: 20,
|
||||
Left: 80,
|
||||
},
|
||||
},
|
||||
Series: []chart.Series{s1, s2, s3},
|
||||
}
|
||||
graph.Elements = []chart.Renderable{chart.LegendLeft(&graph)}
|
||||
return graph
|
||||
}
|
||||
|
||||
func writeGraphToFile(graph chart.Chart) error {
|
||||
f, err := os.OpenFile(imgfile, os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = graph.Render(chart.PNG, f)
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func openGraphFile() error {
|
||||
return goutil.OpenInDefaultApp(imgfile, false)
|
||||
}
|
||||
|
||||
func askFor(err error, q string) (string, error) {
|
||||
if err == nil {
|
||||
return goutil.AskFor(q)
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
func parseFloat(s string, err error) (float64, error) {
|
||||
if err == nil {
|
||||
return strconv.ParseFloat(s, 64)
|
||||
} else {
|
||||
return 0, err
|
||||
}
|
||||
}
|
BIN
plot_example.png
Normal file
BIN
plot_example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
weight_example.png
Normal file
BIN
weight_example.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in New Issue
Block a user