Search code examples
swiftdateinterval

exclude Array of DateInterval from Array of DateInterval


I want to exclude Array of DateInterval from Array of DateInterval. This's my code, but I don't think it will not be helpful.. it goes in infinite loop sometimes and I couldn't solve it.

extension DateInterval {

    static func exclude(_ excludedIntervals: [DateInterval], from periods: [DateInterval]) -> [DateInterval] {
        if excludedIntervals.isEmpty { return periods }
        var resultSlots: [DateInterval] = []

        for period in periods {
            let results = period.exclude(excludedIntervals)
            resultSlots.append(contentsOf: results)
        }

        return resultSlots
    }

    func exclude(_ execludedIntervals: [DateInterval]) -> [DateInterval] {
        if execludedIntervals.isEmpty { return [self] }
        var sortedExecludedIntervals = execludedIntervals.sorted()
        var resultSlots: [DateInterval] = []
        var execludedInterval = sortedExecludedIntervals.removeFirst()
        // remove execludedIntervals from self
        if let intersection = self.intersection(with: execludedInterval) {
            if self.start == intersection.start && self.end > intersection.end {
                let newSlot = DateInterval(start: intersection.end, end: self.end)
                resultSlots.append(contentsOf: newSlot.exclude(sortedExecludedIntervals))
            } else if self.start < intersection.start && self.end == intersection.end {
                let newSlot = DateInterval(start: self.start, end: intersection.start)
                resultSlots.append(contentsOf: newSlot.exclude(sortedExecludedIntervals))
            } else if self.start < intersection.start && self.end > intersection.end {
                let preSlot = DateInterval(start: self.start, end: intersection.start)
                resultSlots.append(contentsOf: preSlot.exclude(sortedExecludedIntervals))
                let postSlot = DateInterval(start: intersection.end, end: self.end)
                resultSlots.append(contentsOf: postSlot.exclude(sortedExecludedIntervals))
            } else {
                // start = start && end = end
                resultSlots = []
                return resultSlots
            }
        }


        return resultSlots
    } 
}

For example, I want to exclude 1 pm- 3pm and 5 pm - 6 pm intervals from an interval 12 pm - 6pm. The function should return 12 pm - 1 pm and 3 pm - 5 pm.


Solution

  • A couple of thoughts:

    1. If I want a method to operate on an array of DateInterval, I’d suggest putting it in an Array (or Sequence) extension, constrained to DateInterval types, rather than a static method on DateInterval:

      extension Array where Element == DateInterval {
          func exclude(_ excludedIntervals: [DateInterval]) -> [DateInterval] { ... }
      }
      
    2. When considering excluding a DateInterval from another, there are tons of different scenarios:

      • You could exclude some small window from the middle of the interval;
      • You could exclude a portion at the start of the interval;
      • You could exclude a portion at the end of the interval; and
      • You could exclude the whole interval.
         

      In my mind, it gets too messy to think of all of those scenarios, so I decided to simplify this a bit and decide:

      • Where exactly does the excluded region intersect with the current interval (and DateInterval supplies a nice method to do that for us);
      • If I “cut out” that intersection from the date interval, I might end up with two intervals, a before interval and an after interval (e.g. if I cut 2pm-3pm out of noon-6pm, the before interval will be noon-2pm and the after interval will be 3pm-6pm);
      • The algorithm then distills down to “if the interval is intersected by the excluded region, replace the original interval with the two other intervals, the one before and the one after”; and
      • Given that I’m mutating the original array of resulting intervals, I’d suggest nested loops, with the outer loop being the intervals to be excluded and the inner loop being the resulting intervals which, because it’s mutating, I’ll iterate through using a while statement, manually checking and adjusting the current index.
         

    That yields:

    extension Array where Element == DateInterval {
        func exclude(_ excludedIntervals: [DateInterval]) -> [DateInterval] {
            var results: [DateInterval] = self
    
            for excludedInterval in excludedIntervals {
                var index = results.startIndex
                while index < results.endIndex {
                    let interval = results[index]
                    if let intersection = interval.intersection(with: excludedInterval) {
                        var before: DateInterval?
                        var after: DateInterval?
    
                        if intersection.start > interval.start {
                            before = DateInterval(start: interval.start, end: intersection.start)
                        }
                        if intersection.end < interval.end {
                            after = DateInterval(start: intersection.end, end: interval.end)
                        }
                        let replacements = [before, after].compactMap { $0 }
                        results.replaceSubrange(index...index, with: replacements)
                        index += replacements.count
                    } else {
                        index += 1
                    }
                }
            }
    
            return results
        }
    }
    

    Then we can consider the exclusion applied to a single DateInterval as just a special case of an array with one item:

    extension DateInterval {
        func exclude(_ excludedIntervals: [DateInterval]) -> [DateInterval] {
            return [self].exclude(excludedIntervals)
        }
    }
    

    So:

    let formatter = ISO8601DateFormatter()
    
    let start = formatter.date(from: "2019-02-09T12:00:00Z")!
    let end = formatter.date(from: "2019-02-09T18:00:00Z")!
    
    let exclude1Start = formatter.date(from: "2019-02-09T13:00:00Z")!
    let exclude1End = formatter.date(from: "2019-02-09T14:00:00Z")!
    
    let exclude2Start = formatter.date(from: "2019-02-09T16:00:00Z")!
    let exclude2End = formatter.date(from: "2019-02-09T17:00:00Z")!
    
    let intervals = DateInterval(start: start, end: end)
        .exclude([
            DateInterval(start: exclude1Start, end: exclude1End),
            DateInterval(start: exclude2Start, end: exclude2End)
        ])
    
    print(intervals)
    

    Will produce:

    [
        2019-02-09 12:00:00 +0000 to 2019-02-09 13:00:00 +0000,
        2019-02-09 14:00:00 +0000 to 2019-02-09 16:00:00 +0000,
        2019-02-09 17:00:00 +0000 to 2019-02-09 18:00:00 +0000
    ]