Month/Day/Year Order from Locale?

In order to correctly arrange horizontal pickers representing Month, Day, Year in a Watch app, I'd like to get the order based on a user's language and region. Ideally, a return value from a function or property as simple as MDY vs. DMY vs. YMD.


How can I get this info from a locale (or ???)?

Post not yet marked as solved Up vote post of DrMiller Down vote post of DrMiller
5.9k views

Replies

This is trickier than it sounds. To start, you’re assuming that month, day and year are the only values that the user must pick to specify a day but that’s not true on all calendars. For example, depending on the context, a user using the Japanese calendar might also want to pick an era (the Japanese calendar starts from the accession of the emperor, so any dates before 8 Jan 1989 are in a different era). If you’re not prepared to handle non-Gregorian calendars, it’d probably be a good idea to hard-wire Gregorian into your app.

Anyway, with regards the component order, the best approach I can think of is to to call NSDateFormatter’s

+dateFormatFromTemplate:options:locale:
method and parse the resulting string.

If you want to force Gregorian in this case, you’ll have to create a custom locale for that. NSLocale’s

+componentsFromLocaleIdentifier:
and
+localeIdentifierFromComponents:
should make that easy.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Sounds like something that should be in the WatchOS framework 😉


I guess I don't understand calendars and locale well enough to comprehend your hints/suggestions. When I created/forced a custom locale for gregorian calendars, the order of the Month/Day (I'm going to leave the year last always) never changes when I go from US to Germany region in the Run scheme. I did come up with *something* that seems to work (for US, Germany, France, Mexico, and Australia???).


NSString *gregorianIdentifier = [NSLocale localeIdentifierFromComponents:@{ NSLocaleCalendar : [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian ] }];
    NSLocale *gregorianLocale = [NSLocale localeWithLocaleIdentifier:gregorianIdentifier];

    // Don't know why I need this???
//  NSDictionary *gregorianComponents = [NSLocale componentsFromLocaleIdentifier:gregorianIdentifier];

    NSDateFormatter *dateFormatter = [NSDateFormatter new];
//  dateFormatter.dateFormat = [NSDateFormatter dateFormatFromTemplate: @"MMM d" options:0 locale:gregorianLocale];
    dateFormatter.dateFormat = [NSDateFormatter dateFormatFromTemplate: @"MMM d" options:0 locale:[NSLocale currentLocale]];
    NSString *localizedString = [dateFormatter stringFromDate:[NSDate date]];
    NSLog(@"localizedString: %@", localizedString);

    NSArray *array = [localizedString componentsSeparatedByString:@" "];
    if ([(NSString *)[array firstObject] length] == 3) {
        NSLog(@"Month first");
    } else {
        NSLog(@"Day first");
    }

