/// <summary> /// Filter a list of occurrences, removing ones that are older than the given startTime /// and cutting anyone with the startTime in the middle, plus set as unavailable any occurrence /// that happens inside the advanceTime (relative to the current machine time). /// </summary> /// <param name="occurences"></param> /// <param name="startTime"></param> /// <param name="advanceTime"></param> /// <returns></returns> private static IEnumerable <CalendarDll.CalendarUtils.AvailabilitySlot> OccurrencesWithAdvanceTimeSlot( IEnumerable <CalendarDll.CalendarUtils.AvailabilitySlot> occurences, DateTimeOffset startTime, double advanceTime) { var notBeforeTime = DateTimeOffset.Now.AddHours(advanceTime); if (startTime < notBeforeTime) { var past = new CalendarDll.CalendarUtils.AvailabilitySlot { StartTime = startTime, EndTime = notBeforeTime, AvailabilityTypeID = (int)AvailabilityType.Unavailable }; yield return(past); } else { // Since the queried time is newer than the 'limit time in advance', // then our new limit is the queried startTime (anything before that // is not wanted now) notBeforeTime = startTime; } // Occurrences need to be filtered to do not include ones that happens older than the advance time / startTime, // not just for performance of the timeline computation, but for consistency since it can breaks the logic there (since // it expects events sorted ascending by startTime, and occurrence with older start than advanceTime slot would break it). // Too, if an occurrence starts before advance but ends after, must create a new slot (cut from intersection -advanceTimeStart- to endtime). foreach (var s in occurences) { if (s.EndTime <= notBeforeTime) { // Excluded, is old continue; } else if (s.StartTime < notBeforeTime) { // Intersection (since endTime is not older than notBeforeTime, by first 'if' check) yield return(new CalendarDll.CalendarUtils.AvailabilitySlot { StartTime = notBeforeTime, EndTime = s.EndTime, AvailabilityTypeID = s.AvailabilityTypeID }); } else { // Just newer, give it 'as is' yield return(s); } } }
/// <summary> /// It optimizes a given timeline, as a list of non overlapping and ordered slots, by merging consecutive /// slots of the same availabilityType in one, getting the smaller timeline list possible to represent /// the same availability. /// </summary> /// <param name="slots">Non overlapping and ordered slots, as coming from GetTimeline; otherwise, results are unexpected. A null slot in the list will throw random exception.</param> /// <returns></returns> static private IEnumerable <CalendarDll.CalendarUtils.AvailabilitySlot> OptimizeTimeline(IEnumerable <CalendarDll.CalendarUtils.AvailabilitySlot> slots) { var enumerate = slots.GetEnumerator(); // Quick return on empty list if (!enumerate.MoveNext()) { yield break; } // Read first: is the initial 'previous one' var prevSlot = enumerate.Current; while (enumerate.MoveNext()) { var currentSlot = enumerate.Current; if (currentSlot.AvailabilityTypeID == prevSlot.AvailabilityTypeID) { // Merge lastSlot and current slot prevSlot = new CalendarDll.CalendarUtils.AvailabilitySlot { AvailabilityTypeID = prevSlot.AvailabilityTypeID, StartTime = prevSlot.StartTime, EndTime = currentSlot.EndTime }; } else { // Merging not possible, return last one and // replace reference with current one yield return(prevSlot); prevSlot = currentSlot; } } // Return the pending last slot yield return(prevSlot); }
/// <summary> /// Gets a timeline of non overlapping slots, without holes (filled in with 'unavailable') /// for the given set of slots, sorted ascending. /// But is NOT optimized, meaning can contains consecutive slots of same availability type; to /// get an optimized, compressed, output, apply OptimizeTimeline to the result. /// </summary> /// <param name="AvailabilitySlots"></param> /// <param name="passNumber">Used only internally, to avoid that recursive calls creates an stack overflow.</param> /// <returns></returns> static private IEnumerable <CalendarDll.CalendarUtils.AvailabilitySlot> GetTimeline(IEnumerable <CalendarDll.CalendarUtils.AvailabilitySlot> AvailabilitySlots, int passNumber) { var enumerate = AvailabilitySlots.GetEnumerator(); // Quick return if (!enumerate.MoveNext()) { yield break; } // First one, as previous on the iteration. var prevsBuffer = new List <CalendarDll.CalendarUtils.AvailabilitySlot>(); prevsBuffer.Add(enumerate.Current); var newsBuffer = new List <CalendarDll.CalendarUtils.AvailabilitySlot>(); // On each iteration, number of queued items in the buffer that can be released var dequeueCount = 0; // On each iteration, number of queued items in the buffer After the dequeueCount // that must be discarded/removed frmo the buffer without return them, because // gets obsolete after perform the analysis (that means that new, fragmented, // ranges were created for that one because of collisions) var discardCount = 0; // On each iteration, current element must or must not be added 'as is' to the queue var addCurrentToBuffer = true; // Implemented several passes when an analyzed lists of 'prevsBuffer' contains more than 1 // element, because how elements may overlap has edge cases that only can be solved // by reordering the list and apply the logic on it. // // NOTE: It's something optmized because rather than re-analyze the whole resultsets when // a complete pass is done [like GetTimeline(GetTimeline(...))], only the specific sections of overlaped ranges // performs a second (or more) passes, and still is effective. // // TODO: Review if the logic updates makes possible to remove the multiple passes feature OR a most effective way to check when // multiple passes are needed (right now just [prevsBuffer.length > 2], maybe other quick logic can discard // cases that match that condiction but don't need a second pass). // IMPORTANT: Right now, on unit tests for different edge cases extra passes are not needed. var needsAnotherPass = false; while (enumerate.MoveNext()) { var current = enumerate.Current; needsAnotherPass = prevsBuffer.Count > 2; foreach (var prev in prevsBuffer) { if (current.StartTime >= prev.EndTime) { // Previous can be dequeue (will not collide with following ranges) dequeueCount++; } else /* current.StartTime < prev.EndTime */ { addCurrentToBuffer = false; discardCount++; // Intersection of events: // |.....prev.....| // |.....current.....| // |.prev..|.new..|..current.| // // TODO: Optimize creation of ranges: only 2 are needed, because the // new range will share its availability with one of the others, allowing // to mix both in one. // // IMPORTANT: current may finish before prev (current inside prev) like // |.............prev..............| // |......current....| // |.prev.|.prev-or-current.|.prev.| // // TODO: If prev has higher or same priority, there is only one range, the prev // TODO: If current has higher priority, three ranges are needed, the current keeps 'as is' // // IMPORTANT: because of multiple ranges overlapping and ones first being longer than following ones, // the split behavior may end creating 'prev' ranges that happens AFTER 'current', because // of that the first cut may end being a prev or current section and 'minDateTime and maxdatetime' are required. // The graph can be something like // |.......prev........| // |....current....| // |p-or-c|.p-or-c.|.prev.....| // ANOTHER EDGE CASE Because multiple overlapping and passes // |.....prev.....| // |...current..| // |...current..|new|.....prev.....| // - Return a reduced version of the prev // (if there is place for one!) var minStart = MinDateTime(prev.StartTime, current.StartTime); var minEnd = MinDateTime(MinDateTime(prev.EndTime, current.EndTime), MaxDateTime(prev.StartTime, current.StartTime)); //if (minEnd < minStart) throw new Exception("TEST minEnd < minStart::" + passNumber + ":" + minEnd.ToString("r") + ":" + minStart.ToString("r")); if (prev.StartTime != current.StartTime) { newsBuffer.Add(new CalendarDll.CalendarUtils.AvailabilitySlot { StartTime = minStart, EndTime = minEnd, AvailabilityTypeID = minStart == current.StartTime ? current.AvailabilityTypeID : prev.AvailabilityTypeID }); } // - New range on the intersection, with the stronger availability var maxStart = MaxDateTime(MinDateTime(prev.EndTime, current.EndTime), MaxDateTime(prev.StartTime, current.StartTime)); //if (maxStart < minEnd) throw new Exception("TEST maxStart < minEnd::" + maxStart.ToString("r") + ":" + minEnd.ToString("r")); newsBuffer.Add(new CalendarDll.CalendarUtils.AvailabilitySlot { StartTime = minEnd, EndTime = maxStart, AvailabilityTypeID = GetPriorityAvailabilitySlot(prev, current).AvailabilityTypeID }); // - Reduced version of the current // (if there is place for one!) if (prev.EndTime != current.EndTime) { var maxEnd = MaxDateTime(prev.EndTime, current.EndTime); //if (maxEnd < maxStart) throw new Exception("TEST maxEnd < maxStart::" + maxEnd.ToString("r") + ":" + maxStart.ToString("r")); newsBuffer.Add(new CalendarDll.CalendarUtils.AvailabilitySlot { StartTime = maxStart, EndTime = maxEnd, AvailabilityTypeID = maxEnd == current.EndTime ? current.AvailabilityTypeID : prev.AvailabilityTypeID }); } } } if (addCurrentToBuffer) { // Current must be in the list prevsBuffer.Add(current); } addCurrentToBuffer = true; // Add the new ones to queue foreach (var ne in newsBuffer) { prevsBuffer.Add(ne); } newsBuffer.Clear(); // Check two latest ranges in order to fill in a hole (if any) if (prevsBuffer.Count > 1) { var preHole = prevsBuffer[prevsBuffer.Count - 2]; var postHole = prevsBuffer[prevsBuffer.Count - 1]; if (postHole.StartTime > preHole.EndTime) { // There is a gap: // fill it with unavailable slot var hole = new CalendarDll.CalendarUtils.AvailabilitySlot { AvailabilityTypeID = (int)LcCalendar.AvailabilityType.Unavailable, StartTime = preHole.EndTime, EndTime = postHole.StartTime }; // Must be inserted in the place of postHole (to keep them sorted), and re-add that after prevsBuffer[prevsBuffer.Count - 1] = hole; prevsBuffer.Add(postHole); dequeueCount++; } } // Dequee: return and remove from buffer that elements that are ready for (var i = 0; i < dequeueCount; i++) { // Since we are modifing the buffer on each iteration, // the element to return and remove is ever the first (0) yield return(prevsBuffer[0]); prevsBuffer.RemoveAt(0); } dequeueCount = 0; // Discard: remove from buffer, but NOT return, elements that get obsolete for (var i = 0; i < discardCount; i++) { // Since we are modifing the buffer on each iteration, // the element to remove is ever the first (0) prevsBuffer.RemoveAt(0); } discardCount = 0; // Multi passes are needed to ensure correct results. if (needsAnotherPass) { // Stack overflow, excessive passes, control: if (passNumber + 1 > MAX_GETTIMELINE_PASSES) { throw new Exception("Impossible to compute availability."); } prevsBuffer = GetTimeline(prevsBuffer.OrderBy(x => x.StartTime), passNumber + 1).ToList(); } } // Return last pending: foreach (var range in prevsBuffer) { yield return(range); } }
static CalendarDll.CalendarUtils.AvailabilitySlot GetPriorityAvailabilitySlot(CalendarDll.CalendarUtils.AvailabilitySlot date1, CalendarDll.CalendarUtils.AvailabilitySlot date2) { var pri1 = AvailabilityPriorities[date1.AvailabilityTypeID]; var pri2 = AvailabilityPriorities[date2.AvailabilityTypeID]; return(pri1 >= pri2 ? date1 : date2); }