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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 89b4e15ea4c1ac3760825032ae36a0694d03ad48
4
- data.tar.gz: 871d6fecf283a3299975f28bb28703610631c407
3
+ metadata.gz: 4b3afa581ea8fc93eaca6c7fc141c3dd9f9d7ae6
4
+ data.tar.gz: ed8508f8e2386a7985ba38fafe1c7090c417dc2b
5
5
  SHA512:
6
- metadata.gz: e05eeb5fa85df464fdf02d6b9d8bba1fe3b2555e18359c0b3d7a297ac835e54f33cce9f4d6b0e433b300ff35712ead73d53f7fee55c2368da1f2e4f3fc9e56b1
7
- data.tar.gz: c604e3304ac14ffcc89c868592aa06364428915a4987dacab4f9b828ee6eb846286a792554427c7b673b170f4b15005825673aace33330fe6c51181d0055e4f9
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. Then run this command:
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
- Enter `nom help` if you're lost:
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}
@@ -1,75 +1,67 @@
1
- class Config
2
- def initialize file
3
- @file = file
1
+ require "nom/helpers"
4
2
 
5
- @config = {}
6
- if File.exists? file
7
- @config = YAML.load_file(file)
8
- end
3
+ module Nom
4
+ class Config
5
+ def initialize file
6
+ @file = file
9
7
 