Hmmm. When I change the Application Region in the Scheme (e.g. to France) and place this code in the viewDidLoad of another iOS application, the currentLocale is "en_FR" and the localizedString is "29 Oct" (and this is why I think this code works for my purposes). But, when I place this same code in the WatchKit extension awakeWithContext (and change the Application Region to France again in the Watchkit App Scheme), and Run the watchkit app, the currentLocale is "en_US" and the result is "Oct 29". Not what I expected. Have not tried this on the device though (haven't figured that out yet 😉.

Sounds like something that should be in the WatchOS framework

Well, you what what to do (-:

Two things:

  • If you do file a enhancement request, please post your bug number, just for the record.

  • Give some thought about what you want to request. For example, would you prefer to have a date picker, à la iOS, or better APIs at the ‘model’ layer? Remember that it’s OK to file more than one enhancement request for any given issue!

I guess I don't understand calendars and locale well enough to comprehend your hints/suggestions.

Fair enough. You’ve posted code with lots of different options, so it’s hard to nail things down one specific test case. What I was expecting you’d do is:

  1. get the user’s current locale (

    +currentLocale
    )
  2. get its locale identifier (

    localeIdentifier
    )
  3. bring it into components (

    +componentsFromLocaleIdentifier:
    )
  4. modify the components force Gregorian

  5. build a new locale identifier (

    +localeIdentifierFromComponents:
    )
  6. create a locale from that (

    +localeWithLocaleIdentifier:
    )
  7. use that to create your date format from a template (

    +dateFormatFromTemplate:options:locale:
    )

That is, you want to honour the user’s locale settings except that you want to override the calendar to Gregorian.

Now, with the abbreviated month (

MMM
) it’s not obvious that you’ll see date reversals (“Oct 30” vs “30 Oct”) in common locales. However, a quick test reveals a wide variety of results. Put this code in a small command line tool and run it on your Mac:
import Foundation

for locID in NSLocale.availableLocaleIdentifiers() {
    let loc = NSLocale(localeIdentifier: locID)
    let dfs = NSDateFormatter.dateFormatFromTemplate("MMM d", options: 0, locale: loc)!
    NSLog("%@ %@", locID, dfs)
}

Look through this output carefully because it reveals some edge cases with the parsing. For example, both Japanese and Chinese locales return the string “M月d日”, indicating numeric months and days with non-numeric markers. Which brings me back to my original comment, that this is trickier than you might think, and also explains why there’s no simplistic “what’s the day-month-year order” API.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Radar #23339905 Request for WKInterfaceDatePicker filed.


I tried the step-by-step approach by Eskimo and in step #5 +localeIdentifierFromComponents: the code actually CRASHED so I appended the gregorian string directly to the identifier. In IB the default picker order is DMY as only the U.S. and a couple other places use MDY and even when I looked at the output for all the variations looking for 3-character long seems to work (I only localize in French, German, Spanish, and Portuguese anyway).


- (void)adjustPickerOrder {

    NSString *nativeIdentifier = [[NSLocale currentLocale] localeIdentifier];
    NSString *gregorianIdentifier = [nativeIdentifier stringByAppendingString:@"@calendar=gregorian"];
    NSLocale *gregorianLocale = [NSLocale localeWithLocaleIdentifier:gregorianIdentifier];

    NSDateFormatter *dateFormatter = [NSDateFormatter new];
    dateFormatter.dateFormat = [NSDateFormatter dateFormatFromTemplate: @"MMM d" options:0 locale:gregorianLocale];
    NSString *localizedString = [dateFormatter stringFromDate:[NSDate date]];

    NSArray *array = [localizedString componentsSeparatedByString:@" "];
    if ([(NSString *)[array firstObject] length] == 3) {
        // Month first, make the switch
        [_monthPicker setHorizontalAlignment:WKInterfaceObjectHorizontalAlignmentLeft];
        [_dayPicker setHorizontalAlignment:WKInterfaceObjectHorizontalAlignmentCenter];
     
    } else {
        // Day first, the default so this is probably redundant
        [_monthPicker setHorizontalAlignment:WKInterfaceObjectHorizontalAlignmentCenter];
        [_dayPicker setHorizontalAlignment:WKInterfaceObjectHorizontalAlignmentLeft];
    }
}

Radar #23339905 Request for WKInterfaceDatePicker filed.

Thanks.

(I only localize in French, German, Spanish, and Portuguese anyway).

Don’t make the assumption that the locale and the language are related in any way. For example, a Spanish user living in Japan might have their language set to Spanish but their locale set to Japan (a common reason to do this is to get the right currency, in this case, Yen not Euro). Thus, even though you don’t localise for Japanese, you still have to worry about the Japanese locale.

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I'm aware of the difference between language and locale but I thought no one would buy my app if it wasn't localized to their language (or fluent in English) 😉


But I thought the entire point of changing the locale calendar to Gregorian would override the user's region settings for the app, e.g. a Spanish user living in Japan with the locale set to Japan would still get the "forced" date format as if their calendar was Gregorian when my app tries to decide MDY vs. DMY, and if the length of the first phrase to appear is not 3, then they get the default DMY. The only thing my pickers get from the system is the short names of the months, the rest are generated items from 1..31, 1995...2030, etc.

But I thought the entire point of changing the locale calendar to Gregorian would override the user's region settings for the app…

Right. My point was a general one: you shouldn’t assume that there’s a relationship between your app’s localisations and the user’s chosen locale.

The only thing my pickers get from the system is the short names of the months, the rest are generated items from 1..31, 1995...2030, etc.

I hope you’re not hard-coding those literal strings. There are locales that use the Gregorian calendar but don’t use Western Arabic numerals. There are also locales, like Japanese and Chinese, where each the numeric values of component are always framed by text.

You could implement this by running each day and month choice through a date formatter that itself is customised with a format string generated from a template. If you do this I recommend that you double check the results against UIDatePicker to see if they are reasonable.

All-in-all this task just keeps getting uglier )-:

Share and Enjoy

Quinn "The Eskimo!"
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

"All-in-all this task just keeps getting uglier )-:"

Yes, indeed 🙂


BTW, my bug report was closed as a duplicate (of course), so anyone reading this struggling with these same ugly issues, please file another (duplicate) bug report.

Basically, I use the currentLocale to print the Todays Date in an array. Then I use the DateFormatter to get the month's name. If the months name is in position 1 (2) of the array then date format is month / day / year. I guess you could loop through the array and find the order of any of the items in this way. Not sure how well it will hold up for other Locales.

Code Block
    NSDate * today = [NSDate date];
    NSString * month = @"MMMM";
    NSDateFormatter *dformat = [[NSDateFormatter alloc] init];
    [dformat setDateFormat:month];
     
    NSArray * dateComponents = [[today descriptionWithLocale:[NSLocale currentLocale]] componentsSeparatedByString:@" "];
     
    NSLog(@"Date String Separated: %@", dateComponents);
   
    if( [[dformat stringFromDate:today] isEqualToString:dateComponents[1]] ){
      NSLog(@"Date Format is : Month / Day / Year");
    }else{
      NSLog(@"Date Format is : Day / Month / Year");
    }

Unfortunately this won’t work reliably. The fundamental problem is on line 6 where you assume that date components are separated by spaces. This is not a valid assumption.

In the absence of any OS improvements — and, alas, I can’t check up on the bug that DrMiller filed at this moment (r. 23339905) — I think the approach I recommended earlier is still your best option.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Here's some modern Swift code based on @eskimo's recommended approach. My requirement was to localize a "month/day" string, e.g. Apr 28, so it would appear as "4/28" in the U.S., but reformatted as "28/4" or similar in other locales.

The +dateFormat(fromTemplate:options:locale) function seems to be smart enough to flip the "M/d" around to match the locale.

@eskimo, does this look like a valid implementation?

Code Block
import Foundation
let now = Date()
for localeIdentifier in Locale.availableIdentifiers {
let locale = Locale(identifier: localeIdentifier)
let forcedLocale = gregorianForcedLocale(using: locale)
let dateFormat = DateFormatter.dateFormat(fromTemplate: "M/d", options: 0, locale: forcedLocale)!
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .gregorian)
formatter.dateFormat = dateFormat
let dateString = formatter.string(from: now)
print(localeIdentifier + ", " + dateFormat + ", " + dateString)
}
func gregorianForcedLocale(using locale: Locale) -> Locale {
let localeIdentifier = locale.identifier
var components = Locale.components(fromIdentifier: localeIdentifier)
components["calendar"] = "gregorian"
let customLocaleIdentifier = Locale.identifier(fromComponents: components)
let customLocale = Locale(identifier: customLocaleIdentifier)
return customLocale
}


does this look like a valid implementation?

It’s hard to say without knowing more about your requirements. There’s a lot of code in there (all the gregorianForcedLocale(using:) stuff) that looks like test code rather than something you plan to deploy.

So, if the goal is to print a localised date that includes the Gregorian month and the day, this is what I’d write:

Code Block
let df = DateFormatter()
df.calendar = Calendar(identifier: .gregorian)
df.setLocalizedDateFormatFromTemplate("Md")
print(df.string(from: Date()))


Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"