# core/classes.py """ Модуль содержит классы для представления ингредиентов и смеси. """ from dataclasses import dataclass class Ingredient: """Представляет ингредиент с именем, единицей измерения и значением.""" def __init__(self, name: str = "Water", unit: str = "ml", value: float = 1.0): self.name = name self.unit = unit self.value = value def to_dict(self): return {"name": self.name, "unit": self.unit, "value": self.value} @staticmethod def from_dict(data): return Ingredient(data["name"], data["unit"], data["value"]) def __repr__(self): return f"{self.name}: {self.value} {self.unit}" @dataclass class IngredientInfo: """ Обёртка для ингредиента в составе смеси, добавляет разбавление и плотность. """ ingredient: Ingredient dilution: float = 1.0 density: float = 1.0 def to_dict(self): return { "ingredient": self.ingredient.to_dict(), "dilution": self.dilution, "density": self.density } @staticmethod def from_dict(data): ing = Ingredient.from_dict(data["ingredient"]) return IngredientInfo( ingredient=ing, dilution=data.get("dilution", 1.0), density=data.get("density", 1.0) ) @property def name(self): return self.ingredient.name @property def unit(self): return self.ingredient.unit @property def value(self): return self.ingredient.value @value.setter def value(self, new_value): self.ingredient.value = new_value class Mixture: """ Основной класс для работы со смесью. Первый ингредиент считается растворителем (его процент пересчитывается автоматически). """ def __init__(self, solvent=None, ings=None, total_amount=1000.0, total_unit="ml"): all_ings = [] if solvent is None: water = Ingredient(name="Water", unit=total_unit, value=100.0) solvent_info = IngredientInfo(ingredient=water) elif isinstance(solvent, Ingredient): solvent_info = IngredientInfo(ingredient=solvent) elif isinstance(solvent, IngredientInfo): solvent_info = solvent else: raise TypeError("solvent must be Ingredient or IngredientInfo") all_ings.append(solvent_info) if ings is not None: if not isinstance(ings, list): raise TypeError("ings must be a list") for item in ings: if isinstance(item, Ingredient): all_ings.append(IngredientInfo(ingredient=item)) elif isinstance(item, IngredientInfo): all_ings.append(item) else: raise TypeError("Each item in ings must be Ingredient or IngredientInfo") self.ings = all_ings self.total_amount = total_amount self.total_unit = total_unit self.comp_solvent_percent() def comp_solvent_percent(self) -> float: """Пересчёт процента растворителя = 100% - сумма остальных.""" if not self.ings: return 0.0 total_others = sum(info.value for info in self.ings[1:]) if total_others > 100.0: raise ValueError("Sum of all ingredient percentages exceeds 100%") self.ings[0].value = 100.0 - total_others return self.ings[0].value def add_ingredient(self, ingredient: Ingredient, dilution: float = 1.0, density: float = 1.0): info = IngredientInfo(ingredient, dilution, density) self.ings.append(info) self.comp_solvent_percent() def remove_ingredient(self, index: int): if index == 0: raise ValueError("Нельзя удалить растворитель (первый ингредиент).") removed = self.ings.pop(index) self.comp_solvent_percent() return removed def get_absolute_ingredients(self): """Возвращает абсолютные значения ингредиентов (с учётом плотности и разбавления).""" abs_list = [] sum_rest = 0.0 for info in self.ings[1:]: abs_value = (info.value / 100.0) * self.total_amount * info.dilution * info.density abs_list.append(Ingredient(name=info.name, unit=info.unit, value=abs_value)) sum_rest += abs_value if sum_rest > self.total_amount: raise ValueError("Сумма ингредиентов (кроме растворителя) превышает общее количество") solvent_abs = self.total_amount - sum_rest solvent = Ingredient(name=self.ings[0].name, unit=self.ings[0].unit, value=solvent_abs) abs_list.insert(0, solvent) return abs_list def get_ingredient_info(self, index: int): return self.ings[index] def set_total_amount(self, new_amount: float, new_unit: str): if new_amount <= 0: raise ValueError("Общее количество должно быть положительным.") if not new_unit.strip(): raise ValueError("Единица измерения не может быть пустой.") self.total_amount = new_amount self.total_unit = new_unit.strip() def set_composition(self, *percentages): expected = len(self.ings) - 1 if len(percentages) != expected: raise ValueError(f"Expected {expected} percentages, got {len(percentages)}") total = sum(percentages) if total > 100.0: raise ValueError(f"Sum of percentages ({total:.2f}) exceeds 100") for i, pct in enumerate(percentages, start=1): if pct < 0: raise ValueError(f"Percentage for {self.ings[i].name} cannot be negative") self.ings[i].value = pct self.comp_solvent_percent() def to_dict(self): return { "ings": [info.to_dict() for info in self.ings], "total_amount": self.total_amount, "total_unit": self.total_unit } @staticmethod def from_dict(data): ings = [IngredientInfo.from_dict(item) for item in data["ings"]] solvent = ings[0] if ings else None rest = ings[1:] if len(ings) > 1 else None return Mixture( solvent=solvent, ings=rest, total_amount=data["total_amount"], total_unit=data["total_unit"] ) def __repr__(self): parts = [] for info in self.ings: parts.append(f"{info.name} {info.value:.1f}% (разб.{info.dilution:.2f}, плотн.{info.density:.2f})") return f"Mixture({', '.join(parts)}, total={self.total_amount} {self.total_unit})"