13

Under iOS 7 or 8, the stock Calendar app does something that I have been unable to figure out.

Under some locales, such as en_US, the Calendar app shows the short (3-letter) month names.

Under other locales, such as de_DE, the Calendar app shows the full month names. Interestingly, the locale en_DE shows the short month names so it seems to be tied to the language more than the region format.

What I can't figure out is how to know which month format to use.

Regardless of my device's locale, NSDateFormatter standaloneShortMonthSymbols gives me the 3-letter month names and NSDateFormatter standaloneMonthSymbols gives me the full month names.

Is also tried:

NSString *monthformat = [NSDateFormatter dateFormatFromTemplate:@"LLL" options:0 locale:[NSLocale currentLocale]];

and that gives back the same LLL for both en_US and de_DE.

Looking at NSLocale there doesn't appear to be any value that determines whether to use short or full month names.

There doesn't appear to be anything in NSCalendar, NSDateFormatter, or NSLocale to help determine which month format to use.

Does anyone have any idea how to make this determination?

Update:

I thought I found a solution but it doesn't work for all locales that I tried. I ran the following code with various locales to see if I could find anything in common between locales that show the short and long months names in the Calendar app:

NSLocale *locale = [NSLocale currentLocale];
NSString *locid = [locale localeIdentifier];
NSLog(@"Locale = %@", locid);

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
NSLog(@"monthSymbols = %@", [formatter monthSymbols]);
NSLog(@"shortMonthSymbols = %@", [formatter shortMonthSymbols]);
NSLog(@"veryShortMonthSymbols = %@", [formatter veryShortMonthSymbols]);
NSLog(@"monthStandaloneSymbols = %@", [formatter standaloneMonthSymbols]);
NSLog(@"shortStandaloneMonthSymbols = %@", [formatter shortStandaloneMonthSymbols]);
NSLog(@"veryShortStandaloneMonthSymbols = %@", [formatter veryShortStandaloneMonthSymbols]);

NSDate *date = [NSDate date];
[formatter setDateStyle:NSDateFormatterShortStyle];
NSLog(@"short date style: %@", [formatter stringFromDate:date]);
[formatter setDateStyle:NSDateFormatterMediumStyle];
NSLog(@"medium date style: %@", [formatter stringFromDate:date]);
[formatter setDateStyle:NSDateFormatterLongStyle];
NSLog(@"long date style: %@", [formatter stringFromDate:date]);
[formatter setDateStyle:NSDateFormatterFullStyle];
NSLog(@"full date style: %@", [formatter stringFromDate:date]);

