Search code examples
swiftdatedate-arithmetic

How would I get the Date of 2:00 AM the next day in Swift?


Somebody asked this question as a comment in an answer I already posted to another question. Its awkward and confusing to have extra questions in comments, so I'm posting this as a new question.

The new question is this:

How can i check if my timestamp is Less than 2:00 am the next day?


Solution

  • The Foundation framework has a rich set of functions to do "Calendrical Calculations".

    The Calendar class has functions for adding "date component" values to dates, or forcing an existing Date to a specific time-of-day. We can combine those to give a variety of results.

    Edit:

    Leo Dabus pointed out in the comments that there is a built-in Calendar function nextDate(after:matching:options:) that lets us calculate a specific time in the next day in one shot.

    That version of the function looks like this:

    extension Date {
        func twoAmNextDay() -> Date {
            let components = DateComponents(hour: 2, minute: 0, second: 0)
            return Calendar.current.nextDate(after: self, matching: components, matchingPolicy: .nextTime)!
    }
    

    Note that my implementation uses a force-unwrap to make the result non-optional. That's bad form. It would be better to make the function throw or return an Optional.

    For completeness, my original function was this:

    extension Date {
        func twoAmNextDay() -> Date {
            guard let tomorrow = Calendar.current.date(byAdding: .day, value: 1, to: self),
                  let result = Calendar.current.date(bySettingHour: 2, minute: 0, second: 0,of: tomorrow) else { fatalError() }
            return result
        }
    }
    

    I don't know if the nextDate(after:matching:options:) based version handles things like the transition between daylight savings time and standard time better, but it's certainly simpler.

    We can include that extension with some other code to make a working demo:

    extension Date {
        func twoAmNextDay() -> Date {
            let components = DateComponents(hour: 2, minute: 0, second: 0)
            return Calendar.current.nextDate(after: self, matching: components, matchingPolicy: .nextTime)!
        }
        func localDateString(dateStyle: DateFormatter.Style = .medium, timeStyle: DateFormatter.Style = .medium) -> String {
            DateFormatter.localizedString(from: self, dateStyle: dateStyle, timeStyle: timeStyle)
        }
        func isOtherDateBefore2AmNextDay(_ otherDate: Date) -> Bool {
            otherDate < self.twoAmNextDay()
        }
    }
    
    let now = Date()
    let secondsInDay = 24.0 * 60.0 * 60.0
    print( "Current date = " + now.localDateString() + ". Tomorrow at 2:00 AM is " + Date().twoAmNextDay().localDateString() + "\n")
    for _ in 1...5 {
        let randomOffset = Double.random(in: -secondsInDay*3...secondsInDay*3)
        let randomizedDate = now.addingTimeInterval(randomOffset)
        print("Randomized date = " + randomizedDate.localDateString())
        let notString = now.isOtherDateBefore2AmNextDay(randomizedDate) ? "" : "not "
        print("Randomized date is " + notString + "before 2AM tomorrow")
        print()
    }
    

    Sample output from that code looks like this:

    Current date = Sep 2, 2022 at 11:07:15 AM. Tomorrow at 2:00 AM is Sep 3, 2022 at 2:00:00 AM
    
    Randomized date = Sep 1, 2022 at 1:59:02 AM
    Randomized date is before 2AM tomorrow
    
    Randomized date = Sep 3, 2022 at 6:21:36 AM
    Randomized date is not before 2AM tomorrow
    
    Randomized date = Sep 3, 2022 at 9:13:19 PM
    Randomized date is not before 2AM tomorrow
    
    Randomized date = Sep 3, 2022 at 9:17:17 PM
    Randomized date is not before 2AM tomorrow
    
    Randomized date = Aug 30, 2022 at 11:49:46 PM
    Randomized date is before 2AM tomorrow