forked from firka/firka
fix: average calculations
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firka/api/client/kreta_client.dart';
|
||||
import 'package:firka_common/ui/components/grade_helpers.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import 'package:firka/data/ios_widget_helper.dart';
|
||||
@@ -249,31 +251,20 @@ class WidgetCacheHelper {
|
||||
);
|
||||
|
||||
final Map<String, double> subjectAverages = {};
|
||||
final Set<String> subjectUids = {};
|
||||
final HashSet<Subject> subjects = HashSet(
|
||||
hashCode: (s) => s.uid.hashCode,
|
||||
equals: (s, s2) => s.uid == s2.uid,
|
||||
);
|
||||
|
||||
for (var grade in grades) {
|
||||
subjectUids.add(grade.subject.uid);
|
||||
}
|
||||
subjects.addAll(grades.map((g) => g.subject));
|
||||
|
||||
double overallSum = 0;
|
||||
int validSubjectCount = 0;
|
||||
|
||||
for (var uid in subjectUids) {
|
||||
final subjectGrades = grades
|
||||
.where((g) => g.subject.uid == uid)
|
||||
.toList();
|
||||
final avg = _calculateWeightedAverage(subjectGrades);
|
||||
if (!avg.isNaN && avg > 0) {
|
||||
subjectAverages[uid] = avg;
|
||||
overallSum += avg;
|
||||
validSubjectCount++;
|
||||
for (var subject in subjects) {
|
||||
final average = grades.getAverageBySubject(subject);
|
||||
if (average != null) {
|
||||
subjectAverages[subject.uid] = average;
|
||||
}
|
||||
}
|
||||
|
||||
final double? overallAverage = validSubjectCount > 0
|
||||
? overallSum / validSubjectCount
|
||||
: null;
|
||||
|
||||
WidgetBreakInfo? currentBreak;
|
||||
|
||||
await updateIOSWidgets(
|
||||
@@ -285,7 +276,7 @@ class WidgetCacheHelper {
|
||||
nextSchoolDayDate: nextSchoolDayDate,
|
||||
grades: grades,
|
||||
subjectAverages: subjectAverages,
|
||||
overallAverage: overallAverage,
|
||||
overallAverage: grades.getSubjectAverage(),
|
||||
currentBreak: currentBreak,
|
||||
);
|
||||
|
||||
@@ -315,26 +306,4 @@ class WidgetCacheHelper {
|
||||
debugPrint('Error clearing iOS widgets: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate weighted average for a list of grades
|
||||
static double _calculateWeightedAverage(List<Grade> grades) {
|
||||
var weightTotal = 0.0;
|
||||
var sum = 0.0;
|
||||
|
||||
for (var grade in grades) {
|
||||
final name = grade.valueType.name?.toLowerCase() ?? '';
|
||||
final isPercentage =
|
||||
name.contains('szazalek') || name.contains('percent');
|
||||
if (isPercentage) continue;
|
||||
|
||||
if (grade.numericValue != null) {
|
||||
var weight = (grade.weightPercentage ?? 100) / 100.0;
|
||||
weightTotal += weight;
|
||||
sum += grade.numericValue! * weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (weightTotal == 0) return double.nan;
|
||||
return sum / weightTotal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,13 @@ import 'package:flutter/material.dart';
|
||||
|
||||
class GradeChart extends StatefulWidget {
|
||||
final List<Grade> grades;
|
||||
const GradeChart({super.key, required this.grades});
|
||||
final bool halfYearFallback;
|
||||
GradeChart({
|
||||
super.key,
|
||||
required List<Grade> grades,
|
||||
this.halfYearFallback = true,
|
||||
}) : grades = grades.where((g) => g.hasClassicValue()).toList()
|
||||
..sort((a, b) => a.recordDate.compareTo(b.recordDate));
|
||||
|
||||
@override
|
||||
State<GradeChart> createState() => _GradeChartState();
|
||||
@@ -20,6 +26,10 @@ class DateSpot extends FlSpot {
|
||||
final DateTime date;
|
||||
|
||||
const DateSpot(super.x, super.y, this.date);
|
||||
|
||||
DateSpot copyWithX(double x) {
|
||||
return DateSpot(x, y, date);
|
||||
}
|
||||
}
|
||||
|
||||
class _GradeChartState extends State<GradeChart> {
|
||||
@@ -36,26 +46,43 @@ class _GradeChartState extends State<GradeChart> {
|
||||
}
|
||||
|
||||
void _computeSpots() {
|
||||
final sortedGrades =
|
||||
widget.grades.where((grade) => grade.shouldIncludeInAverage()).toList()
|
||||
..sort((a, b) => a.recordDate.compareTo(b.recordDate));
|
||||
|
||||
if (sortedGrades.isEmpty) {
|
||||
spots = [DateSpot(0, 0, DateTime.now()), DateSpot(1, 0, DateTime.now())];
|
||||
return;
|
||||
}
|
||||
|
||||
if (sortedGrades.length == 1) {
|
||||
sortedGrades.insert(0, sortedGrades[0]);
|
||||
}
|
||||
|
||||
spots = [];
|
||||
for (var i = 0; i < sortedGrades.length; i++) {
|
||||
final partialAvg = sortedGrades.take(i + 1).getSubjectAverage();
|
||||
for (var i = 0; i < widget.grades.length; i++) {
|
||||
final grade = widget.grades[i];
|
||||
if (!grade.shouldIncludeInAverage()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final partialAvg = widget.grades
|
||||
.take(i + 1)
|
||||
.getSubjectAverage(halfYearFallback: widget.halfYearFallback);
|
||||
|
||||
spots.add(
|
||||
DateSpot(i.toDouble(), partialAvg!, sortedGrades[i].recordDate),
|
||||
DateSpot(spots.length.toDouble(), partialAvg!, grade.recordDate),
|
||||
);
|
||||
}
|
||||
|
||||
if (spots.isEmpty) {
|
||||
for (final grade in widget.grades) {
|
||||
if (grade.hasClassicValue()) {
|
||||
spots.add(
|
||||
DateSpot(
|
||||
spots.length.toDouble(),
|
||||
grade.numericValue!.toDouble(),
|
||||
grade.recordDate,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (spots.isEmpty) {
|
||||
spots.add(DateSpot(0, 0, DateTime.now()));
|
||||
}
|
||||
|
||||
if (spots.length == 1) {
|
||||
spots = [spots[0], spots[0].copyWithX(1)];
|
||||
}
|
||||
}
|
||||
|
||||
List<FlSpot> _smoothSpots(List<FlSpot> input) {
|
||||
@@ -356,8 +383,13 @@ class _GradeChartState extends State<GradeChart> {
|
||||
/// so the navigator does not intercept touch/drag (e.g. for swipe back).
|
||||
class GradeChartWithInteraction extends StatelessWidget {
|
||||
final List<Grade> grades;
|
||||
final bool halfYearFallback;
|
||||
|
||||
const GradeChartWithInteraction({super.key, required this.grades});
|
||||
const GradeChartWithInteraction({
|
||||
super.key,
|
||||
required this.grades,
|
||||
this.halfYearFallback = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -366,7 +398,7 @@ class GradeChartWithInteraction extends StatelessWidget {
|
||||
onPointerDown: (_) => ChartInteractionScope.of(context).value = true,
|
||||
onPointerUp: (_) => ChartInteractionScope.of(context).value = false,
|
||||
onPointerCancel: (_) => ChartInteractionScope.of(context).value = false,
|
||||
child: GradeChart(grades: grades),
|
||||
child: GradeChart(grades: grades, halfYearFallback: halfYearFallback),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class GradeWidget extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (g.valueType.name == 'Szazalekos') {
|
||||
if (g.isInPercentage()) {
|
||||
final gradeColor = appStyle.colors.accent;
|
||||
return FilledCircle(
|
||||
diameter: size,
|
||||
|
||||
@@ -58,20 +58,31 @@ extension GradeListExtension on Iterable<Grade> {
|
||||
return (filtered.length, counts);
|
||||
}
|
||||
|
||||
double? getAverageBySubject(Subject subject) {
|
||||
double? _getAverageBySubject(String uid, {bool halfYearFallback = true}) {
|
||||
return where(
|
||||
(g) => g.subject.uid == subject.uid && g.shouldIncludeInAverage(),
|
||||
).getAverage();
|
||||
(g) => g.subject.uid == uid,
|
||||
).getAverage(halfYearFallback: halfYearFallback);
|
||||
}
|
||||
|
||||
double? getAverageBySubject(Subject subject, {bool halfYearFallback = true}) {
|
||||
return _getAverageBySubject(
|
||||
subject.uid,
|
||||
halfYearFallback: halfYearFallback,
|
||||
);
|
||||
}
|
||||
|
||||
double? getRoundedSubjectAverage({
|
||||
bool halfYearFallback = true,
|
||||
double t1 = 1,
|
||||
double t2 = 0.5,
|
||||
double t3 = 0.5,
|
||||
double t4 = 0.5,
|
||||
}) {
|
||||
final averages = map((g) => g.subject).toSet().map((subject) {
|
||||
final average = getAverageBySubject(subject);
|
||||
final average = getAverageBySubject(
|
||||
subject,
|
||||
halfYearFallback: halfYearFallback,
|
||||
);
|
||||
if (average == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -85,10 +96,15 @@ extension GradeListExtension on Iterable<Grade> {
|
||||
return averages.reduce((sum, avg) => sum + avg) / averages.length;
|
||||
}
|
||||
|
||||
double? getSubjectAverage() {
|
||||
final averages = map(
|
||||
(g) => g.subject,
|
||||
).toSet().map((subject) => getAverageBySubject(subject)).nonNulls;
|
||||
double? getSubjectAverage({bool halfYearFallback = true}) {
|
||||
final averages = where((g) => g.hasClassicValue())
|
||||
.map((g) => g.subject.uid)
|
||||
.toSet()
|
||||
.map(
|
||||
(uid) =>
|
||||
_getAverageBySubject(uid, halfYearFallback: halfYearFallback),
|
||||
)
|
||||
.nonNulls;
|
||||
|
||||
if (averages.isEmpty) {
|
||||
return null;
|
||||
@@ -97,25 +113,31 @@ extension GradeListExtension on Iterable<Grade> {
|
||||
return averages.reduce((sum, avg) => sum + avg) / averages.length;
|
||||
}
|
||||
|
||||
double? getAverage() {
|
||||
double? getAverage({bool halfYearFallback = true}) {
|
||||
if (isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
double weightTotal = 0;
|
||||
double sum = 0;
|
||||
int? fallback;
|
||||
for (Grade grade in this) {
|
||||
if (grade.hasClassicValue() && halfYearFallback) {
|
||||
fallback = grade.numericValue!;
|
||||
}
|
||||
|
||||
if (!grade.shouldIncludeInAverage()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
double weight = (grade.weightPercentage ?? 100) / 100.0;
|
||||
double weight = grade.weightPercentage! / 100.0;
|
||||
weightTotal += weight;
|
||||
sum += grade.numericValue! * weight;
|
||||
}
|
||||
|
||||
if (sum == 0) {
|
||||
return null;
|
||||
// use classification exam results or half-year evaluations
|
||||
return fallback?.toDouble();
|
||||
}
|
||||
|
||||
return sum / weightTotal;
|
||||
@@ -124,25 +146,15 @@ extension GradeListExtension on Iterable<Grade> {
|
||||
|
||||
extension GradeExtension on Grade {
|
||||
bool isInPercentage() {
|
||||
final name = valueType.name.toLowerCase();
|
||||
return name.contains('szazalek') || name.contains('percent');
|
||||
return valueType.name == 'Szazalekos';
|
||||
}
|
||||
|
||||
bool hasClassicValue() {
|
||||
return valueType.name == "Osztalyzat";
|
||||
}
|
||||
|
||||
// https://tudasbazis.ekreta.hu/pages/viewpage.action?pageId=2426172
|
||||
bool shouldIncludeInAverage() {
|
||||
if (isInPercentage()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final typeName = type.name.toLowerCase();
|
||||
if (typeName == 'felevi_jegy_ertekeles' ||
|
||||
typeName == 'evvegi_jegy_ertekeles') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (numericValue == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return weightPercentage != null && hasClassicValue();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user