diff --git a/brew/styles.py b/brew/styles.py index 5ca73ed..70c9479 100644 --- a/brew/styles.py +++ b/brew/styles.py @@ -101,6 +101,21 @@ def og_matches(self, og): """ return (self.og[0] <= og <= self.og[1]) + def og_errors(self, og): + """ + Return list of errors if og doesn't match the style + + :param float og: Original Gravity + :return: List + :rtyle: list + """ + errors = [] + if og < self.og[0]: + errors.append('OG is below style') + if og > self.og[1]: + errors.append('OG is above style') + return errors + def fg_matches(self, fg): """ Determine if fg matches the style @@ -111,6 +126,21 @@ def fg_matches(self, fg): """ return (self.fg[0] <= fg <= self.fg[1]) + def fg_errors(self, fg): + """ + Return list of errors if fg doesn't match the style + + :param float fg: Final Gravity + :return: List + :rtyle: list + """ + errors = [] + if fg < self.fg[0]: + errors.append('FG is below style') + if fg > self.fg[1]: + errors.append('FG is above style') + return errors + def abv_matches(self, abv): """ Determine if abv matches the style @@ -121,6 +151,21 @@ def abv_matches(self, abv): """ return (self.abv[0] <= abv <= self.abv[1]) + def abv_errors(self, abv): + """ + Return list of errors if abv doesn't match the style + + :param float abv: Alcohol by Volume + :return: List + :rtyle: list + """ + errors = [] + if abv < self.abv[0]: + errors.append('ABV is below style') + if abv > self.abv[1]: + errors.append('ABV is above style') + return errors + def ibu_matches(self, ibu): """ Determine if ibu matches the style @@ -131,6 +176,21 @@ def ibu_matches(self, ibu): """ return (self.ibu[0] <= ibu <= self.ibu[1]) + def ibu_errors(self, ibu): + """ + Return list of errors if ibu doesn't match the style + + :param float ibu: IBU + :return: List + :rtyle: list + """ + errors = [] + if ibu < self.ibu[0]: + errors.append('IBU is below style') + if ibu > self.ibu[1]: + errors.append('IBU is above style') + return errors + def color_matches(self, color): """ Determine if color matches the style @@ -141,6 +201,21 @@ def color_matches(self, color): """ return (self.color[0] <= color <= self.color[1]) + def color_errors(self, color): + """ + Return list of errors if color doesn't match the style + + :param float color: Color in SRM + :return: List + :rtyle: list + """ + errors = [] + if color < self.color[0]: + errors.append('Color is below style') + if color > self.color[1]: + errors.append('Color is above style') + return errors + def recipe_matches(self, recipe): """ Determine if a recipe matches the style @@ -162,6 +237,28 @@ def recipe_matches(self, recipe): return True return False + def recipe_errors(self, recipe): + """ + Return list errors if the recipe doesn't match the style + + :param Recipe recipe: A Recipe object + :return: Errors + :rtype: list + """ + recipe_og = recipe.get_original_gravity() + recipe_fg = recipe.get_final_gravity() + recipe_abv = alcohol_by_volume_standard(recipe_og, recipe_fg) + recipe_ibu = recipe.get_total_ibu() + recipe_color = recipe.get_total_wort_color() + + errors = [] + errors.extend(self.og_errors(recipe_og)) + errors.extend(self.fg_errors(recipe_fg)) + errors.extend(self.abv_errors(recipe_abv)) + errors.extend(self.ibu_errors(recipe_ibu)) + errors.extend(self.color_errors(recipe_color)) + return errors + def to_dict(self): style_dict = { 'style': self.style, diff --git a/docs/source/tutorial/matching_styles.rst b/docs/source/tutorial/matching_styles.rst index 3696bf5..a3db041 100644 --- a/docs/source/tutorial/matching_styles.rst +++ b/docs/source/tutorial/matching_styles.rst @@ -66,13 +66,22 @@ In order to match the recipe we use a method on the class: True >>> recipe_color = recipe.get_total_wort_color() >>> style.color_matches(recipe_color) - False + True Interestingly the recipe used in the examples does not match the BJCP style! The only feature that matches the style is the IBUs, but the remaining values for og, fg, abv, and color are all too high. That means its time to correct our recipe. +As a short hand you can also get this information in a more friendly way: + +.. code-block:: python + + >>> style.recipe_matches(recipe) + ['OG is above style', 'FG is above style', 'ABV is above style'] + +This will help you quickly discover the problems with your recipe. + Correcting a Recipe ------------------- @@ -136,7 +145,7 @@ crystal 20L has come down from 0.78 lbs to 0.51 lbs. Let's try this again. False It turns out the recipe still doesn't match. Why? It appears that our color -is off. +is now off after our adjustments. Correcting for Color -------------------- diff --git a/tests/test_styles.py b/tests/test_styles.py index 9cc6dd8..c80ff8d 100644 --- a/tests/test_styles.py +++ b/tests/test_styles.py @@ -150,22 +150,62 @@ def test_og_matches(self): out = self.style.og_matches(1.050) self.assertTrue(out) + def test_og_matches_low(self): + out = self.style.og_errors(1.044) + self.assertEquals(out, ['OG is below style']) + + def test_og_matches_high(self): + out = self.style.og_errors(1.061) + self.assertEquals(out, ['OG is above style']) + def test_fg_matches(self): out = self.style.fg_matches(1.012) self.assertTrue(out) + def test_fg_matches_low(self): + out = self.style.fg_errors(1.009) + self.assertEquals(out, ['FG is below style']) + + def test_fg_matches_high(self): + out = self.style.fg_errors(1.016) + self.assertEquals(out, ['FG is above style']) + def test_abv_matches(self): out = self.style.abv_matches(0.050) self.assertTrue(out) + def test_abv_matches_low(self): + out = self.style.abv_errors(0.044) + self.assertEquals(out, ['ABV is below style']) + + def test_abv_matches_high(self): + out = self.style.abv_errors(0.063) + self.assertEquals(out, ['ABV is above style']) + def test_ibu_matches(self): out = self.style.ibu_matches(33.0) self.assertTrue(out) + def test_ibu_matches_low(self): + out = self.style.ibu_errors(29) + self.assertEquals(out, ['IBU is below style']) + + def test_ibu_matches_high(self): + out = self.style.ibu_errors(51) + self.assertEquals(out, ['IBU is above style']) + def test_color_matches(self): out = self.style.color_matches(7.5) self.assertTrue(out) + def test_color_matches_low(self): + out = self.style.color_errors(4) + self.assertEquals(out, ['Color is below style']) + + def test_color_matches_high(self): + out = self.style.color_errors(11) + self.assertEquals(out, ['Color is above style']) + def test_recipe_matches(self): pale_add = GrainAddition(pale, weight=8.69) @@ -189,6 +229,33 @@ def test_recipe_matches_false(self): out = self.style.recipe_matches(recipe) self.assertFalse(out) + def test_recipe_errors(self): + out = self.style.recipe_errors(recipe) + expected = ['OG is above style', + 'FG is above style', + 'ABV is above style'] + self.assertEquals(out, expected) + + def test_recipe_errors_none(self): + pale_add = GrainAddition(pale, + weight=8.69) + crystal_add = GrainAddition(crystal, + weight=1.02) + pale_ale = Recipe(name='pale ale', + grain_additions=[ + pale_add, + crystal_add, + ], + hop_additions=hop_additions, + yeast=yeast, + percent_brew_house_yield=0.70, + start_volume=7.0, + final_volume=5.0, + ) + out = self.style.recipe_errors(pale_ale) + expected = [] + self.assertEquals(out, expected) + def test_to_json(self): out = self.style.to_json() expected = '{"abv": [0.045, 0.062], "category": "18", "color": [5, 10], "fg": [1.01, 1.015], "ibu": [30, 50], "og": [1.045, 1.06], "style": "American Pale Ale", "subcategory": "B"}' # nopep8