beerxml 0.0.1 → 0.0.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.
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