diff --git a/filcnaplo/lib/helpers/average_helper.dart b/filcnaplo/lib/helpers/average_helper.dart index 1f65934..2826f4f 100644 --- a/filcnaplo/lib/helpers/average_helper.dart +++ b/filcnaplo/lib/helpers/average_helper.dart @@ -1,26 +1,24 @@ +// ignore_for_file: no_leading_underscores_for_local_identifiers + import 'package:filcnaplo_kreta_api/models/grade.dart'; class AverageHelper { static double averageEvals(List grades, {bool finalAvg = false}) { - double average = 0.0; - List ignoreInFinal = ["5,SzorgalomErtek", "4,MagatartasErtek"]; + if (finalAvg) grades.removeWhere((e) => (e.value.value == 0) || (ignoreInFinal.contains(e.gradeType?.id))); - if (finalAvg) { - grades.removeWhere((e) => - (e.value.value == 0) || - (ignoreInFinal.contains(e.gradeType?.id))); - } - - for (var e in grades) { - average += e.value.value * ((finalAvg ? 100 : e.value.weight) / 100); - } - - average = average / - grades - .map((e) => (finalAvg ? 100 : e.value.weight) / 100) - .fold(0.0, (a, b) => a + b); - + double average = + grades.map((e) => e.value.value * e.value.weight / 100.0).fold(0.0, (double a, double b) => a + b) / weightSum(grades, finalAvg: finalAvg); return average.isNaN ? 0.0 : average; } -} \ No newline at end of file + + static double weightSum(List grades, {bool finalAvg = false}) => + grades.map((e) => finalAvg ? 1 : e.value.weight / 100).fold(0, (a, b) => a + b); + + static int howManyNeeded(int grade, List base, double goal, {bool filcgrade = true, double avg = 0, double wsum = 0}) { + double _avg = filcgrade ? averageEvals(base) : avg; + double _wsum = filcgrade ? weightSum(base) : wsum; + if (_avg >= goal) return 0; + return (_wsum * (_avg - goal) / (goal - grade)).ceil(); + } +} diff --git a/filcnaplo/lib/helpers/goalplanner_helper.dart b/filcnaplo/lib/helpers/goalplanner_helper.dart new file mode 100644 index 0000000..0a770ab --- /dev/null +++ b/filcnaplo/lib/helpers/goalplanner_helper.dart @@ -0,0 +1,117 @@ +/* + * Maintainer: DarK + * Translated from C version + * ##Please do NOT modify if you don't know whats going on## + * + * Issue: #59 + * + * Future changes / ideas: + * - `best` should be configurable + */ +import 'dart:math'; + +import 'package:filcnaplo/helpers/average_helper.dart'; +import 'package:filcnaplo_kreta_api/models/grade.dart'; + +/// Generate list of grades that achieve the wanted goal. +/// After generating possible options, it (when doing so would NOT result in empty list) filters with two criteria: +/// - Plan should not contain more than 15 grades +/// - Plan should not contain only one type of grade +/// +/// **Usage**: +/// +/// ```dart +/// List GoalPlanner(double goal, List grades).solve().plan +/// ``` +class GoalPlanner { + final double goal; + final List grades; + List plans = []; + GoalPlanner(this.goal, this.grades); + + bool _allowed(int grade) => grade > goal; + + Avg _addToAvg(Avg base, int grade, int n) => Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n); + List _addToList(List l, T e, int n) { + if (n == 0) return l; + List tmp = l; + for (int i = 0; i < n; i++) tmp.add(e); + return tmp; + } + + void _generate(Generator g) { + // Exit condition 1: Generator has working plan. + if (g.currentAvg.avg >= goal) { + plans.add(Plan(g.plan)); + return; + } + // Exit condition 2: Generator plan will never work. + if (!_allowed(g.gradeToAdd)) return; + for (int i = 0; i < g.max; i++) { + int newGradeToAdd = g.gradeToAdd - 1; + List newPlan = _addToList(g.plan, g.gradeToAdd, i); + Avg newAvg = _addToAvg(g.currentAvg, g.gradeToAdd, i); + int newN = AverageHelper.howManyNeeded(newGradeToAdd, [], goal); + + _generate(Generator(newGradeToAdd, newN, newAvg, newPlan)); + } + } + + Plan solve() { + _generate( + Generator( + 5, + AverageHelper.howManyNeeded( + 5, + [], + goal, + filcgrade: false, + avg: AverageHelper.averageEvals(grades), + wsum: AverageHelper.weightSum(grades), + ), + Avg(AverageHelper.averageEvals(grades), AverageHelper.weightSum(grades)), + [], + ), + ); + + // Calculate Statistics + plans.forEach((e) { + e.sum = e.plan.fold(0, (int a, b) => a + b); + e.avg = e.sum / e.plan.length; + e.sigma = sqrt(e.plan.map((i) => pow(i - e.avg, 2)).fold(0, (num a, b) => a + b) / e.plan.length); + }); + + // filter without aggression + if (plans.where((e) => e.plan.length < 15).isNotEmpty) plans.removeWhere((e) => !(e.plan.length < 15)); + if (plans.where((e) => e.sigma > 1).isNotEmpty) plans.removeWhere((e) => !(e.sigma > 1)); + + return plans[Random().nextInt(plans.length)]; + } +} + +class Avg { + final double avg; + final double n; + + Avg(this.avg, this.n); +} + +class Generator { + final int gradeToAdd; + final int max; + final Avg currentAvg; + final List plan; + + Generator(this.gradeToAdd, this.max, this.currentAvg, this.plan); +} + +class Plan { + final List plan; + int sum = 0; + double avg = 0; + int med = 0; // currently + int mod = 0; // unused + double sigma = 0; + + Plan(this.plan); +}