from typing import Optional from .health_dose_weight_based_types import WeightBasedDose def _check_range(name: str, value: int, low: int, high: int) -> None: if isinstance(value, bool) or not isinstance(value, int) or value < low or value > high: raise ValueError("%s must be a whole number from %d to %d, received %r" % (name, low, high, value)) def _half_up(num: int, den: int) -> int: """num / den rounded half-up, for num >= 0 and den > 0.""" return (2 * num + den) // (2 * den) def _half_away_from_zero(num: int, den: int) -> int: magnitude = _half_up(abs(num), den) return -magnitude if num < 0 else magnitude def weight_based_dose( weight_grams: int, dose_per_kg_micrograms: int, max_single_dose_micrograms: Optional[int], strength_micrograms: int, strength_volume_microlitres: int, measuring_increment_microlitres: int, ) -> WeightBasedDose: """A weight-based dose, capped at the maximum single dose and turned into a volume the measuring device can show. Every quantity is a whole number of micrograms, microlitres or grams and every step is an exact fraction, so a 0.1 mL step never drifts. The volume is the nearest whole number of increments, a tie going down, and never one that would deliver more than the maximum single dose. """ _check_range("weightGrams", weight_grams, 200, 500000) _check_range("dosePerKgMicrograms", dose_per_kg_micrograms, 1, 1000000) if max_single_dose_micrograms is not None: _check_range("maxSingleDoseMicrograms", max_single_dose_micrograms, 1, 100000000) _check_range("strengthMicrograms", strength_micrograms, 1, 100000000) _check_range("strengthVolumeMicrolitres", strength_volume_microlitres, 1, 1000000) _check_range("measuringIncrementMicrolitres", measuring_increment_microlitres, 1, 100000) per_kg = weight_grams * dose_per_kg_micrograms # micrograms x 1000 max_dose = max_single_dose_micrograms capped = max_dose is not None and per_kg > max_dose * 1000 # The target dose as the exact fraction target_num / target_den micrograms. target_num = max_dose if capped else per_kg target_den = 1 if capped else 1000 assert target_num is not None strength = strength_micrograms volume = strength_volume_microlitres increment = measuring_increment_microlitres # Increments needed: target x volume / (strength x increment), nearest, a tie down. steps_num = target_num * volume steps_den = target_den * strength * increment steps, remainder = divmod(steps_num, steps_den) if 2 * remainder > steps_den: steps += 1 # Rounding up to the nearest step must not take the dose over the maximum. if max_dose is not None and steps * increment * strength > max_dose * volume: steps -= 1 if steps == 0: raise ValueError("dose is less than one measuring increment; use a more dilute product or a finer measure") delivered_num = steps * increment * strength # micrograms x volume return WeightBasedDose( calculated_micrograms=_half_up(per_kg, 1000), capped_at_maximum=capped, target_micrograms=_half_up(target_num, target_den), volume_microlitres=steps * increment, delivered_micrograms=_half_up(delivered_num, volume), difference_basis_points=_half_away_from_zero( (delivered_num * target_den - target_num * volume) * 10000, target_num * volume ), )