From f0114e3976e7b397a33b21bed3e8eef02d6f3d93 Mon Sep 17 00:00:00 2001 From: checkedear <271323618+checkedear@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:14:23 +0200 Subject: [PATCH] fix: average calculations --- firka/lib/data/widget.dart | 55 ++++----------- firka/lib/ui/phone/widgets/grade_chart.dart | 70 ++++++++++++++----- firka_common/lib/ui/components/grade.dart | 2 +- .../lib/ui/components/grade_helpers.dart | 68 ++++++++++-------- 4 files changed, 104 insertions(+), 91 deletions(-) diff --git a/firka/lib/data/widget.dart b/firka/lib/data/widget.dart index dd7be4d..38acfc2 100644 --- a/firka/lib/data/widget.dart +++ b/firka/lib/data/widget.dart @@ -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 subjectAverages = {}; - final Set subjectUids = {}; + final HashSet 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 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; - } } diff --git a/firka/lib/ui/phone/widgets/grade_chart.dart b/firka/lib/ui/phone/widgets/grade_chart.dart index 3b6e5b3..99a0cb4 100644 --- a/firka/lib/ui/phone/widgets/grade_chart.dart +++ b/firka/lib/ui/phone/widgets/grade_chart.dart @@ -10,7 +10,13 @@ import 'package:flutter/material.dart'; class GradeChart extends StatefulWidget { final List grades; - const GradeChart({super.key, required this.grades}); + final bool halfYearFallback; + GradeChart({ + super.key, + required List grades, + this.halfYearFallback = true, + }) : grades = grades.where((g) => g.hasClassicValue()).toList() + ..sort((a, b) => a.recordDate.compareTo(b.recordDate)); @override State 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 { @@ -36,26 +46,43 @@ class _GradeChartState extends State { } 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 _smoothSpots(List input) { @@ -356,8 +383,13 @@ class _GradeChartState extends State { /// so the navigator does not intercept touch/drag (e.g. for swipe back). class GradeChartWithInteraction extends StatelessWidget { final List 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), ); } } diff --git a/firka_common/lib/ui/components/grade.dart b/firka_common/lib/ui/components/grade.dart index 539579c..03067fe 100644 --- a/firka_common/lib/ui/components/grade.dart +++ b/firka_common/lib/ui/components/grade.dart @@ -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, diff --git a/firka_common/lib/ui/components/grade_helpers.dart b/firka_common/lib/ui/components/grade_helpers.dart index 0110060..6afb1b6 100644 --- a/firka_common/lib/ui/components/grade_helpers.dart +++ b/firka_common/lib/ui/components/grade_helpers.dart @@ -58,20 +58,31 @@ extension GradeListExtension on Iterable { 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 { 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 { 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 { 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(); } }