beerxml 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (C) 2011 by Brian Palmer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/Rakefile CHANGED
@@ -10,10 +10,11 @@ end
10
10
  RSpec::Core::RakeTask.new(:rcov) do |t|
11
11
  t.rspec_opts = "-c -f d"
12
12
  t.rcov = true
13
+ t.rcov_opts = ["--exclude", "spec,gems/,rubygems/"]
13
14
  end
14
15
 
15
16
  require 'yard'
16
17
  YARD::Rake::YardocTask.new(:doc) do |t|
17
18
  version = Beerxml::VERSION
18
- t.options = ["--title", "beerxml #{version}"]
19
+ t.options = ["--title", "beerxml #{version}", "--files", "LICENSE"]
19
20
  end
@@ -22,8 +22,9 @@ Gem::Specification.new do |s|
22
22
  s.add_dependency "dm-validations", "~> 1.0"
23
23
  s.add_dependency "dm-types", "~> 1.0"
24
24
 
25
- s.add_development_dependency "rspec", "~> 2.0"
26
- s.add_development_dependency "ruby-debug"
25
+ s.add_development_dependency "rspec", "~> 2.5"
26
+ s.add_development_dependency "autotest-standalone"
27
+ s.add_development_dependency "autotest-growl"
27
28
  s.add_development_dependency "rcov"
28
29
  s.add_development_dependency "yard"
29
30
  end
@@ -6,6 +6,15 @@ require 'nokogiri'
6
6
  module Beerxml
7
7
  # This'll have to go eventually, but for now it's nice
8
8
  DataMapper.setup(:default, "abstract::")
9
+
10
+ def self.parse(string_or_io)
11
+ Beerxml::Model.from_xml(Nokogiri::XML(string_or_io).root)
12
+ end
13
+
14
+ def self.round(float, to = 0)
15
+ exp = 10 ** to
16
+ (float * exp).round.to_f / exp
17
+ end
9
18
  end
10
19
 
11
20
  require 'beerxml/model'
@@ -0,0 +1,34 @@
1
+ class Beerxml::Fermentable < Beerxml::Model
2
+ property :name, String, :required => true
3
+ property :type, Enum['Grain', 'Sugar', 'Extract', 'Dry Extract', 'Adjunct'], :required => true
4
+ property :amount, Weight, :required => true
5
+ property :yield, Float, :required => true
6
+ property :color, Float, :required => true
7
+
8
+ property :add_after_boil, Boolean, :default => false
9
+ property :origin, String, :length => 512
10
+ property :supplier, String, :length => 512
11
+ property :notes, String, :length => 65535
12
+ property :coarse_fine_diff, Float
13
+ property :moisture, Float
14
+ property :diastatic_power, Float
15
+ property :protein, Float
16
+ property :max_in_batch, Float
17
+ property :recommend_mash, Boolean
18
+ property :ibu_gal_per_lb, Float
19
+
20
+ # these are not used in the xml
21
+ property :id, Serial
22
+ belongs_to :recipe, :required => false
23
+
24
+ def ppg
25
+ # potential is (yield * 0.001 + 1)
26
+ (self.yield * 0.01) * 46.214
27
+ end
28
+
29
+ def total_ppg(efficiency = nil)
30
+ total = ppg * amount.in_pounds.to_f
31
+ total *= (efficiency * 0.01) unless efficiency.nil? || type != 'Grain'
32
+ total
33
+ end
34
+ end
@@ -1,9 +1,10 @@
1
1
  class Beerxml::Hop < Beerxml::Model
2
2
  property :name, String, :required => true
3
3
  property :alpha, Float, :required => true
4
- property :amount, Float, :required => true
4
+ property :amount, Weight, :required => true
5
5
  property :use, Enum['Boil', 'Dry Hop', 'Mash', 'First Wort', 'Aroma'], :required => true
6
- property :time, Integer, :required => true
6
+ property :time, Time, :required => true
7
+
7
8
  property :notes, String, :length => 65535
8
9
  property :type, Enum[nil, 'Bittering', 'Aroma', 'Both']
9
10
  property :form, Enum[nil, 'Pellet', 'Plug', 'Leaf']
@@ -19,4 +20,17 @@ class Beerxml::Hop < Beerxml::Model
19
20
  # these are not used in the xml
20
21
  property :id, Serial
21
22
  belongs_to :recipe, :required => false
