45

I have this code where convert a String into a date object

let date2 = KeysData[indexPath.row]["starttime"] as? String

let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"

if let date = dateFormatter.dateFromString(date2!) {
   println(date)          
}

I would like to know if the current date falls between the 2 days i got in the array startdate and endate

Exceptions
  • 1,174
  • 2
  • 9
  • 28

8 Answers8

169

Swift ≧ 3

Swift 3 makes this a lot easier.

let fallsBetween = (startDate ... endDate).contains(Date())

Now that NSDate is bridged to the value type Date and Date conforms to Comparable we can just form a ClosedRange<Date> and use the contains method to see if the current date is included.

Caveat: endDate must be greater or equal startDate. Otherwise the range could not be formed and the code would crash with a fatalError.

This is safe:

extension Date {
    func isBetween(_ date1: Date, and date2: Date) -> Bool {
        return (min(date1, date2) ... max(date1, date2)).contains(self)
    }
}
Community
  • 1
  • 1
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • 3
    Might be worth editing your previously accepted answer with this. This is great! – Chris Wagner Jan 04 '17 at 06:42
  • can you please add how to include the starting date ? for some reason it's not including the starting date – XcodeNOOB Mar 13 '17 at 09:32
  • 1
    @XcodeNOOB: Works for me. Swift's `ClosedRange` and `contains` semantics imply that both start and end dates are inclusive: `(startDate...endDate).contains(startDate) == true` and `(startDate...endDate).contains(endDate) == true`. – Nikolai Ruhe Mar 13 '17 at 09:41
  • This won't compile if `startDate, endDate` or contained `date` are optionals. In my case I had to unwrap them `(startDate!...endDate!).contains(myDate!)` works just fine. Added this comment because a lot of Date objects are optional by default. – inokey May 12 '17 at 14:29
  • 5
    Just a warning, if startDate is ever later than endDate the app will crash. Unless you're 100% sure that will never be the case, you may want to add `guard startDate < endDate else {...}` – jchmilew May 15 '17 at 22:26
  • It gives `fatal error: Can't form Range with upperBound < lowerBound` – toing_toing Jun 13 '17 at 18:35
  • Nice and clean, love it – MUH Mobile Inc. Aug 28 '17 at 16:28
  • 1
    Sorry @leviathan but I found your edit contained too much code and was distracting from the simplicity of Swift's solution. I added a note that explains the problem my way. – Nikolai Ruhe Oct 19 '17 at 11:06
  • 1
    @NikolaiRuhe I would also mention for those freaks who are interested in performance, that this is done in O(1) time, since the range doesn't build the whole array of possible dates, but rather a range with a lower/upper bound. and contains method just checks if it is between those bounds. You can look at the implementation on GitHub to assure yourself that this is the case – denis631 Dec 05 '17 at 11:52
  • 1
    @denis631 Swift's `Range` types consist of a lower and an upper bound and never contain elements in between. That's different from Python's `range()` function which builds an array of values. When the bound's type is `Date` it is not even possible (or at least sensible) to create in-between values: Which resolution should be used (Days? Seconds? Nanoseconds?). The date range from above is not a `CountableRange` so it can't be iterated. – Nikolai Ruhe Dec 06 '17 at 07:34
  • (startDate ... endDate).contains(Date()) is enough.. Thx – Hardik Darji Dec 18 '17 at 09:44
  • @HardikDarji end date not getting included – Zalak Patel Dec 21 '17 at 06:06
  • 11
    ```date1...date2 ~= self```? – Roman Filippov Jan 21 '19 at 11:45
  • How can I ignore times while check between two dates? – Krunal Nagvadia Feb 20 '23 at 14:10
  • @NikolaiRuhe I still believe that making explicit that this is O(1) will make this answer even better as denis631 pointed out. I had the same performance concern and i had to check the comments if anyone had answers. If denis631 hadn't made the comment i would not be so sure of the answer. – Julian Osorio Jun 24 '23 at 13:47
60

Swift 2

For a better answer see Swift ≧ 3.

You already have the code for conversion of your date string in KeysData to NSDate. Assuming you have the two dates in startdate and enddate, all you have to do is check if the current date is in between:

let startDate = ...
let endDate = ...

NSDate().isBetween(date: startDate, andDate: endDate)

extension NSDate {
    func isBetweeen(date date1: NSDate, andDate date2: NSDate) -> Bool {
        return date1.compare(self) == self.compare(date2)
    }
}

Edit: If you want to perform an inclusive range check, use this condition:

 extension NSDate {
    func isBetween(date date1: NSDate, andDate date2: NSDate) -> Bool {
        return date1.compare(self).rawValue * self.compare(date2).rawValue >= 0
    }
}
Community
  • 1
  • 1
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • 3
    That's a nice "trick", but perhaps one should add that this checks for "strictly between", i.e. the start and end date are not included (which may or may not be wanted). – Martin R Sep 30 '15 at 07:05
  • @NikolaiRuhe hey, thanks for your answer, but did you solve Martin R note? I faced the same issue too, the start date and end date are the same date, so when I follow your answer, this rang didn't return true – Rawan Dec 15 '15 at 10:27
  • It's perhaps not the safest thing to depend on the underlying value of `NSComparisonResult` in this way. It's an `Int` value because the enum is used by Objective-C which only allows enums of integer type. multiplying the raw values together assumes the values of the underyling integers, which I don't think is guaranteed. – Abizern Sep 12 '18 at 07:41
  • @Abizern I agree that using the raw values in this way is a bit obfuscated. On the other hand: How are the values not guaranteed to be exactly `-1`, `0` or `+1`? The documentation and headers clearly define the integer values. A lot of code would break if the values of `NSComparisonResult` would change at some point. – Nikolai Ruhe Sep 12 '18 at 07:47
  • This is the only example I've seen of using the raw values of the enum. Normally you run code that returns the enum. It's a comparison, not a value that is stored somewhere, so the underlying value doesn't contain any information. If this were a pure swift enum, it wouldn't even have an Int value. It's only because it's an Objective-C enum that it has an underlying value, and depending on the underlying value is inelegant. Sure, it's a clever, hacky solution, but when there are alternative implementations that are easier to read, then it is just clever code for its own sake. – Abizern Sep 12 '18 at 11:04
  • 1
    @Abizern I agree that the second code example with the inclusive check is too obfuscated. – Nikolai Ruhe Sep 12 '18 at 13:09
  • @NikolaiRuhe I have no problem with the updated answer :P – Abizern Sep 13 '18 at 12:29
