Search code examples
iosswiftdateswift5dateformatter

Check if date is 1 second away from new date


I want to check if the current date input is 1 second away from new date (Date format: "2023-05-20T23:59:59.000+05:30"). Below is my Date extension.

extension Date {
    var isOneSecondAway: Bool {
        let calendar = Calendar.current
        let nextStartOfDay = calendar.date(byAdding: .day, value: 1, to: self.startOfDay) ?? self
        let timeIntervalInSeconds = Int(nextStartOfDay.startOfDay.timeIntervalSince(self))
        return timeIntervalInSeconds == 1
    }
    
    var startOfDay: Date {
        return Calendar.current.startOfDay(for: self)
    }
}

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
let targetDateString = "2023-05-20T23:59:59.000+05:30"
guard let targetDate = dateFormatter.date(from: targetDateString) else {return}
print(targetDate.isOneSecondAway)

Expected output for "2023-05-20T23:59:59.000+05:30" is true but its returning false. Am I doing something wrong here?


Solution

  • You need a timezone in order to figure out when the "start of day" is, and to a lesser extent, when the "next day" is.

    You used Calendar.current in your Date extensions, so the timezone of Calendar.current will be used to do the date calculations. This is usually the system timezone. Note that a Date object does not have an associated timezone. Calendar does.

    I suspect that the timezone of Calendar.current isn't at an offset of +05:30 for the date and time that you used.

    If you set the timezone explicitly to be at an offset of +05:30, then the code prints true:

    extension Date {
        var isOneSecondAway: Bool {
            var calendar = Calendar.current
            calendar.timeZone = TimeZone(secondsFromGMT: 5 * 60 * 60 + 30 * 60)!
            let nextStartOfDay = calendar.date(byAdding: .day, value: 1, to: self.startOfDay) ?? self
            let timeIntervalInSeconds = Int(nextStartOfDay.startOfDay.timeIntervalSince(self))
            return timeIntervalInSeconds == 1
        }
        
        var startOfDay: Date {
            var calendar = Calendar.current
            calendar.timeZone = TimeZone(secondsFromGMT: 5 * 60 * 60 + 30 * 60)!
            return calendar.startOfDay(for: self)
        }
    }
    

    Of course, this wouldn't be very useful if your date strings can contain timezones other than +05:30.

    To handle all kinds of timezones, the Date extensions need a timezone parameter, and you need to extract the timezone from the strings.

    extension Date {
        func isOneSecondAway(timeZone: TimeZone) -> Bool {
            var calendar = Calendar.current
            calendar.timeZone = timeZone
            let nextStartOfDay = calendar.date(byAdding: .day, value: 1, to: self.startOfDay(in: timeZone)) ?? self
            let timeIntervalInSeconds = Int(nextStartOfDay.startOfDay(in: timeZone).timeIntervalSince(self))
            return timeIntervalInSeconds == 1
        }
        
        func startOfDay(in timeZone: TimeZone) -> Date {
            var calendar = Calendar.current
            calendar.timeZone = timeZone
            return calendar.startOfDay(for: self)
        }
    }
    
    // ...
    if let targetDate = dateFormatter.date(from: targetDateString),
       let timeZone = TimeZone(iso8601: targetDateString) {
        print(targetDate.isOneSecondAway(timeZone: timeZone))
    }
    

    Here I used a TimeZone initialiser, adapted from this post.

    extension TimeZone {
        init?(iso8601: String) {
            // assuming there are 23 characters before the offset,
            // like your format string suggests
            let tz = iso8601.dropFirst(23) // remove date and time part
            if tz == "Z" {
                self.init(secondsFromGMT: 0)
            } else if tz.count == 3 { // assume +/-HH
                if let hour = Int(tz) {
                    self.init(secondsFromGMT: hour * 3600)
                    return
                }
            } else if tz.count == 5 { // assume +/-HHMM
                if let hour = Int(tz.dropLast(2)), let min = Int(tz.dropFirst(3)) {
                    self.init(secondsFromGMT: (hour * 60 + min) * 60)
                    return
                }
            } else if tz.count == 6 { // assime +/-HH:MM
                let parts = tz.components(separatedBy: ":")
                if parts.count == 2 {
                    if let hour = Int(parts[0]), let min = Int(parts[1]) {
                        self.init(secondsFromGMT: (hour * 60 + min) * 60)
                        return
                    }
                }
            }
    
            return nil
        }
    }