Coverage for cookbook/helper/unit_conversion_helper.py: 100%
64 statements
« prev ^ index » next coverage.py v7.4.0, created at 2023-12-29 01:02 +0100
« prev ^ index » next coverage.py v7.4.0, created at 2023-12-29 01:02 +0100
1from django.core.cache import caches
2from decimal import Decimal
4from cookbook.helper.cache_helper import CacheHelper
5from cookbook.models import Ingredient, Unit
7CONVERSION_TABLE = {
8 'weight': {
9 'g': 1000,
10 'kg': 1,
11 'ounce': 35.274,
12 'pound': 2.20462
13 },
14 'volume': {
15 'ml': 1000,
16 'l': 1,
17 'fluid_ounce': 33.814,
18 'pint': 2.11338,
19 'quart': 1.05669,
20 'gallon': 0.264172,
21 'tbsp': 67.628,
22 'tsp': 202.884,
23 'imperial_fluid_ounce': 35.1951,
24 'imperial_pint': 1.75975,
25 'imperial_quart': 0.879877,
26 'imperial_gallon': 0.219969,
27 'imperial_tbsp': 56.3121,
28 'imperial_tsp': 168.936,
29 },
30}
32BASE_UNITS_WEIGHT = list(CONVERSION_TABLE['weight'].keys())
33BASE_UNITS_VOLUME = list(CONVERSION_TABLE['volume'].keys())
36class ConversionException(Exception):
37 pass
40class UnitConversionHelper:
41 space = None
43 def __init__(self, space):
44 """
45 Initializes unit conversion helper
46 :param space: space to perform conversions on
47 """
48 self.space = space
50 @staticmethod
51 def convert_from_to(from_unit, to_unit, amount):
52 """
53 Convert from one base unit to another. Throws ConversionException if trying to convert between different systems (weight/volume) or if units are not supported.
54 :param from_unit: str unit to convert from
55 :param to_unit: str unit to convert to
56 :param amount: amount to convert
57 :return: Decimal converted amount
58 """
59 system = None
60 if from_unit in BASE_UNITS_WEIGHT and to_unit in BASE_UNITS_WEIGHT:
61 system = 'weight'
62 if from_unit in BASE_UNITS_VOLUME and to_unit in BASE_UNITS_VOLUME:
63 system = 'volume'
65 if not system:
66 raise ConversionException('Trying to convert units not existing or not in one unit system (weight/volume)')
68 return Decimal(amount / Decimal(CONVERSION_TABLE[system][from_unit] / CONVERSION_TABLE[system][to_unit]))
70 def base_conversions(self, ingredient_list):
71 """
72 Calculates all possible base unit conversions for each ingredient give.
73 Converts to all common base units IF they exist in the unit database of the space.
74 For useful results all ingredients passed should be of the same food, otherwise filtering afterwards might be required.
75 :param ingredient_list: list of ingredients to convert
76 :return: ingredient list with appended conversions
77 """
78 base_conversion_ingredient_list = ingredient_list.copy()
79 for i in ingredient_list:
80 try:
81 conversion_unit = i.unit.name
82 if i.unit.base_unit:
83 conversion_unit = i.unit.base_unit
85 # TODO allow setting which units to convert to? possibly only once conversions become visible
86 units = caches['default'].get(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, None)
87 if not units:
88 units = Unit.objects.filter(space=self.space, base_unit__in=(BASE_UNITS_VOLUME + BASE_UNITS_WEIGHT)).all()
89 caches['default'].set(CacheHelper(self.space).BASE_UNITS_CACHE_KEY, units, 60 * 60) # cache is cleared on unit save signal so long duration is fine
91 for u in units:
92 try:
93 ingredient = Ingredient(amount=self.convert_from_to(conversion_unit, u.base_unit, i.amount), unit=u, food=ingredient_list[0].food, )
94 if not any((x.unit.name == ingredient.unit.name or x.unit.base_unit == ingredient.unit.name) for x in base_conversion_ingredient_list):
95 base_conversion_ingredient_list.append(ingredient)
96 except ConversionException:
97 pass
98 except Exception:
99 pass
101 return base_conversion_ingredient_list
103 def get_conversions(self, ingredient):
104 """
105 Converts an ingredient to all possible conversions based on the custom unit conversion database.
106 After that passes conversion to UnitConversionHelper.base_conversions() to get all base conversions possible.
107 :param ingredient: Ingredient object
108 :return: list of ingredients with all possible custom and base conversions
109 """
110 conversions = [ingredient]
111 if ingredient.unit:
112 for c in ingredient.unit.unit_conversion_base_relation.all():
113 if c.space == self.space:
114 r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
115 if r and r not in conversions:
116 conversions.append(r)
117 for c in ingredient.unit.unit_conversion_converted_relation.all():
118 if c.space == self.space:
119 r = self._uc_convert(c, ingredient.amount, ingredient.unit, ingredient.food)
120 if r and r not in conversions:
121 conversions.append(r)
123 conversions = self.base_conversions(conversions)
125 return conversions
127 def _uc_convert(self, uc, amount, unit, food):
128 """
129 Helper to calculate values for custom unit conversions.
130 Converts given base values using the passed UnitConversion object into a converted Ingredient
131 :param uc: UnitConversion object
132 :param amount: base amount
133 :param unit: base unit
134 :param food: base food
135 :return: converted ingredient object from base amount/unit/food
136 """
137 if uc.food is None or uc.food == food:
138 if unit == uc.base_unit:
139 return Ingredient(amount=amount * (uc.converted_amount / uc.base_amount), unit=uc.converted_unit, food=food, space=self.space)
140 else:
141 return Ingredient(amount=amount * (uc.base_amount / uc.converted_amount), unit=uc.base_unit, food=food, space=self.space)