Les enseignants ont besoin de moyennes à jour immédiatement après la publication ou modification des notes, sans attendre un batch nocturne. Le système recalcule via Domain Events synchrones : statistiques d'évaluation (min/max/moyenne/médiane), moyennes matières pondérées (normalisation /20), et moyenne générale par élève. Les résultats sont stockés dans des tables dénormalisées avec cache Redis (TTL 5 min). Trois endpoints API exposent les données avec contrôle d'accès par rôle. Une commande console permet le backfill des données historiques au déploiement.
103 lines
2.7 KiB
PHP
103 lines
2.7 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace App\Scolarite\Domain\Service;
|
||
|
||
use App\Scolarite\Domain\Model\Evaluation\ClassStatistics;
|
||
|
||
use function array_sum;
|
||
use function count;
|
||
use function intdiv;
|
||
use function round;
|
||
use function sort;
|
||
|
||
final class AverageCalculator
|
||
{
|
||
/**
|
||
* Moyenne matière pondérée par les coefficients, normalisée sur /20.
|
||
* Les notes absent/dispensé doivent être exclues en amont.
|
||
*
|
||
* Formule : Σ(note_sur_20 × coef) / Σ(coef)
|
||
*
|
||
* @param list<GradeEntry> $grades
|
||
*/
|
||
public function calculateSubjectAverage(array $grades): ?float
|
||
{
|
||
if ($grades === []) {
|
||
return null;
|
||
}
|
||
|
||
$sumWeighted = 0.0;
|
||
$sumCoef = 0.0;
|
||
|
||
foreach ($grades as $entry) {
|
||
$normalizedValue = $entry->gradeScale->convertTo20($entry->value);
|
||
$sumWeighted += $normalizedValue * $entry->coefficient->value;
|
||
$sumCoef += $entry->coefficient->value;
|
||
}
|
||
|
||
return round($sumWeighted / $sumCoef, 2);
|
||
}
|
||
|
||
/**
|
||
* Moyenne générale : moyenne arithmétique des moyennes matières.
|
||
* Les matières sans note sont exclues en amont.
|
||
*
|
||
* @param list<float> $subjectAverages
|
||
*/
|
||
public function calculateGeneralAverage(array $subjectAverages): ?float
|
||
{
|
||
if ($subjectAverages === []) {
|
||
return null;
|
||
}
|
||
|
||
return round(array_sum($subjectAverages) / count($subjectAverages), 2);
|
||
}
|
||
|
||
/**
|
||
* Statistiques de classe pour une évaluation : min, max, moyenne, médiane.
|
||
* Les absents et dispensés doivent être exclus en amont.
|
||
*
|
||
* @param list<float> $values
|
||
*/
|
||
public function calculateClassStatistics(array $values): ClassStatistics
|
||
{
|
||
if ($values === []) {
|
||
return new ClassStatistics(
|
||
average: null,
|
||
min: null,
|
||
max: null,
|
||
median: null,
|
||
gradedCount: 0,
|
||
);
|
||
}
|
||
|
||
sort($values);
|
||
$count = count($values);
|
||
|
||
return new ClassStatistics(
|
||
average: round(array_sum($values) / $count, 2),
|
||
min: $values[0],
|
||
max: $values[$count - 1],
|
||
median: $this->calculateMedian($values),
|
||
gradedCount: $count,
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param list<float> $sortedValues Valeurs déjà triées par ordre croissant
|
||
*/
|
||
private function calculateMedian(array $sortedValues): float
|
||
{
|
||
$count = count($sortedValues);
|
||
$middle = intdiv($count, 2);
|
||
|
||
if ($count % 2 === 0) {
|
||
return round(($sortedValues[$middle - 1] + $sortedValues[$middle]) / 2, 2);
|
||
}
|
||
|
||
return $sortedValues[$middle];
|
||
}
|
||
}
|