$input['date'] ?? today(), 'mood' => max(1, min(10, (int) ($input['mood'] ?? 5))), 'energy' => max(1, min(10, (int) ($input['energy'] ?? 5))), 'stress' => max(1, min(10, (int) ($input['stress'] ?? 5))), 'sleep_hours' => max(0, min(24, (float) ($input['sleep_hours'] ?? 0))), 'sleep_feeling' => max(1, min(5, (int) ($input['sleep_feeling'] ?? 3))), 'sport_minutes' => max(0, min(1440, (int) ($input['sport_minutes'] ?? 0))), 'sport_type' => $sportTypes[0] ?? '', 'sport_types' => $sportTypes, 'walk_minutes' => max(0, min(1440, (int) ($input['walk_minutes'] ?? 0))), 'note' => trim((string) ($input['note'] ?? '')), ]; } public function evaluate(array $entry, array $settings, ?array $previousEntry = null): array { $entry = $this->normalize($entry); $previousEntry = $previousEntry !== null ? $this->normalize($previousEntry) : null; $scoring = $settings['scoring']; $ratings = $this->sortedRatings($settings['ratings'] ?? []); $sportTypes = find_sport_types($settings, $entry['sport_types']); $sportBonus = $this->sportBonusPoints($entry, $previousEntry, $settings, $sportTypes); $components = [ 'mood' => $entry['mood'] * (float) $scoring['mood_multiplier'], 'energy' => $entry['energy'] * (float) $scoring['energy_multiplier'], 'stress' => (11 - $entry['stress']) * (float) $scoring['stress_multiplier'], 'sleep_hours' => $this->sleepDurationPoints((float) $entry['sleep_hours'], $scoring['sleep_duration_points']), 'sleep_feeling' => $entry['sleep_feeling'] * (float) $scoring['sleep_feeling_multiplier'], 'sport_minutes' => $this->bandPoints((int) $entry['sport_minutes'], $scoring['sport_bands']), 'sport_bonus' => $sportBonus, 'walk_minutes' => $this->bandPoints((int) $entry['walk_minutes'], $scoring['walk_bands']), 'note' => $entry['note'] === '' ? 0 : (float) $scoring['journal_points'], ]; $total = round(array_sum($components), 1); $maxTotal = round( (10 * (float) $scoring['mood_multiplier']) + (10 * (float) $scoring['energy_multiplier']) + (10 * (float) $scoring['stress_multiplier']) + max(array_map('floatval', $scoring['sleep_duration_points'])) + (5 * (float) $scoring['sleep_feeling_multiplier']) + $this->maxBandPoints($scoring['sport_bands']) + $this->maxSportBonusPoints($settings) + $this->maxBandPoints($scoring['walk_bands']) + (float) $scoring['journal_points'], 1 ); $label = $this->labelForScore($total, $ratings); $guardrail = null; foreach ($settings['guardrails'] ?? [] as $rule) { $moodMatch = $entry['mood'] <= (int) ($rule['mood_max'] ?? 10); $energyLimit = $rule['energy_max'] ?? null; $energyMatch = $energyLimit === null || $entry['energy'] <= (int) $energyLimit; if ($moodMatch && $energyMatch) { $capped = $this->capLabel($label, (string) ($rule['cap_label'] ?? $label), $ratings); if ($capped !== $label) { $guardrail = (string) ($rule['cap_label'] ?? ''); $label = $capped; } } } return [ 'components' => $components, 'total' => $total, 'max_total' => $maxTotal, 'label' => $label, 'guardrail' => $guardrail, 'sentiment' => $this->sentimentForLabel($label, $ratings), 'percentage' => $maxTotal > 0 ? round(($total / $maxTotal) * 100, 1) : 0, 'sport_type' => $sportTypes[0] ?? null, 'sport_types' => $sportTypes, ]; } private function sleepDurationPoints(float $hours, array $points): float { if ($hours < 4) { return (float) ($points['lt4'] ?? 0); } if ($hours >= 10) { return (float) ($points['h10plus'] ?? 0); } $anchors = [ 4.0 => (float) ($points['h4'] ?? 0), 5.0 => (float) ($points['h5'] ?? 0), 6.0 => (float) ($points['h6'] ?? 0), 7.0 => (float) ($points['h7'] ?? 0), 8.0 => (float) ($points['h8'] ?? 0), 9.0 => (float) ($points['h9'] ?? 0), 10.0 => (float) ($points['h10plus'] ?? 0), ]; $lowerHour = floor($hours); $upperHour = ceil($hours); if ($lowerHour === $upperHour) { return (float) ($anchors[(float) $lowerHour] ?? 0); } $lowerPoints = $anchors[(float) $lowerHour] ?? 0.0; $upperPoints = $anchors[(float) $upperHour] ?? 0.0; $fraction = $hours - $lowerHour; return round($lowerPoints + (($upperPoints - $lowerPoints) * $fraction), 1); } private function bandPoints(int $value, array $bands): float { foreach ($bands as $band) { if ($value >= (int) ($band['min'] ?? 0) && $value <= (int) ($band['max'] ?? 0)) { return (float) ($band['points'] ?? 0); } } $last = end($bands); return (float) ($last['points'] ?? 0); } private function maxBandPoints(array $bands): float { $max = 0.0; foreach ($bands as $band) { $max = max($max, (float) ($band['points'] ?? 0)); } return $max; } private function maxSportBonusPoints(array $settings): float { $max = 0.0; $perGroup = []; foreach (normalized_sport_types($settings) as $type) { $bonus = (float) ($type['bonus_points'] ?? 0); if ($bonus <= 0) { continue; } if (!empty($type['allow_consecutive'])) { $max += $bonus; continue; } $group = (string) ($type['recovery_group'] ?? $type['id'] ?? ''); if ($group === '') { $group = (string) ($type['id'] ?? ''); } $perGroup[$group] = max((float) ($perGroup[$group] ?? 0), $bonus); } foreach ($perGroup as $bonus) { $max += $bonus; } return $max; } private function sportBonusPoints(array $entry, ?array $previousEntry, array $settings, array $currentSportTypes): float { if ((int) $entry['sport_minutes'] <= 0 || $currentSportTypes === []) { return 0.0; } $previousGroups = []; if ($previousEntry !== null && (int) $previousEntry['sport_minutes'] > 0) { foreach (find_sport_types($settings, $previousEntry['sport_types']) as $type) { $group = (string) ($type['recovery_group'] ?? $type['id'] ?? ''); if ($group !== '') { $previousGroups[$group] = true; } } } $groupBonuses = []; $total = 0.0; foreach ($currentSportTypes as $type) { $bonus = (float) ($type['bonus_points'] ?? 0); if ($bonus <= 0) { continue; } $group = (string) ($type['recovery_group'] ?? $type['id'] ?? ''); if ($group === '') { $group = (string) ($type['id'] ?? ''); } if (!empty($type['allow_consecutive'])) { $total += $bonus; continue; } if (isset($previousGroups[$group])) { continue; } $groupBonuses[$group] = max((float) ($groupBonuses[$group] ?? 0), $bonus); } foreach ($groupBonuses as $bonus) { $total += $bonus; } return $total; } private function sortedRatings(array $ratings): array { usort($ratings, static fn (array $a, array $b): int => ((int) $a['min']) <=> ((int) $b['min'])); return $ratings; } private function labelForScore(float $score, array $ratings): string { foreach ($ratings as $rating) { if ($score >= (float) $rating['min'] && $score <= (float) $rating['max']) { return (string) $rating['label']; } } if ($score < (float) ($ratings[0]['min'] ?? 0)) { return (string) ($ratings[0]['label'] ?? 'unbewertet'); } return (string) ($ratings[count($ratings) - 1]['label'] ?? 'unbewertet'); } private function capLabel(string $current, string $cap, array $ratings): string { $order = array_map(static fn (array $rating): string => (string) $rating['label'], $ratings); $currentIndex = array_search($current, $order, true); $capIndex = array_search($cap, $order, true); if ($currentIndex === false || $capIndex === false) { return $current; } return $currentIndex > $capIndex ? $cap : $current; } private function sentimentForLabel(string $label, array $ratings): string { $order = array_values(array_map(static fn (array $rating): string => (string) $rating['label'], $ratings)); $index = array_search($label, $order, true); if ($index === false || count($order) <= 1) { return 'steady'; } $ratio = $index / max(count($order) - 1, 1); return match (true) { $ratio <= 0.1 => 'storm', $ratio <= 0.35 => 'heavy', $ratio <= 0.65 => 'steady', $ratio <= 0.9 => 'bright', default => 'radiant', }; } }