0

May I know, what is reliable way, to calculate day differences without taking time into consideration?

A similar question is asked before. However, the highest voted and accepted answer isn't entirely accurate - https://stackoverflow.com/a/28163560/72437

The code is broken, when dealing with Day light saving case. You can run the following code in Playground

Use startOfDay (Broken)

import UIKit

struct LocalDate: Equatable {
    let year: Int
    let month: Int
    let day: Int
}

struct LocalTime: Equatable, Codable {
    let hour: Int
    let minute: Int
}

extension Date {
    var startOfDay: Date {
        return Calendar.current.startOfDay(for: self)
    }
    
    static func of(localDate: LocalDate, localTime: LocalTime) -> Date {
        var dateComponents = DateComponents()
        dateComponents.year = localDate.year
        dateComponents.month = localDate.month
        dateComponents.day = localDate.day
        dateComponents.hour = localTime.hour
        dateComponents.minute = localTime.minute
        dateComponents.second = 0
        
        return Calendar.current.date(from: dateComponents)!
    }
   
    func adding(_ component: Calendar.Component, _ value: Int) -> Date {
        return Calendar.current.date(byAdding: component, value: value, to: self)!
    }
}


// During 22 March 2021, Tehran will advance by 1 hour from 00:00 AM, to 01:00 AM.

let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
    NSTimeZone.default = oldDefault
}

// Just a random local time. We will use 'startOfDay' to perform local time resetting.
let localTime = LocalTime(hour: 2, minute: 59)

let localDate1 = LocalDate(year: 2021, month: 3, day: 22)
let localDate2 = LocalDate(year: 2021, month: 3, day: 23)

let date1 = Date.of(localDate: localDate1, localTime: localTime).startOfDay
let date2 = Date.of(localDate: localDate2, localTime: localTime).startOfDay

let components = Calendar.current.dateComponents([.day], from: date1, to: date2)

/*
 
 date1 Monday, March 22, 2021 at 1:00:00 AM Iran Daylight Time
 date2 Tuesday, March 23, 2021 at 12:00:00 AM Iran Daylight Time
 diff in day is Optional(0)
 
 */
print("date1 \(date1.description(with: .current))")
print("date2 \(date2.description(with: .current))")
print("diff in day is \(components.day)")

The different of day should be 1, without taking time into consideration. However, due to day light saving, the computed hour difference is 23 hours instead of 24 hours.

We are then getting 0 day difference.


One of the workaround, is using 12:00 (noon) as local time, with an assumption there is no place in this world, where day light saving occurs during 12:00. I am not sure how solid is this assumption. Such assumption seems to be pretty fragile. What if one day government decides to admen day light saving to be at 12:00?

Use 12:00 (Seems to work. But, how solid it is?)

import UIKit

struct LocalDate: Equatable {
    let year: Int
    let month: Int
    let day: Int
}

struct LocalTime: Equatable, Codable {
    let hour: Int
    let minute: Int
}

extension Date {
    var startOfDay: Date {
        return Calendar.current.startOfDay(for: self)
    }
    
    static func of(localDate: LocalDate, localTime: LocalTime) -> Date {
        var dateComponents = DateComponents()
        dateComponents.year = localDate.year
        dateComponents.month = localDate.month
        dateComponents.day = localDate.day
        dateComponents.hour = localTime.hour
        dateComponents.minute = localTime.minute
        dateComponents.second = 0
        
        return Calendar.current.date(from: dateComponents)!
    }
   
    func adding(_ component: Calendar.Component, _ value: Int) -> Date {
        return Calendar.current.date(byAdding: component, value: value, to: self)!
    }
}


// During 22 March 2021, Tehran will advance by 1 hour from 00:00 AM, to 01:00 AM.

let tehranTimeZone = TimeZone(identifier: "Asia/Tehran")!
let oldDefault = NSTimeZone.default
NSTimeZone.default = tehranTimeZone
defer {
    NSTimeZone.default = oldDefault
}

// Use noon
let localTime = LocalTime(hour: 12, minute: 00)

let localDate1 = LocalDate(year: 2021, month: 3, day: 22)
let localDate2 = LocalDate(year: 2021, month: 3, day: 23)

let date1 = Date.of(localDate: localDate1, localTime: localTime)
let date2 = Date.of(localDate: localDate2, localTime: localTime)

let components = Calendar.current.dateComponents([.day], from: date1, to: date2)

/*
 
 date1 Monday, March 22, 2021 at 12:00:00 PM Iran Daylight Time
 date2 Tuesday, March 23, 2021 at 12:00:00 PM Iran Daylight Time
 diff in day is Optional(1)
 
 */
print("date1 \(date1.description(with: .current))")
print("date2 \(date2.description(with: .current))")
print("diff in day is \(components.day)")

May I know, what is reliable way, to calculate day differences without taking time into consideration?

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • While you can never say that daylight savings would never change at midday, in practice that would seem to be unlikely; It would cause a lot of disruption and confusion which is why it normally happens in the early hours of the morning. The other option may be to use UTC for your calendar. It depends on what your input data is - A string? – Paulw11 Nov 18 '21 at 06:11
  • Exactly what @Paulw11 said. Perhaps use `Calendar(identifier: .gregorian)` rather than `Calendar.current`. Also, I would not recommend adding these convenience methods to Date. Those methods assume that you are using Calendar.current. If you use different calendars in different places you will get weird results. – Rob C Nov 18 '21 at 07:21
  • https://stackoverflow.com/questions/24723431/swift-days-between-two-nsdates/28163560?noredirect=1#comment123766958_28163560 – Emin Bugra Saral Nov 25 '21 at 11:33

1 Answers1

1

Date is a precise point in time, hence expressible as a TimeInterval (aka Double) from an exact moment in time (that'll be reference date aka January 1st 2001 00:00 GMT+0).

Thus that same point in time is differently calculated between TimeZones through Calendar: if the TimeZone has daylight savings, then the calendar take it into account.

Therefore when you operate through a Calendar adopting DateComponents you should keep that in mind. Depending on what you are trying to do in your application it could be useful to just adopt a private Calendar instance set to adopt TimeZone(secondsFromGMT: 0)! for calculating dates as absolutes values. As in:

extension Calendar {
    static let appCal: Self = {
         // I'm used to reason with Gregorian calendar
         var cal = Calendar(identifier: .gregorian) 
         // I just need this calendar for executing absolute time calculations
         cal.timeZone = TimeZone(secondsFromGMT: 0)!
         return cal
    }()

}
valeCocoa
  • 344
  • 1
  • 8