Constructing NSDate using DateComponent gives inconsistent results - swift

I've created an extension for NSDate which removes the time component to allow equality checks for NSDate based on date alone. I have achieved this by taking the original NSDate object, obtaining the day, month and year using the DateComponent class and then constructing a new NSDate using the information obtained. Although the NSDate objects obtained look correct when printed to the console (i.e. timestamp is 00:00:00) and using the NSDate.compare function on two identical dates returns NSComparisonResult.OrderedSame, if you deconstruct them using DateComponent once more, some of them have the hour set to 1. This appears to be a random event with this error being present about 55% of the time. Forcing the hour, minute and second properties of DateComponent to zero before constructing the new NSDate rather than assuming they will default to these values does not rectify the situation. Ensuring the timezone is set helps a little but again does not fix it.
I am guessing there may be a rounding error somewhere (possibly in my test code), I've fluffed the conversion or there is a Swift bug but would appreciate comments. Code and output from a unit test below.
Code as follows:
extension NSDate {
// creates a NSDate object with time set to 00:00:00 which allows equality checks on dates alone
var asDateOnly: NSDate {
get {
let userCalendar = NSCalendar.currentCalendar()
let dayMonthYearUnits: NSCalendarUnit = .CalendarUnitDay | .CalendarUnitMonth | .CalendarUnitYear
var dateComponents = userCalendar.components(dayMonthYearUnits, fromDate: self)
dateComponents.timeZone = NSTimeZone(name: "GMT")
// dateComponents.hour = 0
// dateComponents.minute = 0
// dateComponents.second = 0
let result = userCalendar.dateFromComponents(dateComponents)!
return result
}
}
Test func:
func testRemovingTimeComponentFromRandomNSDateObjectsAlwaysResultsInNSDateSetToMidnight() {
var dates = [NSDate]()
let dateRange = NSDate.timeIntervalSinceReferenceDate()
for var i = 0; i < 30; i++ {
let randomTimeInterval = Double(arc4random_uniform(UInt32(dateRange)))
let date = NSDate(timeIntervalSinceReferenceDate: randomTimeInterval).asDateOnly
let dateStrippedOfTime = date.asDateOnly
// get the hour, minute and second components from the stripped date
let userCalendar = NSCalendar.currentCalendar()
var hourMinuteSecondUnits: NSCalendarUnit = .CalendarUnitHour | .CalendarUnitMinute | .CalendarUnitSecond
var dateComponents = userCalendar.components(hourMinuteSecondUnits, fromDate: dateStrippedOfTime)
dateComponents.timeZone = NSTimeZone(name: "GMT")
XCTAssertTrue((dateComponents.hour == 0) && (dateComponents.minute == 0) && (dateComponents.second == 0), "Time components were not set to zero - \nNSDate: \(date) \nIndex: \(i) H: \(dateComponents.hour) M: \(dateComponents.minute) S: \(dateComponents.second)")
}
}
Output:
testRemovingTimeComponentFromRandomNSDateObjectsAlwaysResultsInNSDateSetToMidnight] : XCTAssertTrue failed - Time components were not set to zero -
NSDate: 2009-06-19 00:00:00 +0000
Index: 29 H: 1 M: 0 S: 0

I am sure that your test dates you created randomly will contain dates that live in DST (Daylight Saving Time) hence the 1 hour offset — indeed a clock would show 0:00.
Your code is anyway overly complicated and not timezone aware, as you overwrite it.
Preparation: create to dates on the same day with 5 hours apart.
var d1 = NSDate()
let cal = NSCalendar.currentCalendar()
d1 = cal.dateBySettingUnit(NSCalendarUnit.CalendarUnitHour, value: 12, ofDate: d1, options: nil)!
d1 = cal.dateBySettingUnit(NSCalendarUnit.CalendarUnitMinute, value: 0, ofDate: d1, options: nil)!
d1 is noon at users location today.
let comps = NSDateComponents()
comps.hour = 5;
var d2 = cal.dateByAddingComponents(comps, toDate: d1, options: nil)!
d2 five hours later
Comparison: This comparison will yield equal, as the dates are on the same day
let b = cal.compareDate(d1, toDate: d2, toUnitGranularity: NSCalendarUnit.CalendarUnitDay)
if b == .OrderedSame {
println("equal")
} else {
println("not equal")
}
The following will yield non equal, as the dates are not in the same hour
let b = cal.compareDate(d1, toDate: d2, toUnitGranularity: NSCalendarUnit.CalendarUnitHour)
if b == .OrderedSame {
println("equal")
} else {
println("not equal")
}
Display the dates with a date formatter, as it will take DST and timezones in account.

