/* * Maintainer: DarK * Translated from C version * Minimal Working Fixed @ 2022.12.25 * ##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:refilc_kreta_api/models/category.dart'; import 'package:refilc_kreta_api/models/grade.dart'; import 'package:refilc_kreta_api/models/subject.dart'; import 'package:refilc_kreta_api/models/teacher.dart'; import 'package:flutter/foundation.dart' show listEquals; /// 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; 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 = g.max; i >= 0; i--) { int newGradeToAdd = g.gradeToAdd - 1; List newPlan = GoalPlannerHelper._addToList(g.plan, g.gradeToAdd, i); Avg newAvg = GoalPlannerHelper._addToAvg(g.currentAvg, g.gradeToAdd, i); int newN = GoalPlannerHelper.howManyNeeded( newGradeToAdd, grades + newPlan .map((e) => Grade( id: '', date: DateTime(0), value: GradeValue(e, '', '', 100), teacher: Teacher.fromString(''), description: '', form: '', groupId: '', type: GradeType.midYear, subject: GradeSubject.fromJson({}), mode: Category.fromJson({}), seenDate: DateTime(0), writeDate: DateTime(0), )) .toList(), goal); _generate(Generator(newGradeToAdd, newN, newAvg, newPlan)); } } List solve() { _generate( Generator( 5, GoalPlannerHelper.howManyNeeded( 5, grades, goal, ), Avg(GoalPlannerHelper.averageEvals(grades), GoalPlannerHelper.weightSum(grades)), [], ), ); // Calculate Statistics for (var e in plans) { 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 < 30).isNotEmpty) { plans.removeWhere((e) => !(e.plan.length < 30)); } if (plans.where((e) => e.sigma > 1).isNotEmpty) { plans.removeWhere((e) => !(e.sigma > 1)); } return plans; } } 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); String get dbString { var finalString = ''; for (var i in plan) { finalString += "$i,"; } return finalString; } @override bool operator ==(other) => other is Plan && listEquals(plan, other.plan); @override int get hashCode => Object.hashAll(plan); } class GoalPlannerHelper { static Avg _addToAvg(Avg base, int grade, int n) => Avg((base.avg * base.n + grade * n) / (base.n + n), base.n + n); static List _addToList(List l, T e, int n) { if (n == 0) return l; List tmp = l; for (int i = 0; i < n; i++) { tmp = tmp + [e]; } return tmp; } static int howManyNeeded(int grade, List base, double goal) { double avg = averageEvals(base); double wsum = weightSum(base); if (avg >= goal) return 0; if (grade * 1.0 == goal) return -1; int candidate = (wsum * (avg - goal) / (goal - grade)).floor(); return (candidate * grade + avg * wsum) / (candidate + wsum) < goal ? candidate + 1 : candidate; } static double averageEvals(List grades, {bool finalAvg = false}) { 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; } static double weightSum(List grades, {bool finalAvg = false}) => grades .map((e) => finalAvg ? 1 : e.value.weight / 100) .fold(0, (a, b) => a + b); }