aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgutmet <me.git@gutmet.org>2019-01-01 19:18:13 +0100
committergutmet <me.git@gutmet.org>2019-01-01 19:18:13 +0100
commit30e1bc8250382e73fec63f26da383bd5fff4c6c2 (patch)
treeea95443c45ffa61dffef838db779c6ba22566919
downloadhdiet-30e1bc8250382e73fec63f26da383bd5fff4c6c2.tar.gz
purge history since March 2016
-rw-r--r--.gitignore8
-rw-r--r--License18
-rw-r--r--Readme.md56
-rw-r--r--go.mod8
-rw-r--r--hdiet.go327
-rw-r--r--plot_example.pngbin0 -> 60402 bytes
-rw-r--r--weight_example.pngbin0 -> 12389 bytes
7 files changed, 417 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ee749da
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+diet.txt
+weight.txt
+go.sum
+.hdiet
+weightplot.png
+hdiet
+.gitdist
+
diff --git a/License b/License
new file mode 100644
index 0000000..ddc9ea7
--- /dev/null
+++ b/License
@@ -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/>.
+
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 0000000..bf0e291
--- /dev/null
+++ b/Readme.md
@@ -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)
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..97504cc
--- /dev/null
+++ b/go.mod
@@ -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
+)
diff --git a/hdiet.go b/hdiet.go
new file mode 100644
index 0000000..33f4d7a
--- /dev/null
+++ b/hdiet.go
@@ -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
+ }
+}
diff --git a/plot_example.png b/plot_example.png
new file mode 100644
index 0000000..8213af8
--- /dev/null
+++ b/plot_example.png
Binary files differ
diff --git a/weight_example.png b/weight_example.png
new file mode 100644
index 0000000..1d6ee52
--- /dev/null
+++ b/weight_example.png
Binary files differ