How calculate days to person birthday with Swift? - ios

I have a Date variable with a person's date birthday. I would like to know how many days remains before this person next birthday. It should be calculated from today date to current year birthday date.
How can this be done with Swift? Also it will be great to consider February 29 in leap years.

To the guys who tried to close this: This is about birthday which has totally different rules from days.
Birthdays are complicated. Your birthday was the date of the moment when you were born, in the timezone where you were born. Considering that Samoa = UTC+14 and Baker Island = UTC-12, it is possible that people born at the same moment in time have birthdays that are two days apart.
So to store somebody's birthday, not the moment of birth, you either store year/month/day, or if you want to store it as a point in time, you store the beginning of that day in UTC, with the understanding that this is to specify a day, and must not be converted to local time.
Now when does your birthday repeat? If the person is born on D/M/Y and D/M is not February 29th, then the next birthday is either D/M/current year or D/M/next year. It is D/M/current year if that date is in the future, otherwise D/M/next year.
If the person is born on February 29th, then you have to determine when officially the next birthday is if the year is not a leap year - this will be February 28th or March 1st, depending on which rules apply.
We also need to clarify what "number of days" means. If my birthday is on April 1st, and now it is March 31st, one second to midnight, my birthday will be one second from now. However, I will assume that the result is supposed to be "one day from now".
So here is the algorithm:
Step 1: Find day/month/year when the person was born.
Step 2: Determine the current time, and convert it to local day/month/year. Determine the current time only once to avoid problems if this calculation is done nanoseconds before midnight.
Step 3: Determine the year when the birthday repeats: If day/month of birthday is greater than current day/month, then the year when the birthday repeats is the current year, otherwise the next year. This is also correct if the birthday was on Feb. 29th.
Step 4: Determine the day/month when the birthday repeats: This is the same as the day/month of the birthday, unless the birthday was on Feb. 29th and the year when the birthday repeats is not a leap year. In that case, the birthday repeats on Feb 28th or March 1st, depending on which rules you decide to apply.
Step 5: Convert the current day/month/year + 12 hours to UTC. Convert the date when the birthday repeats + 12 hours to UTC. Calculate the difference in seconds (which the OS should do for you). Divide by 86,400, then round to the nearest integer. The "+12 hours" and "round to nearest integer" make sure that you have no problems with daylight savings time, leap seconds etc.
Writing this code in Swift or any other language should be no problem.

It depends on what you are looking to use the days value for but here is a small function that will return a Double describing the amount of days until a given Date. Martin R gave a really good answer here and my answer is mainly based on theirs with a little bit of documentation added.
/// This function takes a `Date` parameter and returns an `Int` describing how many days away the `Date` is into the future (accounting for leap years by making 2/29 dates land on 3/1 in non-leap years).
/// - Parameter date: The `Date` object to be evaluated.
func daysUntil(birthday: Date) -> Int {
let cal = Calendar.current
let today = cal.startOfDay(for: Date())
let date = cal.startOfDay(for: birthday)
let components = cal.dateComponents([.day, .month], from: date)
let nextDate = cal.nextDate(after: today, matching: components, matchingPolicy: .nextTimePreservingSmallerComponents)
return cal.dateComponents([.day], from: today, to: nextDate ?? today).day ?? 0
}

Related

Repeat Notifications monthly on specific day

