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