10
- @defaults = {
11
- # format: [ key, description, default_value, type ]
12
- "rate" => [ "how much weight you want to lose per week", 0.5, Float ],
13
- "goal" => [ "your target weight", nil, Float],
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
- def guess_image_viewer
22
- if RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/
23
- "start"
24
- elsif RbConfig::CONFIG['host_os'] =~ /darwin/
25
- "open"
26
- elsif RbConfig::CONFIG['host_os'] =~ /linux|bsd/
27
- "xdg-open"
28
- else
29
- nil
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
- def has key
34
- @config.has_key?(key) or (@defaults.has_key?(key) and not @defaults[key][1].nil?)
35
- end
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
- def get key
38
- v = nil
39
- if @config.has_key?(key)
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
- v = @defaults[key][1]
44
+ raise "Unknown configuration option '#{key}'"
51
45
  end
52
- else
53
- raise "Unknown configuration option '#{key}'"
54
- end
55
46
 
56
- if @defaults[key][2] == Float
57
- v.to_f
58
- elsif @defaults[key][2] == Date
59
- if v.class == Date
60
- v
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
- Date.parse(v)
56
+ v
63
57
  end
64
- else
65
- v
66
58
  end
67
- end
68
59
 
69
- def print_usage
70
- puts "Configuration options (put these in #{@file}):"
71
- @defaults.each do |key, value|
72
- puts " #{key}".ljust(34)+value[0].capitalize+(value[1].nil? ? "" : " (default: '#{value[1]}')")
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
@@ -1,21 +1,23 @@
1
- class FoodEntry
2
- attr_reader :date, :kcal, :description
1
+ module Nom
2
+ class FoodEntry
3
+ attr_reader :date, :kcal, :description
3
4
 
4
- def self.from_line line
5
- date, kcal, description = line.split(" ", 3)
6
- date = Date.parse(date)
7
- kcal = kcal.to_i
8
- description.chomp!
9
- FoodEntry.new(date, kcal, description)
10
- end
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
- def initialize date, kcal, description
13
- @date = date
14
- @kcal = kcal
15
- @description = description
16
- end
13
+ def initialize date, kcal, description
14
+ @date = date
15
+ @kcal = kcal
16
+ @description = description
17
+ end
17
18
 
18
- def to_s
19
- "#{@date} #{@kcal} #{@description}\n"
19
+ def to_s
20
+ "#{@date} #{@kcal} #{@description}\n"
21
+ end
20
22
  end
21
23
  end
@@ -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
@@ -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
- class Nom
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
- @weights = WeightDatabase.new(File.join(@nom_dir, "weight"))
23
- @inputs = read_file("input", FoodEntry)
24
+ @weights = WeightDatabase.new(File.join(@nom_dir, "weight"))
25
+ @inputs = read_file("input", FoodEntry)
24
26
 
25
- if @weights.empty?
26
- print "Welcome to nom! Please enter your current weight: "
27
- weight [STDIN.gets.chomp]
28
- end
27
+ date = truncate_date
28
+ @inputs.delete_if{|i| i.date < date}
29
+ @weights.truncate(date)
29
30
 
30
- date = truncate_date
31
- @inputs.delete_if{|i| i.date < date}
32
- @weights.truncate(date)
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
- precompute_inputs_at
40
- precompute_base_rate_at
41
- end
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
- def status
44
- kg_lost = @weights.moving_average_at(@weights.first) - @weights.moving_average_at(@weights.last_real)
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
- log_since([@weights.first,Date.today-1].max)
54
- end
55
-
56
- def log
57
- log_since(@weights.first)
58
- end
59
-
60
- def grep args
61
- term = args.join(" ")
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
- if inputs.empty?
66
- puts "(no matching entries found)"
55
+ log_since([@weights.first,Date.today-1].max)
67
56
  end
68
57
 
69
- inputs.each do |i|
70
- entry(quantize(i.kcal), i.date.to_s+" "+i.description)
58
+ def log
59
+ log_since(@weights.first)
71
60
  end
72
61
 
73
- separator
74
- entry(quantize(inputs.inject(0){|sum, i| sum+i.kcal}), "total")
75
- end
62
+ def grep args
63
+ term = args.join(" ")
76
64
 
77
- def weight args
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
- date = Date.today
83
- weight = args.pop.to_f
67
+ if inputs.empty?
68
+ puts "(no matching entries found)"
69
+ end
84
70
 
85
- open(File.join(@nom_dir,"weight"), "a") do |f|
86
- f << "#{date} #{weight}\n"
87
- end
71
+ inputs.each do |i|
72
+ entry(quantize(i.kcal), i.date.to_s+" "+i.description)
73
+ end
88
74
 
89
- initialize
90
- plot
91
- end
75
+ separator
76
+ entry(quantize(inputs.inject(0){|sum, i| sum+i.kcal}), "total")
77
+ end
92
78
 
93
- def nom args
94
- nom_entry args, (Time.now-5*60*60).to_date
95
- end
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
- def yesterday args
98
- nom_entry args, Date.today-1
99
- end
84
+ date = Date.today
85
+ weight = args.pop.to_f
100
86
 
101
- def search args
102
- puts "Previous log entries:"
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
- def plot
128
- raise "To use this subcommand, please install 'gnuplot'." unless which("gnuplot")
95
+ def nom args
96
+ nom_entry args, (Time.now-5*60*60).to_date
97
+ end
129
98
 
130
- weight_dat = Tempfile.new("weight")
131
- (@weights.first).upto(plot_end) do |date|
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
- input_dat = Tempfile.new("input")
152
- input_dat << "#{@weights.first-1}\t0\t0\n"
153
- (@weights.first).upto(Date.today) do |date|
154
- input_dat << "#{date}\t"
155
- if consumed_at(date) == 0
156
- input_dat << "-"
157
- else
158
- input_dat << quantize(consumed_at(date))
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
- svg = Tempfile.new(["plot", ".svg"])
167
- svg.close
168
- ObjectSpace.undefine_finalizer(svg) # prevent the svg file from being deleted
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
- plt_erb = IO.read(File.join(File.dirname(File.expand_path(__FILE__)), "nom.plt.erb"))
168
+ svg = Tempfile.new(["plot", ".svg"])
169
+ svg.close
170
+ ObjectSpace.undefine_finalizer(svg) # prevent the svg file from being deleted
171
171
 
172
- plt = Tempfile.new("plt")
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
- system("gnuplot "+plt.path)
174
+ plt = Tempfile.new("plt")
175
+ plt << ERB.new(plt_erb).result(binding)
176
+ plt.close
177
177
 
178
- image_viewer = @config.get("image_viewer")
179
- system(image_viewer+" "+svg.path)
180
- end
178
+ system("gnuplot "+plt.path)
181
179
 
182
- def edit
183
- edit_file "input"
184
- end
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
- def config_usage
191
- @config.print_usage
192
- end
184
+ def edit
185
+ Helpers::open_file File.join(@nom_dir, "input")
186
+ end
193
187
 
194
- private
188
+ def editw
189
+ Helpers::open_file File.join(@nom_dir, "weight")
190
+ end
195
191
 
196
- def nom_entry args, date
197
- summands = args.pop.split("+")
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
- if kcal == 0
204
- raise "energy term cannot be zero"
196
+ def config_usage
197
+ @config.print_usage
205
198
  end
206
199
 
207
- description = args.join(" ")
208
- entry = FoodEntry.new(date, kcal, description)
200
+ private
209
201
 
210
- open(File.join(@nom_dir,"input"), "a") do |f|
211
- if not @inputs.empty? and entry.date != @inputs.last.date
212
- f << "\n"
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
- @inputs << entry
218
- if @inputs_at[date].nil?
219
- @inputs_at[date] = []
220
- end
221
- @inputs_at[date] << entry
209
+ if kcal == 0
210
+ raise "energy term cannot be zero"
211
+ end
222
212
 
223
- status
224
- end
213
+ description = args.join(" ")
214
+ entry = FoodEntry.new(date, kcal, description)
225
215
 
226
- def edit_file filename
227
- editor = ENV["EDITOR"]
228
- editor = "vim" if editor.nil?
229
- system("#{editor} #{ENV["HOME"]}/.nom/#{filename}")
230
- end
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
- def allowed_kcal date, r=nil
233
- if r.nil?
234
- r = @weights.rate_at(date, goal, rate)
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
- if date > @weights.last
237
- date = @weights.last
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
- def consumed_at date
243
- inputs_at(date).inject(0){ |sum, i| sum+i.kcal }
244
- end
242
+ def consumed_at date
243
+ inputs_at(date).inject(0){ |sum, i| sum+i.kcal }
244
+ end
245
245
 
246
- def kg_to_go
247
- @weights.moving_average_at(Date.today) - goal
248
- end
246
+ def kg_to_go
247
+ @weights.moving_average_at(Date.today) - goal
248
+ end
249
249
 
250
- def kcal_to_burn
251
- kcal_per_kg_body_fat = 7000
252
- kg_to_go * kcal_per_kg_body_fat
253
- end
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
- def days_to_go
256
- kcal_to_burn.abs/(rate*1000)
257
- end
255
+ def days_to_go
256
+ kcal_to_burn.abs/(rate*1000)
257
+ end
258
258
 
259
- def plot_end
260
- @weights.last
261
- end
259
+ def plot_end
260
+ @weights.last
261
+ end
262
262
 
263
- def balance_start
264
- if @config.has("balance_start")
265
- @config.get("balance_start")
266
- else
267
- @weights.first
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
- def balance_end
272
- Date.today-1
273
- end
271
+ def balance_end
272
+ Date.today-1
273
+ end
274
274
 
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)
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
- def truncate_date
286
- first_start = @weights.first
285
+ def truncate_date
286
+ if Date.today - @weights.last_real > 30
287
+ return Date.today
288
+ end
287
289
 
