feat: Calculer automatiquement les moyennes après chaque saisie de notes
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.
This commit is contained in:
102
backend/src/Scolarite/Domain/Service/AverageCalculator.php
Normal file
102
backend/src/Scolarite/Domain/Service/AverageCalculator.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user