bread_calculator 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a454bc456276e0bac262c1f2bca15af7fb9c2687
4
+ data.tar.gz: 2c7578cc8609953f1391bede4fa2b9ca864013c2
5
+ SHA512:
6
+ metadata.gz: acce0ad69d786710cb19cccb1f152615e1ba2605ee354d9a825678b4bdbf12a85e2ff967edc9b10529887b34bc2fc800d3212c329716c109014d351e16df7f2e
7
+ data.tar.gz: 20fcd4a6426ae2091cfae4b77aa545247258b0669faf8710542500475ee954d67cef348e7af29c7504c7640a5973f37962f5699802da96b84c35621a330f1700
data/README.md ADDED
@@ -0,0 +1,22 @@
1
+ bread-calculator
2
+ ---------
3
+
4
+ A ruby gem to calculate baker's percentages
5
+
6
+ Installation
7
+ ---------
8
+
9
+
10
+ Inspiration and History
11
+ ---------
12
+
13
+ License
14
+ ---------
15
+ © 2014 Noah Birnel
16
+ BSD license
17
+
18
+
19
+
20
+
21
+
22
+
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bread_calculator'
4
+
5
+ @format = 'r'
6
+
7
+ loop { case ARGV[0]
8
+ when /-summary/ then @format = 'r.summary'; ARGV.shift; break
9
+ when /-weight/ then @format = 'r.weight'; ARGV.shift; break
10
+ when /-scale-by/ then ARGV.shift; @format = "r.scale_by #{ARGV.shift}"; break
11
+ when /--/ then ARGV.shift; break
12
+ when /^-/ then usage("Unknown option: #{ARGV[0].inspect}")
13
+ else break
14
+ end; }
15
+
16
+ ARGV.each do |arg|
17
+ parser = BreadCalculator::Parser.new arg
18
+ r = parser.parse(arg)
19
+ puts eval("#{@format}")
20
+ end
@@ -0,0 +1,15 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'bread_calculator'
3
+ s.version = '0.0.0'
4
+ s.date = '2014-02-28'
5
+ s.summary = "calculate baker's percentages"
6
+ s.description = "a gem and command-line wrapper to generate baker's
7
+ percentages from a bread recipe"
8
+ s.authors = ['Noah Birnel']
9
+ s.email = 'nbirnel@gmail.com'
10
+ s.homepage = 'http://github.com/nbirnel/bread-calculator'
11
+ s.files = ['README.md', 'bread_calculator.gemspec', 'lib/bread_calculator.rb', 'spec/bread_calculator_spec.rb', 'bin/bread-calculator']
12
+ s.has_rdoc = true
13
+ s.executables = ['bread-calculator']
14
+ s.license = 'DWTFYW'
15
+ end
@@ -0,0 +1,316 @@
1
+ ##
2
+ # Classes for manipulating bread recipes and baker's percentages
3
+
4
+ module BreadCalculator
5
+
6
+ ##
7
+ # This class represents an ingredient in a Recipe
8
+
9
+ class Ingredient
10
+ attr_accessor :info, :name, :quantity, :units, :type
11
+
12
+ ##
13
+ # Creates a new ingredient +name+, with the optional qualities +info+.
14
+ #
15
+ # +info+ should usually contain <tt>:quantity, :units, :type</tt>.
16
+ # +:type+, in the context of bakers' percentage, would be +:flours+,
17
+ # +:liquids+, or +:additives+.
18
+
19
+ def initialize name, info={}
20
+ @units = 'grams'
21
+ @name = name
22
+ @info = info
23
+ info.each do |k,v|
24
+ instance_variable_set("@#{k}", v)
25
+ end
26
+ end
27
+
28
+ ##
29
+ # Returns a new Ingredient, scaled from current instance by +ratio+
30
+
31
+ def scale_by ratio
32
+ scaled = Hash.new
33
+ self.info.each do |k, v|
34
+ scaled[k] = v
35
+ scaled[k] = v*ratio if k == :quantity
36
+ end
37
+ Ingredient.new(self.name, scaled)
38
+ end
39
+
40
+ ##
41
+ # Print a nice text version of Ingredient
42
+
43
+ def to_s
44
+ #FIXME check for existance
45
+ "\t#{@quantity} #{@units} #{@name}\n"
46
+ end
47
+ end
48
+
49
+ ##
50
+ # This class represents a discrete step in a Recipe.
51
+
52
+ class Step
53
+ attr_reader :techniques, :ingredients
54
+
55
+ ##
56
+ # Creates a new step with the the optional array +techniques+,
57
+ # which consists of ministep strings, and +Ingredients+.
58
+ #
59
+ # This is intended to read something like:
60
+ # <tt>"Mix:", @flour, @water, "thoroughly."</tt>
61
+ #
62
+ # or:
63
+ #
64
+ # <tt>"Serve forth."</tt>
65
+
66
+ def initialize *args
67
+ self.techniques = args.flatten
68
+ end
69
+
70
+ ##
71
+ # Sets +Step.techniques+ to +args+, and defines +Step.ingredients+
72
+ def techniques= args
73
+ @techniques = args
74
+ @ingredients = args.select{|arg| arg.is_a? Ingredient}
75
+ end
76
+
77
+ ##
78
+ # Print a nice text version of Step
79
+
80
+ def to_s
81
+ out = ''
82
+ self.techniques.each do |t|
83
+ tmp = t.is_a?(Ingredient) ? t.to_s : "#{t.chomp}\n"
84
+ out << tmp
85
+ end
86
+ out << "\n"
87
+ out
88
+ end
89
+ end
90
+
91
+ ##
92
+ # This class represents a recipe.
93
+
94
+ class Recipe
95
+ attr_reader :steps, :metadata
96
+
97
+ ##
98
+ # Creates a new Recipe with hash +metadata+ and array of Steps +steps+
99
+ #
100
+ # +metadata+ is freeform, but most likely should include +:name+.
101
+ # Other likely keys are:
102
+ # <tt>:prep_time, :total_time, :notes, :history, :serves, :makes,
103
+ # :attribution</tt>.
104
+
105
+ def initialize metadata, steps
106
+ @metadata = metadata
107
+ @steps = steps
108
+ @ingredients = self.ingredients
109
+ end
110
+
111
+ ##
112
+ # Returns an array of all Ingredients in Recipe
113
+
114
+ def ingredients
115
+ a = Array.new
116
+ self.steps.each do |step|
117
+ step.ingredients.each do |ing|
118
+ a << ing
119
+ end
120
+ end
121
+ a
122
+ end
123
+
124
+ ##
125
+ # Returns the total weight of Ingredients in Recipe
126
+
127
+ def weight
128
+ self.ingredients.map{|i| i.quantity}.reduce(:+)
129
+ end
130
+
131
+ #FIXME make this a method_missing so we can add new types on the fly
132
+ #RENÉE - 'end.' is weird or no?
133
+ #FIXME how do I get this into rdoc?
134
+ [:flours, :liquids, :additives].each do |s|
135
+ define_method("total_#{s}") do
136
+ instance_variable_get("@ingredients").select{|i| i.type == s}.map do |i|
137
+ i.quantity
138
+ end.reduce(:+)
139
+ end
140
+ end
141
+
142
+ alias_method 'bakers_percent_100', 'total_flours'
143
+
144
+ ##
145
+ # Returns the baker's percentage of a weight
146
+
147
+ def bakers_percent weight
148
+ weight / bakers_percent_100.to_f
149
+ end
150
+
151
+ ##
152
+ # Returns a Formula
153
+
154
+ def bakers_percent_formula
155
+ ratio = 100.0 / self.total_flours
156
+ self.scale_by ratio
157
+ end
158
+
159
+ ##
160
+ # Returns new Recipe scaled by +ratio+
161
+
162
+ def scale_by ratio
163
+ new_steps = self.steps.map do |s|
164
+ step_args = s.techniques.map do |t|
165
+ t.is_a?(Ingredient) ? t.scale_by(ratio) : t
166
+ end
167
+ Step.new step_args
168
+ end
169
+
170
+ Recipe.new self.metadata, new_steps
171
+ end
172
+
173
+ ##
174
+ # Returns a Summary
175
+
176
+ def summary
177
+ types = Hash.new
178
+ [:flours, :liquids, :additives].each do |s|
179
+ types["total_#{s}"] = self.bakers_percent eval("self.total_#{s}")
180
+ end
181
+
182
+ l_ingredients = Hash.new
183
+ self.ingredients.map do |i|
184
+ l_ingredients[i.name] = self.bakers_percent i.quantity
185
+ end
186
+ Summary.new types, l_ingredients
187
+ end
188
+
189
+ ##
190
+ # Print a nice text version of Recipe
191
+
192
+ def to_s
193
+ out = ''
194
+ self.metadata.each{|k,v| out << "#{k}: #{v}\n"}
195
+ out << "--------------------\n"
196
+ self.steps.each{|s| out << s.to_s }
197
+ out
198
+ end
199
+
200
+ end
201
+
202
+ ##
203
+ # This class converts a nearly free-form text file to a Recipe
204
+
205
+ class Parser
206
+
207
+ ##
208
+ # Create a new parser for Recipe +name+.
209
+
210
+ def initialize name
211
+ @name = name
212
+
213
+ @i = 0
214
+ @args = @steps = []
215
+ @steps[0] = BreadCalculator::Step.new
216
+
217
+ @in_prelude = true
218
+ @prelude = ''
219
+ end
220
+
221
+ ##
222
+ # Parse our text
223
+
224
+ def parse input
225
+
226
+ IO.foreach(input) do |line|
227
+ new_step && next if line =~ /(^-)|(^\s*$)/
228
+ @prelude << line && next if @in_prelude
229
+
230
+ @args << preprocess(line.chomp)
231
+ end
232
+
233
+ close_step
234
+ # because we made a spurious one to begin with
235
+ @steps.shift
236
+ metadata = {
237
+ :name => @name,
238
+ :notes => @prelude,
239
+ }
240
+
241
+ Recipe.new metadata, @steps
242
+ end
243
+
244
+ private
245
+
246
+ def new_step
247
+ @in_prelude = false
248
+ close_step
249
+
250
+ @args = []
251
+ @i += 1
252
+ @steps[@i] = BreadCalculator::Step.new
253
+ end
254
+
255
+ def close_step
256
+ @steps[@i].techniques = @args
257
+ end
258
+
259
+ def preprocess line
260
+ ing_regex = /^\s+((?<qty>[0-9.]+\s*)(?<units>g)?\s+)?(?<item>.*)/
261
+ h = Hash.new
262
+ if ing_regex =~ line
263
+ match = Regexp.last_match
264
+ h[:quantity] = match[:qty].strip.to_f
265
+ h[:units] = match[:units]
266
+ ingredient = match[:item].strip
267
+
268
+ #FIXME refactor
269
+ h[:type] = :additives #if it doesn't match anything else
270
+ h[:type] = :flours if ingredient =~ /flour/
271
+ h[:type] = :flours if ingredient =~ /meal/
272
+ h[:type] = :liquids if ingredient =~ /liquid/
273
+ h[:type] = :liquids if ingredient =~ /water/
274
+ h[:type] = :liquids if ingredient =~ /egg/
275
+ h[:type] = :liquids if ingredient =~ /mashed/
276
+ h[:type] = :liquids if ingredient =~ /milk/
277
+ h[:type] = :additives if ingredient =~ /dry/
278
+ h[:type] = :additives if ingredient =~ /powdered/
279
+
280
+ ing = BreadCalculator::Ingredient.new ingredient, h
281
+ else
282
+ line.strip
283
+ end
284
+ end
285
+
286
+ end
287
+
288
+ ##
289
+ # This class represents a summary of a Recipe - no Steps or units, just
290
+ # baker's percentages of each ingredient, and a prelude of baker's
291
+ # percentages for flours, liquids, and additives.
292
+
293
+ class Summary
294
+ attr_accessor :types, :ingredients
295
+
296
+ ##
297
+ # Create a new Summary of +types+ and +ingredients+
298
+
299
+ def initialize types, ingredients
300
+ @types = types
301
+ @ingredients = ingredients
302
+ end
303
+
304
+ ##
305
+ # Print it nicely
306
+
307
+ def to_s
308
+ out = ''
309
+ self.types.each{|k,v| out << "#{k}: #{v}\n"}
310
+ out << "--------------------\n"
311
+ self.ingredients.each{|k,v| out << "#{k}: #{v}\n"}
312
+ out
313
+ end
314
+ end
315
+
316
+ end
@@ -0,0 +1,84 @@
1
+ require "#{File.dirname(__FILE__)}/../lib/bread_calculator"
2
+
3
+ describe BreadCalculator do
4
+ before do
5
+ @ww = BreadCalculator::Ingredient.new "whole wheat flour", :quantity => 300, :units => 'grams', :type=>:flours
6
+ @ap = BreadCalculator::Ingredient.new "all purpose flour", :quantity => 700, :units => 'grams', :type=>:flours
7
+ @water = BreadCalculator::Ingredient.new "water", :quantity => 550, :units => 'grams', :type=>:liquids
8
+ @egg = BreadCalculator::Ingredient.new "egg", :quantity => 40, :units => 'grams', :type=>:liquids
9
+ @milk = BreadCalculator::Ingredient.new "dry milk", :quantity => 40, :units => 'grams', :type=>:additives
10
+ @raisins = BreadCalculator::Ingredient.new "raisins", :quantity => 50, :units => 'grams', :type=>:additives
11
+ @yeast = BreadCalculator::Ingredient.new "yeast", :quantity => 20, :units => 'grams', :type=>:additives
12
+ @proof = BreadCalculator::Step.new 'Rehydrate', @yeast
13
+ @wet = BreadCalculator::Step.new 'in', @water, @egg
14
+ @dry = BreadCalculator::Step.new 'Mix together:', @ww, @ap, @milk, 'in a large bowl'
15
+ @mix = BreadCalculator::Step.new 'Combine wet and dry ingredients with', @raisins
16
+ @bake = BreadCalculator::Step.new 'Form a loaf, rise for 2 hours, Bake at 375° for 45 minutes.'
17
+ @meta = {:notes => 'nice sandwich bread'}
18
+ @recipe = BreadCalculator::Recipe.new @meta, [@proof, @wet, @dry, @mix, @bake]
19
+ end
20
+
21
+ describe BreadCalculator::Ingredient do
22
+ it 'has a quantity' do
23
+ @ww.quantity = 100
24
+ end
25
+ end
26
+
27
+ describe BreadCalculator::Step do
28
+ it 'can be called without arguments' do
29
+ BreadCalculator::Step.new
30
+ end
31
+ end
32
+
33
+ describe BreadCalculator::Recipe do
34
+ it 'lists all ingredients and quantities' do
35
+ @recipe.ingredients.length.should eq 7
36
+ end
37
+
38
+ it 'displays total weight' do
39
+ @recipe.weight.should eq 1700
40
+ end
41
+
42
+ it 'displays total liquids' do
43
+ @recipe.total_liquids.should eq 590
44
+ end
45
+
46
+ it 'pretty prints' do
47
+ pending
48
+ @recipe.to_s.is_a?(String).should be_true
49
+ end
50
+
51
+ it 'generates a baker\'s percentage summary' do
52
+ @recipe.summary.is_a?(BreadCalculator::Summary).should be_true
53
+ end
54
+
55
+ it 'scales' do
56
+ @scaled = @recipe.scale_by(2)
57
+ ( @scaled.weight == @recipe.weight * 2.0 ).should be_true
58
+ end
59
+
60
+ it 'generates a baker\'s percentage formula' do
61
+ @recipe.bakers_percent_formula.is_a?(BreadCalculator::Recipe).should be_true
62
+ @recipe.bakers_percent_formula.total_flours.should eq 100.0
63
+ end
64
+
65
+ end
66
+
67
+ describe BreadCalculator::Parser do
68
+ before do
69
+ @sample = "#{File.dirname(__FILE__)}/../sample/sandwich-bread.recipe"
70
+ @parser = BreadCalculator::Parser.new 'Sandwich Bread'
71
+ @r = @parser.parse "#{@sample}"
72
+ end
73
+
74
+ it 'gets a recipe from a text file' do
75
+ @r.is_a?(BreadCalculator::Recipe).should be_true
76
+ end
77
+
78
+ it 'gets a recipe from a standard in' do
79
+ pending
80
+ end
81
+
82
+ end
83
+
84
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bread_calculator
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Noah Birnel
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-28 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |-
14
+ a gem and command-line wrapper to generate baker's
15
+ percentages from a bread recipe
16
+ email: nbirnel@gmail.com
17
+ executables:
18
+ - bread-calculator
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - README.md
23
+ - bread_calculator.gemspec
24
+ - lib/bread_calculator.rb
25
+ - spec/bread_calculator_spec.rb
26
+ - bin/bread-calculator
27
+ homepage: http://github.com/nbirnel/bread-calculator
28
+ licenses:
29
+ - DWTFYW
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options: []
33
+ require_paths:
34
+ - lib
35
+ required_ruby_version: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ required_rubygems_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ requirements: []
46
+ rubyforge_project:
47
+ rubygems_version: 2.0.7
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: calculate baker's percentages
51
+ test_files: []