nom 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|

|
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
|