23
+
24
+ def tinseth(post_boil_og, batch_size) # batch size is gallons or Unit
25
+ bigness = 1.65 * 0.000125**(post_boil_og - 1)
26
+ boil_factor = (1 - 2.72 ** (-0.04 * time.in_minutes.to_f)) / 4.15
27
+ utilization = bigness * boil_factor
28
+ ibus = utilization * (aau * 0.01 * 7490) / U(batch_size, 'gallons').to_f
29
+ Beerxml.round(ibus, 1)
30
+ end
31
+ alias_method :ibus, :tinseth
32
+
33
+ def aau
34
+ alpha * amount.in('ounces').to_f
35
+ end
22
36
  end
@@ -1,10 +1,43 @@
1
1
  class Beerxml::Model
2
2
  include DataMapper::Resource
3
+ require 'beerxml/properties'
4
+ include Beerxml::Properties
5
+
6
+ ##########################
7
+
8
+ @models = {}
9
+ @plurals = {}
3
10
 
4
11
  def self.beerxml_name
5
12
  name.split('::').last.upcase
6
13
  end
7
14
 
15
+ def self.beerxml_plural_name
16
+ "#{beerxml_name}S"
17
+ end
18
+
19
+ def self.inherited(klass)
20
+ super
21
+ @models[klass.beerxml_name] = klass
22
+ @plurals["#{klass.beerxml_name}S"] = klass
23
+ end
24
+
25
+ ##########################
26
+
27
+ # Takes a Nokogiri node, figures out what sort of class it is, and parses it.
28
+ # Raises if it's not a Beerxml::Model class (or collection).
29
+ def self.from_xml(node)
30
+ if model = @models[node.name]
31
+ model.new.from_xml(node)
32
+ elsif model = @plurals[node.name]
33
+ collection_from_xml(node)
34
+ else
35
+ raise "Unknown BeerXML node type: #{node.name}"
36
+ end
37
+ end
38
+
39
+ # Parse a Nokogiri node, reading in the properties defined on this model.
40
+ # Assumes the node is of the correct type.
8
41
  def from_xml(node)
9
42
  properties.each do |property|
10
43
  read_xml_field(node, property.name.to_s)
@@ -14,17 +47,26 @@ class Beerxml::Model
14
47
  child_model = rel.child_model
15
48
  next unless child_model.ancestors.include?(Beerxml::Model)
16
49
 
17
- model_name = child_model.beerxml_name
18
- child_wrapper_node = (node>model_name.pluralize.upcase).first
19
- next unless child_wrapper_node
20
-
21
- (child_wrapper_node>model_name).each do |child_node|
22
- self.send(rel.name) << child_model.new.from_xml(child_node)
50
+ # look for the plural element in the children of this node
51
+ # e.g., for Hop, see if there's any HOPS element.
52
+ (node>child_model.beerxml_plural_name).each do |child_wrapper_node|
53
+ collection = Beerxml::Model.collection_from_xml(child_wrapper_node)
54
+ self.send(rel.name).concat(collection)
23
55
  end
24
56
  end
25
57
  self
26
58
  end
27
59
 
60
+ # takes a collection root xml node, like <HOPS>, and returns an array of the
61
+ # child model objects inside the node.
62
+ def self.collection_from_xml(collection_node)
63
+ model = @plurals[collection_node.name]
64
+ raise("Unknown model: #{collection_node.name}") unless model
65
+ (collection_node>model.beerxml_name).map do |child_node|
66
+ model.new.from_xml(child_node)
67
+ end
68
+ end
69
+
28
70
  def read_xml_field(node, attr_name, node_name = attr_name.upcase)
29
71
  child = (node>node_name).first
30
72
  return unless child
@@ -33,6 +75,8 @@ class Beerxml::Model
33
75
  self.send("#{attr_name}=", child)
34
76
  end
35
77
 
78
+ ##########################
79
+
36
80
  def to_beerxml(parent = Nokogiri::XML::Document.new)
37
81
  # TODO: do we raise an error if not valid?
38
82
  node = Nokogiri::XML::Node.new(self.class.beerxml_name, parent)
@@ -55,5 +99,5 @@ class Beerxml::Model
55
99
  end
56
100
  end
57
101
 
