Am I using NSCalendarUnitWeekOfMonth and NSCalendarUnitWeekOfYear improperly? - objective-c

everyone. I have a quick question regarding the use of NSCalendar and the constant NSCalendarUnitWeekOfMonth (and/or NSCalendarUnitWeekOfYear). I want to get a loop-able range of weeks in a certain month and year. I thought doing so would be quite straight-forward. Of course, however, it hasn't been. What am I doing wrong? Here's what I've got:
- (NSRange)rangeOfWeeksInMonth:(NSUInteger)month year:(NSUInteger)year
{
NSDateComponents *comps = [NSDateComponents new];
comps.month = month;
comps.year = year;
NSDate *date = [self.calendar dateFromComponents:comps];
return [self.calendar rangeOfUnit:NSCalendarUnitWeekOfMonth // or ... WeekOfYear
inUnit:NSCalendarUnitMonth
forDate:date];
}
I figured that if I input month = 4 and year = 2014, the above method would return something like:
=> NSRange: {14, 5}
In other words, 14 would be the week number in the year and 5 would be the number of weeks in the month.
When using the NSWeekCalendarUnit constant, the above example output is exactly what I get. However, after checking out the constants in NSCalendar.h, NSWeekCalendarUnit has been deprecated as of OS X 10.9. Apple's docs don't mention it anywhere, but I generally take cues from the actual header files when it comes to working with stuff I'm not sure about.
Anyway, because I don't want to get behind in the times, I tried both NSCalendarUnitWeekOfMonth and NSCalendarUnitWeekOfYear, and the returned range for both is {NSNotFound, NSNotFound}. What's the deal? Anyone have a clue as to why Apple doesn't write documentation for this stuff?

Related

Why does NSDateFormatter return nil date for these 4 time zones?

