Files

184 lines
7.0 KiB
Python

# 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})"