288
- if @config.has("start_date")
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 gap.nil?
296
- first_start
292
+ if @config.has("start_date")
293
+ user_start = @config.get("start_date")
294
+ [user_start, first_start].max
297
295
  else
298
- gap[1]
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
- def quantize kcal
304
- return (1.0*kcal/@config.get("unit")).round
305
- end
307
+ def quantize kcal
308
+ return (1.0*kcal/@config.get("unit")).round
309
+ end
306
310
 
307
- def format_date date
308
- if date == Date.today
309
- return "Today"
310
- elsif date == Date.today-1
311
- return "Yesterday"
312
- else
313
- return date.to_s
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
- def format_duration days
318
- if days <= 7
319
- n = days.round(1)
320
- unit = "day"
321
- elsif days <= 7*4
322
- n = (days/7.0).round(1)
323
- unit = "week"
324
- else
325
- n = (days/7.0/4.0).round(1)
326
- unit = "month"
327
- end
328
- "#{n} #{unit}#{n == 1 ? "" : "s"}"
329
- end
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
- def entry value, text=""
332
- puts "#{" "*(6-value.to_s.length)}(#{value}) #{text}"
333
- end
335
+ def entry value, text=""
336
+ puts "#{" "*(6-value.to_s.length)}(#{value}) #{text}"
337
+ end
334
338
 
335
- def separator
336
- puts "---------------------"
337
- end
339
+ def separator
340
+ puts "---------------------"
341
+ end
338
342
 
339
- def log_since start
340
- remaining = 0
341
- start.upto(Date.today) do |date|
342
- remaining += allowed_kcal(date)
343
- puts
344
- puts "#{format_date(date)}: (#{quantize(allowed_kcal(date))})"
345
- puts
346
- remaining = allowed_kcal(date)
347
- inputs_at(date).each do |i|
348
- entry(quantize(i.kcal), i.description)
349
- remaining -= i.kcal
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
- def read_file name, klass
360
- result = []
361
- file = File.join(@nom_dir,name)
362
- FileUtils.touch(file)
363
- IO.readlines(file).each do |line|
364
- next if line == "\n"
365
- result << klass::from_line(line)
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
- def goal
371
- @config.get("goal")
372
- end
373
-
374
- def rate
375
- @config.get("rate")
376
- end
374
+ def goal
375
+ @config.get("goal")
376
+ end
377
377
 
378
- def inputs_at date
379
- @inputs_at[date] || []
380
- end
378
+ def rate
379
+ @config.get("rate")
380
+ end
381
381
 
382
- def precompute_inputs_at
383
- @inputs_at = {}
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
- def precompute_base_rate_at
391
- alpha = 0.05
392
- @base_rate_at = {@weights.first => @weights.at(@weights.first)*25*1.2}
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
- (@weights.first+1).upto(@weights.last) do |d|
395
- intake = consumed_at(d-1)
396
- if intake == 0
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
- def which(cmd)
412
- exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
413
- ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
414
- exts.each { |ext|
415
- exe = File.join(path, "#{cmd}#{ext}")
416
- return exe if File.executable?(exe) && !File.directory?(exe)
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