I want to repeat notifications monthly on a specific day, but my concern is, what if the user chooses the 31? Then the Notification would only fire every 2 months and never in February?
Because the day component would not match.
Is it possible to set the day to the last day of the month, so when for example February is the next month and the user selected the 31. then it would fire on the last day of February?
I could go the non repeating way and add the notifications manually, but then i would have to face the 64 scheduled notification limit.
Thanks for your help in advance.
The Calendar module is the way to go. Handling dates manually can get very tricky.
import Calendar
let selectedDate = "31/01/2020"
// Convert string to Date
let dateF = DateFormatter()
dateF.dateFormat = "dd/MM/yyyy"
let myDate = dateF.date(from: selectedDate)!
// Advancing date by a month, to get end of next month.
Calendar.current.date(byAdding: .month, value: 1, to: myDate) // "Feb 29, 2020 at 12:00 AM"
In the example above, you can see how to advance date by a month.
If the user chooses say the 31 of a month, the calendar automatically calculates the end of the next month accurately, even if it has only 28, 29, 30 or 31 days.
One option would be to use remote notifications and handle them on a server. This would involve a bit more work and a server, but it is more flexible and allows you to change the notifications without the user needing an app update. Link to the Apple Doc for Remote Notifications.
The other option would be the use Calendar.
When you do your logic to send the notification, you can test if today is the last day of the month. If the user chooses to get the notifications on the 31st, and say today is the last day of the month and is the 30th, you can still send the notification because you know there isn't a 31st in this specific month.

iOS : How to check if date-time selected is valid time during day light savings?

I have a special case to check where a user inputs the future date and time and I need to verify if that time is valid (what do I mean by valid is explained below) considering user might be affected by daylight saving in his timezone.
For Example:
Assume the user's timezone is Adelaide, Australia. Open the link to see how timezone affects in Adelaide OR see below.
4 Oct 2020 - Daylight Saving Time Starts
When local standard time is about to reach
Sunday, 4 October 2020, 2:00:00 am clocks are turned forward 1 hour to
Sunday, 4 October 2020, 3:00:00 am local daylight time instead.
Now based on the above information my understanding is if my user selected the date-time between
4 October 2020, 2:01:00 am - 4 October 2020, 2:59:00 am
it's not valid as the hour is forwarded to 3 am.
How can I validate that in an iOS app? (Assuming i)
Basically I need to inform the user that the time selected is affected by DST and users need to select a different time.
I've looked into Date and Timezone APIs and couldn't seem to find anything which can validate this.
Thanks in advance, any help would be appreciated.
I guess the easiest way would be to convert the string (or whatever the user enters, seems you are not using the UIDatePicker) into a date and check if this is possible.
For Mid-Europe, DST is starting on March 29th, 2020, so
let df = DateFormatter()
df.locale = Locale(identifier: "de_DE")
df.timeZone = TimeZone(identifier:"Europe/Berlin")
df.dateFormat="yyyy-MM-dd hh:mm"
if let theDate = df.date(from: "2020-03-29 02:01")
{
print(theDate)
} else {
print ("Illegal date")
}
will print Illegal date.
A little more tricky is switch back to non-DST, because (in Mid-Europe) there are two possible dates for 2020-10-25 02:01 - it could mean winter or summer time with two different UTC representations.

Ruby convert decimal duration (day|week|month|year) to end date

