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

1from django.core.cache import caches 

2from decimal import Decimal 

3 

4from cookbook.helper.cache_helper import CacheHelper 

5from cookbook.models import Ingredient, Unit 

6 

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} 

31 

32BASE_UNITS_WEIGHT = list(CONVERSION_TABLE['weight'].keys()) 

33BASE_UNITS_VOLUME = list(CONVERSION_TABLE['volume'].keys()) 

34 

35 

36class ConversionException(Exception): 

37 pass 

38 

39 

40class UnitConversionHelper: 

41 space = None 

42 

43 def __init__(self, space): 

44 """ 

45 Initializes unit conversion helper 

46 :param space: space to perform conversions on 

47 """ 

48 self.space = space 

49 

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' 

64 

65 if not system: 

66 raise ConversionException('Trying to convert units not existing or not in one unit system (weight/volume)') 

67 

68 return Decimal(amount / Decimal(CONVERSION_TABLE[system][from_unit] / CONVERSION_TABLE[system][to_unit])) 

69 

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 

84 

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 

90 

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 

100 

101 return base_conversion_ingredient_list 

102 

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) 

122 

123 conversions = self.base_conversions(conversions) 

124 

125 return conversions 

126 

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)