58
- require 'beerxml/hop'
59
- require 'beerxml/recipe'
102
+ %w(hop recipe fermentable).
103
+ each { |f| require "beerxml/#{f}" }
@@ -0,0 +1,93 @@
1
+ require 'beerxml/unit'
2
+
3
+ # Custom DM property types for the various BeerXML data types.
4
+ module Beerxml::Properties
5
+
6
+ class Property < DataMapper::Property::Float
7
+ def custom?
8
+ true
9
+ end
10
+
11
+ def load(value)
12
+ return if value.nil?
13
+ if value.is_a?(Beerxml::Unit)
14
+ raise(ArgumentError, 'Weight required') unless value.type == unit_type
15
+ value
16
+ else
17
+ U(value, base_unit)
18
+ end
19
+ end
20
+
21
+ def dump(value)
22
+ return if value.nil?
23
+ value.in(base_unit).to_f
24
+ end
25
+
26
+ def typecast_to_primitive(value)
27
+ load(value)
28
+ end
29
+ end
30
+
31
+ # Represents a weight. Default scale is kilograms, since that's what
32
+ # BeerXML uses. But can handle conversion/display of other units.
33
+ class Weight < Property
34
+ def unit_type
35
+ 'weight'
36
+ end
37
+ def base_unit
38
+ 'kilograms'
39
+ end
40
+ end
41
+
42
+ # A volume, in liters.
43
+ class Volume < Property
44
+ def unit_type
45
+ 'volume'
46
+ end
47
+ def base_unit
48
+ 'liters'
49
+ end
50
+ end
51
+
52
+ # A temperature, in deg C.
53
+ class Temperature < Property
54
+ def unit_type
55
+ 'temperature'
56
+ end
57
+ def base_unit
58
+ 'C'
59
+ end
60
+ end
61
+
62
+ # A time, in minutes.
63
+ class Time < Property
64
+ def unit_type
65
+ 'time'
66
+ end
67
+ def base_unit
68
+ 'minutes'
69
+ end
70
+ end
71
+
72
+ # Time, but in days.
73
+ class TimeInDays < Time
74
+ def base_unit
75
+ 'days'
76
+ end
77
+ end
78
+
79
+ # Appendix A: http://www.beerxml.com/beerxml.htm
80
+ # "all fields that are defined for display only may also use a unit
81
+ # tag after them". For example “3.45 gal” is an acceptable value.
82
+ class DisplayWeight < Weight
83
+ end
84
+
85
+ class DisplayVolume < Volume
86
+ end
87
+
88
+ class DisplayTemperature < Temperature
89
+ end
90
+
91
+ class DisplayTime < Time
92
+ end
93
+ end
@@ -2,9 +2,58 @@ class Beerxml::Recipe < Beerxml::Model
2
2
  include DataMapper::Resource
3
3
 
4
4
  property :name, String, :required => true
5
+ property :type, Enum['Extract', 'Partial Mash', 'All Grain'], :required => true
6
+ # has 1, :style, :required => true
7
+ property :brewer, String, :required => true
8
+ property :batch_size, Volume, :required => true
9
+ property :boil_size, Volume, :required => true
10
+ property :boil_time, Time, :required => true
11
+ property :efficiency, Float
12
+ validates_presence_of :efficiency, :if => proc { |t| t.type != 'Extract' }
5
13
 
6
14
  has n, :hops
15
+ has n, :fermentables
16
+ # has n, :miscs
17
+ # has n, :yeasts
18
+ # has n, :waters
19
+
20
+ property :asst_brewer, String
21
+ property :notes, String, :length => 65535
22
+ property :taste_notes, String, :length => 65535
23
+ property :taste_rating, Float
24
+ property :og, Float
25
+ property :fg, Float
26
+ property :fermentation_stages, Integer
27
+ property :primary_age, TimeInDays
28
+ property :primary_temp, Temperature
29
+ property :secondary_age, TimeInDays
30
+ property :secondary_temp, Temperature
31
+ property :tertiary_age, TimeInDays
32
+ property :tertiary_temp, Temperature
33
+ property :age, TimeInDays
34
+ property :age_temp, Temperature
35
+ property :date, String
36
+ property :carbonation, Float
37
+ property :forced_carbonation, Boolean
38
+ property :priming_sugar_name, String
39
+ property :carbonation_temp, Temperature
40
+ property :priming_sugar_equiv, Float
41
+ property :keg_priming_factor, Float
42
+ # has 1, :equipment
43
+ # has 1, :mash
44
+ # validates_presence_of :mash, :if => proc { |t| t.type != 'Extract' }
7
45
 
8
46
  # these are not used in the xml
9
47
  property :id, Serial
48
+
49
+ def tinseth
50
+ Beerxml.round(hops.select { |h| h.use == 'Boil' }.inject(0) { |v, h| v + h.tinseth(og, batch_size) }, 1)
51
+ end
52
+ alias_method :ibus, :tinseth
53
+
54
+ def calculate_og
55
+ total_ppg = fermentables.inject(0) { |v, f| v + f.total_ppg(efficiency) }
56
+ og = 1 + ((total_ppg / batch_size.in_gallons.to_f) * 0.001)
57
+ Beerxml.round(og, 3)
58
+ end
10
59
  end