I have a start_at, a decimal quantity and an interval which is one of day | week | month | year.
start_at = Time.parse('2016-01-01 00:00:00 UTC') # leap year
quantity = BigDecimal.new('1.998') # changed from 2.998, should end on 2/29/16 sometime
interval = 'month' # could be any of day|week|month|year
With whole numbers, I've used duration i.e. 1.month, and I looked at Date#advance, though it only recognizes integer values.
It would seem simple but I cannot find anything in the standard libraries or in ActiveSupport.
References:
SO answer potentially used for input to Date#advance?
SO explanation of duration
Question
How can I establish the end_at date from a decimal?
Why? What purpose?
Proration to the second for a given amount and given interval.
Expectations
I'm looking for an end_at to the second as accurate as possible with respect to advancing the next interval(s) by the decimal quantity. Given interval = 'month', for the fractional part, when you pass the start of the month, means you are in that month and using it's duration. For example, January 2016 is 31 days, while February (leap) is 29 days (only in the leap year).
I'd say your best option is to use Ruby's date methods to advance time based on the whole number of the decimal, then calculate how many seconds your fraction is of your current interval.
So for 1.998 months, advance time 1 month. Find the current month you are in and get the .998 of the seconds in that month (i.e. for July, 31x24x60x60x.998) and then advance time that many seconds.
What does advancing time a fractional month mean?
Lets say we have the following date 2015-01-01 00:00:00 UTC. It is easy to advance exactly 1 whole month, we simply increment the number that represents months: 2015-02-01 00:00:00 UTC. Alternatively, we could view this as adding 31 days, which we know is the number of days in January.
But what if we want to advance 0.5 months from 2015-01-01 00:00:00 UTC?
We can't just increment like we did when advancing a whole month. Since we know January has 31 days, perhaps we could just advance 15.5 days: 2015-01-16 12:00:00 UTC. That sort of works.
How about 1.5 months from 2015-01-01 00:00:00 UTC? If we combine our previous approaches, we'd first increment, getting us to 0.5 left to advance and 2015-02-01 00:00:00 UTC. Then we'd take half of 28 and get to 2015-02-15 00:00:00 UTC.
But wait, what if instead we took the total number of days between the two months and then took 3/4 of that? Like 2(month) * (3/4), which would simplify to (3(month)) / 2, or 1.5(month). Lets try it.
(28 days + 31 days) * 0.75 = 44.25 days
Now adding that to 2015-01-01 00:00:00 UTC we get 2015-02-14 06:00:00 UTC. That's three-quarters of a day off from our other answer.
The problem here is that the length of a month varies. So fractional months are not consistently definable.
Imagine you have two oranges. One contains a little bit more juice than the other (perhaps 31ml and 29ml of juice). Your recipe calls for the juice of 1.5 oranges. Depending on which one you decide to cut in half, you could have either 44.5 ml or 45.5 ml. But if your recipe calls for 40 ml of orange juice, you can pretty consistently measure that. Much like you can consistently (kind of) increment a date by 40 days.
Time is really tricky. We have leap seconds, leap years, inconsistent units (months), timezones, daylight saving time, etc... to take into account. Depending on your use case, you could attempt to approximate fractional months, but I'd highly recommend trying to avoid the need for dealing with fractional months.

Why compareDate from NSCalendar seems to need to set an UTC timeZone to work properly?

