Weitere Optimierung am Stundenkonto (CorrectOverlappingRecords)

This commit is contained in:
2026-07-20 14:39:34 +02:00
parent 309e288a44
commit 57d418d4c5
2 changed files with 124 additions and 34 deletions

View File

@@ -1236,9 +1236,38 @@ namespace BeWo.Report.ReportObjects
//do nothing
}
/// <summary>
/// Verknüpft einen bereits verarbeiteten ("alten") Record mit seinem Zeit-Span und
/// seiner Einfüge-Reihenfolge. Der Span wird gecacht (statt pro Paar neu berechnet) und
/// bei In-Place-Korrekturen des Records aktualisiert; der Index stellt die exakte
/// Verarbeitungsreihenfolge wieder her, wenn Kandidaten aus mehreren Tages-Buckets stammen.
/// </summary>
private sealed class OverlapEntry
{
public readonly StundenkontoServiceRecord Record;
public readonly DateTimeSpan Span;
public readonly int Index;
public OverlapEntry(StundenkontoServiceRecord record, DateTimeSpan span, int index)
{
Record = record;
Span = span;
Index = index;
}
}
public virtual void CorrectOverlappingTimes(IList<StundenkontoServiceRecord> serviceRecords)
{
List<StundenkontoServiceRecord> oldRecords = new List<StundenkontoServiceRecord>();
// Tages-Index über bereits verarbeitete Records. Zwei Records können sich nur
// überlappen, wenn sich ihre Zeitintervalle schneiden - und damit teilen sie
// mindestens einen Kalendertag. Statt jeden neuen Record gegen ALLE alten zu
// prüfen (O(n²)), vergleichen wir ihn nur gegen die Records der Tage, die sein
// Span berührt. Da Korrekturen Intervalle ausschließlich verkleinern (Start wandert
// nach vorne, Ende bleibt), ist die Ausdehnung bei der Registrierung eine sichere
// Obermenge - es kann nie eine neue Überlappung entstehen. Durch Iteration in
// Einfüge-Reihenfolge (Index) bleibt das Ergebnis bit-identisch zur alten Version.
var oldRecordsByDay = new Dictionary<DateTime, List<OverlapEntry>>();
int insertionIndex = 0;
foreach (var newRecord in serviceRecords)
{
@@ -1264,34 +1293,19 @@ namespace BeWo.Report.ReportObjects
EndDateTime = newRecord.Start.AddMinutes((double)minuten)
};
foreach (var oldRecord in oldRecords)
foreach (var oldEntry in CollectOverlapCandidates(oldRecordsByDay, newSpan))
{
var oldRecord = oldEntry.Record;
if (!CorrectOverlappingTime(oldRecord, newRecord))
{
continue;
}
var oldSpan = new DateTimeSpan
{
StartDateTime = oldRecord.Start,
EndDateTime = oldRecord.Start.AddMinutes((double)oldRecord.RoundedDuration)
};
if (oldRecord.GroupRoundedDuration.HasValue)
{
oldSpan.EndDateTime = oldRecord.Start.AddMinutes((double)oldRecord.GroupRoundedDuration);
}
// Gecachter Span; wird bei der Korrektur unten in-place mit oldRecord synchron gehalten.
var oldSpan = oldEntry.Span;
if (oldSpan.Overlaps(newSpan))
{
DateTime start = newSpan.StartDateTime;
if (oldSpan.StartDateTime > start)
start = oldSpan.StartDateTime;
DateTime end = newSpan.EndDateTime;
if (oldSpan.EndDateTime < end)
end = oldSpan.EndDateTime;
if (newSpan.StartDateTime >= oldSpan.StartDateTime)
{
newSpan.StartDateTime = oldSpan.EndDateTime;
@@ -1337,8 +1351,84 @@ namespace BeWo.Report.ReportObjects
}
}
oldRecords.Add(newRecord);
// newRecord wird zum "alten" Record: in alle Tages-Buckets seiner (ggf. bereits
// gekürzten) Ausdehnung einsortieren. newSpan ist zugleich der gecachte Span.
RegisterOldRecord(oldRecordsByDay, new OverlapEntry(newRecord, newSpan, insertionIndex++));
}
}
/// <summary>
/// Sammelt alle bereits verarbeiteten Records, deren Tag der übergebene Span berührt, in
/// exakter Einfüge-Reihenfolge. Der häufigste Fall (Record innerhalb eines Tages) wird ohne
/// Zwischenliste/Sortierung bedient; nur bei tagesübergreifenden Spans werden mehrere Buckets
/// zusammengeführt, dedupliziert und nach Einfüge-Index sortiert.
/// </summary>
private static IEnumerable<OverlapEntry> CollectOverlapCandidates(
Dictionary<DateTime, List<OverlapEntry>> oldRecordsByDay, DateTimeSpan newSpan)
{
DateTime firstDay = newSpan.StartDateTime.Date;
DateTime lastDay = newSpan.EndDateTime.Date;
if (lastDay < firstDay)
{
lastDay = firstDay;
}
if (firstDay == lastDay)
{
// Ein Bucket ist bereits in Einfüge-Reihenfolge -> direkt verwenden.
List<OverlapEntry> bucket;
if (oldRecordsByDay.TryGetValue(firstDay, out bucket))
{
return bucket;
}
return System.Linq.Enumerable.Empty<OverlapEntry>();
}
var candidates = new List<OverlapEntry>();
var seen = new HashSet<int>();
for (DateTime day = firstDay; day <= lastDay; day = day.AddDays(1))
{
List<OverlapEntry> bucket;
if (!oldRecordsByDay.TryGetValue(day, out bucket))
{
continue;
}
foreach (var entry in bucket)
{
// Ein tagesübergreifender Record steht in mehreren Buckets -> nur einmal prüfen.
if (seen.Add(entry.Index))
{
candidates.Add(entry);
}
}
}
candidates.Sort((a, b) => a.Index.CompareTo(b.Index));
return candidates;
}
/// <summary>
/// Registriert einen Record in allen Tages-Buckets, die seine Ausdehnung berührt.
/// </summary>
private static void RegisterOldRecord(
Dictionary<DateTime, List<OverlapEntry>> oldRecordsByDay, OverlapEntry entry)
{
DateTime firstDay = entry.Span.StartDateTime.Date;
DateTime lastDay = entry.Span.EndDateTime.Date;
if (lastDay < firstDay)
{
lastDay = firstDay;
}
for (DateTime day = firstDay; day <= lastDay; day = day.AddDays(1))
{
List<OverlapEntry> bucket;
if (!oldRecordsByDay.TryGetValue(day, out bucket))
{
bucket = new List<OverlapEntry>();
oldRecordsByDay[day] = bucket;
}
bucket.Add(entry);
}
}

