BOIL_EVAPORATION = 0.0 GAL_PER_LITER = 1.0 / LITER_PER_GAL GRAIN_TYPE_DME = u"dme" GRAIN_TYPE_LME = u"lme" HOP_TYPE_PELLET = u"pellet" HOP_UTILIZATION_SCALE_PELLET = 1.1  # 110% HOPS_CONSTANT_IMPERIAL = MG_PER_OZ * GAL_PER_LITER HOPS_CONSTANT_SI = 1.0 IMPERIAL_TYPES = {
    u"volume": u"gallon",
    u"weight_large": u"lbs",
    u"weight_small": u"oz",
    u"temperature": u"degF",
} IMPERIAL_UNITS = u"imperial" LITER_PER_GAL = 3.78541 PPG_DME = 44.0 PPG_CEREAL = 30.0 SI_TYPES = {
    u"volume": u"liter",
    u"weight_large": u"kg",
    u"weight_small": u"mg",
    u"temperature": u"degC",
} SI_UNITS = u"metric" WATER_WEIGHT_IMPERIAL = 8.32  # lbs / gallon WATER_WEIGHT_SI = 1.0  # kg / liter class ColorException(BrewdayException):
    pass class RecipeException(BrewdayException):
    pass class GrainAddition(object):
    """
    A representation of the grain as added to a Recipe.
    """

    def __init__(
        self, grain, weight=None, grain_type=GRAIN_TYPE_CEREAL, units=IMPERIAL_UNITS
    ):
        """
        :param Grain grain: The Grain object
        :param float weight: The weight of the grain addition
        :param str grain_type: The type of the grain being used
        :param str units: The units
        """
        self.grain = grain
        self.weight = weight
        self.grain_type = grain_type

        # Manage units
        self.set_units(units)

    def set_units(self, units):
        """
        Set the units and unit types

        :param str units: The units
        """
        self.units = validate_units(units)
        if self.units == IMPERIAL_UNITS:
            self.types = IMPERIAL_TYPES
        elif self.units == SI_UNITS:
            self.types = SI_TYPES

    def change_units(self):
        """
        Change units of the class from one type to the other

        :return: Grain Addition in new unit type
        :rtype: GrainAddition
        """
        if self.units == IMPERIAL_UNITS:
            weight = self.weight * KG_PER_POUND
            units = SI_UNITS
        elif self.units == SI_UNITS:
            weight = self.weight * POUND_PER_KG
            units = IMPERIAL_UNITS
        return GrainAddition(self.grain, weight=weight, units=units)

    def __str__(self):
        if sys.version_info[0] >= 3:
            return self.__unicode__()
        else:
            return self.__unicode__().encode(u"utf8")

    def __unicode__(self):
        return u"{grain}, weight {weight} {weight_large}".format(
            grain=self.grain, weight=self.weight, **self.types
        )

    def __repr__(self):
        out = u"{0}({1}".format(type(self).__name__, repr(self.grain))
        if self.weight:
            out = u"{0}, weight={1}".format(out, self.weight)
        if self.grain_type:
            out = u"{0}, grain_type='{1}'".format(out, self.grain_type)
        out = u"{0}, units='{1}'".format(out, self.units)
        out = u"{0})".format(out)
        return out

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        if (
            (abs(1.0 - self.weight / other.weight) < WEIGHT_TOLERANCE)
            and (self.grain_type == other.grain_type)
            and (self.units == other.units)
            and (self.grain == other.grain)
        ):
            return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def get_cereal_weight(self, ppg=PPG_CEREAL):
        """
        Get the weight of the addition in cereal weight

        :param float ppg: The potential points per gallon
        :return: Cereal weight
        :rtype: float
        """
        return self.convert_to_cereal(ppg=ppg).weight

    def get_lme_weight(self):
        """
        Get the weight of the addition in Liquid Malt Extract weight

        :return: LME weight
        :rtype: float
        """
        return self.convert_to_lme().weight

    def get_dme_weight(self):
        """
        Get the weight of the addition in Dry Malt Extract weight

        :return: Dry weight
        :rtype: float
        """
        return self.convert_to_dme().weight

    def get_weight_map(self):
        """
        Get map of grain weights by type

        :return: Grain weights
        :rtype: dict
        """
        return {
            u"grain_weight": round(self.get_cereal_weight(), 2),
            u"lme_weight": round(self.get_lme_weight(), 2),
            u"dry_weight": round(self.get_dme_weight(), 2),
        }

    def convert_to_cereal(self, ppg=None, brew_house_yield=1.0):
        """
        Convert Grain Addition to GRAIN_TYPE_CEREAL

        :param float ppg: The potential points per gallon
        :param float brew_house_yield: The brew house yield as a percentage
        :return: GrainAddition of type GRAIN_TYPE_CEREAL
        :rtype: GrainAddition
        """
        if self.grain_type == GRAIN_TYPE_CEREAL:
            return self
        if not ppg:
            raise GrainException("Must provide PPG to convert to cereal")

        validate_percentage(brew_house_yield)
        new_grain = self.grain.convert_to_cereal(ppg=ppg)
        ppg_factor = self.grain.ppg / new_grain.ppg

        # When converting away from cereal BHY works in reverse
        weight = self.weight * ppg_factor / brew_house_yield
        return GrainAddition(
            new_grain, weight=weight, grain_type=GRAIN_TYPE_CEREAL, units=self.units
        )

    def convert_to_lme(self, ppg=PPG_LME, brew_house_yield=1.0):
        """
        Convert Grain Addition to GRAIN_TYPE_LME

        :param float ppg: The potential points per gallon
        :param float brew_house_yield: The brew house yield as a percentage
        :return: GrainAddition of type GRAIN_TYPE_LME
        :rtype: GrainAddition
        """
        if self.grain_type == GRAIN_TYPE_LME:
            return self

        # BHY applies to cereal grains
        validate_percentage(brew_house_yield)
        if self.grain_type == GRAIN_TYPE_DME:
            brew_house_yield = 1.0
        new_grain = self.grain.convert_to_lme(ppg=ppg)
        ppg_factor = self.grain.ppg / new_grain.ppg
        weight = self.weight * ppg_factor * brew_house_yield
        return GrainAddition(
            new_grain, weight=weight, grain_type=GRAIN_TYPE_LME, units=self.units
        )

    def convert_to_dme(self, ppg=PPG_DME, brew_house_yield=1.0):
        """
        Convert Grain Addition to GRAIN_TYPE_DME

        :param float ppg: The potential points per gallon
        :param float brew_house_yield: The brew house yield as a percentage
        :return: GrainAddition of type GRAIN_TYPE_DME
        :rtype: GrainAddition
        """
        if self.grain_type == GRAIN_TYPE_DME:
            return self

        # BHY applies to cereal grains
        validate_percentage(brew_house_yield)
        if self.grain_type == GRAIN_TYPE_LME:
            brew_house_yield = 1.0
        new_grain = self.grain.convert_to_dme(ppg=ppg)
        ppg_factor = self.grain.ppg / new_grain.ppg
        weight = self.weight * ppg_factor * brew_house_yield
        return GrainAddition(
            new_grain, weight=weight, grain_type=GRAIN_TYPE_DME, units=self.units
        )

    @property
    def gu(self):
        return self.get_gravity_units()

    def get_gravity_units(self):
        """
        Get the gravity units for the Grain Addition
        :return: Gravity Units as PPG or HWE depending on units
        :rtype: float
        """
        # Pick the attribute based on units
        if self.units == IMPERIAL_UNITS:
            attr = u"ppg"
        if self.units == SI_UNITS:
            attr = u"hwe"

        return getattr(self.grain, attr) * self.weight

    def to_dict(self):
        grain_data = self.grain.to_dict()
        return {
            u"name": grain_data.pop("name"),
            u"data": grain_data,
            u"weight": round(self.weight, 2),
            u"grain_type": self.grain_type,
            u"units": self.units,
        }

    def to_json(self):
        return json.dumps(self.to_dict(), sort_keys=True)

    @classmethod
    def validate(cls, grain_data):
        required_fields = [(u"name", str), (u"weight", float)]
        optional_fields = [
            (u"color", (int, float)),
            (u"ppg", (int, float)),
            (u"hwe", (int, float)),
            (u"grain_type", str),
            (u"units", str),
        ]
        validate_required_fields(grain_data, required_fields)
        validate_optional_fields(grain_data, optional_fields)

    def format(self):
        kwargs = {}
        kwargs.update(self.to_dict())
        kwargs.update(self.types)
        msg = textwrap.dedent(
            u"""\
                {name} Addition
                -----------------------------------
                Grain Type:        {grain_type}
                Weight:            {weight:0.2f} {weight_large}""".format(
                **kwargs
            )
        )
        return msg class HopAddition(object):
    """
    A representation of the Hop as added to a Recipe.
    """

    def __init__(
        self,
        hop,
        weight=None,
        boil_time=None,
        hop_type=HOP_TYPE_PELLET,
        utilization_cls=HopsUtilizationGlennTinseth,
        utilization_cls_kwargs=None,
        units=IMPERIAL_UNITS,
    ):
        """
        :param Hop hop: The Hop object
        :param float weight: The weight of the hop addition
        :param float boil_time: The amount of time the hop is boiled
        :param float hop_type: The type of the hop being used
        :param HopsUtilization utilization_cls: The utilization class used for calculation
        :param dict utilization_cls_kwargs: The kwargs to initialize the utilization_cls object
        :param str units: The units
        """  # noqa
        self.hop = hop
        self.weight = weight
        self.boil_time = boil_time
        self.hop_type = validate_hop_type(hop_type)
        self.utilization_cls_kwargs = utilization_cls_kwargs or {}
        self.utilization_cls = utilization_cls(
            self, **self.utilization_cls_kwargs
        )  # noqa

        # Manage units
        self.set_units(units)

    def set_units(self, units):
        """
        Set the units and unit types

        :param str units: The units
        """
        self.units = validate_units(units)
        if self.units == IMPERIAL_UNITS:
            self.types = IMPERIAL_TYPES
        elif self.units == SI_UNITS:
            self.types = SI_TYPES

    def change_units(self):
        """
        Change units of the class from one type to the other

        :return: Hop Addition in new unit type
        :rtype: HopAddition
        """
        if self.units == IMPERIAL_UNITS:
            weight = self.weight * MG_PER_OZ
            units = SI_UNITS
        elif self.units == SI_UNITS:
            weight = self.weight * OZ_PER_MG
            units = IMPERIAL_UNITS
        return HopAddition(
            self.hop,
            weight=weight,
            boil_time=self.boil_time,
            utilization_cls_kwargs={u"units": units},
            units=units,
        )

    def __str__(self):
        if sys.version_info[0] >= 3:
            return self.__unicode__()
        else:
            return self.__unicode__().encode(u"utf8")

    def __unicode__(self):
        return u"{hop}, {weight} {weight_small}, {boil_time} min, {hop_type}".format(  # noqa
            hop=self.hop,
            weight=self.weight,
            boil_time=self.boil_time,
            hop_type=self.hop_type,
            **self.types
        )

    def __repr__(self):
        out = u"{0}({1}".format(type(self).__name__, repr(self.hop))
        if self.weight:
            out = u"{0}, weight={1}".format(out, self.weight)
        if self.boil_time:
            out = u"{0}, boil_time={1}".format(out, self.boil_time)
        if self.hop_type:
            out = u"{0}, hop_type='{1}'".format(out, self.hop_type)
        if self.utilization_cls:
            out = u"{0}, utilization_cls={1}".format(
                out, type(self.utilization_cls).__name__
            )  # noqa
        if self.utilization_cls_kwargs:
            out = u"{0}, utilization_cls_kwargs={1}".format(
                out, str(self.utilization_cls_kwargs)
            )  # noqa
        out = u"{0}, units='{1}'".format(out, self.units)
        out = u"{0})".format(out)
        return out

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        if (
            (self.hop == other.hop)
            and (self.boil_time == other.boil_time)
            and (abs(1.0 - self.weight / other.weight) < WEIGHT_TOLERANCE)
            and (self.hop_type == other.hop_type)
            and (self.units == other.units)
        ):
            return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def to_dict(self):
        hop_data = self.hop.to_dict()
        return {
            u"name": hop_data.pop(u"name"),
            u"data": hop_data,
            u"weight": round(self.weight, 2),
            u"boil_time": round(self.boil_time, 1),
            u"hop_type": self.hop_type,
            u"utilization_cls": str(self.utilization_cls),
            u"utilization_cls_kwargs": self.utilization_cls_kwargs,
            u"units": self.units,
        }

    def to_json(self):
        return json.dumps(self.to_dict(), sort_keys=True)

    @classmethod
    def validate(cls, hop_data):
        required_fields = [(u"name", str), (u"weight", float), (u"boil_time", float)]
        optional_fields = [
            (u"percent_alpha_acids", float),
            (u"hop_type", str),
            (u"units", str),
        ]
        validate_required_fields(hop_data, required_fields)
        validate_optional_fields(hop_data, optional_fields)

    def format(self):
        kwargs = {}
        kwargs.update(self.to_dict())
        kwargs.update(self.types)
        msg = textwrap.dedent(
            u"""\
                {name} Addition
                -----------------------------------
                Hop Type:     {hop_type}
                AA %:         {data[percent_alpha_acids]:0.1%}
                Weight:       {weight:0.2f} {weight_small}
                Boil Time:    {boil_time:0.1f} min""".format(
                **kwargs
            )
        )
        return msg

    def get_ibus(self, sg, final_volume):
        """
        Get the IBUs

        :param float sg: The specific gravity of the wort
        :param float final_volume: The final volume of the wort
        :return: The IBUs of the wort
        :rtype: float
        """
        return self.utilization_cls.get_ibus(sg, final_volume)

    def get_alpha_acid_units(self):
        """
        Get Alpha Acid Units

        :return: alpha acid units
        :rtype: float
        """
        alpha_acids = self.hop.percent_alpha_acids * 100
        if self.units == IMPERIAL_UNITS:
            return self.weight * alpha_acids
        elif self.units == SI_UNITS:
            return self.weight * OZ_PER_MG * alpha_acids def alcohol_by_volume_alternative(og, fg):
    """
    Alcohol by Volume Alternative Calculation

    :param float og: Original Gravity
    :param float fg: Final Gravity
    :return: Alcohol by Volume decimal percentage
    :rtype: float

    Alternate Formula:

    A more complex equation which attempts to provide greater accuracy at higher gravities is:

    :math:`\\text{ABV} = \\frac{76.08 \\times \\big( \\text{og} - \\text{fg} \\big)}{1.775 - \\text{og}} \\times \\frac{\\text{fg}}{0.794}`

    This comes from Balling's famous formula, where the Original Extract and
    Real Extract values have been converted using the simple Plato to SG
    equation, giving Alcohol by Weight.  This is then converted to Alcohol
    by Volume multiplying by the ratio of Final Gravity to Density of Ethanol.

    The alternate equation reports a higher ABV for higher gravity beers.
    This equation is just a different take on it. Scientists rarely agree
    when it comes to equations. There will probably be another equation for
    ABV down the road.

    The complex formula, and variations on it come from:

    * Ritchie Products Ltd, (Zymurgy, Summer 1995, vol. 18, no. 2)
    * Michael L. Hall's article Brew by the Numbers: Add Up What's in Your Beer, and Designing Great Beers by Daniels.

    Source:

    * http://www.brewersfriend.com/2011/06/16/alcohol-by-volume-calculator-updated/
    """  # noqa
    # Density listed (possibly incorrectly) from Zymergy Mag
    DENSITY_ETHANOL = 0.794
    return (76.08 * (og - fg) / (1.775 - og)) * (fg / DENSITY_ETHANOL) / 100.0 def alcohol_by_volume_standard(og, fg):
    """
    Alcohol by Volume Standard Calculation

    :param float og: Original Gravity
    :param float fg: Final Gravity
    :return: Alcohol by Volume decimal percentage
    :rtype: float

    Most brewing sites use this basic formula:

    :math:`\\text{ABV} = \\big(\\text{og} - \\text{fg}\\big) \\times 131.25`

    This equation was created before the computer age.  It is easy to do by
    hand, and over time became the accepted formula for home brewers!

    Variations on this equation which report within tenths of each other
    come from The Joy of Homebrewing Method by Charlie Papazian, Bee Lee's
    Method, Beer Advocate Method. Some variations use 131 instead of 131.25.
    The resulting difference is pretty minor.

    Source:

    * http://www.brewersfriend.com/2011/06/16/alcohol-by-volume-calculator-updated/
    * http://www.brewmorebeer.com/calculate-percent-alcohol-in-beer/

    :math:`\\text{ABV} = \\frac{46.07 \\text{g/mol C2H6O}}{44.0095 \\text{g/mol CO2}} \\times \\frac{1.0}{0.7936} \\times 100 \\times (og - fg)`
    """  # noqa
    return (og - fg) * ABV_CONST / 100.0 def alcohol_by_weight(abv):
    """
    Alcohol by Weight from ABV

    :param float abv: Alcohol by Volume
    :return: Alcohol by Weight
    :rtype: float
    """
    return abv * ALCOHOL_SPECIFIC_GRAVITY def final_gravity_from_abv_standard(og, abv):
    """
    Final Gravity from ABV Standard

    :param float og: Original Gravity
    :param float abv: Alcohol by Volume decimal percentage
    :return: Final Gravity
    :rtype: float
    """
    return og - (abv * 100.0) / ABV_CONST def calculate_mcu(grain_weight, beer_color, final_volume, units=IMPERIAL_UNITS):
    """
    Calculate MCU from Grain

    :param float grain_weight: Grain weight in lbs or kg
    :param float beer_color: Beer color in deg Lovibond
    :param float final_volume: Final Volume in gal or liters
    :param str units: The units

    Source:

    * http://beersmith.com/blog/2008/04/29/beer-color-understanding-srm-lovibond-and-ebc/
    """  # noqa
    validate_units(units)
    if units == SI_UNITS:
        grain_weight = grain_weight * POUND_PER_KG
        final_volume = final_volume * GAL_PER_LITER

    mcu = grain_weight * beer_color / final_volume
    return mcu def calculate_srm(mcu):
    """
    General SRM calculation uses the Morey Power Equation

    :param float mcu: The Malt Color Units
    :return: SRM Color
    :rtype: float
    :raises ColorException: If the SRM is > 50.0
    """
    return calculate_srm_morey(mcu) def calculate_srm_daniels(mcu):
    """
    Daniels Equation for SRM

    :param float mcu: The Malt Color Units
    :return: SRM Color
    :rtype: float
    :raises ColorException: If the MCU is < 11.0
    """  # noqa
    if mcu < 11.0:
        raise ColorException(u"Daniels equation does not work for MCU < 11.0")
    srm = (mcu * 0.2) + 8.4
    return srm def calculate_srm_morey(mcu):
    """
    Morey Equation for SRM

    :param float mcu: The Malt Color Units
    :return: SRM Color
    :rtype: float
    :raises ColorException: If the SRM is > 50.0

    Source:

    * http://www.morebeer.com/brewingtechniques/beerslaw/morey.html
    * http://beersmith.com/blog/2008/04/29/beer-color-understanding-srm-lovibond-and-ebc/
    """  # noqa
    srm = 1.4922 * (mcu ** 0.6859)
    if srm > 50.0:
        raise ColorException(u"Morey equation does not work above SRM 50.0")
    return srm def calculate_srm_mosher(mcu):
    """
    Mosher Equation for SRM

    :param float mcu: The Malt Color Units
    :return: SRM Color
    :rtype: float
    :raises ColorException: If the MCU is < 7.0
    """  # noqa
    if mcu < 7.0:
        raise ColorException(u"Mosher equation does not work for MCU < 7.0")
    srm = (mcu * 0.3) + 4.7
    return srm def srm_to_ebc(srm):
    """
    Convert SRM to EBC Color

    :param float srm: SRM Color
    :return: EBC Color
    :rtype: float
    """
    return srm * 1.97 def get_wort_correction(
    boil_gravity, boil_volume, final_gravity, final_volume, efficiency=PPG_DME
):
    """
    Get amount of sugar to add to correct wort

    Calculate how much additional DME or LME must be added to correct the boil
    to hit target final gravity.

    :param float boil_gravity: The gravity of the boil in GU
    :param float boil_volume: The volume of the boil
    :param float final_gravity: The desired final gravity in GU
    :param float final_volume: The desired final volume
    :param float efficiency: The efficiency of the medium to add in PPG (GU/lbs/gallon)
    :return: The amount of DME or LME to add to the boil in lbs
    :rtype: float
    """  # noqa
    return (
        final_gravity * final_volume - boil_gravity * boil_volume
    ) / efficiency  # noqa class HopsUtilizationGlennTinseth(HopsUtilization):
    """
    Glenn Tinseth Hops Utilization Method

    Best for all grain brewing.
    """

    def __str__(self):
        return u"Glenn Tinseth"

    @classmethod
    def get_bigness_factor(cls, sg):
        """
        Get Bigness Factor

        :param float sg: Specific Gravity
        :return: Bigness Factor
        :rtype: float

        The Bigness factor accounts for reduced utilization due to higher wort
        gravities. Use an average gravity value for the entire boil to account
        for changes in the wort volume.

        :math:`\\text{Bigness factor} = 1.65 \\times 0.000125^{\\text{wort gravity} - 1}`
        """  # noqa
        return 1.65 * 0.000125 ** (sg - 1)

    @classmethod
    def get_boil_time_factor(cls, boil_time):
        """
        Get Boil Time Factor

        :param float boil_time: Boil Time in minutes
        :return: Boil Time Factor
        :rtype: float

        The Boil Time factor accounts for the change in utilization due to
        boil time:

        :math:`\\text{Boil Time factor} = \\frac{1 - e^{-0.04 \\times \\text{time in mins}}}{4.15}`
        """  # noqa
        return (1 - math.exp(-0.04 * boil_time)) / 4.15

    @classmethod
    def get_percent_utilization(cls, sg, boil_time):
        """
        Get the percent utilization

        :param float sg: Specific Gravity
        :param float boil_time: The Boil Time in minutes
        :return: The percent utilization
        :rtype: float

        Source:

        * http://www.realbeer.com/hops/research.html
        * http://www.rooftopbrew.net/ibu.php
        """
        bigness_factor = cls.get_bigness_factor(sg)
        boil_time_factor = cls.get_boil_time_factor(boil_time)
        return bigness_factor * boil_time_factor def gu_to_sg(gu):
    """
    Gravity Units to Specific Gravity

    :param float gu: Gravity Units
    :return: Specific Gravity
    :rtype: float
    """
    return 1 + (gu / 1000.0) def sg_to_gu(sg):
    """
    Specific Gravity to Gravity Units

    :param float sg: Specific Gravity
    :return: Gravity Units
    :rtype: float
    """
    return (sg - 1) * 1000.0 def sg_to_plato(sg):
    """
    Specific Gravity to Degrees Plato

    :param float sg: Specific Gravity
    :return: Degrees Plato
    :rtype: float

    :math:`\\text{Plato} = \\frac{\\big(\\text{SG} - 1\\big) \\times 1000}{4}`

    The more precise calculation of Plato is:

    :math:`\\text{Plato} = -616.868 + 1111.14 \\times sg - 630.272 \\times sg^2 + 135.997 \\times sg^3`

    Source:

    * http://www.brewersfriend.com/2012/10/31/on-the-relationship-between-plato-and-specific-gravity/
    """  # noqa
    # return (sg - 1.0) * 1000 / 4
    return ((135.997 * sg - 630.272) * sg + 1111.14) * sg - 616.868 def validate_grain_type(grain_type):
    """
    Validate a grain type

    :param str grain_type: Type of Grain
    :return: grain type
    :rtype: str
    :raises ValidatorException: If grain type is unknown
    """
    if grain_type in GRAIN_TYPE_LIST:
        return grain_type
    raise ValidatorException(
        u"Unkown grain type '{}', must use {}".format(
            grain_type, u", ".join(GRAIN_TYPE_LIST)
        )
    ) def validate_hop_type(hop_type):
    """
    Validate a hop type

    :param str hop_type: Type of Grain
    :return: hop type
    :rtype: str
    :raises ValidatorException: If hop type is unknown
    """
    if hop_type in HOP_TYPE_LIST:
        return hop_type
    raise ValidatorException(
        u"Unkown hop type '{}', must use {}".format(hop_type, u", ".join(HOP_TYPE_LIST))
    ) def validate_optional_fields(data, optional_fields, data_field=u"data"):
    """
    Validate fields which are optional as part of the data.

    :param dict data: A python dictionary to check for required fields
    :param list(tuple) optional_fields: Values and types to check for in data
    :param str data_field: The key in the data dictionary containing the optional fields
    :raises ValidatorException: Optional field is of the wrong type

    The format is a list of tuples where the first element is a string with
    a value that should be a key found in the data dict and
    where the second element is a python type or list/tuple of
    python types to check the field against.
    """  # noqa
    # If no optional data field present then return
    if data_field not in data:
        return
    for field, field_type in optional_fields:
        if field in data[data_field]:
            if field_type == str:
                try:
                    field_type = unicode
                except NameError:
                    field_type = str
            # With optional fields only check the type as they are overrides
            # and not all overrides need to be present
            if not isinstance(data[data_field][field], field_type):
                raise ValidatorException(
                    u"Optional field '{}' in '{}' is not of type '{}'".format(  # noqa
                        field, data_field, field_type
                    )
                ) def validate_percentage(percent):
    """
    Validate decimal percentage

    :param float percent: Percentage between 0.0 and 1.0
    :return: percentage
    :rtype: float
    :raises ValidatorException: If decimal percentage not between 0.0 and 1.0
    """
    if 0.0 <= percent <= 1.0:
        return percent
    raise ValidatorException(u"Percentage values should be in decimal format") def validate_required_fields(data, required_fields):
    """
    Validate fields which are required as part of the data.

    :param dict data: A python dictionary to check for required fields
    :param list(tuple) required_fields: Values and types to check for in data
    :raises ValidatorException: Required field is missing from data
    :raises ValidatorException: Required field is of the wrong type

    The format is a list of tuples where the first element is a string with
    a value that should be a key found in the data dict and
    where the second element is a python type or list/tuple of
    python types to check the field against.
    """
    for field, field_type in required_fields:
        if field not in data:
            raise ValidatorException(
                u"Required field '{}' missing from data".format(field)  # noqa
            )
        if field_type == str:
            try:
                field_type = unicode
            except NameError:
                field_type = str
        if not isinstance(data[field], field_type):
            raise ValidatorException(
                u"Required field '{}' is not of type '{}'".format(  # noqa
                    field, field_type
                )
            ) def validate_units(units):
    """
    Validate units

    :param str units: Unit type
    :return: units
    :rtype: str
    :raises ValidatorException: If units is unknown
    """
    if units in [IMPERIAL_UNITS, SI_UNITS]:
        return units
    raise ValidatorException(
        u"Unkown units '{}', must use {} or {}".format(units, IMPERIAL_UNITS, SI_UNITS)
    )
