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