From 9b085d65fd98172fdcd45bff99a5fd539af33740 Mon Sep 17 00:00:00 2001 From: Kiel Gillard Date: Wed, 22 Nov 2023 18:18:25 +1100 Subject: [PATCH] Add timeIntervalSince(_:) and dateIntervalSince(_:) API to compute date and/or time differences between date intervals. --- .../FoundationEssentials/DateInterval.swift | 133 +++++++++++++++++ .../DateIntervalTests.swift | 134 ++++++++++++++++++ 2 files changed, 267 insertions(+) diff --git a/Sources/FoundationEssentials/DateInterval.swift b/Sources/FoundationEssentials/DateInterval.swift index 04d2c55d1..a508a9277 100644 --- a/Sources/FoundationEssentials/DateInterval.swift +++ b/Sources/FoundationEssentials/DateInterval.swift @@ -147,6 +147,139 @@ public struct DateInterval : Comparable, Hashable, Codable, Sendable { } return false } + + /** + Returns the seconds between `self` and `date` or `nil` if there is no difference in time between them. + + For example, given this interval and this date on a timeline: + ``` + |-----| <-- time interval --> * + ``` + Returns a negative time interval when `date` is a moment greater than or equal to the end of `self` because the receiver specifies a range of times earlier than `date`. + + ``` + * <-- time interval --> |-----| + ``` + Returns a positive time interval when `date` is a moment less than or equal to (before) the start of `self` because the receiver specifies a range of times later than `date`. + + A return value of `0` indicates `date` is equal to either the start or end moments of `self`. + + A return value of `nil` indicates the `date` is between the start and end dates (`date` is both greater than the start and less than the end moments of `self`): + ``` + |--*--| + ``` + */ + public func timeIntervalSince(_ date: Date) -> TimeInterval? { + if end <= date { + return end.timeIntervalSince(date) + } else if date <= start { + return start.timeIntervalSince(date) + } else { + return nil + } + } + + /** + Returns the date interval between `self` and `date` or `nil` if there is no difference in time between them. + + For example, given this interval and this date on a timeline: + ``` + * <-- duration --> |-----| + ``` + Returns a value whose start is `date` and whose `duration` is the time between the `date` and the end of `self`. + + ``` + |-----| <-- duration --> * + ``` + Returns a value whose start is the end of `self` and whose `duration` is the time between the `date` and the the end of `self`. + + A return value with a duration of `0` indicates `date` is equal to the start or end of `self`. + + A return value of `nil` indicates there are no moments between `date` and `self` (`date` is both greater than the start and less than the end moments of `self`): + ``` + |--*--| + ``` + */ + public func dateIntervalSince(_ date: Date) -> DateInterval? { + if date <= start { + return DateInterval(start: date, end: start) + + } else if end <= date { + return DateInterval(start: end, end: date) + + } else { + return nil + } + } + + /** + Returns the seconds between `self` and `dateInterval` or `nil` if there is no difference in time between them. + + For example, given these two intervals on a timeline: + ``` + |-----| <-- time interval --> |-----| + ``` + Returns a negative time interval when `self` ends before `dateInterval` starts. A postive time interval indicates `self` starts after `dateInterval` ends. + + A return value of `0` indicates `self` starts or ends where `dateInterval` ends or starts (in other words, they intersect at their opposing start/end moments): + ``` + |-----|-----| + ``` + + A return value of `nil` indicates `self` and `dateInterval` do not have any time between them: + ``` + |--|-----|--| + ``` + */ + public func timeIntervalSince(_ dateInterval: DateInterval) -> TimeInterval? { + if end <= dateInterval.start { + return end.timeIntervalSince(dateInterval.start) + + } else if dateInterval.end <= start { + return start.timeIntervalSince(dateInterval.end) + + } else { + return nil + } + } + + /** + Returns the date interval between `self` and `dateInterval` or `nil` if there is no difference in time between them. + + For example, given these two intervals on a timeline: + ``` + |-----| <-- duration --> |-----| + ``` + The latest start date and the earliest end date between `self` and `dateInterval` is determined. Returns a date interval whose start is the earliest end date and whose duration is the difference in time between the latest start and earliest end. + + A return value with a duration of `0` indicates `self` and `dateInterval` form an unbroken, continous interval (in other words, they intersect at opposing starts/ends): + ``` + |-----|-----| + ``` + + A return value of `nil` indicates that no interval exists between `self` and `dateInterval`: + ``` + |--|-----|--| + ``` + */ + public func dateIntervalSince(_ dateInterval: DateInterval) -> DateInterval? { + let earliestEnd: Date + let duration: TimeInterval + + if end <= dateInterval.start { + earliestEnd = end + duration = dateInterval.start.timeIntervalSince(end) + + } else if dateInterval.end <= start { + earliestEnd = dateInterval.end + duration = start.timeIntervalSince(dateInterval.end) + + } else { + return nil + } + + return DateInterval(start: earliestEnd, duration: duration) + } public func hash(into hasher: inout Hasher) { hasher.combine(start) diff --git a/Tests/FoundationEssentialsTests/DateIntervalTests.swift b/Tests/FoundationEssentialsTests/DateIntervalTests.swift index 95d7773cd..0b4efef1c 100644 --- a/Tests/FoundationEssentialsTests/DateIntervalTests.swift +++ b/Tests/FoundationEssentialsTests/DateIntervalTests.swift @@ -50,6 +50,140 @@ final class DateIntervalTests : XCTestCase { let testInterval3 = DateInterval(start: start, duration: 100.0) XCTAssertNotEqual(testInterval1, testInterval3) } + + func test_intervalsBetweenDateIntervalAndDate() { + let earlier = Date(timeIntervalSince1970: 0) + let middle = Date(timeIntervalSince1970: 5) + let later = Date(timeIntervalSince1970: 10) + + let start = Date(timeIntervalSince1970: 1) + let duration: TimeInterval = 8 + let end = start.addingTimeInterval(duration) // 9 + let testInterval1 = DateInterval(start: start, duration: duration) + + // * --- |testInterval1| + let t1 = testInterval1.timeIntervalSince(earlier) + let d1 = testInterval1.dateIntervalSince(earlier) + XCTAssertEqual(t1, 1) + XCTAssertEqual(d1, DateInterval(start: earlier, end: start)) + + // |testInterval1| --- * + let t2 = testInterval1.timeIntervalSince(later) + let d2 = testInterval1.dateIntervalSince(later) + XCTAssertEqual(t2, -1) + XCTAssertEqual(d2, DateInterval(start: end, end: later)) + + // | testInterval1 * | + let t3 = testInterval1.timeIntervalSince(middle) + let d3 = testInterval1.dateIntervalSince(middle) + XCTAssertEqual(t3, nil) + XCTAssertEqual(d3, nil) + + // equal to start/end + XCTAssertEqual(testInterval1.timeIntervalSince(start), 0) + XCTAssertEqual(testInterval1.dateIntervalSince(start), DateInterval(start: start, duration: 0)) + XCTAssertEqual(testInterval1.timeIntervalSince(end), 0) + XCTAssertEqual(testInterval1.dateIntervalSince(end), DateInterval(start: end, duration: 0)) + } + + func test_intervalsBetweenDateIntervals() { + // Tests for intervals of zero or more duration between subjects. + // |testInterval1|testInterval2| + let testInterval1 = DateInterval(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSinceReferenceDate: 0)) + let testInterval2 = DateInterval(start: Date(timeIntervalSinceReferenceDate: 0), end: Date(timeIntervalSinceReferenceDate: 100)) + let t1 = testInterval1.timeIntervalSince(testInterval2) + XCTAssertEqual(t1, 0) + + let t2 = testInterval2.timeIntervalSince(testInterval1) + XCTAssertEqual(t2, 0) + + let d1 = testInterval1.dateIntervalSince(testInterval2) + XCTAssertEqual(d1?.start, testInterval1.end) + XCTAssertEqual(d1?.duration, 0) + + let d2 = testInterval2.dateIntervalSince(testInterval1) + XCTAssertEqual(d2?.start, testInterval1.end) + XCTAssertEqual(d2?.duration, 0) + + XCTAssertEqual(d1, d2) + + // |testInterval3|-----|testInterval4| + let testInterval3 = DateInterval(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSinceReferenceDate: 0)) + let testInterval4 = DateInterval(start: Date(timeIntervalSinceReferenceDate: 1), end: Date(timeIntervalSinceReferenceDate: 100)) + let t3 = testInterval3.timeIntervalSince(testInterval4) + XCTAssertEqual(t3, -1) + + let t4 = testInterval4.timeIntervalSince(testInterval3) + XCTAssertEqual(t4, 1) + + let d3 = testInterval3.dateIntervalSince(testInterval4) + let d4 = testInterval4.dateIntervalSince(testInterval3) + XCTAssertEqual(d3?.duration, 1) + XCTAssertEqual(d3?.start, testInterval3.end) + XCTAssertEqual(d4?.duration, 1) + XCTAssertEqual(d4?.start, testInterval3.end) + + // Tests for non-existing intervals between subjects. + // |testInterval5| + // |testInterval6| + // + // As a single timeline: |555|565656|666| + let testInterval5 = DateInterval(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSinceReferenceDate: 0)) + let testInterval6 = DateInterval(start: Date(timeIntervalSinceReferenceDate: -1), end: Date(timeIntervalSinceReferenceDate: 100)) + let t5 = testInterval5.timeIntervalSince(testInterval6) + XCTAssertEqual(t5, nil) + + let t6 = testInterval6.timeIntervalSince(testInterval5) + XCTAssertEqual(t6, nil) + + let d5 = testInterval5.dateIntervalSince(testInterval6) + XCTAssertEqual(d5, nil) + + let d6 = testInterval6.dateIntervalSince(testInterval5) + XCTAssertEqual(d6, nil) + + // |---testInterval7---| + // |testInterval8| + // + // As a single timeline: |777|787878|777| + let testInterval7 = DateInterval(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSinceReferenceDate: 0)) + let testInterval8 = DateInterval(start: Date(timeIntervalSince1970: 10), end: Date(timeIntervalSince1970: 20)) + let t7 = testInterval7.timeIntervalSince(testInterval8) + XCTAssertEqual(t7, nil) + + let t8 = testInterval8.timeIntervalSince(testInterval7) + XCTAssertEqual(t8, nil) + + let d7 = testInterval7.dateIntervalSince(testInterval8) + XCTAssertEqual(d7, nil) + + let d8 = testInterval8.dateIntervalSince(testInterval7) + XCTAssertEqual(d8, nil) + + // |testInterval9| + // |testInterval10---| + let testInterval9 = DateInterval(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSinceReferenceDate: 0)) + let testInterval10 = DateInterval(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSinceReferenceDate: 100)) + let t9 = testInterval9.timeIntervalSince(testInterval10) + XCTAssertEqual(t9, nil) + + let t10 = testInterval10.timeIntervalSince(testInterval9) + XCTAssertEqual(t10, nil) + + let d9 = testInterval9.dateIntervalSince(testInterval10) + XCTAssertEqual(d9, nil) + + let d10 = testInterval10.dateIntervalSince(testInterval9) + XCTAssertEqual(d10, nil) + + // |testInterval11| on itself + let testInterval11 = DateInterval(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSinceReferenceDate: 0)) + let t11 = testInterval11.timeIntervalSince(testInterval11) + XCTAssertEqual(t11, nil) + + let d11 = testInterval11.dateIntervalSince(testInterval11) + XCTAssertEqual(d11, nil) + } func test_hashing() { // dateWithString("2019-04-04 17:09:23 -0700")