9

For Swift 4.2+ I used this extension based on answer above:

extension Date {
    func isBetween(_ date1: Date, and date2: Date) -> Bool {
        return (min(date1, date2) ... max(date1, date2)) ~= self
    }
}

But be careful. If this extension doesn't include your start date (date1), then check the time of your dates. May be you'll need to cut the time from dates to fix it. For example, like this:

let myDateWithoutTime = Calendar.current.startOfDay(for: myDate)
Eugene Babich
  • 1,271
  • 15
  • 25
4

DateInterval has a .contains function for this (see docs):

extension Date {
    func isBetween(_ date1: Date, _ date2: Date) -> Bool {
        date1 < date2
            ? DateInterval(start: date1, end: date2).contains(self)
            : DateInterval(start: date2, end: date1).contains(self)
    }
}

Then you use it like this:

let date = Date()
let date1 = Date(timeIntervalSinceNow: 1000)
let date2 = Date(timeIntervalSinceNow: -1000)
date.isBetween(date1, date2) // true

Note that is inclusive of the end date if that's what you need, or add a guard to capture it:

guard self != max(date1, date2) || self == min(date1, date2) else { return false }

After some refactoring you can end up with something like this:

extension Date {
    func isBetween(_ date1: Date, _ date2: Date) -> Bool {
        let minDate = min(date1, date2)
        let maxDate = max(date1, date2)

        guard self != minDate else { return true }
        guard self != maxDate else { return false }

        return DateInterval(start: minDate, end: maxDate).contains(self)
    }
}

Then this will pass the following test cases:

XCTAssert(
    try XCTUnwrap(Date(fromString: "2020/01/15 09:30")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 09:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00"))
    )
)

XCTAssert(
    try XCTUnwrap(Date(fromString: "2020/01/16 01:00")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 23:00")),
        try XCTUnwrap(Date(fromString: "2020/01/16 04:00"))
    )
)

XCTAssert(
    try XCTUnwrap(Date(fromString: "2020/01/15 10:00")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 10:30"))
    )
)

XCTAssert(
    try XCTUnwrap(Date(fromString: "2020/01/15 10:00")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00"))
    )
)

XCTAssert(
    try XCTUnwrap(Date(fromString: "2020/01/15 09:00")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 08:00"))
    )
)

XCTAssert(
    try XCTUnwrap(Date(fromString: "2020/01/15 08:00")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 08:00"))
    )
)

XCTAssertFalse(
    try XCTUnwrap(Date(fromString: "2020/01/15 09:00")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 13:00"))
    )
)

XCTAssertFalse(
    try XCTUnwrap(Date(fromString: "2020/01/15 10:30")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 10:30"))
    )
)

XCTAssertFalse(
    try XCTUnwrap(Date(fromString: "2020/01/15 10:00")).isBetween(
        try XCTUnwrap(Date(fromString: "2020/01/15 10:00")),
        try XCTUnwrap(Date(fromString: "2020/01/15 08:00"))
    )
)

let date = Date()
let date1 = Date(timeIntervalSinceNow: 1000)
let date2 = Date(timeIntervalSinceNow: -1000)
XCTAssert(date.isBetween(date1, date2))
TruMan1
  • 33,665
  • 59
  • 184
  • 335
2
extension Date
{
    func isBetween(startDate:Date, endDate:Date)->Bool
    {
         return (startDate.compare(self) == .orderedAscending) && (endDate.compare(self) == .orderedDescending)
    }
}
luhuiya
  • 2,129
  • 21
  • 20
2

None of the answers here explicitly cover the potentially more precise boundary tests which differ between the start and the end.

e.g. An event which starts at 0900 and ends at 1000 should have cases which return the following results:

08:59:59    false
09:00:00    true
09:59:59    true
10:00:00    false

If this is your desired outcome then the following extension will work for you:

extension Date {

    func isBetween(_ startDate: Date, and endDate: Date) -> Bool {
        return startDate <= self && self < endDate
    }

}
Leon
  • 3,614
  • 1
  • 33
  • 46
1

Compact Swifty version from Roman's comment:

extension Date {
    func isBetween(_ start: Date, _ end: Date) -> Bool {
        start...end ~= self
    }
}

Then use it:

if Date().isBetween(someDate, someOtherDate) {
    // Current date is between those dates
}
budiDino
  • 13,044
  • 8
  • 95
  • 91
-1
extension Date {

    func isBetweeen(date date1: Date, andDate date2: Date) -> Bool {
        return date1.timeIntervalSince1970 < self.timeIntervalSince1970 && date2.timeIntervalSince1970 > self.timeIntervalSince1970
    }

}
Rashid
  • 1,515
  • 16
  • 16