[formatter setDateStyle:NSDateFormatterNoStyle];
[formatter setDateFormat:@"M"];
NSLog(@"M date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"MM"];
NSLog(@"MM date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"MMM"];
NSLog(@"MMM date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"MMMM"];
NSLog(@"MMMM date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"MMMMM"];
NSLog(@"MMMMM date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"L"];
NSLog(@"L date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"LL"];
NSLog(@"LL date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"LLL"];
NSLog(@"LLL date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"LLLL"];
NSLog(@"LLLL date format: %@", [formatter stringFromDate:date]);
[formatter setDateFormat:@"LLLLL"];
NSLog(@"LLLLL date format: %@", [formatter stringFromDate:date]);

I had tested with en_US, en_GB, es_ES, de_DE, fr_FR, and it_IT. The French and German locales show the full month name in the Calendar app while the rest show the short name.

The one thing that looked promising with the test code is that only the French and German locales have a period at the end of the shortMonthSymbols.

So then I ran the following code to find all locales that use punctuation in the short month symbols and those that don't:

NSMutableArray *hasDot = [[NSMutableArray alloc] init];
NSMutableArray *noDot = [[NSMutableArray alloc] init];
NSCharacterSet *letters = [NSCharacterSet letterCharacterSet];
NSArray *locales = [NSLocale availableLocaleIdentifiers];
for (NSString *locid in locales) {
    NSLocale *locale = [NSLocale localeWithLocaleIdentifier:locid];
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setLocale:locale];
    NSArray *shortNames = [formatter shortMonthSymbols];
    //NSLog(@"locale: %@, short names: %@", locid, shortNames[10]);
    NSString *nov = shortNames[10];
    unichar char1 = [nov characterAtIndex:0];
    unichar charN = [nov characterAtIndex:nov.length - 1];
    if ([letters characterIsMember:char1] && [letters characterIsMember:charN]) {
        [noDot addObject:locid];
    } else {
        [hasDot addObject:locid];
    }
}

NSLog(@"no dot: %@", [noDot sortedArrayUsingSelector:@selector(compare:)]);
NSLog(@"has dot: %@", [hasDot sortedArrayUsingSelector:@selector(compare:)]);

Scanning through the results I saw that the Dutch locales used a period in the short month symbols. But a quick test of the Calendar app revealed that the Calendar app showed short month names when the device was set to Dutch (nl_NL). Ugh.

Update 2:

I've tested a few more locales. The following show long month names:

fr_FR, de_DE, ru_RU, sv_SE (actually all locales for each of these languages)

the following (and I'm sure many more) show the short month:

en_US, en_GB, es_ES, it_IT, nl_NL, ca_ES, uk_UA, ro_RO (actually all locales for each of these languages)

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • I guess I always figured that NSDateFormatterStyle controlled that, if you didn't give a full date format. I would guess that NSDateFormatterMediumStyle would give the short month names. – Hot Licks Nov 13 '14 at 00:08
  • The various `NSDateFormatterXXXStyle` are for formatting a full date (day, month, and year). I just need the month names just like as shown in the Calendar app. – rmaddy Nov 13 '14 at 00:16
  • So you want the month name, in default locale format, without anything else? "Mar" for en_US and "Marts" for de_DE? – Hot Licks Nov 13 '14 at 00:19
  • Yes. As I've said a few times, I want the same month names as shown on the Calendar app. – rmaddy Nov 13 '14 at 00:20
  • Hmmm... I would have sworn that NSCalendar (or perhaps NSDateComponents) had properties that were arrays of the short and long names for months and weekdays, but I don't see them in the latest spec. I'm guessing they got deprecated. – Hot Licks Nov 13 '14 at 00:37
  • I mention those methods in my question. They are shown in the docs for `NSDateComponents` but supposedly they were moved to `NSCalendar` with iOS 8.0. – rmaddy Nov 13 '14 at 00:39
  • Ah, yes, there they are with NSDateFormatter. So what you need is a clever way to figure out if the month format is full or abbreviated. Have you tried checking the formatter's default dateStyle value? – Hot Licks Nov 13 '14 at 00:46
  • That's the whole point of my question - how to know which format to use. What default style do you mean? The various `NSDateFormtterXXXStyle` values don't apply here. – rmaddy Nov 13 '14 at 00:49
  • I mean *read* the `dateStyle` value from a "virgin" NSDateFormatter that's only had it's locale set. See if that doesn't return different values for de_DE vs en_US. – Hot Licks Nov 13 '14 at 00:52
  • The default style is `NSDateFormatterNoStyle`. That doesn't help. – rmaddy Nov 13 '14 at 00:56
  • Well, then, format Jan 1 and scan the result for a match to the first element of one of the arrays. – Hot Licks Nov 13 '14 at 01:16
  • I think the answer is a bit more arbitrary. What an app, including bundled apps do with one locale and another locale need not be the same. There's no rule that the same presets of NSDateStyleFormatterxxxStyle have to be used for each locale. There can and will be scenarios that do not match up and that apple may even get wrong. Much of what they use is straight from the Unicode CLDR or the ICU library, but they customize from there and their designers customize as they see fit. Not always a universal rule. Timezone abbreviations are a good example of this. – uchuugaka Nov 15 '14 at 08:43
  • It (NSCalendar) seems to do something similar to the `NSDateFormatterMediumStyle`. Apple states in the documentation that NSDateFormatterMediumStyle: "Specifies a medium style, typically with abbreviated text..." My limited experience was that this style varied by language. – Bill Nov 18 '14 at 01:10
  • @Bill Medium style with the de_DE locale doesn't even show the month in text, it shows it as a number. – rmaddy Nov 18 '14 at 01:16
  • @rmaddy That's why I said similar. With en_US it gives a 3 character month. It varies with each language. I can't find the reference yet, but I thought Apple warned about this behavior (subject to change) with NSDateFormatterMediumStyle. – Bill Nov 18 '14 at 01:29
  • @Bill I really don't get your point. What about the Medium style is supposed to help me know whether to show the short or full month name when a given locale shows the full month name in the Calendar app but the Medium style for the same locale shows a numeric month? If you have an answer that works for most locales, please post an answer. – rmaddy Nov 18 '14 at 02:04

1 Answers1

17

Once every so often, there comes a question worth looking into. Rick, for you I debugged the Calendar app (can be done using attaching to MobileCal process). It all comes down to EventKitUI`CurrentLocaleRequiresUnabbrevatedMonthNames which answers the desired question.

Let's look at its disassembly:

EventKitUI`CurrentLocaleRequiresUnabbrevatedMonthNames:
0x102c6bec7:  pushq  %rbp
0x102c6bec8:  movq   %rsp, %rbp
0x102c6becb:  pushq  %r15
0x102c6becd:  pushq  %r14
0x102c6becf:  pushq  %rbx
0x102c6bed0:  subq   $0xb8, %rsp
0x102c6bed7:  movq   0x14a3aa(%rip), %r15      ; (void *)0x0000000104e93070: __stack_chk_guard
0x102c6bede:  movq   (%r15), %rax
0x102c6bee1:  movq   %rax, -0x20(%rbp)
0x102c6bee5:  cmpq   $0x0, 0x1c01fb(%rip)      ; CurrentLocaleRequiresUnabbrevatedMonthNames.usesFullLengthMonthNames + 6
0x102c6beed:  je     0x102c6beff               ; CurrentLocaleRequiresUnabbrevatedMonthNames + 56
0x102c6beef:  movb   0x1c01eb(%rip), %al       ; CurrentLocaleRequiresUnabbrevatedMonthNames.hasChecked
0x102c6bef5:  xorb   $0x1, %al
0x102c6bef7:  testb  $0x1, %al
0x102c6bef9:  je     0x102c6c0d6               ; CurrentLocaleRequiresUnabbrevatedMonthNames + 527
0x102c6beff:  movq   0x1b583a(%rip), %rdi      ; (void *)0x00000001025dae58: NSLocale
0x102c6bf06:  movq   0x1aef23(%rip), %rsi      ; "currentLocale"
0x102c6bf0d:  movq   0x14a524(%rip), %r14      ; (void *)0x0000000104945000: objc_msgSend
0x102c6bf14:  callq  *%r14
0x102c6bf17:  movq   %rax, %rdi
0x102c6bf1a:  callq  0x102d29920               ; symbol stub for: objc_retainAutoreleasedReturnValue
0x102c6bf1f:  movq   %rax, %rbx
0x102c6bf22:  movq   0x14a227(%rip), %rax      ; (void *)0x00000001025a3cd8: NSLocaleLanguageCode
0x102c6bf29:  movq   (%rax), %rdx
0x102c6bf2c:  movq   0x1ae12d(%rip), %rsi      ; "objectForKey:"
0x102c6bf33:  movq   %rbx, %rdi
0x102c6bf36:  callq  *%r14
0x102c6bf39:  movq   %rax, %rdi
0x102c6bf3c:  callq  0x102d29920               ; symbol stub for: objc_retainAutoreleasedReturnValue
0x102c6bf41:  movq   %rax, %r14
0x102c6bf44:  movq   %rbx, %rdi
0x102c6bf47:  callq  *0x14a4f3(%rip)           ; (void *)0x00000001049429b0: objc_release
0x102c6bf4d:  movq   0x1c0194(%rip), %rdi      ; __languagesRequiringUnabbreviatedMonthNames
0x102c6bf54:  testq  %rdi, %rdi
0x102c6bf57:  jne    0x102c6c0b0               ; CurrentLocaleRequiresUnabbrevatedMonthNames + 489
0x102c6bf5d:  leaq   0x15425c(%rip), %rax      ; @"ru"
0x102c6bf64:  movq   %rax, -0xd0(%rbp)
0x102c6bf6b:  leaq   0x1524ce(%rip), %rax      ; @"de"
0x102c6bf72:  movq   %rax, -0xc8(%rbp)
0x102c6bf79:  leaq   0x154260(%rip), %rax      ; @"fr"
0x102c6bf80:  movq   %rax, -0xc0(%rbp)
0x102c6bf87:  leaq   0x154272(%rip), %rax      ; @"fi"
0x102c6bf8e:  movq   %rax, -0xb8(%rbp)
0x102c6bf95:  leaq   0x154284(%rip), %rax      ; @"pt"
0x102c6bf9c:  movq   %rax, -0xb0(%rbp)
0x102c6bfa3:  leaq   0x154296(%rip), %rax      ; @"no"
0x102c6bfaa:  movq   %rax, -0xa8(%rbp)
0x102c6bfb1:  leaq   0x1542a8(%rip), %rax      ; @"nb"
0x102c6bfb8:  movq   %rax, -0xa0(%rbp)
0x102c6bfbf:  leaq   0x1542ba(%rip), %rax      ; @"nn"
0x102c6bfc6:  movq   %rax, -0x98(%rbp)
0x102c6bfcd:  leaq   0x1542cc(%rip), %rax      ; @"sv"
0x102c6bfd4:  movq   %rax, -0x90(%rbp)
0x102c6bfdb:  leaq   0x1542de(%rip), %rax      ; @"he"
0x102c6bfe2:  movq   %rax, -0x88(%rbp)
0x102c6bfe9:  leaq   0x1542f0(%rip), %rax      ; @"th"
0x102c6bff0:  movq   %rax, -0x80(%rbp)
0x102c6bff4:  leaq   0x154305(%rip), %rax      ; @"hi"
0x102c6bffb:  movq   %rax, -0x78(%rbp)
0x102c6bfff:  leaq   0x15431a(%rip), %rax      ; @"bn"
0x102c6c006:  movq   %rax, -0x70(%rbp)
0x102c6c00a:  leaq   0x15432f(%rip), %rax      ; @"mr"
0x102c6c011:  movq   %rax, -0x68(%rbp)
0x102c6c015:  leaq   0x154344(%rip), %rax      ; @"ur"
0x102c6c01c:  movq   %rax, -0x60(%rbp)
0x102c6c020:  leaq   0x154359(%rip), %rax      ; @"te"
0x102c6c027:  movq   %rax, -0x58(%rbp)
0x102c6c02b:  leaq   0x15436e(%rip), %rax      ; @"ta"
0x102c6c032:  movq   %rax, -0x50(%rbp)
0x102c6c036:  leaq   0x154383(%rip), %rax      ; @"gu"
0x102c6c03d:  movq   %rax, -0x48(%rbp)
0x102c6c041:  leaq   0x154398(%rip), %rax      ; @"kn"
0x102c6c048:  movq   %rax, -0x40(%rbp)
0x102c6c04c:  leaq   0x1543ad(%rip), %rax      ; @"ml"
0x102c6c053:  movq   %rax, -0x38(%rbp)
0x102c6c057:  leaq   0x1543c2(%rip), %rax      ; @"ne"
0x102c6c05e:  movq   %rax, -0x30(%rbp)
0x102c6c062:  leaq   0x1543d7(%rip), %rax      ; @"pa"
0x102c6c069:  movq   %rax, -0x28(%rbp)
0x102c6c06d:  movq   0x1b55ec(%rip), %rdi      ; (void *)0x00000001025d9cd8: NSArray
0x102c6c074:  movq   0x1ae5cd(%rip), %rsi      ; "arrayWithObjects:count:"
0x102c6c07b:  leaq   -0xd0(%rbp), %rdx
0x102c6c082:  movl   $0x16, %ecx
0x102c6c087:  callq  *0x14a3ab(%rip)           ; (void *)0x0000000104945000: objc_msgSend
0x102c6c08d:  movq   %rax, %rdi
0x102c6c090:  callq  0x102d29920               ; symbol stub for: objc_retainAutoreleasedReturnValue
0x102c6c095:  movq   0x1c004c(%rip), %rdi      ; __languagesRequiringUnabbreviatedMonthNames
0x102c6c09c:  movq   %rax, 0x1c0045(%rip)      ; __languagesRequiringUnabbreviatedMonthNames
0x102c6c0a3:  callq  *0x14a397(%rip)           ; (void *)0x00000001049429b0: objc_release
0x102c6c0a9:  movq   0x1c0038(%rip), %rdi      ; __languagesRequiringUnabbreviatedMonthNames
0x102c6c0b0:  movq   0x1ae6c1(%rip), %rsi      ; "containsObject:"
0x102c6c0b7:  movq   %r14, %rdx
0x102c6c0ba:  callq  *0x14a378(%rip)           ; (void *)0x0000000104945000: objc_msgSend
0x102c6c0c0:  movb   %al, 0x1c001b(%rip)       ; CurrentLocaleRequiresUnabbrevatedMonthNames.usesFullLengthMonthNames
0x102c6c0c6:  movb   $0x1, 0x1c0013(%rip)      ; __overlayCalendarGeneration + 7
0x102c6c0cd:  movq   %r14, %rdi
0x102c6c0d0:  callq  *0x14a36a(%rip)           ; (void *)0x00000001049429b0: objc_release
0x102c6c0d6:  movb   0x1c0005(%rip), %al       ; CurrentLocaleRequiresUnabbrevatedMonthNames.usesFullLengthMonthNames
0x102c6c0dc:  movq   (%r15), %rcx
0x102c6c0df:  cmpq   -0x20(%rbp), %rcx
0x102c6c0e3:  jne    0x102c6c0f3               ; CurrentLocaleRequiresUnabbrevatedMonthNames + 556
0x102c6c0e5:  addq   $0xb8, %rsp
0x102c6c0ec:  popq   %rbx
0x102c6c0ed:  popq   %r14
0x102c6c0ef:  popq   %r15
0x102c6c0f1:  popq   %rbp
0x102c6c0f2:  retq   
0x102c6c0f3:  callq  0x102d29a1c               ; symbol stub for: __stack_chk_fail

As you can see, it creates an array of locales that require unabbreviated month names. It then compares if the current locale is one of these locales.

Hardcoded in the code.

For abbreviated months, it uses the LLL format (as seen in EventKitUI`CalStringForMonth), and for unabbreviated months, it uses the LLLL format (as seen in EventKitUI`CalLongStringForMonth).

Cheers

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
  • I was afraid it was just a hardcoded list in the Calendar app. Where did you find this `CurrentLocaleRequiresUnabbrevatedMonthNames` method? – rmaddy Nov 19 '14 at 21:06
  • It's actually in `EventKitUI`, so you can theoretically `extern` it and use it. I printed the hierarchy of the month view in the MobileCal window, and noticed the months are labels. So put a breakpoint on `[UILabel setText:]` which was caught in `_reloadMonthNameLabel` which is an internal `MobileCal` method, but that one calls functions from `EventKitUI`. – Léo Natan Nov 19 '14 at 21:10
  • I guess you could hide your use of the function using `dlsym` and make some C string manipulation to hide `CurrentLocaleRequiresUnabbrevatedMonthNames`. – Léo Natan Nov 19 '14 at 21:13
  • That's some great detective work Leo. It's not the answer I hoped for but it certainly answers the question of how the Calendar app knows what to do - it's simply hardcoded. Thanks. I spent the day digging into raw Unicode data files for many of the locales. I even look inside the Calendar.app looking for clues but I never tried actually debugging the running Calendar app. – rmaddy Nov 19 '14 at 21:14
  • Always fun to debug Apple code. You can attach to all processes in the Simulator, and 64-bit assembly is spelled out much better by the disassembler. AS you see above, you can really read the code. – Léo Natan Nov 19 '14 at 21:16
  • about the disassembly: you said *it creates an array of locales that require unabbreviated month names*. When you're coding `[]` means an array. How did you find out there was an array? What syntax correlates to an array here? – mfaani Mar 20 '18 at 16:15
  • When looking in the disassembly, I saw a long list of strings, followed by `arrayWithObjects:count:` which is a method on NSArray: https://developer.apple.com/documentation/foundation/nsarray/1460096-arraywithobjects So basically what is created here is an id* array (or id[]), which is sent to the NSArray method. – Léo Natan Mar 20 '18 at 16:18