View File

@@ -56,16 +56,16 @@ namespace HwHilfswerkInklusionUndTeilhabe
return dd;
}
public override bool AddServiceRecordToDuration(ServiceRecord item)
public override bool AddServiceRecordToDuration(StundenkontoServiceRecord item)
{
if (HatEmployeeAmbulantContract(item.Employee.GetValidContractForDate(item.Start.Value)))
if (HatEmployeeAmbulantContract(item.Employee.GetValidContractForDate(item.Start)))
{
return true;
}
if (item.ServiceDescription != null && item.ServiceDescription.ServiceCategory != null)
if (item.ServiceDescription != null && item.ServiceDescription.Category != null)
{
if (item.ServiceDescription.ServiceCategory.Name == "Arbeitszeit")
if (item.ServiceDescription.Category.Name == "Arbeitszeit")
{
return true;
}
@@ -74,21 +74,21 @@ namespace HwHilfswerkInklusionUndTeilhabe
return false;
}
protected override ServiceRecordSummary BerechneStunden(MitarbeiterstundenkontoRO.EmployeeDetail empDetail, IList<ServiceRecord> groupFilteredRecords, DateTime berichtsAnfang,
protected override ServiceRecordSummary BerechneStunden(MitarbeiterstundenkontoRO.EmployeeDetail empDetail, IList<StundenkontoServiceRecord> groupFilteredRecords, DateTime berichtsAnfang,
DateTime berichtsEnde)
{
decimal flsGesamt = 0;
decimal arbeitszeit = 0;
var sum = new ServiceRecordSummary();
sum.RecordsImBerichtszeitraum = new List<ServiceRecord>();
sum.RecordsImBerichtszeitraum = new List<StundenkontoServiceRecord>();
//base.CorrectOverlappingTimes(groupFilteredRecords);
foreach (var item in groupFilteredRecords)
{
if (!IsServiceRecordFehlzeit(item))
{
if (item.Start.Value >= berichtsAnfang && item.Start.Value < berichtsEnde)
if (item.Start >= berichtsAnfang && item.Start < berichtsEnde)
{
//if (AddServiceRecordToDuration(item))
//{
@@ -104,23 +104,23 @@ namespace HwHilfswerkInklusionUndTeilhabe
}
if (item.ServiceDescription != null &&
item.ServiceDescription.ServiceCategory != null)
item.ServiceDescription.Category != null)
{
if (item.Employee != null && HatEmployeeAmbulantContract(item.Employee.GetValidContractForDate(item.Start.Value)))
if (item.Employee != null && HatEmployeeAmbulantContract(item.Employee.GetValidContractForDate(item.Start)))
{
arbeitszeit += duration;
if (item.ServiceDescription.ServiceCategory.IsBillable)
if (item.ServiceDescription.Category.IsBillable)
{
flsGesamt += duration;
}
}
else
{
if (item.ServiceDescription.ServiceCategory.Name.ToLower().Contains("arbeitszeit"))
if (item.ServiceDescription.Category.Name.ToLower().Contains("arbeitszeit"))
{
arbeitszeit += duration;
}
else if (item.ServiceDescription.ServiceCategory.IsBillable)
else if (item.ServiceDescription.Category.IsBillable)
{
flsGesamt += duration;
}