Related

Convert current time to local time in date format(Without converting it to string) in swift

I want to convert current time(in UTC) to my local time zone. But I don't want it in string I want it in date format itself.
I have written following code:
let currentDate = Date()
let dateFormatter = DateFormatter()
dateFormatter.timeZone = TimeZone.current
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
Now after that, mostly people have done
let dateString = dateFormatter.string(from: now)
Its okay, I am getting correct time in my local timezone but in string format. I want it in date format.
Again, if I do
let currentDateInLocal = dateFormatter.date(from: dateString) //converting string to date
I am getting the date in UTC format. How I can get the date in local timezone in date format?
You said "I want to convert current time(in UTC) to my local time zone. But I don't want it in string I want it in date format itself."
A Date object does not have a time zone. A Date records an instant in time, anywhere in the world. Imagine there is a bright flash in the sky all over the world at the same instant. What time did that happen? It depends on the time zone you are in. However, a Date object could capture that instant. You'd then convert that Date to a specific time zone if you wanted to describe the time of day the event occurred.
So your question doesn't really make sense.
I suggest using the DateFormatter class method localizedString(from:dateStyle:timeStyle:) to display a date in your local time zone:
e.g.
print(DateFormatter.localizedString(
from: Date(),
dateStyle: .medium,
timeStyle: .medium))
That lets you view a Date object in your local time zone without needing to create a DateFormatter.
I know you said you don't want it in a string format, however you can simply convert it over after to a date object.
you're welcome to use this, i created this function based on my own string format, change that to whatever you need and boom. enjoy
//this function will allow you to easily convert your date string into readable current context
func convertDateToNow(createdAt:String) -> String{
//create a date to represent the seconds since the start of the internet
let currentDate = Date().timeIntervalSince1970
//create a dateformatter
let dateFormatter = DateFormatter()
//use the locale on the device to set the the related time
dateFormatter.locale = Locale.current
//set the formate of which our date will be coming in at
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
//create a created at date for the post based off its createdAt date and get the seconds since the start of the internet
let createdDate = dateFormatter.date(from: "\(createdAt)")?.timeIntervalSince1970
//now we are left with two values depending on age of post either a few thousands off or it could be years in seconds. so we minus the current date on the users phone seconds from the created date seconds and voilla
let secondsDifference = currentDate - createdDate!
//use our specially created function to convert the seconds difference into time values
let (d,h,m,s) = secondsToHoursMinutesSeconds(seconds: Int(secondsDifference))
//test the values
// print("d:",d)
// print("h:",h)
// print("m:",m)
// print("s:",s)
//
//this will hold the final output of the string
var finalDateLabel:String!
//set the datelabel on the cell accordingly
//create arithmetic for the date features
if d != 0 {
//check to see if the label is a day old
if d == 1 {
finalDateLabel = "\(d) day ago"
}
}else if h != 0{
//set the date label
finalDateLabel = "\(h) hour ago"
if h == 1 {
finalDateLabel = "\(h) hour ago"
}else if h >= 2 {
finalDateLabel = "\(h) hours ago"
}
}else if m != 0{
//set the date label
finalDateLabel = "\(m) minutes ago"
if m == 1 {
finalDateLabel = "\(m) minute ago"
}
}else if s != 0{
//set the date label
finalDateLabel = "\(s) seconds ago"
if s == 1 {
finalDateLabel = "\(s) second ago"
}
}
return finalDateLabel
}
//to help convert the story
func secondsToHoursMinutesSeconds (seconds : Int) -> (Int ,Int, Int, Int) {
return (seconds / 86400, seconds / 3600, (seconds % 3600) / 60, (seconds % 3600) % 60)
}

