bread_calculator 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +22 -0
- data/bin/bread-calculator +20 -0
- data/bread_calculator.gemspec +15 -0
- data/lib/bread_calculator.rb +316 -0
- data/spec/bread_calculator_spec.rb +84 -0
- metadata +51 -0
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,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: []
|