diff --git a/Report/ReportObjects/MitarbeiterstundenkontoBerechnung.cs b/Report/ReportObjects/MitarbeiterstundenkontoBerechnung.cs index 1402fa746..3bb2cd049 100644 --- a/Report/ReportObjects/MitarbeiterstundenkontoBerechnung.cs +++ b/Report/ReportObjects/MitarbeiterstundenkontoBerechnung.cs @@ -1236,9 +1236,38 @@ namespace BeWo.Report.ReportObjects //do nothing } + /// + /// 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. + /// + 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 serviceRecords) { - List oldRecords = new List(); + // 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>(); + 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++)); + } + } + + /// + /// 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. + /// + private static IEnumerable CollectOverlapCandidates( + Dictionary> 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 bucket; + if (oldRecordsByDay.TryGetValue(firstDay, out bucket)) + { + return bucket; + } + return System.Linq.Enumerable.Empty(); + } + + var candidates = new List(); + var seen = new HashSet(); + for (DateTime day = firstDay; day <= lastDay; day = day.AddDays(1)) + { + List 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; + } + + /// + /// Registriert einen Record in allen Tages-Buckets, die seine Ausdehnung berührt. + /// + private static void RegisterOldRecord( + Dictionary> 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 bucket; + if (!oldRecordsByDay.TryGetValue(day, out bucket)) + { + bucket = new List(); + oldRecordsByDay[day] = bucket; + } + bucket.Add(entry); } } diff --git a/ReportImp/HwHilfswerkInklusionUndTeilhabe/CustomMitarbeiterstundenkontoBerechnung.cs b/ReportImp/HwHilfswerkInklusionUndTeilhabe/CustomMitarbeiterstundenkontoBerechnung.cs index ddd4ea5fa..8fb26e006 100644 --- a/ReportImp/HwHilfswerkInklusionUndTeilhabe/CustomMitarbeiterstundenkontoBerechnung.cs +++ b/ReportImp/HwHilfswerkInklusionUndTeilhabe/CustomMitarbeiterstundenkontoBerechnung.cs @@ -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 groupFilteredRecords, DateTime berichtsAnfang, + protected override ServiceRecordSummary BerechneStunden(MitarbeiterstundenkontoRO.EmployeeDetail empDetail, IList groupFilteredRecords, DateTime berichtsAnfang, DateTime berichtsEnde) { decimal flsGesamt = 0; decimal arbeitszeit = 0; var sum = new ServiceRecordSummary(); - sum.RecordsImBerichtszeitraum = new List(); + sum.RecordsImBerichtszeitraum = new List(); //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; }