import json
import sys
import textwrap
from .constants import BOIL_EVAPORATION
from .constants import GAL_PER_LITER
from .constants import GRAIN_TYPE_DME
from .constants import GRAIN_TYPE_LME
from .constants import HOP_TYPE_PELLET
from .constants import HOP_UTILIZATION_SCALE_PELLET
from .constants import HOPS_CONSTANT_IMPERIAL
from .constants import HOPS_CONSTANT_SI
from .constants import IMPERIAL_TYPES
from .constants import IMPERIAL_UNITS
from .constants import LITER_PER_GAL
from .constants import PPG_DME
from .constants import PPG_CEREAL
from .constants import SI_TYPES
from .constants import SI_UNITS
from .constants import WATER_WEIGHT_IMPERIAL
from .constants import WATER_WEIGHT_SI
from .exceptions import ColorException
from .exceptions import RecipeException
from .grains import GrainAddition
from .hops import HopAddition
from .utilities.abv import alcohol_by_volume_alternative
from .utilities.abv import alcohol_by_volume_standard
from .utilities.abv import alcohol_by_weight
from .utilities.abv import final_gravity_from_abv_standard
from .utilities.color import calculate_mcu
from .utilities.color import calculate_srm
from .utilities.color import calculate_srm_daniels
from .utilities.color import calculate_srm_morey
from .utilities.color import calculate_srm_mosher
from .utilities.color import srm_to_ebc
from .utilities.efficiency import get_wort_correction
from .utilities.hops import HopsUtilizationGlennTinseth
from .utilities.sugar import gu_to_sg
from .utilities.sugar import sg_to_gu
from .utilities.sugar import sg_to_plato
from .validators import validate_grain_type
from .validators import validate_hop_type
from .validators import validate_optional_fields
from .validators import validate_percentage
from .validators import validate_required_fields
from .validators import validate_units
# -*- coding: utf-8 -*-


__all__ = [u"Recipe", u"RecipeBuilder"]


class Recipe(object):
    """
    A representation of a Recipe that can be brewed to make beer.
    """

    grain_lookup = {}
    hop_lookup = {}

    def __init__(
        self,
        name,
        grain_additions=None,
        hop_additions=None,
        yeast=None,
        brew_house_yield=0.70,
        start_volume=7.0,
        final_volume=5.0,

        units=IMPERIAL_UNITS,