I create two dates like the following:
let date1 = stringToDate("2015-02-12 12:29:29")!
let date2 = stringToDate("2015-02-11 19:18:49")!
func stringToDate(var dateString: String) -> NSDate? {
let dateFormatter = NSDateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
dateFormatter.timeZone = NSTimeZone(name: "UTC")
return dateFormatter.dateFromString(dateString)
}
As you can see, the two date are different and are not on the same day.
To test if two dates are on the same day, I use the following method:
func isSameDayThan(date1: NSDate, date2: NSDate) -> Bool {
let calendar = NSCalendar.currentCalendar()
calendar.timeZone = NSTimeZone(abbreviation: "GMT+10")!
return calendar.compareDate(date1, toDate: date2, toUnitGranularity: .DayCalendarUnit) == .OrderedSame
}
There I don't precise any timeZone in the calendar. The local TimeZone of my device is set to GMT+10.
In that case, isSameDayThan(date1, date2)-> true
If I change the timeZone to something inferior or equal to GMT+04, then I get isSameDayThan(date1, date2)-> false.
What I don't understand is that the result is different depending on the timeZone, but I am comparing two NSDate() and NSDate() has nothing to do with time zone if I'm not wrong.
The timezone comes into play because you compare the dates with a granularity that is timezone dependent. So you are actually comparing against the local representation of the date. The point in time model that is often used to describe NSDate doesn't know about days and weeks. From a abstract standpoint (i.e. the point in time that is the same everywhere in the universe) it actually doesn't even know about seconds.
Anyway, if you would compare with == you would obviously not need a timezone. That's the only comparison that is truly independent from the local representation. If two points in time are exactly the same they are equal. Easy.
Everything beyond a straight == comparison has to be converted into local units. Not only you have to use the correct calendar, but you have to use the correct timezone as well.
Luckily there are no calendars that have days that are shorter or longer than 24 hours. And there are no timezones that differ in seconds either. Because we know that, you can actually see if dates are within the same minute with an easy calculation. e.g.:
Int(date1.timeIntervalSince1970 / 60) == Int(date2.timeIntervalSince1970 / 60)
No calendar needed because we (currently) don't have calendars that have minutes that are not 60 seconds long. No timezone needed, because we don't have timezones with offsets that differ in the number of seconds.
But we have a few timezones that have offsets that are only fractions of an hour. For example India Time Zone which has an offset of +05:30. So starting with hours the boundaries of the granularity units are timezone dependent.
If you have two NSDates which are set to 9:25 and 9:35 UTC, they are in the same hour if you compare in any timezone that has an offset that does not differ in the number of minutes (e.g. 00 in +x:00). They are in the same hour in UTC, they are in the same hour in UTC+5:00 and UTC-5:00.
But if you compare in India Time Zone these two dates are actually in different hours. Because 9:25 UTC in IST is 2:55, and 9:35 UTC is 3:05 in IST.
In your example you are comparing to the granularity of the day. Which needs to take all timezones into account. But we can still ignore the calendar, because all calendars use days that are 24 hours long.
But if you would compare to the granularity of a week, month or year you would have to take the calendar into account as well. There are calendars that have totally different months. Just because two dates are in the same month in gregorian calendars doesn't mean that they are in the same month in hebrew calendars.
Yes, it's complicated. And that's the reason all date calculation appear so verbose. People often try to hide the complexity behind a fancy helper function. Which often leads to problems. So be aware of creation functions like isSameDay().
Each time you compare a date you have to make the decision what timezone and calendar to use. If you rely on helper functions you will miss the one instance where you should actually compare against UTC instead of the local timezone.
TL;DR: If you compare with granularity you should always set the correct calendar and the correct timezone.
Th two dates are different days in the UTC time zone. But in the GMT+10 time zone they are both the same day - February 12.
2015-02-12 12:29:29 UTC = 2015-02-12 22:29:29 UTC+10
2015-02-11 19:18:49 UTC = 2015-02-12 05:18:49 UTC+10
By Default, the comparison is done in the local time zone but your date objects were specifically created in the UTC time zone.
If you create the NSDate objects from the strings using the default time zone and compare them using the default time zone, then the dates would have two different days.

How does NSMonthCalendarUnit and NSYearCalendarUnit behave exactly when used with UILocalNotification repeatInterval?

How does NSMonthCalendarUnit behave exactly related to UILocalNotification repeatInterval ?
Find same day next month ?
or
Find the date for 30 days later ?
For example:
if i create a UILocalNotification with date:27/02/2015 and repeatInterval:NSMonthCalendarUnit when will be the next notification ? Do not mind whether February is 27 or 28. Just consider it is always 28. It is not the real question.
Will repetition be on 27/03/2015 or 29/03/2015 ?
Same question also applies for NSYearCalendarUnit. Is it just a addition of 365 days or does it mean same day, same month of next year (27/03/2016) ?
I would post this as an edit, but it's a bit long.
I imagine this does is based on the unit. For instance, if you create a uilocalnotification with date Feb 2, 2015 and give it an NSMonthCalendarUnit, it will call it again at every month interval for the same date.
Broken down into components Feb 2 2015 is NSDayCalendarUnit=2, NSMonthCalendarUnit=2 NSYearCalendarUnit=2015. So, the notification will be called whenever NSDayCalendarUnit=2 and NSYearCalendarUnit=2015 for any NSMonthCalendarUnit. (so if you did a leap-year date at the end of the month, like jan 30, and it was a leap year month in february, it wouldn't get called.) So, this would have the same day and year for ever NSMonthCalendarUnit. The same goes for year, except for years.
It would not make much sense for this to be an addition of days. Imagine setting the NSMonth interval for a date on the first and having it be returned on the 31st of the same month, that wouldn't be a very practical API.

Resources