Swift get the remaining seconds of time on target string date

The current date time today was May 9, 2020 10:03 PM, and I have a target string date with the value of 2020-05-09 22:07:30 with the format of yyyy-MM-dd HH:mm:ss.
How can I get the remaining date from that 2 date and print the value with the string of 04:30 as the range of those 2 dates are 4 minutes and 30 seconds
What I can only do is convert the milliseconds to time format like
func msToTime(ms: Int) {
let seconds = ms % 60
let minutes = ms / 60000
return String(format: "%0.2d:%0.2d",minutes,seconds)
}
Output 04:30
But I don't know how to get the range of milliseconds from today's date time to target's date time.
Or if there's any other easier way to do it?
You can use Calendar and DateComponents to easily calculate differences between dates in whatever units you desire. For example, this gets the difference in minutes and seconds:
let dateformatter = DateFormatter()
dateformatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let date = dateformatter.date(from: "2020-05-09 22:07:30")!
let now = Date()
let components = Calendar.current.dateComponents([.minute, .second], from: now, to: date)
print("difference: \(components.minute!):\(components.second!)")
A straightforward way, with no calculation of any kind needed:
let d1 = Date()
let s = "2020-05-09 22:07:30"
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm:ss"
f.timeZone = TimeZone.current // or whatever
if let d2 = f.date(from: s) {
let f = DateComponentsFormatter()
f.unitsStyle = .positional
let result = f.string(from: d1, to: d2)
}
If you don't like the resulting string format of result, you can eliminate pieces of it. — However, note that this works only because no full days are involved. It isn't clear what the range of possible inputs might be or what the desired output would be if the second date were three months into the future, for example.

Time and Date Calculations

Using date format "EEEE:dd:MMM:HH:mm" returns the correct date and time, however, the day of the week is incorrect. For example a 750 nautical mile voyage conducted at 7.5 knots will take 100 hours. If I use my code to calculate the arrival time using a start of, say, Friday 1 Nov at 12:00 it returns Sunday 5 Nov at 16:00. Time and date are correct but day of the week is not. Should be Tuesday.
#IBAction func Calculate(_ sender: UIButton) {
let userCalendar = Calendar.current
let dateMakerFormatter = DateFormatter()
dateMakerFormatter.dateFormat = "EEEE:dd:MMM:HH:mm"
let distance = (Distance.text! as NSString).floatValue
let speed = (GndSpd.text! as NSString).floatValue
let calcDT = ((distance / speed) * 3600)
if var date = dateMakerFormatter.date(from: (DTG.text!)) {
date = dateMakerFormatter.date(from: (DTG.text!))!
var timeAdj = DateComponents()
timeAdj.second = Int(calcDT)
if var adj = userCalendar.date(byAdding: timeAdj, to: date) {
adj = userCalendar.date(byAdding: timeAdj, to: date)!
CalcDTG.text = dateMakerFormatter.string(from: adj)
}
}
}
You should use d for Day, not D
dateMakerFormatter.dateFormat = "EEEE:dd:MMM:HH:mm"
DateFormatter
You can't say the day of the week is incorrect when you're not giving a year.
The date formatter seems to ignore the day of the week when creating a date:
let dateMakerFormatter = DateFormatter()
dateMakerFormatter.dateFormat = "EEEE:dd:MMM:HH:mm"
let date = dateMakerFormatter.date(from: "Friday:01:Nov:12:00")!
print(date) -> 2000-11-01 12:00:00 +0000
print(dateMakerFormatter.string(from: date)) -> Wednesday:01:Nov:12:00
Hey presto, you're now in the year 2000, where 5 November did fall on a Sunday.
The important takeaway you need is that you should never, ever, ever, use strings to pass around date values in your code. Use Date. If you're getting a date from an API response, change it to a date on ingestion. If you're getting one from user entry, use a date picker or other control. If you're getting one from a string the user is typing in, I'd politely suggest you're making unnecessary work for yourself, but do make sure you fill in all the details the user doesn't give you.

