bread_calculator 0.0.0
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 +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: []
|