487 lines
22 KiB
C#
487 lines
22 KiB
C#
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using LehrerApp.Core.Interfaces;
|
|
using LehrerApp.Core.Models;
|
|
using LehrerApp.Core.Services;
|
|
using System.Collections.ObjectModel;
|
|
|
|
namespace LehrerApp.Desktop.ViewModels.Groups;
|
|
|
|
public sealed record StudentGroupMember(string Name);
|
|
public sealed record StudentPairCandidate(Guid Id, string Name);
|
|
|
|
public sealed class StudentPairRule
|
|
{
|
|
public Guid FirstId { get; }
|
|
public Guid SecondId { get; }
|
|
public string Display { get; }
|
|
public IRelayCommand RemoveCommand { get; }
|
|
|
|
public StudentPairRule(StudentPairCandidate first, StudentPairCandidate second, Action<StudentPairRule> remove)
|
|
{
|
|
FirstId = first.Id;
|
|
SecondId = second.Id;
|
|
Display = $"{first.Name} + {second.Name}";
|
|
RemoveCommand = new RelayCommand(() => remove(this));
|
|
}
|
|
|
|
public bool Matches(Guid first, Guid second) =>
|
|
FirstId == first && SecondId == second || FirstId == second && SecondId == first;
|
|
}
|
|
|
|
public sealed class GeneratedStudentGroup
|
|
{
|
|
public string Title { get; }
|
|
public ObservableCollection<StudentGroupMember> Members { get; } = [];
|
|
|
|
public GeneratedStudentGroup(int number, IEnumerable<Student> students)
|
|
{
|
|
Title = $"Gruppe {number}";
|
|
foreach (var student in students.OrderBy(s => s.LastName, StringComparer.CurrentCultureIgnoreCase)
|
|
.ThenBy(s => s.FirstName, StringComparer.CurrentCultureIgnoreCase))
|
|
Members.Add(new StudentGroupMember(student.FullName));
|
|
}
|
|
}
|
|
|
|
/// <summary>Erzeugt Gruppen fuer eine spaetere Planung oder aus der heutigen Anwesenheit.</summary>
|
|
public partial class StudentGroupGeneratorViewModel : ObservableObject
|
|
{
|
|
public const string FutureModeLabel = "Für später · alle Aktiven";
|
|
public const string AdHocModeLabel = "Ad hoc · heute Anwesende";
|
|
|
|
private readonly IStudentRepository _students;
|
|
private readonly IGroupMembershipRepository _memberships;
|
|
private readonly IParticipationSessionRepository _sessions;
|
|
private readonly IParticipationRepository _participation;
|
|
private readonly IGradeRepository _grades;
|
|
private readonly IGroupRepository _groupRepository;
|
|
private readonly Random _random;
|
|
private readonly List<Student> _participants = [];
|
|
private readonly Dictionary<Guid, double?> _performance = [];
|
|
private readonly HashSet<Guid> _supportStudentIds = [];
|
|
private string _attendanceWarning = "";
|
|
|
|
[ObservableProperty] private StudentDrawGroup? _selectedGroup;
|
|
[ObservableProperty] private string _participantModeName = FutureModeLabel;
|
|
[ObservableProperty] private int _targetGroupSize = 4;
|
|
[ObservableProperty] private int _targetGroupCount = 6;
|
|
[ObservableProperty] private double _sizePriority = 75;
|
|
[ObservableProperty] private double _countPriority = 50;
|
|
[ObservableProperty] private double _performanceDistribution = 65;
|
|
[ObservableProperty] private bool _distributeSupportStudents = true;
|
|
[ObservableProperty] private StudentPairCandidate? _selectedPairStudentA;
|
|
[ObservableProperty] private StudentPairCandidate? _selectedPairStudentB;
|
|
[ObservableProperty] private double _forcedPairPriority = 85;
|
|
[ObservableProperty] private double _excludedPairPriority = 85;
|
|
[ObservableProperty] private string _pairingMessage = "";
|
|
[ObservableProperty] private string _statusText = "Optionen wählen und Gruppen zusammenstellen.";
|
|
[ObservableProperty] private string _warningText = "";
|
|
[ObservableProperty] private bool _hasGeneratedGroups;
|
|
|
|
public ObservableCollection<StudentDrawGroup> Groups { get; } = [];
|
|
public ObservableCollection<GeneratedStudentGroup> GeneratedGroups { get; } = [];
|
|
public ObservableCollection<StudentPairCandidate> PairCandidates { get; } = [];
|
|
public ObservableCollection<StudentPairRule> ForcedPairs { get; } = [];
|
|
public ObservableCollection<StudentPairRule> ExcludedPairs { get; } = [];
|
|
public string[] ParticipantModeOptions { get; } = [FutureModeLabel, AdHocModeLabel];
|
|
public bool CanGenerate => SelectedGroup is not null && _participants.Count >= 2;
|
|
public string SizePriorityLabel => PriorityLabel(SizePriority);
|
|
public string CountPriorityLabel => PriorityLabel(CountPriority);
|
|
public string PerformanceDistributionLabel => PerformanceDistribution switch
|
|
{
|
|
<= -70 => "stark homogen",
|
|
< -20 => "eher homogen",
|
|
<= 20 => "zufällig gemischt",
|
|
< 70 => "eher heterogen",
|
|
_ => "stark heterogen",
|
|
};
|
|
public string ForcedPairPriorityLabel => PriorityLabel(ForcedPairPriority);
|
|
public string ExcludedPairPriorityLabel => PriorityLabel(ExcludedPairPriority);
|
|
|
|
public StudentGroupGeneratorViewModel(
|
|
IStudentRepository students,
|
|
IGroupRepository groups,
|
|
IGroupMembershipRepository memberships,
|
|
IParticipationSessionRepository sessions,
|
|
IParticipationRepository participation,
|
|
IGradeRepository grades,
|
|
Guid? initialGroupId = null,
|
|
Random? random = null)
|
|
{
|
|
_students = students;
|
|
_groupRepository = groups;
|
|
_memberships = memberships;
|
|
_sessions = sessions;
|
|
_participation = participation;
|
|
_grades = grades;
|
|
_random = random ?? Random.Shared;
|
|
|
|
foreach (var group in groups.GetAll().OrderBy(g => g.Name, StringComparer.CurrentCultureIgnoreCase))
|
|
Groups.Add(new StudentDrawGroup(group.Id, group.Name));
|
|
SelectedGroup = Groups.FirstOrDefault(g => g.Id == initialGroupId) ?? Groups.FirstOrDefault();
|
|
}
|
|
|
|
partial void OnSelectedGroupChanged(StudentDrawGroup? value)
|
|
{
|
|
ForcedPairs.Clear();
|
|
ExcludedPairs.Clear();
|
|
ReloadParticipants();
|
|
}
|
|
partial void OnParticipantModeNameChanged(string value) => ReloadParticipants();
|
|
partial void OnTargetGroupSizeChanged(int value) => InvalidateResult();
|
|
partial void OnTargetGroupCountChanged(int value) => InvalidateResult();
|
|
partial void OnSizePriorityChanged(double value)
|
|
{
|
|
OnPropertyChanged(nameof(SizePriorityLabel));
|
|
InvalidateResult();
|
|
}
|
|
partial void OnCountPriorityChanged(double value)
|
|
{
|
|
OnPropertyChanged(nameof(CountPriorityLabel));
|
|
InvalidateResult();
|
|
}
|
|
partial void OnPerformanceDistributionChanged(double value)
|
|
{
|
|
OnPropertyChanged(nameof(PerformanceDistributionLabel));
|
|
InvalidateResult();
|
|
}
|
|
partial void OnDistributeSupportStudentsChanged(bool value) => InvalidateResult();
|
|
partial void OnForcedPairPriorityChanged(double value)
|
|
{
|
|
OnPropertyChanged(nameof(ForcedPairPriorityLabel));
|
|
InvalidateResult();
|
|
}
|
|
partial void OnExcludedPairPriorityChanged(double value)
|
|
{
|
|
OnPropertyChanged(nameof(ExcludedPairPriorityLabel));
|
|
InvalidateResult();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void AddForcedPair() => AddPairRule(ForcedPairs, ExcludedPairs);
|
|
|
|
[RelayCommand]
|
|
private void AddExcludedPair() => AddPairRule(ExcludedPairs, ForcedPairs);
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGenerate))]
|
|
private void Generate()
|
|
{
|
|
var groupCount = ResolveGroupCount(_participants.Count);
|
|
var capacities = Enumerable.Range(0, groupCount)
|
|
.Select(i => _participants.Count / groupCount + (i < _participants.Count % groupCount ? 1 : 0))
|
|
.ToArray();
|
|
var assignments = BuildAssignments(capacities);
|
|
|
|
GeneratedGroups.Clear();
|
|
for (var i = 0; i < assignments.Count; i++)
|
|
GeneratedGroups.Add(new GeneratedStudentGroup(i + 1, assignments[i]));
|
|
|
|
HasGeneratedGroups = true;
|
|
StatusText = $"{_participants.Count} Schüler · {groupCount} Gruppen · Größen " +
|
|
string.Join("/", capacities.Distinct().OrderBy(x => x));
|
|
UpdateWarning(groupCount, assignments);
|
|
}
|
|
|
|
private void ReloadParticipants()
|
|
{
|
|
_participants.Clear();
|
|
_performance.Clear();
|
|
_supportStudentIds.Clear();
|
|
PairCandidates.Clear();
|
|
_attendanceWarning = "";
|
|
|
|
if (SelectedGroup is not null)
|
|
{
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var activeMemberships = _memberships.GetByGroup(SelectedGroup.Id)
|
|
.Where(m => GroupMembershipService.IsActiveOn(m, today))
|
|
.ToList();
|
|
var activeIds = activeMemberships.Select(m => m.StudentId).ToHashSet();
|
|
foreach (var membership in activeMemberships.Where(m => m.Niveau == Niveau.Foerder))
|
|
_supportStudentIds.Add(membership.StudentId);
|
|
var active = _students.GetByGroup(SelectedGroup.Id)
|
|
.Where(s => s.IsActive && activeIds.Contains(s.Id)).ToList();
|
|
_participants.AddRange(ParticipantModeName == AdHocModeLabel
|
|
? FilterByTodayAttendance(SelectedGroup.Id, active, today)
|
|
: active);
|
|
LoadPerformance(SelectedGroup.Id);
|
|
foreach (var student in _participants.OrderBy(s => s.LastName).ThenBy(s => s.FirstName))
|
|
PairCandidates.Add(new StudentPairCandidate(student.Id, student.FullName));
|
|
}
|
|
|
|
SelectedPairStudentA = null;
|
|
SelectedPairStudentB = null;
|
|
|
|
InvalidateResult();
|
|
OnPropertyChanged(nameof(CanGenerate));
|
|
GenerateCommand.NotifyCanExecuteChanged();
|
|
}
|
|
|
|
private IEnumerable<Student> FilterByTodayAttendance(Guid groupId, List<Student> active, DateOnly today)
|
|
{
|
|
var todaySessions = _sessions.GetByGroup(groupId).Where(s => s.Date == today)
|
|
.OrderBy(s => s.CreatedAt).ToList();
|
|
if (todaySessions.Count == 0)
|
|
{
|
|
_attendanceWarning = "Heute wurde noch keine Anwesenheit erfasst; deshalb werden alle aktiven Schüler einbezogen.";
|
|
return active;
|
|
}
|
|
|
|
var latestStatus = new Dictionary<Guid, AttendanceStatus?>();
|
|
foreach (var session in todaySessions)
|
|
foreach (var entry in _participation.GetBySession(session.Id))
|
|
if (entry.Attendance is not null) latestStatus[entry.StudentId] = entry.Attendance;
|
|
|
|
var unknown = active.Count(s => !latestStatus.ContainsKey(s.Id));
|
|
if (unknown > 0)
|
|
_attendanceWarning = $"Für {unknown} Schüler fehlt heute ein Anwesenheitsstatus; sie werden vorsichtshalber einbezogen.";
|
|
return active.Where(s => !latestStatus.TryGetValue(s.Id, out var status) || status == AttendanceStatus.Present);
|
|
}
|
|
|
|
private void LoadPerformance(Guid groupId)
|
|
{
|
|
var system = _groupRepository.GetById(groupId)?.GradingSystem ?? GradingSystem.Grades1To6;
|
|
var gradesByStudent = _grades.GetByGroup(groupId).GroupBy(g => g.StudentId)
|
|
.ToDictionary(g => g.Key, g => g.Select(x => ParseGradeScore(x.Value, system)).Where(x => x.HasValue)
|
|
.Select(x => x!.Value).ToList());
|
|
var entries = _sessions.GetByGroup(groupId).SelectMany(s => _participation.GetBySession(s.Id)).ToList();
|
|
|
|
foreach (var student in _participants)
|
|
{
|
|
var signals = gradesByStudent.GetValueOrDefault(student.Id, []).ToList();
|
|
signals.AddRange(entries.Where(e => e.StudentId == student.Id).SelectMany(e => e.Ratings)
|
|
.Select(r => (Math.Clamp(r.Value, -2, 2) + 2) / 4.0));
|
|
_performance[student.Id] = signals.Count == 0 ? null : signals.Average();
|
|
}
|
|
}
|
|
|
|
private List<List<Student>> BuildAssignments(int[] capacities)
|
|
{
|
|
var result = capacities.Select(_ => new List<Student>()).ToList();
|
|
var strength = Math.Abs(PerformanceDistribution) / 100.0;
|
|
var mean = _performance.Values.Where(x => x.HasValue).Select(x => x!.Value).DefaultIfEmpty(0.5).Average();
|
|
var ranked = _participants.Select(s => new
|
|
{
|
|
Student = s,
|
|
Score = (_performance.GetValueOrDefault(s.Id) ?? mean) * strength + _random.NextDouble() * (1 - strength),
|
|
}).OrderByDescending(x => x.Score).ToList();
|
|
|
|
if (PerformanceDistribution < -20)
|
|
{
|
|
var cursor = 0;
|
|
for (var i = 0; i < result.Count; i++)
|
|
for (var j = 0; j < capacities[i]; j++) result[i].Add(ranked[cursor++].Student);
|
|
}
|
|
else if (PerformanceDistribution > 20)
|
|
{
|
|
var sums = new double[result.Count];
|
|
foreach (var item in ranked)
|
|
{
|
|
var target = Enumerable.Range(0, result.Count).Where(i => result[i].Count < capacities[i])
|
|
.OrderBy(i => result[i].Count == 0 ? -1 : sums[i] / result[i].Count)
|
|
.ThenBy(_ => _random.NextDouble()).First();
|
|
result[target].Add(item.Student);
|
|
sums[target] += item.Score;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
var shuffled = ranked.OrderBy(_ => _random.Next()).Select(x => x.Student).ToList();
|
|
var cursor = 0;
|
|
for (var i = 0; i < result.Count; i++)
|
|
for (var j = 0; j < capacities[i]; j++) result[i].Add(shuffled[cursor++]);
|
|
}
|
|
OptimizeAssignments(result);
|
|
return result;
|
|
}
|
|
|
|
private void OptimizeAssignments(List<List<Student>> groups)
|
|
{
|
|
var best = AssignmentPenalty(groups);
|
|
var improved = true;
|
|
var passes = 0;
|
|
while (improved && passes++ < 100)
|
|
{
|
|
improved = false;
|
|
(int AGroup, int AIndex, int BGroup, int BIndex, double Score)? bestSwap = null;
|
|
for (var firstGroup = 0; firstGroup < groups.Count; firstGroup++)
|
|
for (var secondGroup = firstGroup + 1; secondGroup < groups.Count; secondGroup++)
|
|
for (var firstIndex = 0; firstIndex < groups[firstGroup].Count; firstIndex++)
|
|
for (var secondIndex = 0; secondIndex < groups[secondGroup].Count; secondIndex++)
|
|
{
|
|
(groups[firstGroup][firstIndex], groups[secondGroup][secondIndex]) =
|
|
(groups[secondGroup][secondIndex], groups[firstGroup][firstIndex]);
|
|
var score = AssignmentPenalty(groups);
|
|
(groups[firstGroup][firstIndex], groups[secondGroup][secondIndex]) =
|
|
(groups[secondGroup][secondIndex], groups[firstGroup][firstIndex]);
|
|
if (score + 0.000001 < best && (bestSwap is null || score < bestSwap.Value.Score))
|
|
bestSwap = (firstGroup, firstIndex, secondGroup, secondIndex, score);
|
|
}
|
|
if (bestSwap is { } swap)
|
|
{
|
|
(groups[swap.AGroup][swap.AIndex], groups[swap.BGroup][swap.BIndex]) =
|
|
(groups[swap.BGroup][swap.BIndex], groups[swap.AGroup][swap.AIndex]);
|
|
best = swap.Score;
|
|
improved = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
private double AssignmentPenalty(List<List<Student>> groups)
|
|
{
|
|
var mean = _performance.Values.Where(x => x.HasValue).Select(x => x!.Value).DefaultIfEmpty(0.5).Average();
|
|
var distributionWeight = Math.Abs(PerformanceDistribution) / 100.0;
|
|
double performancePenalty;
|
|
if (PerformanceDistribution < -20)
|
|
{
|
|
performancePenalty = groups.Sum(group =>
|
|
{
|
|
var values = group.Select(s => _performance.GetValueOrDefault(s.Id) ?? mean).ToList();
|
|
var groupMean = values.Average();
|
|
return values.Sum(value => Math.Pow(value - groupMean, 2));
|
|
});
|
|
}
|
|
else if (PerformanceDistribution > 20)
|
|
{
|
|
var averages = groups.Select(group => group.Average(s => _performance.GetValueOrDefault(s.Id) ?? mean)).ToList();
|
|
performancePenalty = averages.Sum(value => Math.Pow(value - averages.Average(), 2));
|
|
}
|
|
else performancePenalty = 0;
|
|
|
|
var supportPenalty = 0.0;
|
|
if (DistributeSupportStudents && _supportStudentIds.Count > 0)
|
|
{
|
|
var counts = groups.Select(g => g.Count(s => _supportStudentIds.Contains(s.Id))).ToList();
|
|
supportPenalty = counts.Sum(count => Math.Pow(count - counts.Average(), 2)) * 2.0;
|
|
}
|
|
|
|
var groupByStudent = groups.SelectMany((group, index) => group.Select(s => (s.Id, index)))
|
|
.ToDictionary(x => x.Id, x => x.index);
|
|
var forcedPenalty = ForcedPairs.Count(rule => PairViolated(rule, groupByStudent, mustBeTogether: true)) *
|
|
PairWeight(ForcedPairPriority);
|
|
var excludedPenalty = ExcludedPairs.Count(rule => PairViolated(rule, groupByStudent, mustBeTogether: false)) *
|
|
PairWeight(ExcludedPairPriority);
|
|
return performancePenalty * distributionWeight + supportPenalty + forcedPenalty + excludedPenalty;
|
|
}
|
|
|
|
private int ResolveGroupCount(int participantCount)
|
|
{
|
|
var maxGroups = Math.Max(1, participantCount / 2);
|
|
var requestedCount = Math.Clamp(TargetGroupCount, 1, maxGroups);
|
|
var sizeBasedCount = Math.Clamp((int)Math.Ceiling(participantCount / (double)Math.Max(2, TargetGroupSize)), 1, maxGroups);
|
|
if (CountPriority >= 99 && SizePriority < 99) return requestedCount;
|
|
if (SizePriority >= 99 && CountPriority < 99) return sizeBasedCount;
|
|
|
|
return Enumerable.Range(1, maxGroups).MinBy(count =>
|
|
{
|
|
var sizeDeviation = Math.Abs(participantCount / (double)count - TargetGroupSize) / Math.Max(2, TargetGroupSize);
|
|
var countDeviation = Math.Abs(count - requestedCount) / (double)Math.Max(1, requestedCount);
|
|
return sizeDeviation * (0.05 + SizePriority / 100.0) +
|
|
countDeviation * (0.05 + CountPriority / 100.0);
|
|
});
|
|
}
|
|
|
|
private void InvalidateResult()
|
|
{
|
|
GeneratedGroups.Clear();
|
|
HasGeneratedGroups = false;
|
|
StatusText = _participants.Count >= 2
|
|
? $"{_participants.Count} Schüler stehen zur Einteilung bereit."
|
|
: "Für eine Gruppeneinteilung werden mindestens zwei Schüler benötigt.";
|
|
UpdateWarning(null);
|
|
}
|
|
|
|
private void UpdateWarning(int? resolvedCount, List<List<Student>>? assignments = null)
|
|
{
|
|
var warnings = new List<string>();
|
|
if (_attendanceWarning.Length > 0) warnings.Add(_attendanceWarning);
|
|
if (_participants.Count >= 2)
|
|
{
|
|
if (_participants.Count != Math.Max(2, TargetGroupSize) * TargetGroupCount)
|
|
{
|
|
var resolution = resolvedCount is null ? "" : $" Verwendet werden {resolvedCount} Gruppen.";
|
|
warnings.Add($"Zielgröße {TargetGroupSize} und {TargetGroupCount} Gruppen sind bei {_participants.Count} Schülern nicht gleichzeitig exakt erfüllbar.{resolution}");
|
|
}
|
|
}
|
|
if (assignments is not null)
|
|
{
|
|
var groupByStudent = assignments.SelectMany((group, index) => group.Select(s => (s.Id, index)))
|
|
.ToDictionary(x => x.Id, x => x.index);
|
|
var unavailable = ForcedPairs.Concat(ExcludedPairs).Count(rule =>
|
|
!groupByStudent.ContainsKey(rule.FirstId) || !groupByStudent.ContainsKey(rule.SecondId));
|
|
var forcedOpen = ForcedPairs.Count(rule => PairViolated(rule, groupByStudent, mustBeTogether: true));
|
|
var excludedOpen = ExcludedPairs.Count(rule => PairViolated(rule, groupByStudent, mustBeTogether: false));
|
|
if (unavailable > 0) warnings.Add($"{unavailable} Paarregel(n) sind wegen fehlender Teilnehmender heute nicht anwendbar.");
|
|
if (forcedOpen > 0) warnings.Add($"{forcedOpen} gewünschte Paarung(en) konnten mit den aktuellen Prioritäten nicht erfüllt werden.");
|
|
if (excludedOpen > 0) warnings.Add($"{excludedOpen} ausgeschlossene Paarung(en) konnten mit den aktuellen Prioritäten nicht getrennt werden.");
|
|
}
|
|
WarningText = string.Join(" ", warnings);
|
|
}
|
|
|
|
private void AddPairRule(ObservableCollection<StudentPairRule> target,
|
|
ObservableCollection<StudentPairRule> opposite)
|
|
{
|
|
PairingMessage = "";
|
|
if (SelectedPairStudentA is null || SelectedPairStudentB is null)
|
|
{
|
|
PairingMessage = "Bitte zwei Schüler auswählen.";
|
|
return;
|
|
}
|
|
if (SelectedPairStudentA.Id == SelectedPairStudentB.Id)
|
|
{
|
|
PairingMessage = "Bitte zwei verschiedene Schüler auswählen.";
|
|
return;
|
|
}
|
|
if (target.Any(rule => rule.Matches(SelectedPairStudentA.Id, SelectedPairStudentB.Id)))
|
|
{
|
|
PairingMessage = "Diese Paarregel ist bereits vorhanden.";
|
|
return;
|
|
}
|
|
var conflicting = opposite.FirstOrDefault(rule => rule.Matches(SelectedPairStudentA.Id, SelectedPairStudentB.Id));
|
|
if (conflicting is not null) opposite.Remove(conflicting);
|
|
StudentPairRule? created = null;
|
|
created = new StudentPairRule(SelectedPairStudentA, SelectedPairStudentB, rule =>
|
|
{
|
|
target.Remove(rule);
|
|
InvalidateResult();
|
|
});
|
|
target.Add(created);
|
|
SelectedPairStudentA = null;
|
|
SelectedPairStudentB = null;
|
|
InvalidateResult();
|
|
}
|
|
|
|
private static bool PairViolated(StudentPairRule rule, IReadOnlyDictionary<Guid, int> groups,
|
|
bool mustBeTogether)
|
|
{
|
|
if (!groups.TryGetValue(rule.FirstId, out var first) || !groups.TryGetValue(rule.SecondId, out var second))
|
|
return false;
|
|
return mustBeTogether ? first != second : first == second;
|
|
}
|
|
|
|
private static double PairWeight(double priority) => 0.05 + priority / 100.0 * 8.0;
|
|
|
|
private static string PriorityLabel(double value) => value switch
|
|
{
|
|
>= 90 => "nahezu fix",
|
|
>= 65 => "hohe Priorität",
|
|
>= 35 => "mittlere Priorität",
|
|
>= 10 => "flexibel",
|
|
_ => "nur Richtwert",
|
|
};
|
|
|
|
private static double? ParseGradeScore(string value, GradingSystem system)
|
|
{
|
|
var text = value.Trim();
|
|
var tendency = 0.0;
|
|
if (text.EndsWith('+')) { tendency = system == GradingSystem.Grades1To6 ? -1.0 / 3 : 1.0 / 3; text = text[..^1]; }
|
|
else if (text.EndsWith('-')) { tendency = system == GradingSystem.Grades1To6 ? 1.0 / 3 : -1.0 / 3; text = text[..^1]; }
|
|
if (!double.TryParse(text, System.Globalization.NumberStyles.Number,
|
|
System.Globalization.CultureInfo.InvariantCulture, out var parsed)) return null;
|
|
parsed += tendency;
|
|
return system == GradingSystem.Grades1To6
|
|
? Math.Clamp((6 - parsed) / 5.0, 0, 1)
|
|
: Math.Clamp(parsed / 15.0, 0, 1);
|
|
}
|
|
}
|