Getting first week of the Year NSCalendar

I am trying to put the dates of the beginning of the week in a app.
However in 2017, this results in un-expected behavior, as this year has the week '1' 2 times in it,
starting Sunday Jan 1st, and Sun Dec 31.
My program finds the latter. Giving me the result of
'01-01-2018' where is should be, '02-01-2017'
How can i make sure my program finds the first date available?
this is my code:
getFirstDay(Int(weekNummer)!, year: yearWrap)
func getFirstDay(weekNumber:Int, year:Int)->String?{
let Calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let dayComponent = NSDateComponents()
dayComponent.weekOfYear = weekNumber
dayComponent.weekday = 2
dayComponent.year = year
var date = Calendar.dateFromComponents(dayComponent)
if ((weekNumber == 1 || weekNumber == 52 || weekNumber == 53) && Calendar.components(.Month, fromDate: date!).month != 1 ){
dayComponent.year = year - 1
date = Calendar.dateFromComponents(dayComponent)
}
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
return String(dateFormatter.stringFromDate(date!))
}
Let do NSCalendar the complete date math
func firstDayOfWeek(week: Int, inYear year : Int) -> NSString {
let calendar = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let components = NSDateComponents()
components.weekOfYear = week
components.yearForWeekOfYear = year
components.weekday = 2 // First weekday of the week (Sunday = 1)
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "dd-MM-yyyy"
let date = calendar.dateFromComponents(components)!
return dateFormatter.stringFromDate(date)
}
firstDayOfWeek(1, inYear : 2017) // 02-01-2017
firstDayOfWeek(1, inYear : 2018) // 01-01.2018

Swift get local date instance

please help me to get local date and the start of the day, I mean the midnight. For getting local date I'm using code below, but I dont think it is right
var calendar = NSCalendar(calendarIdentifier: NSGregorianCalendar)
calendar!.timeZone = NSTimeZone.localTimeZone()
let components = NSDateComponents()
components.second = NSTimeZone.localTimeZone().secondsFromGMT
let today = calendar!.dateByAddingComponents(components, toDate: NSDate(), options: nil)
But I can't get the midnight of the day, it keeps returning time 21.00
var comps = calendar!.components(NSCalendarUnit.YearCalendarUnit | .MonthCalendarUnit | .DayCalendarUnit | .HourCalendarUnit | .MinuteCalendarUnit | .SecondCalendarUnit, fromDate: today!)
comps.hour = 0
comps.minute = 0
comps.second = 0
let startToday = calendar!.dateFromComponents(comps)!
even this return 21.00
calendar!.startOfDayForDate(today)
NSDate does not have an attached timezone. For some head-scratching reason, Apple decided to always display the date in GMT when you print it. After enough hair loss on this, I decided to write my own description method see the date in my local time zone:
extension NSDate {
public var localTimeString : String {
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
formatter.timeZone = NSTimeZone.localTimeZone()
return formatter.stringFromDate(self)
}
}
let now = NSDate()
let flags : NSCalendarUnit = [.Day, .Month, .Year]
let gregorian = NSCalendar(calendarIdentifier: NSCalendarIdentifierGregorian)!
let components = gregorian.components(flags, fromDate: now)
let today12AM = gregorian.dateFromComponents(components)!
print(today12AM)
print(today12AM.localTimeString)
Edit:
You can construct a point in time manually:
let x = gregorian.dateWithEra(1, year: 2015, month: 8, day: 15, hour: 0, minute: 0, second: 0, nanosecond: 0)!
print(x)
print(x.localTimeString)
print(x.timeIntervalSince1970) // seconds since midnight Jan 1, 1970
print(today12AM.timeIntervalSince1970)
x is no different than today12AM. As I said, NSDate has no built-in timezone. When you use print, it converts your date into GMT. That's why it appears superficially different from the same point in time when expressed in your own timezone.