Our app works quite a bit with dates, but at this time we only support the Gregorian calendar and an app wide NSCalendar instance is initialized as follows:
NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
The docs of the above method states that "The returned calendar defaults to the current locale and default time zone." However, when running the app on a device with the region set to "United Kingdom", calling [appCalendar firstWeekday]
returned a value of 1 (Sunday) rather than the expected 2 (Monday). If I run [[NSCalendar currentCalendar] firstWeekday]
, the correct value of 2 is returned. At first I thought that a locale may not be set on "appCalendar", but logging revealed it had one, though it lacked a countrycode etc., which the "currentCalendar" instance does have and which allows it to return the correct firstWeekDay.
Should a locale explicitly be set on the object returned from calendarWithIdentifier
and if so, are there any considerations in doing so?
Update
Based on zrzka's answer below, I recommend that a locale is explicitly set when initializing a calendar with an identifier e.g.
NSCalendar *appCalendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
appCalendar.locale = [NSLocale currentLocale];
The documentation is wrong:
The returned calendar defaults to the current locale and default time zone.
It should be:
The returned calendar defaults to the system locale and default time zone.
CFCalendarRef CFCalendarCreateWithIdentifier(CFAllocatorRef allocator, CFStringRef identifier) {
if (allocator == NULL) allocator = __CFGetDefaultAllocator();
__CFGenericValidateType(allocator, CFAllocatorGetTypeID());
__CFGenericValidateType(identifier, CFStringGetTypeID());
// return NULL until Chinese calendar is available
if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar) {
// if (identifier != kCFGregorianCalendar && identifier != kCFBuddhistCalendar && identifier != kCFJapaneseCalendar && identifier != kCFIslamicCalendar && identifier != kCFIslamicCivilCalendar && identifier != kCFHebrewCalendar && identifier != kCFChineseCalendar) {
if (CFEqual(kCFGregorianCalendar, identifier)) identifier = kCFGregorianCalendar;
else if (CFEqual(kCFBuddhistCalendar, identifier)) identifier = kCFBuddhistCalendar;
else if (CFEqual(kCFJapaneseCalendar, identifier)) identifier = kCFJapaneseCalendar;
else if (CFEqual(kCFIslamicCalendar, identifier)) identifier = kCFIslamicCalendar;
else if (CFEqual(kCFIslamicCivilCalendar, identifier)) identifier = kCFIslamicCivilCalendar;
else if (CFEqual(kCFHebrewCalendar, identifier)) identifier = kCFHebrewCalendar;
// else if (CFEqual(kCFChineseCalendar, identifier)) identifier = kCFChineseCalendar;
else return NULL;
}
struct __CFCalendar *calendar = NULL;
uint32_t size = sizeof(struct __CFCalendar) - sizeof(CFRuntimeBase);
calendar = (struct __CFCalendar *)_CFRuntimeCreateInstance(allocator, CFCalendarGetTypeID(), size, NULL);
if (NULL == calendar) {
return NULL;
}
calendar->_identifier = (CFStringRef)CFRetain(identifier);
calendar->_locale = NULL;
calendar->_localeID = CFLocaleGetIdentifier(CFLocaleGetSystem());
calendar->_tz = CFTimeZoneCopyDefault();
calendar->_cal = NULL;
return (CFCalendarRef)calendar;
}
_locale
is initialized with NULL
and _localeID
is initialized with locale identifier of the system locale (which is an empty string on iPhone & simulator). _cal
is set to NULL
.
CFIndex CFCalendarGetFirstWeekday(CFCalendarRef calendar) {
CF_OBJC_FUNCDISPATCHV(CFCalendarGetTypeID(), CFIndex, calendar, firstWeekday);
__CFGenericValidateType(calendar, CFCalendarGetTypeID());
if (!calendar->_cal) __CFCalendarSetupCal(calendar);
if (calendar->_cal) {
return ucal_getAttribute(calendar->_cal, UCAL_FIRST_DAY_OF_WEEK);
}
return -1;
}
So, because _cal
is NULL
, __CFCalendarSetupCal
is called.
static void __CFCalendarSetupCal(CFCalendarRef calendar) {
calendar->_cal = __CFCalendarCreateUCalendar(calendar->_identifier, calendar->_localeID, calendar->_tz);
}
Which calls __CFCalendarCreateUCalendar
with _localeID
which is an empty string.
I can confirm this behavior on iOS 11, 12 & 13. The source code is for something called CF-Lite, but I went further and disassembled actual CoreFoundation framework and it does the same thing ...
call _CFLocaleGetSystem ; _CFLocaleGetSystem
mov rdi, rax ; argument "cf" for method _CFRetain
call _CFRetain ; _CFRetain
mov qword [r15+0x18], rax
call _CFTimeZoneCopyDefault ; _CFTimeZoneCopyDefault
mov qword [r15+0x20], rax
mov rbx, qword [r15+0x10]
mov rdi, qword [r15+0x18] ; argument "locale" for method _CFLocaleGetIdentifier
call _CFLocaleGetIdentifier ; _CFLocaleGetIdentifier
mov rdx, qword [r15+0x20] ; argument #3 for method ___CFCalendarCreateUCalendar
mov rdi, rbx ; argument #1 for method ___CFCalendarCreateUCalendar
mov rsi, rax ; argument #2 for method ___CFCalendarCreateUCalendar
call ___CFCalendarCreateUCalendar ; ___CFCalendarCreateUCalendar
... using an empty identifier from CFLocaleGetIdentifier
from CFLocaleGetSystem
.
When you check the CFCalendarCreateWithIdentifier
documentation, there's not a word about current locale, time zone, ...
What's even more interesting is the difference (section Discussion) for these two methods:
+calendarWithIdentifier:
-initWithCalendarIdentifier:
But there's no difference, calendarWithIdentifier:
just calls the alloc
& initWithCalendarIdentifier:
.
push rbp
mov rbp, rsp
push r14
push rbx
mov rbx, rdx
mov rsi, qword [0x3cb478] ; argument "selector" for method _objc_msgSend, @selector(alloc)
mov r14, qword [_objc_msgSend_390220] ; _objc_msgSend_390220
call r14 ; Jumps to 0x553ae0 (_objc_msgSend), _objc_msgSend
mov rsi, qword [0x3cc768] ; argument "selector" for method _objc_msgSend, @selector(initWithCalendarIdentifier:)
...
I believe it's a documentation problem which should be reported to Apple (did it, FB7740798).