@@ -0,0 +1,168 @@
1
+ module Beerxml
2
+ # Unit system with conversions between units of the same type. Very much
3
+ # a work in progress.
4
+ class Unit
5
+ attr_reader :type, :unit
6
+
7
+ Units = {}
8
+ BaseUnits = {}
9
+ UnitToType = {}
10
+
11
+ def in(unit, *args)
12
+ self.clone.in!(unit, *args)
13
+ end
14
+ alias_method :to, :in
15
+
16
+ def in!(new_unit, *args)
17
+ new_unit = new_unit.to_s
18
+ new_unit_type = UnitToType[new_unit]
19
+ raise(ArgumentError, "Unknown unit: #{new_unit}") if new_unit_type.nil?
20
+ if new_unit_type != type
21
+ raise(ArgumentError, "New unit: #{new_unit} not compatible with current unit: #{unit}")
22
+ end
23
+ @unit = new_unit
24
+ self
25
+ end
26
+ alias_method :to!, :in!
27
+ alias_method :unit=, :in!
28
+
29
+ def base_unit
30
+ BaseUnits[type]
31
+ end
32
+
33
+ def to_f
34
+ @value * Units[type][unit]
35
+ end
36
+
37
+ def initialize(amount, unit = nil)
38
+ if amount.is_a?(Unit)
39
+ if unit
40
+ amount = amount.to(unit).to_f
41
+ else
42
+ amount, unit = amount.to_f, amount.unit
43
+ end
44
+ elsif !unit
45
+ amount, unit = amount.to_s.split(/\s+/, 2)
46
+ end
47
+ unit = unit.to_s
48
+
49
+ @type = UnitToType[unit]
50
+ self.unit = unit
51
+ @value = Float(amount) / Units[type][unit]
52
+ end
53
+
54
+ def is?(unit)
55
+ self.unit == unit ||
56
+ (UnitToType[self.unit] == UnitToType[unit] &&
57
+ Units[type][self.unit] == Units[type][unit])
58
+ end
59
+
60
+ def self.add_type(type, base_unit_name, *aliases)
61
+ Units[type] = {}
62
+ Units[type][base_unit_name] = 1.0
63
+ UnitToType[base_unit_name] = type
64
+ BaseUnits[type] = base_unit_name
65
+ aliases.each { |a| add_alias(type, base_unit_name, a) }
66
+ end
67
+ def self.add(type, factor, name, *aliases)
68
+ Units[type][name] = factor.to_f
69
+ aliases.each { |a| add_alias(type, name, a) }
70
+ UnitToType[name] = type
71
+ end
72
+ def self.add_alias(type, name, new_alias)
73
+ base = Units[type][name]
74
+ raise(ArgumentError, "Unknown base: #{name}") unless base
75
+ Units[type][new_alias] = base
76
+ UnitToType[new_alias] = type
77
+ end
78
+
79
+ def ==(rhs)
80
+ # TODO: hard-coding this precision stinks...
81
+ if rhs.is_a?(self.class)
82
+ rhs = rhs.in(base_unit)
83
+ @value.between?(rhs.to_f - 0.01, rhs.to_f + 0.01)
84
+ elsif rhs.is_a?(Numeric)
85
+ to_f.between?(rhs.to_f - 0.01, rhs.to_f + 0.01)
86
+ else
87
+ raise ArgumentError, "#{rhs.inspect} is not a #{self.class.name}"
88
+ end
89
+ end
90
+
91
+ def method_missing(meth, *a, &b)
92
+ if meth.to_s =~ %r{\A(to|in)_([^!]+)(!)?\z}
93
+ send($3 ? :in! : :in, $2)
94
+ else
95
+ super
96
+ end
97
+ end
98
+
99
+ # todo: remove this, to_beerxml is using it (and incorrectly at that)
100
+ def to_s
101
+ to_f.to_s
102
+ end
103
+
104
+ def inspect
105
+ "[#{to_f} #{unit}]"
106
+ end
107
+
108
+ def *(rhs)
109
+ if rhs.is_a?(Numeric)
110
+ ret = Unit.new(self.to_f * rhs, self.unit)
111
+ elsif rhs.is_a?(Unit) && rhs.type == type
112
+ ret = Unit.new(self.to_f * rhs.to(self.unit).to_f, self.unit)
113
+ else
114
+ super
115
+ end
116
+ end
117
+
118
+ # Assumes earth gravity ;)
119
+ module Weight
120
+ def self.included(k)
121
+ k.add_type('weight', 'kg', 'kilogram', 'kilograms')
122
+ k.add('weight', 1000, 'g', 'gram', 'grams')
123
+ k.add('weight', 2.204622, 'lb', 'pound', 'pounds', 'lbs')
124
+ k.add('weight', 35.273961, 'oz', 'ounce', 'ounces')
125
+ end
126
+ end
127
+ include Weight
128
+
129
+ module Volume
130
+ def self.included(k)
131
+ k.add_type('volume', 'liters', 'l', 'liter')
132
+ k.add('volume', 0.26417, 'gallons', 'gallon')
133
+ end
134
+ end
135
+ include Volume
136
+
137
+ module Time
138
+ def self.included(k)
139
+ k.add_type('time', 'day', 'days')
140
+ k.add('time', 24, 'hour', 'hours')
141
+ k.add('time', 1440, 'minute', 'minutes')
142
+ end
143
+ end
144
+ include Time
145
+
146
+ module Temperature
147
+ def self.included(k)
148
+ k.add_type('temperature', 'c', 'C', 'celsius')
149
+ # k.add('temperature', proc {}, 'f', 'F', 'fahrenheit')
150
+ end
151
+ end
152
+ include Temperature
153
+
154
+ def self.apply_to_numeric!
155
+ Beerxml::Unit::UnitToType.each do |unit, v|
156
+ Numeric.class_eval(<<-METHOD, __FILE__, __LINE__+1)
157
+ def #{unit}
158
+ Beerxml::Unit.new(self, #{unit.inspect})
159
+ end
160
+ METHOD
161
+ end
162
+ end
163
+ end
164
+ end
165
+
166
+ def U(*a)
167
+ Beerxml::Unit.new(*a)
168
+ end
@@ -1,3 +1,3 @@
1
1
  module Beerxml
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -0,0 +1,27 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ require 'beerxml/unit'
4
+
5
+ describe Beerxml::Hop do
6
+ describe "IBUs" do
7
+ it "should calculate IBUs using the Tinseth formula" do
8
+ hop = Beerxml::Hop.new(:name => "Cascade",
9
+ :alpha => 6,
10
+ :amount => U('0.5 oz'),
11
+ :use => 'Boil',
12
+ :time => 20)
13
+ hop.should be_valid
14
+ hop.tinseth(1.065, 5.5).should == 5
15
+ # tinseth by default
16
+ hop.ibus(1.065, 5.5).should == 5
17
+
18
+ hop2 = Beerxml::Hop.new(:name => 'Goldings',
19
+ :alpha => 5,
20
+ :amount => U(4.5, 'oz'),
21
+ :use => 'Boil',
22
+ :time => 60)
23
+ hop2.should be_valid
24
+ hop2.ibus(1.081, 11).should == 26.7
25
+ end
26
+ end
27
+ end
@@ -1,10 +1,6 @@
1
- require 'spec/spec_helper'
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
2
 
3
3
  describe "beerxml.com examples" do
4
- def read_xml(example)
5
- Nokogiri::XML(File.read("examples/beerxml.com/#{example}.xml"))
6
- end
7
-
8
4
  it "should parse the first recipe and its hops" do
9
5
  recipe = Beerxml::Recipe.new.from_xml(read_xml("recipes").root.children[1])
10
6
 
@@ -29,4 +25,127 @@ describe "beerxml.com examples" do
29
25
  hop2.should be_valid
30
26
  hop2.attributes.should == hop.attributes
31
27
  end
28
+
29
+ def assert_hops(hops)
30
+ hops.size.should == 5
31
+ hops.each { |h| h.should(be_a(Beerxml::Hop)) && h.should(be_valid) }
32
+ hops.map(&:name).should == ["Cascade", "Galena", "Goldings, B.C.", "Northern Brewer", "Tettnang"]
33
+ end
34
+
35
+ describe "auto-discovery of root node types" do
36
+ it "should discover singular nodes" do
37
+ hop = Beerxml::Model.from_xml(read_xml("hops").root.children[1])
38
+ hop.should be_a(Beerxml::Hop)
39
+ hop.should be_valid
40
+ hop.name.should == "Cascade"
41
+ end
42
+
43
+ it "should discover plural nodes" do
44
+ hops = Beerxml::Model.from_xml(read_xml("hops").root)
45
+ assert_hops(hops)
46
+ end
47
+ end
48
+
49
+ it "should parse given a string or IO" do
50
+ hops = Beerxml.parse(File.read(filename("hops")))
51
+ assert_hops(hops)
52
+ hops = Beerxml.parse(File.open(filename("hops"), "rb"))
53
+ assert_hops(hops)
54
+ end
55
+
56
+ describe "sanity checks" do
57
+ def check_parse(klass, file, node, attrs)
58
+ model = klass.new.from_xml(read_xml(file).root.children[node])
59
+ model.should be_valid
60
+ model.attributes.reject { |k,v| v.nil? }.should == attrs
61
+ model
62
+ end
63
+ it "should parse fermentables" do
64
+ check_parse(Beerxml::Fermentable, "grain", 5, {
65
+ :name => 'Munich Malt',
66
+ :type => 'Grain',
67
+ :amount => 0.0,
68
+ :yield => 80.0,
69
+ :color => 9.0,
70
+ :add_after_boil => false,
71
+ :origin => 'Germany',
72
+ :notes => "Malty-sweet flavor characteristic and adds a reddish amber color to the beer.
73
+ Does not contribute signficantly to body or head retention.
74
+ Use for: Bock, Porter, Marzen, Oktoberfest beers",
75
+ :coarse_fine_diff => 1.3,
76
+ :moisture => 5.0,
77
+ :diastatic_power => 72.0,
78
+ :protein => 11.5,
79
+ :max_in_batch => 80.0,
80
+ :recommend_mash => true,
81
+ :ibu_gal_per_lb => 0.0,
82
+ :supplier => '',
83
+ })
84
+ end
85
+ it "should parse hops" do
86
+ check_parse(Beerxml::Hop, "hops", 7, {
87
+ :name => 'Northern Brewer',
88
+ :origin => 'Germany',
89
+ :alpha => 8.5,
90
+ :amount => 0.0,
91
+ :use => 'Boil',
92
+ :time => 0.0,
93
+ :notes => %{Also called Hallertauer Northern Brewers
94
+ Use for: Bittering and finishing both ales and lagers of all kinds
95
+ Aroma: Fine, dry, clean bittering hop. Unique flavor.
96
+ Substitute: Hallertauer Mittelfrueh, Hallertauer
97
+ Examples: Anchor Steam, Old Peculiar, },
98
+ :type => 'Both',
99
+ :form => 'Pellet',
100
+ :beta => 4.0,
101
+ :hsi => 35.0,
102
+ })
103
+ end
104
+ it "should parse recipes" do
105
+ recipe = check_parse(Beerxml::Recipe, "recipes", 3, {
106
+ :name => 'Dry Stout',
107
+ :type => 'All Grain',
108
+ :brewer => 'Brad Smith',
109
+ :asst_brewer => '',
110
+ :batch_size => 18.92716800,
111
+ :boil_size => 20.81988500,
112
+ :boil_time => 60,
113
+ :efficiency => 72.0,
114
+ :notes => %{A very simple all grain beer that produces a great Guiness-style taste every time. So light in body that I have even made black and tans with it using a full body pale ale in the bottom of the glass.},
115
+ :taste_notes => %{One of my favorite stock beers - I always keep a keg on hand. Rich flavored dry Irish Stout that is very simple to make. Perfect every time!},
116
+ :taste_rating => 44.0,
117
+ :og => 1.038,
118
+ :fg => 1.012,
119
+ :carbonation => 2.3,
120
+ :fermentation_stages => 2,
121
+ :primary_age => 4,
122
+ :primary_temp => 20.000,
123
+ :secondary_age => 7,
124
+ :secondary_temp => 20.000,
125
+ :tertiary_age => 0,
126
+ :age => 7,
127
+ :age_temp => 5.000,
128
+ :date => '4/1/2003',
129
+ })
130
+ recipe.hops.map(&:attributes).should == [
131
+ :name => 'Goldings, East Kent',
132
+ :origin => 'United Kingdom',
133
+ :alpha => 5.0,
134
+ :amount => 0.0637860,
135
+ :use => 'Boil',
136
+ :time => 60,
137
+ :notes => %{Used For: General purpose hops for bittering/finishing all British Ales
138
+ Aroma: Floral, aromatic, earthy, slightly sweet spicy flavor
139
+ Substitutes: Fuggles, BC Goldings
140
+ Examples: Bass Pale Ale, Fullers ESB, Samual Smith's Pale Ale
141
+ },
142
+ :type => 'Aroma',
143
+ :form => 'Pellet',
144
+ :beta => 3.5,
145
+ :hsi => 35.0,
146
+ :recipe_id => nil,
147
+ ]
148
+ recipe.fermentables.map(&:name).should == ['Pale Malt (2 Row) UK', 'Barley, Flaked', 'Black Barley (Stout)']
149
+ end
150
+ end
32
151
  end
@@ -0,0 +1,15 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ describe Beerxml::Recipe do
4
+ it "should calculate IBUs using the tinseth method" do
5
+ recipe = Beerxml::Recipe.new.from_xml(read_xml("recipes").root.children[1])
6
+ recipe.should be_valid
7
+ recipe.ibus.round.should == 32
8
+ end
9
+
10
+ it "should calculate the OG for an all grain batch" do
11
+ recipe = Beerxml::Recipe.new.from_xml(read_xml("recipes").root.children[1])
12
+ recipe.should be_valid
13
+ recipe.calculate_og.should == 1.056
14
+ end
15
+ end
@@ -1,9 +1,15 @@
1
- require 'bundler'
2
- Bundler.require(:default, :development)
3
-
4
1
  require 'beerxml'
5
2
 
6
- require 'ruby-debug'
3
+ require 'rspec'
7
4
 
8
5
  RSpec.configure do |c|
6
+ def filename(example)
7
+ "examples/beerxml.com/#{example}.xml"
8
+ end
9
+ def read_file(example)
10
+ File.read(filename(example))
11
+ end
12
+ def read_xml(example)
13
+ Nokogiri::XML(read_file(example))
14
+ end
9
15
  end
@@ -0,0 +1,123 @@
1
+ require "#{File.dirname(__FILE__)}/spec_helper"
2
+
3
+ class Float
4
+ def near?(rhs, prec = 0.01)
5
+ self.between?(rhs - prec, rhs + prec)
6
+ end
7
+ end
8
+
9
+ describe Beerxml::Unit do
10
+ it "should allow constructing with base units" do
11
+ w = U('153.27 kg')
12
+ w.unit.should == 'kg'
13
+ w.is?('kg').should be_true
14
+ w.is?('kilograms').should be_true
15
+ w.is?('g').should be_false
16
+ w.to_f.should == 153.27
17
+ w.should == 153.27
18
+ w.should == U(153.27, 'kg')
19
+ w.should == U('153.27', 'kg')
20
+ end
21
+
22
+ it "should reject unknown units" do
23
+ proc { U('153') }.should raise_error(ArgumentError)
24
+ proc { U('153 glorbs') }.should raise_error(ArgumentError)
25
+ proc { U(153, 'glorbs') }.should raise_error(ArgumentError)
26
+ end
27
+
28
+ it "should require an amount" do
29
+ proc { U('kg') }.should raise_error(ArgumentError)
30
+ proc { U('hai kg') }.should raise_error(ArgumentError)
31
+ proc { U('hai', 'kg') }.should raise_error(ArgumentError)
32
+ end
33
+
34
+ it "should allow symbols for units" do
35
+ U(2, :kg).should == U(2, 'kilograms')
36
+ end
37
+
38
+ it "should allow constructing with different units" do
39
+ w = U(53.97, 'oz')
40
+ w.in('kg').should == U(1.53, 'kg')
41
+ w.should == U('53.97 oz')
42
+ end
43
+
44
+ it "should allow constructing from another compatible Unit object" do
45
+ w = U(53.97, 'oz')
46
+ w2 = U(w) # defaults to oz, same units as copying object
47
+ w3 = U(w, 'kg')
48
+ w.should == w2
49
+ w.should == w3
50
+ w2.should == w3
51
+ w2.should == 53.97
52
+ w3.should == 1.53
53
+ end
54
+
55
+ describe "equality" do
56
+ it "should compare between units" do
57
+ w = U(3.5, 'kg')
58
+ lbs = w.in('pounds')
59
+ lbs.to_f.should be_near(7.7162)
60
+ w.should == lbs
61
+ w.should == 3.5
62
+ w.in('lbs').should == 7.716
63
+ end
64
+
65
+ it "should not compare with strings" do
66
+ w = U(1, 'kg')
67
+ proc { w == "5" }.should raise_error(ArgumentError)
68
+ end
69
+
70
+ it "should not allow equality between incompatible units" do
71
+ w = U(53.97, 'oz')
72
+ v = U(2, 'gallons')
73
+ proc { w == v }.should raise_error(ArgumentError)
74
+ end
75
+ end
76
+
77
+ it "should infer in_* methods for defined units" do
78
+ w = U(1, 'kg')
79
+ w.in_grams.should == 1000
80
+ w.in_kilograms.should == 1
81
+ end
82
+
83
+ describe "changing units in place" do
84
+ it "should allow changing units with attr writer" do
85
+ w = U(1, 'kg')
86
+ w.to_f.should == 1
87
+ w.unit = 'g'
88
+ w.to_f.should == 1000
89
+ end
90
+
91
+ it "should not allow changing to an unknown unit" do
92
+ w = U(1, 'kg')
93
+ proc { w.in!('blorg') }.should raise_error(ArgumentError)
94
+ end
95
+
96
+ it "should not allow changing to an incompatible unit" do
97
+ w = U(1, 'kg')
98
+ proc { w.in!('minutes') }.should raise_error(ArgumentError)
99
+ end
100
+ end
101
+
102
+ describe "changing units with in()" do
103
+ it "should handle basic conversions" do
104
+ w = U(1.53, 'kg')
105
+ w.in('grams').should == 1530
106
+ w.in('lbs').to_f.should be_near(3.37)
107
+ w.in('lbs').in('ounces').to_f.should be_near(53.97)
108
+ w.in('pounds').in('grams').in('kg').to_f.should be_near(1.53)
109
+ end
110
+
111
+ it "should not allow changing to an unknown unit" do
112
+ w = U(1, 'kg')
113
+ proc { w.in('blorg') }.should raise_error(ArgumentError)
114
+ proc { w.in_blorg }.should raise_error(ArgumentError)
115
+ end
116
+
117
+ it "should not allow changing to an incompatible unit" do
118
+ w = U(1, 'kg')
119
+ proc { w.in('minutes') }.should raise_error(ArgumentError)
120
+ proc { w.in_hours }.should raise_error(ArgumentError)
121
+ end
122
+ end
123
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: beerxml
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 27
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 0
9
- - 1
10
- version: 0.0.1
9
+ - 2
10
+ version: 0.0.2
11
11
  platform: ruby
12
12
  authors:
13
13
  - Brian Palmer
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-03-06 00:00:00 -07:00
18
+ date: 2011-03-07 00:00:00 -07:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -86,15 +86,15 @@ dependencies:
86
86
  requirements:
87
87
  - - ~>
88
88
  - !ruby/object:Gem::Version
89
- hash: 3
89
+ hash: 9
90
90
  segments:
91
91
  - 2
92
- - 0
93
- version: "2.0"
92
+ - 5
93
+ version: "2.5"
94
94
  type: :development
95
95
  version_requirements: *id005
96
96
  - !ruby/object:Gem::Dependency
97
- name: ruby-debug
97
+ name: autotest-standalone
98
98
  prerelease: false
99
99
  requirement: &id006 !ruby/object:Gem::Requirement
100
100
  none: false
@@ -108,7 +108,7 @@ dependencies:
108
108
  type: :development
109
109
  version_requirements: *id006
110
110
  - !ruby/object:Gem::Dependency
111
- name: rcov
111
+ name: autotest-growl
112
112
  prerelease: false
113
113
  requirement: &id007 !ruby/object:Gem::Requirement
114
114
  none: false
@@ -122,7 +122,7 @@ dependencies:
122
122
  type: :development
123
123
  version_requirements: *id007
124
124
  - !ruby/object:Gem::Dependency
125
- name: yard
125
+ name: rcov
126
126
  prerelease: false
127
127
  requirement: &id008 !ruby/object:Gem::Requirement
128
128
  none: false
@@ -135,6 +135,20 @@ dependencies:
135
135
  version: "0"
136
136
  type: :development
137
137
  version_requirements: *id008
138
+ - !ruby/object:Gem::Dependency
139
+ name: yard
140
+ prerelease: false
141
+ requirement: &id009 !ruby/object:Gem::Requirement
142
+ none: false
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ hash: 3
147
+ segments:
148
+ - 0
149
+ version: "0"
150
+ type: :development
151
+ version_requirements: *id009
138
152
  description: |-
139
153
  Library for parsing and generating beerxml (http://www.beerxml.com/).
140
154
  More than that, this library also contains various methods for doing calculations on beer ingredients and recipes, among other helpers.
@@ -149,6 +163,7 @@ extra_rdoc_files: []
149
163
  files:
150
164
  - .gitignore
151
165
  - Gemfile
166
+ - LICENSE
152
167
  - README.md
153
168
  - Rakefile
154
169
  - beerxml.gemspec
@@ -163,12 +178,18 @@ files:
163
178
  - examples/beerxml.com/water.xml
164
179
  - examples/beerxml.com/yeast.xml
165
180
  - lib/beerxml.rb
181
+ - lib/beerxml/fermentable.rb
166
182
  - lib/beerxml/hop.rb
167
183
  - lib/beerxml/model.rb
184
+ - lib/beerxml/properties.rb
168
185
  - lib/beerxml/recipe.rb
186
+ - lib/beerxml/unit.rb
169
187
  - lib/beerxml/version.rb
188
+ - spec/hops_spec.rb
170
189
  - spec/parsing_spec.rb
190
+ - spec/recipes_spec.rb
171
191
  - spec/spec_helper.rb
192
+ - spec/units_spec.rb
172
193
  has_rdoc: true
173
194
  homepage: https://github.com/codekitchen/beerxml
174
195
  licenses: []
@@ -204,5 +225,8 @@ signing_key:
204
225
  specification_version: 3
205
226
  summary: Library for parsing and generating beerxml (http://www.beerxml.com/)
206
227
  test_files:
228
+ - spec/hops_spec.rb
207
229
  - spec/parsing_spec.rb
230
+ - spec/recipes_spec.rb
208
231
  - spec/spec_helper.rb
232
+ - spec/units_spec.rb