nom 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +41 -38
- data/bin/nom +2 -1
- data/lib/nom/config.rb +50 -58
- data/lib/nom/food_entry.rb +18 -16
- data/lib/nom/helpers.rb +42 -0
- data/lib/nom/nom.rb +326 -321
- data/lib/nom/weight_database.rb +91 -89
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4b3afa581ea8fc93eaca6c7fc141c3dd9f9d7ae6
|
4
|
+
data.tar.gz: ed8508f8e2386a7985ba38fafe1c7090c417dc2b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a4b99e91c622dd061d28f0c8f2ed3753d25f6663723119aa21ed4e36e02b421499a5c5d3f0c1739dd5e1ac7fabb0de47f99a297c30def77862c122534772b07a
|
7
|
+
data.tar.gz: 4882c4781deeb1c98f1926d2d6e338574368db5e7d42808b091b88f030f7564c69db1779052711ad65a9c04b1a71eca3f2d119e2dedba981702019e4ad9bf710
|
data/README.md
CHANGED
@@ -4,50 +4,17 @@
|
|
4
4
|
|
5
5
|
# Installation
|
6
6
|
|
7
|
-
You'll need Ruby, Rubygems and gnuplot.
|
7
|
+
You'll need Ruby, Rubygems and gnuplot. On Windows, make sure that gnuplot's binary directory is added to your `PATH` during installation.
|
8
|
+
|
9
|
+
Then run this command:
|
8
10
|
|
9
11
|
$ gem install nom
|
10
12
|
|
11
13
|
When you run `nom` for the first time, it will ask for your current and your desired weight.
|
12
14
|
|
13
|
-
# Basics
|
14
|
-
|
15
|
-
*nom* operates on three files in the directory `~/.nom/`: `config` contains configuration settings, `input` contains stuff you ate, `weight` contains weight measurements. You can edit them by hand.
|
16
|
-
|
17
|
-
By default, energy quantities will have the unit "kcal". You can change this by adding a line like `unit: 0.239` to your `~/.nom/config`, which means you want to use the unit "0.239 kcal" (= "1 kJ"). Energy quantities are displayed in parentheses: `(42)`
|
18
|
-
|
19
|
-
Weight quantities are displayed as "kg", but you can use arbitrary units, like pounds.
|
20
|
-
|
21
15
|
# Usage
|
22
16
|
|
23
|
-
|
24
|
-
|
25
|
-
Available subcommands:
|
26
|
-
status Display a short food log
|
27
|
-
w, weight <weight> Report a weight measurement
|
28
|
-
s, search <term> Search for a food item in the web
|
29
|
-
n, nom <description> <energy> Report that you ate something
|
30
|
-
y, yesterday <desc.> <energy> Like nom, but for yesterday
|
31
|
-
p, plot Plot a weight/intake graph
|
32
|
-
l, log Display the full food log
|
33
|
-
g, grep <term> Search in the food log
|
34
|
-
e, edit Edit the input file
|
35
|
-
ew, editw Edit the weight file
|
36
|
-
help Print this help
|
37
|
-
There are some useful defaults:
|
38
|
-
(no arguments) status
|
39
|
-
<number> weight <number>
|
40
|
-
<term> search <term>
|
41
|
-
<term> <number> nom <term> <number>
|
42
|
-
Configuration options (put these in /home/seb/.nom/config):
|
43
|
-
rate How much weight you want to lose per week (default: '0.5')
|
44
|
-
goal Your target weight
|
45
|
-
image_viewer Your preferred svg viewer, for example 'eog -f', 'firefox', 'chromium' (default: 'xdg-open')
|
46
|
-
unit Your desired base unit in kcal (default: '1')
|
47
|
-
start_date The first day that should be considered by nom [yyyy-mm-dd]
|
48
|
-
balance_start The day from which on nom should keep track of a energy balance [yyyy-mm-dd]
|
49
|
-
|
50
|
-
So, call `nom` without arguments to get a summary of your current status:
|
17
|
+
Call `nom` without arguments to get a summary of your current status:
|
51
18
|
|
52
19
|
$ nom
|
53
20
|
5.3 kg down (34%), 10.3 kg to go!
|
@@ -91,9 +58,45 @@ Enter your weight regularly:
|
|
91
58
|
And get nice graphs. The upper graph shows weight over time, with a weighted (no pun intended) moving average, a weight prediction, and a green finish line. The lower graph shows daily energy intake targets and actual intake:
|
92
59
|
|
93
60
|
$ nom plot
|
94
|
-
|
61
|
+
|
95
62
|
![Graphs of weight and input over time](http://files.morr.cc/nom-0.1.0.svg)
|
96
63
|
|
64
|
+
Enter `nom help` if you're lost:
|
65
|
+
|
66
|
+
Available subcommands:
|
67
|
+
status Display a short food log
|
68
|
+
w, weight <weight> Report a weight measurement
|
69
|
+
s, search <term> Search for a food item in the web
|
70
|
+
n, nom <description> <energy> Report that you ate something
|
71
|
+
y, yesterday <desc.> <energy> Like nom, but for yesterday
|
72
|
+
p, plot Plot a weight/intake graph
|
73
|
+
l, log Display the full food log
|
74
|
+
g, grep <term> Search in the food log
|
75
|
+
e, edit Edit the input file
|
76
|
+
ew, editw Edit the weight file
|
77
|
+
c, config Edit the config file (see below for options)
|
78
|
+
help Print this help
|
79
|
+
There are some useful defaults:
|
80
|
+
(no arguments) status
|
81
|
+
<number> weight <number>
|
82
|
+
<term> search <term>
|
83
|
+
<term> <number> nom <term> <number>
|
84
|
+
Configuration options (put these in /home/seb/.nom/config):
|
85
|
+
rate How much weight you want to lose per week (default: '0.5')
|
86
|
+
goal Your target weight
|
87
|
+
image_viewer Your preferred svg viewer, for example 'eog -f', 'firefox', 'chromium' (default: 'xdg-open')
|
88
|
+
unit Your desired base unit in kcal (default: '1')
|
89
|
+
start_date The first day that should be considered by nom [yyyy-mm-dd]
|
90
|
+
balance_start The day from which on nom should keep track of a energy balance [yyyy-mm-dd]
|
91
|
+
|
92
|
+
# Conventions
|
93
|
+
|
94
|
+
*nom* operates on three files in the directory `~/.nom/`: `config` contains configuration settings, `input` contains stuff you ate, `weight` contains weight measurements. You can edit them by hand.
|
95
|
+
|
96
|
+
By default, energy quantities will have the unit "kcal". You can change this by adding a line like `unit: 0.239` to your `~/.nom/config`, which means you want to use the unit "0.239 kcal" (= "1 kJ"). Energy quantities are displayed in parentheses: `(42)`
|
97
|
+
|
98
|
+
Weight quantities are displayed as "kg", but you can use arbitrary units, like pounds.
|
99
|
+
|
97
100
|
## License: GPLv2+
|
98
101
|
|
99
102
|
*nom* 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 2 of the License, or (at your option) any later version.
|
data/bin/nom
CHANGED
@@ -15,10 +15,11 @@ commands = [
|
|
15
15
|
[ "grep", "g", "<term>", "Search in the food log" ],
|
16
16
|
[ "edit", "e", nil, "Edit the input file" ],
|
17
17
|
[ "editw", "ew", nil, "Edit the weight file" ],
|
18
|
+
[ "config", "c", nil, "Edit the config file (see below for options)" ],
|
18
19
|
[ "help", nil, nil, "Print this help" ],
|
19
20
|
]
|
20
21
|
|
21
|
-
nom = Nom.new
|
22
|
+
nom = Nom::Nom.new
|
22
23
|
|
23
24
|
cmd_name = ARGV.shift or "status"
|
24
25
|
command = commands.find{|c| c[0] == cmd_name or c[1] == cmd_name}
|
data/lib/nom/config.rb
CHANGED
@@ -1,75 +1,67 @@
|
|
1
|
-
|
2
|
-
def initialize file
|
3
|
-
@file = file
|
1
|
+
require "nom/helpers"
|
4
2
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
module Nom
|
4
|
+
class Config
|
5
|
+
def initialize file
|
6
|
+
@file = file
|
9
7
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
"image_viewer" => [ "your preferred SVG viewer, for example 'eog -f', 'firefox', 'chromium'", guess_image_viewer, String ],
|
15
|
-
"unit" => [ "your desired base unit in kcal", 1, Float ],
|
16
|
-
"start_date" => [ "the first day that should be considered by nom [yyyy-mm-dd]", nil, Date ],
|
17
|
-
"balance_start" => [ "the day from which on nom should keep track of a energy balance [yyyy-mm-dd]", nil, Date ],
|
18
|
-
}
|
19
|
-
end
|
8
|
+
@config = {}
|
9
|
+
if File.exists? file
|
10
|
+
@config = YAML.load_file(file)
|
11
|
+
end
|
20
12
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
13
|
+
@defaults = {
|
14
|
+
# format: [ key, description, default_value, type ]
|
15
|
+
"rate" => [ "how much weight you want to lose per week", 0.5, Float ],
|
16
|
+
"goal" => [ "your target weight", nil, Float],
|
17
|
+
"image_viewer" => [ "your preferred SVG viewer, for example 'eog -f', 'firefox', 'chromium'", Helpers::default_program, String ],
|
18
|
+
"unit" => [ "your desired base unit in kcal", 1, Float ],
|
19
|
+
"start_date" => [ "the first day that should be considered by nom [yyyy-mm-dd]", nil, Date ],
|
20
|
+
"balance_start" => [ "the day from which on nom should keep track of a energy balance [yyyy-mm-dd]", nil, Date ],
|
21
|
+
}
|
30
22
|
end
|
31
|
-
end
|
32
23
|
|
33
|
-
|
34
|
-
|
35
|
-
|
24
|
+
def has key
|
25
|
+
@config.has_key?(key) or (@defaults.has_key?(key) and not @defaults[key][1].nil?)
|
26
|
+
end
|
36
27
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
v = @config[key]
|
41
|
-
elsif @defaults.has_key?(key)
|
42
|
-
if @defaults[key][1].nil?
|
43
|
-
print "Please enter #{@defaults[key][0]}: "
|
44
|
-
@config[key] = STDIN.gets.chomp
|
45
|
-
open(@file, "w") do |f|
|
46
|
-
f << @config.to_yaml
|
47
|
-
end
|
28
|
+
def get key
|
29
|
+
v = nil
|
30
|
+
if @config.has_key?(key)
|
48
31
|
v = @config[key]
|
32
|
+
elsif @defaults.has_key?(key)
|
33
|
+
if @defaults[key][1].nil?
|
34
|
+
print "Please enter #{@defaults[key][0]}: "
|
35
|
+
@config[key] = STDIN.gets.chomp
|
36
|
+
open(@file, "w") do |f|
|
37
|
+
f << @config.to_yaml
|
38
|
+
end
|
39
|
+
v = @config[key]
|
40
|
+
else
|
41
|
+
v = @defaults[key][1]
|
42
|
+
end
|
49
43
|
else
|
50
|
-
|
44
|
+
raise "Unknown configuration option '#{key}'"
|
51
45
|
end
|
52
|
-
else
|
53
|
-
raise "Unknown configuration option '#{key}'"
|
54
|
-
end
|
55
46
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
47
|
+
if @defaults[key][2] == Float
|
48
|
+
v.to_f
|
49
|
+
elsif @defaults[key][2] == Date
|
50
|
+
if v.class == Date
|
51
|
+
v
|
52
|
+
else
|
53
|
+
Date.parse(v)
|
54
|
+
end
|
61
55
|
else
|
62
|
-
|
56
|
+
v
|
63
57
|
end
|
64
|
-
else
|
65
|
-
v
|
66
58
|
end
|
67
|
-
end
|
68
59
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
60
|
+
def print_usage
|
61
|
+
puts "Configuration options (put these in #{@file}):"
|
62
|
+
@defaults.each do |key, value|
|
63
|
+
puts " #{key}".ljust(34)+value[0].capitalize+(value[1].nil? ? "" : " (default: '#{value[1]}')")
|
64
|
+
end
|
73
65
|
end
|
74
66
|
end
|
75
67
|
end
|
data/lib/nom/food_entry.rb
CHANGED
@@ -1,21 +1,23 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
module Nom
|
2
|
+
class FoodEntry
|
3
|
+
attr_reader :date, :kcal, :description
|
3
4
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
5
|
+
def self.from_line line
|
6
|
+
date, kcal, description = line.split(" ", 3)
|
7
|
+
date = Date.parse(date)
|
8
|
+
kcal = kcal.to_i
|
9
|
+
description.chomp!
|
10
|
+
FoodEntry.new(date, kcal, description)
|
11
|
+
end
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
def initialize date, kcal, description
|
14
|
+
@date = date
|
15
|
+
@kcal = kcal
|
16
|
+
@description = description
|
17
|
+
end
|
17
18
|
|
18
|
-
|
19
|
-
|
19
|
+
def to_s
|
20
|
+
"#{@date} #{@kcal} #{@description}\n"
|
21
|
+
end
|
20
22
|
end
|
21
23
|
end
|
data/lib/nom/helpers.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Nom
|
2
|
+
class Helpers
|
3
|
+
def Helpers::open_file filename
|
4
|
+
program = if filename =~ /\.svg$/
|
5
|
+
default_program
|
6
|
+
else
|
7
|
+
# let's assume it's a text file
|
8
|
+
default_editor
|
9
|
+
end
|
10
|
+
|
11
|
+
if program.nil?
|
12
|
+
raise "Couldn't find a program to open '#{filename}'. Please file a bug."
|
13
|
+
end
|
14
|
+
system("#{program} #{filename}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def Helpers::default_program
|
18
|
+
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
19
|
+
"start"
|
20
|
+
elsif RbConfig::CONFIG['host_os'] =~ /darwin/
|
21
|
+
"open"
|
22
|
+
elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
|
23
|
+
"xdg-open"
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def Helpers::default_editor
|
30
|
+
ENV["EDITOR"] ||
|
31
|
+
if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
|
32
|
+
"notepad"
|
33
|
+
elsif RbConfig::CONFIG['host_os'] =~ /darwin/
|
34
|
+
"open -a TextEdit"
|
35
|
+
elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
|
36
|
+
"vi"
|
37
|
+
else
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/nom/nom.rb
CHANGED
@@ -8,414 +8,419 @@ require "erb"
|
|
8
8
|
require "nom/food_entry"
|
9
9
|
require "nom/config"
|
10
10
|
require "nom/weight_database"
|
11
|
+
require "nom/helpers"
|
12
|
+
|
13
|
+
module Nom
|
14
|
+
class Nom
|
15
|
+
def initialize
|
16
|
+
@nom_dir = File.join(Dir.home,".nom")
|
17
|
+
if not Dir.exists? @nom_dir
|
18
|
+
puts "Creating #{@nom_dir}"
|
19
|
+
Dir.mkdir(@nom_dir)
|
20
|
+
end
|
11
21
|
|
12
|
-
|
13
|
-
def initialize
|
14
|
-
@nom_dir = File.join(Dir.home,".nom")
|
15
|
-
if not Dir.exists? @nom_dir
|
16
|
-
puts "Creating #{@nom_dir}"
|
17
|
-
Dir.mkdir(@nom_dir)
|
18
|
-
end
|
19
|
-
|
20
|
-
@config = Config.new(File.join(@nom_dir, "config"))
|
22
|
+
@config = Config.new(File.join(@nom_dir, "config"))
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
+
@weights = WeightDatabase.new(File.join(@nom_dir, "weight"))
|
25
|
+
@inputs = read_file("input", FoodEntry)
|
24
26
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
end
|
27
|
+
date = truncate_date
|
28
|
+
@inputs.delete_if{|i| i.date < date}
|
29
|
+
@weights.truncate(date)
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@weights.interpolate_gaps!
|
35
|
-
@weights.precompute_moving_average!(0.1, 0.1, goal, rate)
|
36
|
-
@weights.predict_weights!(rate, goal, 30)
|
37
|
-
@weights.precompute_moving_average!(0.1, 0.1, goal, rate)
|
31
|
+
if @weights.empty?
|
32
|
+
print "Welcome to nom! Please enter your current weight: "
|
33
|
+
weight [STDIN.gets.chomp]
|
34
|
+
end
|
38
35
|
|
39
|
-
|
40
|
-
|
41
|
-
|
36
|
+
@weights.interpolate_gaps!
|
37
|
+
@weights.precompute_moving_average!(0.1, 0.1, goal, rate)
|
38
|
+
@weights.predict_weights!(rate, goal, 30)
|
39
|
+
@weights.precompute_moving_average!(0.1, 0.1, goal, rate)
|
42
40
|
|
43
|
-
|
44
|
-
|
45
|
-
print "#{kg_lost.round(1)} kg down"
|
46
|
-
if kg_lost+kg_to_go > 0
|
47
|
-
print " (#{(100*kg_lost/(kg_lost+kg_to_go)).round}%)"
|
41
|
+
precompute_inputs_at
|
42
|
+
precompute_base_rate_at
|
48
43
|
end
|
49
|
-
print ", #{kg_to_go.round(1)} kg to go!"
|
50
|
-
print " You'll reach your goal in approximately #{format_duration(days_to_go)}."
|
51
|
-
puts
|
52
44
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
inputs = @inputs.select{|i| i.description =~ Regexp.new(term, Regexp::IGNORECASE)}
|
45
|
+
def status
|
46
|
+
kg_lost = @weights.moving_average_at(@weights.first) - @weights.moving_average_at(@weights.last_real)
|
47
|
+
print "#{kg_lost.round(1)} kg down"
|
48
|
+
if kg_lost+kg_to_go > 0
|
49
|
+
print " (#{(100*kg_lost/(kg_lost+kg_to_go)).round}%)"
|
50
|
+
end
|
51
|
+
print ", #{kg_to_go.round(1)} kg to go!"
|
52
|
+
print " You'll reach your goal in approximately #{format_duration(days_to_go)}."
|
53
|
+
puts
|
64
54
|
|
65
|
-
|
66
|
-
puts "(no matching entries found)"
|
55
|
+
log_since([@weights.first,Date.today-1].max)
|
67
56
|
end
|
68
57
|
|
69
|
-
|
70
|
-
|
58
|
+
def log
|
59
|
+
log_since(@weights.first)
|
71
60
|
end
|
72
61
|
|
73
|
-
|
74
|
-
|
75
|
-
end
|
62
|
+
def grep args
|
63
|
+
term = args.join(" ")
|
76
64
|
|
77
|
-
|
78
|
-
if @weights.real?(Date.today)
|
79
|
-
raise "You already entered a weight for today. Use `nom editw` to modify it."
|
80
|
-
end
|
65
|
+
inputs = @inputs.select{|i| i.description =~ Regexp.new(term, Regexp::IGNORECASE)}
|
81
66
|
|
82
|
-
|
83
|
-
|
67
|
+
if inputs.empty?
|
68
|
+
puts "(no matching entries found)"
|
69
|
+
end
|
84
70
|
|
85
|
-
|
86
|
-
|
87
|
-
|
71
|
+
inputs.each do |i|
|
72
|
+
entry(quantize(i.kcal), i.date.to_s+" "+i.description)
|
73
|
+
end
|
88
74
|
|
89
|
-
|
90
|
-
|
91
|
-
|
75
|
+
separator
|
76
|
+
entry(quantize(inputs.inject(0){|sum, i| sum+i.kcal}), "total")
|
77
|
+
end
|
92
78
|
|
93
|
-
|
94
|
-
|
95
|
-
|
79
|
+
def weight args
|
80
|
+
if @weights.real?(Date.today)
|
81
|
+
raise "You already entered a weight for today. Use `nom editw` to modify it."
|
82
|
+
end
|
96
83
|
|
97
|
-
|
98
|
-
|
99
|
-
end
|
84
|
+
date = Date.today
|
85
|
+
weight = args.pop.to_f
|
100
86
|
|
101
|
-
|
102
|
-
|
103
|
-
grep(args)
|
104
|
-
term = args.join(" ")
|
105
|
-
puts
|
106
|
-
term = term.encode("ISO-8859-1")
|
107
|
-
url = "http://fddb.info/db/de/suche/?udd=0&cat=site-de&search=#{URI.escape(term)}"
|
108
|
-
|
109
|
-
page = Nokogiri::HTML(open(url))
|
110
|
-
results = page.css(".standardcontent a").map{|a| a["href"]}.select{|href| href.include? "lebensmittel"}
|
111
|
-
|
112
|
-
results[0..4].each do |result|
|
113
|
-
page = Nokogiri::HTML(open(result))
|
114
|
-
title = page.css(".breadcrumb a").last.text
|
115
|
-
brand = page.css(".standardcontent p a").select{|a| a["href"].include? "hersteller"}.first.text
|
116
|
-
puts "#{title} (#{brand})"
|
117
|
-
|
118
|
-
page.css(".serva").each do |serving|
|
119
|
-
size = serving.css("a.servb").text
|
120
|
-
kcal = serving.css("div")[5].css("div")[1].text.to_i
|
121
|
-
#kj = serving.css("div")[2].css("div")[1].text.to_i
|
122
|
-
puts " (#{quantize(kcal)}) #{size}"
|
87
|
+
open(File.join(@nom_dir,"weight"), "a") do |f|
|
88
|
+
f << "#{date} #{weight}\n"
|
123
89
|
end
|
90
|
+
|
91
|
+
initialize
|
92
|
+
plot
|
124
93
|
end
|
125
|
-
end
|
126
94
|
|
127
|
-
|
128
|
-
|
95
|
+
def nom args
|
96
|
+
nom_entry args, (Time.now-5*60*60).to_date
|
97
|
+
end
|
129
98
|
|
130
|
-
|
131
|
-
|
132
|
-
weight_dat << "#{date}\t"
|
133
|
-
if @weights.real?(date)
|
134
|
-
weight_dat << "#{@weights.at(date)}"
|
135
|
-
else
|
136
|
-
weight_dat << "-"
|
137
|
-
end
|
138
|
-
if date <= @weights.last_real
|
139
|
-
weight_dat << "\t#{@weights.moving_average_at(date)}\t"
|
140
|
-
else
|
141
|
-
weight_dat << "\t-"
|
142
|
-
end
|
143
|
-
if date >= @weights.last_real
|
144
|
-
weight_dat << "\t#{@weights.moving_average_at(date)}\n"
|
145
|
-
else
|
146
|
-
weight_dat << "\t-\n"
|
147
|
-
end
|
99
|
+
def yesterday args
|
100
|
+
nom_entry args, Date.today-1
|
148
101
|
end
|
149
|
-
weight_dat.close
|
150
102
|
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
103
|
+
def search args
|
104
|
+
puts "Previous log entries:"
|
105
|
+
grep(args)
|
106
|
+
term = args.join(" ")
|
107
|
+
puts
|
108
|
+
term = term.encode("ISO-8859-1")
|
109
|
+
url = "http://fddb.info/db/de/suche/?udd=0&cat=site-de&search=#{URI.escape(term)}"
|
110
|
+
|
111
|
+
page = Nokogiri::HTML(open(url))
|
112
|
+
results = page.css(".standardcontent a").map{|a| a["href"]}.select{|href| href.include? "lebensmittel"}
|
113
|
+
|
114
|
+
results[0..4].each do |result|
|
115
|
+
page = Nokogiri::HTML(open(result))
|
116
|
+
title = page.css(".breadcrumb a").last.text
|
117
|
+
brand = page.css(".standardcontent p a").select{|a| a["href"].include? "hersteller"}.first.text
|
118
|
+
puts "#{title} (#{brand})"
|
119
|
+
|
120
|
+
page.css(".serva").each do |serving|
|
121
|
+
size = serving.css("a.servb").text
|
122
|
+
kcal = serving.css("div")[5].css("div")[1].text.to_i
|
123
|
+
#kj = serving.css("div")[2].css("div")[1].text.to_i
|
124
|
+
puts " (#{quantize(kcal)}) #{size}"
|
125
|
+
end
|
159
126
|
end
|
160
|
-
input_dat << "\t#{quantize(allowed_kcal(date, 0))}"
|
161
|
-
input_dat << "\t#{quantize(allowed_kcal(date))}"
|
162
|
-
input_dat << "\n"
|
163
127
|
end
|
164
|
-
input_dat.close
|
165
128
|
|
166
|
-
|
167
|
-
|
168
|
-
|
129
|
+
def plot
|
130
|
+
raise "To use this subcommand, please install 'gnuplot'." unless which("gnuplot")
|
131
|
+
|
132
|
+
weight_dat = Tempfile.new("weight")
|
133
|
+
(@weights.first).upto(plot_end) do |date|
|
134
|
+
weight_dat << "#{date}\t"
|
135
|
+
if @weights.real?(date)
|
136
|
+
weight_dat << "#{@weights.at(date)}"
|
137
|
+
else
|
138
|
+
weight_dat << "-"
|
139
|
+
end
|
140
|
+
if date <= @weights.last_real
|
141
|
+
weight_dat << "\t#{@weights.moving_average_at(date)}\t"
|
142
|
+
else
|
143
|
+
weight_dat << "\t-"
|
144
|
+
end
|
145
|
+
if date >= @weights.last_real
|
146
|
+
weight_dat << "\t#{@weights.moving_average_at(date)}\n"
|
147
|
+
else
|
148
|
+
weight_dat << "\t-\n"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
weight_dat.close
|
152
|
+
|
153
|
+
input_dat = Tempfile.new("input")
|
154
|
+
input_dat << "#{@weights.first-1}\t0\t0\n"
|
155
|
+
(@weights.first).upto(Date.today) do |date|
|
156
|
+
input_dat << "#{date}\t"
|
157
|
+
if consumed_at(date) == 0
|
158
|
+
input_dat << "-"
|
159
|
+
else
|
160
|
+
input_dat << quantize(consumed_at(date))
|
161
|
+
end
|
162
|
+
input_dat << "\t#{quantize(allowed_kcal(date, 0))}"
|
163
|
+
input_dat << "\t#{quantize(allowed_kcal(date))}"
|
164
|
+
input_dat << "\n"
|
165
|
+
end
|
166
|
+
input_dat.close
|
169
167
|
|
170
|
-
|
168
|
+
svg = Tempfile.new(["plot", ".svg"])
|
169
|
+
svg.close
|
170
|
+
ObjectSpace.undefine_finalizer(svg) # prevent the svg file from being deleted
|
171
171
|
|
172
|
-
|
173
|
-
plt << ERB.new(plt_erb).result(binding)
|
174
|
-
plt.close
|
172
|
+
plt_erb = IO.read(File.join(File.dirname(File.expand_path(__FILE__)), "nom.plt.erb"))
|
175
173
|
|
176
|
-
|
174
|
+
plt = Tempfile.new("plt")
|
175
|
+
plt << ERB.new(plt_erb).result(binding)
|
176
|
+
plt.close
|
177
177
|
|
178
|
-
|
179
|
-
system(image_viewer+" "+svg.path)
|
180
|
-
end
|
178
|
+
system("gnuplot "+plt.path)
|
181
179
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
def editw
|
187
|
-
edit_file "weight"
|
188
|
-
end
|
180
|
+
image_viewer = @config.get("image_viewer")
|
181
|
+
system(image_viewer+" "+svg.path)
|
182
|
+
end
|
189
183
|
|
190
|
-
|
191
|
-
|
192
|
-
|
184
|
+
def edit
|
185
|
+
Helpers::open_file File.join(@nom_dir, "input")
|
186
|
+
end
|
193
187
|
|
194
|
-
|
188
|
+
def editw
|
189
|
+
Helpers::open_file File.join(@nom_dir, "weight")
|
190
|
+
end
|
195
191
|
|
196
|
-
|
197
|
-
|
198
|
-
kcal = summands.inject(0) do |sum, summand|
|
199
|
-
factors = summand.split("x")
|
200
|
-
sum + factors.map{ |f| f.to_f }.inject(1){ |p,f| p*f }
|
192
|
+
def config
|
193
|
+
Helpers::open_file File.join(@nom_dir, "config")
|
201
194
|
end
|
202
195
|
|
203
|
-
|
204
|
-
|
196
|
+
def config_usage
|
197
|
+
@config.print_usage
|
205
198
|
end
|
206
199
|
|
207
|
-
|
208
|
-
entry = FoodEntry.new(date, kcal, description)
|
200
|
+
private
|
209
201
|
|
210
|
-
|
211
|
-
|
212
|
-
|
202
|
+
def nom_entry args, date
|
203
|
+
summands = args.pop.split("+")
|
204
|
+
kcal = summands.inject(0) do |sum, summand|
|
205
|
+
factors = summand.split("x")
|
206
|
+
sum + factors.map{ |f| f.to_f }.inject(1){ |p,f| p*f }
|
213
207
|
end
|
214
|
-
f << entry.to_s
|
215
|
-
end
|
216
208
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
end
|
221
|
-
@inputs_at[date] << entry
|
209
|
+
if kcal == 0
|
210
|
+
raise "energy term cannot be zero"
|
211
|
+
end
|
222
212
|
|
223
|
-
|
224
|
-
|
213
|
+
description = args.join(" ")
|
214
|
+
entry = FoodEntry.new(date, kcal, description)
|
225
215
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
216
|
+
open(File.join(@nom_dir,"input"), "a") do |f|
|
217
|
+
if not @inputs.empty? and entry.date != @inputs.last.date
|
218
|
+
f << "\n"
|
219
|
+
end
|
220
|
+
f << entry.to_s
|
221
|
+
end
|
231
222
|
|
232
|
-
|
233
|
-
|
234
|
-
|
223
|
+
@inputs << entry
|
224
|
+
if @inputs_at[date].nil?
|
225
|
+
@inputs_at[date] = []
|
226
|
+
end
|
227
|
+
@inputs_at[date] << entry
|
228
|
+
|
229
|
+
status
|
235
230
|
end
|
236
|
-
|
237
|
-
|
231
|
+
|
232
|
+
def allowed_kcal date, r=nil
|
233
|
+
if r.nil?
|
234
|
+
r = @weights.rate_at(date, goal, rate)
|
235
|
+
end
|
236
|
+
if date > @weights.last
|
237
|
+
date = @weights.last
|
238
|
+
end
|
239
|
+
@base_rate_at[date] + r*1000
|
238
240
|
end
|
239
|
-
@base_rate_at[date] + r*1000
|
240
|
-
end
|
241
241
|
|
242
|
-
|
243
|
-
|
244
|
-
|
242
|
+
def consumed_at date
|
243
|
+
inputs_at(date).inject(0){ |sum, i| sum+i.kcal }
|
244
|
+
end
|
245
245
|
|
246
|
-
|
247
|
-
|
248
|
-
|
246
|
+
def kg_to_go
|
247
|
+
@weights.moving_average_at(Date.today) - goal
|
248
|
+
end
|
249
249
|
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
250
|
+
def kcal_to_burn
|
251
|
+
kcal_per_kg_body_fat = 7000
|
252
|
+
kg_to_go * kcal_per_kg_body_fat
|
253
|
+
end
|
254
254
|
|
255
|
-
|
256
|
-
|
257
|
-
|
255
|
+
def days_to_go
|
256
|
+
kcal_to_burn.abs/(rate*1000)
|
257
|
+
end
|
258
258
|
|
259
|
-
|
260
|
-
|
261
|
-
|
259
|
+
def plot_end
|
260
|
+
@weights.last
|
261
|
+
end
|
262
262
|
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
263
|
+
def balance_start
|
264
|
+
if @config.has("balance_start")
|
265
|
+
@config.get("balance_start")
|
266
|
+
else
|
267
|
+
@weights.first
|
268
|
+
end
|
268
269
|
end
|
269
|
-
end
|
270
270
|
|
271
|
-
|
272
|
-
|
273
|
-
|
271
|
+
def balance_end
|
272
|
+
Date.today-1
|
273
|
+
end
|
274
274
|
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
275
|
+
def kcal_balance
|
276
|
+
sum = 0
|
277
|
+
balance_start.upto(balance_end) do |d|
|
278
|
+
if consumed_at(d) != 0
|
279
|
+
sum += consumed_at(d) - allowed_kcal(d)
|
280
|
+
end
|
280
281
|
end
|
282
|
+
sum
|
281
283
|
end
|
282
|
-
sum
|
283
|
-
end
|
284
284
|
|
285
|
-
|
286
|
-
|
285
|
+
def truncate_date
|
286
|
+
if Date.today - @weights.last_real > 30
|
287
|
+
return Date.today
|
288
|
+
end
|
287
289
|
|
288
|
-
|
289
|
-
user_start = @config.get("start_date")
|
290
|
-
[user_start, first_start].max
|
291
|
-
else
|
292
|
-
# find the last gap longer than 30 days
|
293
|
-
gap = @weights.find_gap(30)
|
290
|
+
first_start = @weights.first
|
294
291
|
|
295
|
-
if
|
296
|
-
|
292
|
+
if @config.has("start_date")
|
293
|
+
user_start = @config.get("start_date")
|
294
|
+
[user_start, first_start].max
|
297
295
|
else
|
298
|
-
gap
|
296
|
+
# find the last gap longer than 30 days
|
297
|
+
gap = @weights.find_gap(30)
|
298
|
+
|
299
|
+
if gap.nil?
|
300
|
+
first_start
|
301
|
+
else
|
302
|
+
gap[1]
|
303
|
+
end
|
299
304
|
end
|
300
305
|
end
|
301
|
-
end
|
302
306
|
|
303
|
-
|
304
|
-
|
305
|
-
|
307
|
+
def quantize kcal
|
308
|
+
return (1.0*kcal/@config.get("unit")).round
|
309
|
+
end
|
306
310
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
311
|
+
def format_date date
|
312
|
+
if date == Date.today
|
313
|
+
return "Today"
|
314
|
+
elsif date == Date.today-1
|
315
|
+
return "Yesterday"
|
316
|
+
else
|
317
|
+
return date.to_s
|
318
|
+
end
|
314
319
|
end
|
315
|
-
end
|
316
320
|
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
321
|
+
def format_duration days
|
322
|
+
if days <= 7
|
323
|
+
n = days.round(1)
|
324
|
+
unit = "day"
|
325
|
+
elsif days <= 7*4
|
326
|
+
n = (days/7.0).round(1)
|
327
|
+
unit = "week"
|
328
|
+
else
|
329
|
+
n = (days/7.0/4.0).round(1)
|
330
|
+
unit = "month"
|
331
|
+
end
|
332
|
+
"#{n} #{unit}#{n == 1 ? "" : "s"}"
|
333
|
+
end
|
330
334
|
|
331
|
-
|
332
|
-
|
333
|
-
|
335
|
+
def entry value, text=""
|
336
|
+
puts "#{" "*(6-value.to_s.length)}(#{value}) #{text}"
|
337
|
+
end
|
334
338
|
|
335
|
-
|
336
|
-
|
337
|
-
|
339
|
+
def separator
|
340
|
+
puts "---------------------"
|
341
|
+
end
|
338
342
|
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
343
|
+
def log_since start
|
344
|
+
remaining = 0
|
345
|
+
start.upto(Date.today) do |date|
|
346
|
+
remaining += allowed_kcal(date)
|
347
|
+
puts
|
348
|
+
puts "#{format_date(date)}: (#{quantize(allowed_kcal(date))})"
|
349
|
+
puts
|
350
|
+
remaining = allowed_kcal(date)
|
351
|
+
inputs_at(date).each do |i|
|
352
|
+
entry(quantize(i.kcal), i.description)
|
353
|
+
remaining -= i.kcal
|
354
|
+
end
|
355
|
+
separator
|
356
|
+
entry(quantize(remaining), "remaining (#{(100-100.0*remaining/allowed_kcal(date)).round}% used)")
|
357
|
+
end
|
358
|
+
if kcal_balance > 0
|
359
|
+
entry(quantize(kcal_balance.abs), "too much since #{balance_start}")
|
350
360
|
end
|
351
|
-
separator
|
352
|
-
entry(quantize(remaining), "remaining (#{(100-100.0*remaining/allowed_kcal(date)).round}% used)")
|
353
|
-
end
|
354
|
-
if kcal_balance > 0
|
355
|
-
entry(quantize(kcal_balance.abs), "too much since #{balance_start}")
|
356
361
|
end
|
357
|
-
end
|
358
362
|
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
363
|
+
def read_file name, klass
|
364
|
+
result = []
|
365
|
+
file = File.join(@nom_dir,name)
|
366
|
+
FileUtils.touch(file)
|
367
|
+
IO.readlines(file).each do |line|
|
368
|
+
next if line == "\n"
|
369
|
+
result << klass::from_line(line)
|
370
|
+
end
|
371
|
+
result
|
366
372
|
end
|
367
|
-
result
|
368
|
-
end
|
369
373
|
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
def rate
|
375
|
-
@config.get("rate")
|
376
|
-
end
|
374
|
+
def goal
|
375
|
+
@config.get("goal")
|
376
|
+
end
|
377
377
|
|
378
|
-
|
379
|
-
|
380
|
-
|
378
|
+
def rate
|
379
|
+
@config.get("rate")
|
380
|
+
end
|
381
381
|
|
382
|
-
|
383
|
-
|
384
|
-
@inputs.each do |i|
|
385
|
-
@inputs_at[i.date] = [] if @inputs_at[i.date].nil?
|
386
|
-
@inputs_at[i.date] << i
|
382
|
+
def inputs_at date
|
383
|
+
@inputs_at[date] || []
|
387
384
|
end
|
388
|
-
end
|
389
385
|
|
390
|
-
|
391
|
-
|
392
|
-
|
386
|
+
def precompute_inputs_at
|
387
|
+
@inputs_at = {}
|
388
|
+
@inputs.each do |i|
|
389
|
+
@inputs_at[i.date] = [] if @inputs_at[i.date].nil?
|
390
|
+
@inputs_at[i.date] << i
|
391
|
+
end
|
392
|
+
end
|
393
393
|
|
394
|
-
|
395
|
-
|
396
|
-
|
394
|
+
def precompute_base_rate_at
|
395
|
+
alpha = 0.05
|
396
|
+
@base_rate_at = {@weights.first => @weights.at(@weights.first)*25*1.2}
|
397
|
+
|
398
|
+
(@weights.first+1).upto(@weights.last) do |d|
|
399
|
+
intake = consumed_at(d-1)
|
400
|
+
if intake == 0
|
401
|
+
@base_rate_at[d] = @base_rate_at[d-1]
|
402
|
+
next
|
403
|
+
end
|
404
|
+
loss = @weights.moving_average_at(d-1) - @weights.moving_average_at(d)
|
405
|
+
kcal_per_kg_body_fat = 7000
|
406
|
+
burned_kcal = loss*kcal_per_kg_body_fat
|
407
|
+
new_base_rate_estimation = intake + burned_kcal
|
408
|
+
@base_rate_at[d] = alpha*new_base_rate_estimation + (1-alpha)*@base_rate_at[d-1]
|
409
|
+
end
|
410
|
+
(@weights.last+1).upto(Date.today) do |d|
|
397
411
|
@base_rate_at[d] = @base_rate_at[d-1]
|
398
|
-
next
|
399
412
|
end
|
400
|
-
loss = @weights.moving_average_at(d-1) - @weights.moving_average_at(d)
|
401
|
-
kcal_per_kg_body_fat = 7000
|
402
|
-
burned_kcal = loss*kcal_per_kg_body_fat
|
403
|
-
new_base_rate_estimation = intake + burned_kcal
|
404
|
-
@base_rate_at[d] = alpha*new_base_rate_estimation + (1-alpha)*@base_rate_at[d-1]
|
405
413
|
end
|
406
|
-
(@weights.last+1).upto(Date.today) do |d|
|
407
|
-
@base_rate_at[d] = @base_rate_at[d-1]
|
408
|
-
end
|
409
|
-
end
|
410
414
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
415
|
+
def which(cmd)
|
416
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
417
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
418
|
+
exts.each { |ext|
|
419
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
420
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
421
|
+
}
|
422
|
+
end
|
423
|
+
return nil
|
418
424
|
end
|
419
|
-
return nil
|
420
425
|
end
|
421
426
|
end
|