Try running this in iOS6 (haven't tested pre iOS6):
NSDateFormatter *julianDayDateFormatter = nil;
julianDayDateFormatter = [[NSDateFormatter alloc] init];
[julianDayDateFormatter setDateFormat:#"g"];
for (NSString *timeZone in [NSTimeZone knownTimeZoneNames]) {
julianDayDateFormatter.timeZone = [NSTimeZone timeZoneWithName: timeZone];
NSDate *date = [julianDayDateFormatter dateFromString:[NSString stringWithFormat:#"%d", 2475213]];
if (date == nil)
NSLog(#"timeZone = %#", timeZone);
}
and you get the following output:
America/Bahia
America/Campo_Grande
America/Cuiaba
America/Sao_Paulo
Can anyone explain why these four time zones behave like this with NSDateFormatter set to julian day numbers? All other time zones makes NSDateFormatter return actual NSDates.
I have a suspicion. Only a suspicion, but a pretty strong one.
That value represents October 19th 2064. The Brazilian time zones observe daylight saving time starting at local midnight - that's when their clocks go forward, so midnight itself doesn't exist. October 19th is one of those transitions.
Here's some sample code using Noda Time, my .NET date/time API. It checks whether the start of the day in every time zone it knows about is actually midnight:
using System;
using NodaTime;
class Test
{
static void Main()
{
var localDate = new LocalDate(2064, 10, 19);
var provider = DateTimeZoneProviders.Tzdb;
foreach (var id in provider.Ids)
{
var zone = provider[id];
var startOfDay = zone.AtStartOfDay(localDate).LocalDateTime.TimeOfDay;
if (startOfDay != LocalTime.Midnight)
{
Console.WriteLine(id);
}
}
}
}
That produces a very similar list:
America/Bahia
America/Campo_Grande
America/Cuiaba
America/Sao_Paulo
Brazil/East
I suspect Brazil/East may be an alias for America/Sao_Paolo, which is why it's not on your list.
Anyway, to get back to your Julian day issue - I suspect the formatter always wants to return an NSDate * which is at the local midnight. That doesn't exist for October 19th 2064 in those time zones... hence it returns nil. Personally I'd suggest it should return the 1am value instead, but hey...
Credits to Jon Skeet for putting me on the right track. However, I just want to clarify his answer in an iOS context.
When you ask NSDateFormatter to convert a julian day number into an NSDate, you can only specify whole numbers (usually you can specify a decimal part for the hours/minutes/secs of the day) in the string to be parsed.
Because Apple demarcates julian days at midnight (as opposed to noon in astronomy, read more here: http://www.unicode.org/reports/tr35/#Date_Field_Symbol_Table) and some midnights simply doesn't exists (thanks for pointing that out #JonSkeet) NSDateFormatter identifies that that particular point in time doesn't exist in that time zone and returns nil.
For the record, iOS5 does not behave like this and I agree with Jon Skeet, that NSDateFormatter should return an NSDate adjusted for DST instead of nil, as that particular julian day in fact exists! I filed a bug with Apple.

In Objective-C, to get the current hour and minute as integers, we need to use NSDateComponents and NSCalendar?

I'd like to get the current hour and minute as integers. So if right now is 3:16am, I'd like to get the two integers: 3 and 16.
But it looks like [NSDate date] will give the number of seconds since 1970, or it can give a string of the current time representation, but there is no easy way to get them as integers?
I see a post in Getting current time, but it involved NSDateComponents and NSCalendar? That's way too complicated... all that was need is something like
NSDate *date = [NSDate date];
int hour = [date getHour]; // which is not possible
Is there a simpler way than using 3 classes NSDate, NSDateComponents, and NSCalendar to get the current hour as an integer, or typically, in Objective-C, would we typically still use C language's localtime and tm to get the hour as an integer?
How you interpret the seconds since 1970 depends on the calendar that you are using. There is simply no other option. Fortunately it is not that difficult to set up. See the 'Data and Time Programming Guide' for lots of examples. In your case:
// Assume you have a 'date'
NSCalendar *gregorianCal = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
NSDateComponents *dateComps = [gregorianCal components: (NSHourCalendarUnit | NSMinuteCalendarUnit)
fromDate: date];
// Then use it
[dateComps minute];
[dateComps hour];
So it really isn't that complicated.
Also note that you could create a 'Class Category' to encapsulate this as:
#interface NSDate (MyGregorianDateComponents)
- (NSInteger) getGregorianHour;
- (NSInteger) getGregorianMinute;
#end
NSDate just holds the time that has passed since a certain reference date, to get more meaningful numbers out of this (eg. after taking care of DST, leap years and all the other stupid time stuff), you have to use NSDateComponents with the appropriate NSCalendar.
My class can help.
https://github.com/TjeerdVurig/Vurig-Calendar/blob/master/Vurig%20Calendar/NSDate%2Bconvenience.m
I'm sure you can figure out the minute part :)

Calculating time 90 minutes prior to due date/time in Objective C (iPhone app)

This is a completely noobish question, but I spent 2 hours yesterday trying to make it work, and I'm obviously missing something very basic.
What I need to do is take input from user of date/time and count back 90 minutes for an alert.
Could someone please post an example calculation, where you have a var that holds user input and a new var that receives the result of this computation? (all done in Objective C for use in an iPhone app) Thank you!
I suspect you could do something like:
NSDate *alertDate = [userDate dateByAddingTimeInterval:-5400.0];
I think this should work:
NSDate * alarmDate = [NSDate dateWithTimeInterval:5400 sinceDate:userDefinedDate];
NSDate * now = [NSDate date];
NSTimeInterval wait = [now timeIntervalSinceDate:alarmDate];
[self performSelector:#selector(callAlarm) withObject:nil afterDelay:fabs(wait)];
Although I do agree with Nick too, adding your work its much more productive..
Assuming you have a UIDatePicker, your target date will already be in an NSDate object. If it's coming from another source, you're probably ending up with it in an NSDate object, either from a string via an NSDateFormatter or by some other means.
From an NSDate object, you can get an NSTimeInterval relative to some absolute date. That's a C primitive type (it's a double in practice, but obviously don't code to depend on that) that you can do arithmetic directly on. So you can subtract 90 minutes directly from that. There are then various + dateWithTimeInterval... class methods on NSDate that will allow you to get a date from the result.

Date since 1600 to NSDate?

I have a date that's stored as a number of days since January 1, 1600 that I need to deal with. This is a legacy date format that I need to read many, many times in my application.
Previously, I'd been creating a calendar, empty date components and root date like this:
self.gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar
] autorelease];
id rootComponents = [[[NSDateComponents alloc] init] autorelease];
[rootComponents setYear: 1600];
[rootComponents setMonth: 1];
[rootComponents setDay: 1];
self.rootDate = [gregorian dateFromComponents: rootComponents];
self.offset = [[[NSDateComponents alloc] init] autorelease];
Then, to convert the integer later to a date, I use this:
[offset setDay: theLegacyDate];
id eventDate = [gregorian dateByAddingComponents: offset
toDate: rootDate
options: 0];
(I never change any values in offset anywhere else.)
The problem is I'm getting a different time for rootDate on iOS vs. Mac OS X. On Mac OS X, I'm getting midnight. On iOS, I'm getting 8:12:28. (So far, it seems to be consistent about this.) When I add my number of days later, the weird time stays.
OS | legacyDate | rootDate | eventDate
======== | ========== | ==========================|==========================
Mac OS X | 143671 | 1600-01-01 00:00:00 -0800 | 1993-05-11 00:00:00 -0700
iOS | 143671 | 1600-01-01 08:12:28 +0000 | 1993-05-11 07:12:28 +0000
In the previous release of my product, I didn't care about the time; now I do. Why the weird time on iOS, and what should I do about it? (I'm assuming the hour difference is DST.)
I've tried setting the hour, minute and second of rootComponents to 0. This has no impact. If I set them to something other than 0, it adds them to 8:12:28. I've been wondering if this has something to do with leap seconds or other cumulative clock changes.
Or is this entirely the wrong approach to use on iOS?
I imagine you're right about the leap seconds/cumulative clock changes accounting for the time issue. Are the dates you're dealing with actually in the past, or is it purely an arbitrary epoch?
In either case, you could try defining a new epoch that's much closer to present day (say, the Cocoa epoch). Calculate a day delta between the new epoch and the old and save it as a constant. When you need to process a date, apply this delta to the date and then use your existing NSCalendar technique, but with your new epoch instead of the old. That will hopefully avoid the clock drift issue you're seeing.
It looks like the right answer is to make things simpler. Instead of making a rootDate, I just build the date from components every time. This should be no slower, and is still keeps the code really close to the idea.
Initial setup:
self.gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar
] autorelease];
self.components = [[[NSDateComponents alloc] init] autorelease];
[components setYear: 1600];
[components setMonth: 1];
(Obviously, properties and ivars are adjusted.)
Later, to actually convert a legacy date to a NSDate:
[components setDay: 1 + theLegacyDate];
id eventDate = [gregorian dateFromComponents: components];
This has these advantages for me:
It users fewer ivars.
It's less code.
It always returns midnight on that day, regardless of whether DST is in effect.
Note that iOS takes into account very obscure rules for various time zones. It is most likely that midnight, Jan 1st. 1600 in your timezone actually was at 7:12:28 UTC. There have been many cases where people complained about bugs in date conversions and then someone figured out that actually they are in a time zone that made some strange calendar change many years ago.
You need to find out first what exact NSDate your data represents. "Number of days since Jan 1st 1600" is nonsense, because you need a time zone! What you should do: Find a "legacy" number where you know what day it is supposed to represent. For example, if you "know" that 143671 is supposed to be 11th May 1993 in your time zone, then start with that date as the root date and add (x - 143671) days to it.

NSCalendar problem with BC era

Greetings,
Recently I faced a big problem (as it seems to me) with NSCalendar class.
In my task I need to work with a large time periods starting from 4000BC to 2000AD (Gregorian calendar). In some place I was forced to increment some NSDate by 100 year interval. When incrementing the years in AD timeline (0->...) everything worked fine, but when I tried the same thing with BC i was a little confused.
The problem is, when you try to add 100 years to 3000BC [edited] year, you get 3100BC [edited] no matter what... Personally i found it strange and illogical. The right result should be 2900BC.
Here is the code sample for you to see this "not right" behavior:
NSCalendar *gregorian = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
// initing
NSDateComponents *comps = [[[NSDateComponents alloc] init] autorelease];
[comps setYear:-1000];
NSDate *date = [gregorian dateFromComponents:comps];
// math
NSDateComponents *deltaComps = [[[NSDateComponents alloc] init] autorelease];
[deltaComps setYear:100];
date = [gregorian dateByAddingComponents:deltaComps toDate:date options:0];
// output
NSString *dateFormat = #"yyyy GG";
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:dateFormat];
NSLog(#"%#", [formatter stringFromDate:date]);
What can you say about this behavior? Is this how it should work or is this a bug? I'm confused :S.
BTW.: the method [NSCalendar components:fromDate:toDate:options:] doesn't allow us to calculate the difference between years in BC era... additional 'WHY?' in this Pandora's box.
P.S.: I was digging through official documentation and other resources but found nothing regarding this problem (or maybe it's intended to work so and I'm an idiot?).
I found a simple workaround for this bug.
Here it is:
#interface NSCalendar (EraFixes)
- (NSDate *)dateByAddingComponentsRegardingEra:(NSDateComponents *)comps toDate:(NSDate *)date options:(NSUInteger)opts;
#end
#implementation NSCalendar (EraFixes)
- (NSDate *)dateByAddingComponentsRegardingEra:(NSDateComponents *)comps toDate:(NSDate *)date options:(NSUInteger)opts
{
NSDateComponents *toDateComps = [self components:NSEraCalendarUnit fromDate:date];
NSDateComponents *compsCopy = [[comps copy] autorelease];
if ([toDateComps era] == 0) //B.C. era
{
if ([comps year] != NSUndefinedDateComponent) [compsCopy setYear:-[comps year]];
}
return [self dateByAddingComponents:compsCopy toDate:date options:opts];
}
#end
If you wonder why I invert only years, the answer is simple, every other component except years is incrementing and decrementing in the right way (I haven't tested them all, but months and days seem to work fine).
EDIT: removed mistakenly added autorelease, thanks John.
It's a bug and or a feature. The Apple doc never says what they mean by adding components to the calendrical date. It's perfectly free for them to define "adding a component" to the BCE date as just the addition to the year component.
Yes I agree with you that it's counterintuitive and I think it's a bug.
You need to convert your NSDate to either
the second from the UNIX epoch (1.1.1970) using -timeIntervalSince1970
the second from the OS X epoch (1.1.2001) using -timeIntervalSinceReferenceDate
You can then perform the calculation, and convert it back to an NSDate. I think it's a bad idea to work in the Gregorian calendar all the time... It would be better to convert to the Gregorian calendar just before you show it on the GUI.
Imagine that you have Date with 1st moment of our era - AD 0001-01-01 00:00:00. What was the moment before? BC 0001-01-01 00:00:01. If Cocoa developers used basic arithmetic's for this task, you would get AD 0000-12-31 23:59:59. Is that reasonable for Gregorian calendar? I guess not. So, it seems to me that the most convenient way to implement calendar was to use Era flag, and change "time direction" when dealing with BC era to get human-readable dates in every case.
BTW.: [NSCalendar dateByAddingComponents:toDate:options:] really behaves strange and is unable to count time interval between BC dates, I checked too. So, for BC dates you may use workaround, e.g. by translating dates to AD and then finding diff.