My app ingests data from a web service (PHP) which provides dates in this format:
endDate = {
date = "2020-09-30 16:16:08.000000";
timezone = "-04:00";
"timezone_type" = 1;
};
This is the code I have been using to convert to NSDate, and it works as far as I can tell, in every test, but it fails on a few devices according to user reports and debug logs.
Note that the correct conversion of this date determines if content is unlocked in the app, so when it fails, customers contact us about it.
NSDictionary* dateDict = [responseDict objectForKey:#"endDate"];
NSString* strEndDate = [dateDict objectForKey:#"date"];
NSString* strOffset = [dateDict objectForKey:#"timezone"];
NSTimeInterval zoneSeconds = 0;
NSRange rng = [strOffset rangeOfString:#":"];
if (rng.location != NSNotFound && rng.location >= 1)
{
NSString* hoursOnly = [strOffset substringToIndex:rng.location];
NSInteger offsetValue = [hoursOnly integerValue];
zoneSeconds = (3600 * offsetValue);
}
NSDateFormatter* df = [[NSDateFormatter alloc] init];
NSTimeZone *timeZone = [NSTimeZone timeZoneForSecondsFromGMT:zoneSeconds];
[df setTimeZone:timeZone];
[df setDateFormat:#"yyyy-MM-dd HH:mm:ss.000000"];
NSDate* newEndDate = [df dateFromString:strEndDate];
However, debug logs from a few users show that the dateFromString call is failing and returning nil.
We have one user who has 2 iOS devices, and using the same account (same date) the app performs as expected on one of them, but fails on the other. Same Apple ID, both running iOS12. Debug logs show both devices received the same date from the server, yet one of them failed to convert the date from a string to NSDate.
My assumption so far is that there is some setting or configuration on the device(s) where this fails that is different. But I have fiddled with calendar and date settings all day, and cannot get this to fail. I know the user in question has both devices configured to the same time zone.
Is there a better, more correct way to do this date conversion which might be more robust?
When using an arbitrary date format it's highly recommended to set the locale of the date formatter to the fixed value en_US_POSIX.
Rather than calculating the seconds from GMT it might be more efficient to strip the milliseconds with regular expression, append the string time zone and use an appropriate date format.
This code uses more contemporary syntax to set date formatter properties with dot notation and dictionary literal key subscription
NSDictionary *dateDict = responseDict[#"endDate"];
NSString *strEndDate = dateDict[#"date"];
NSString *strTimeZone = dateDict[#"timezone"];
NSString *dateWithoutMilliseconds = [strEndDate stringByReplacingOccurrencesOfString:#"\\.\\d+" withString:#"" options:NSRegularExpressionSearch range:NSMakeRange(0, strEndDate.length)];
NSString *dateWithTimeZone = [NSString stringWithFormat:#"%#%#", dateWithoutMilliseconds, strTimeZone];
NSDateFormatter *df = [[NSDateFormatter alloc] init];
df.locale = [NSLocale localeWithLocaleIdentifier:#"en_US_POSIX"];
df.dateFormat = #"yyyy-MM-dd HH:mm:ssZZZZZ"];
NSDate *newEndDate = [df dateFromString:dateWithTimeZone];
The question was actually similar to (What is the best way to deal with the NSDateFormatter locale "feechur"?) as was suggested originally, but it was this other question (NSDateFormatter fails to return a datetime for UK region with 12 hour clock set) which really made it click for me - its the UK region with the 12hour clock which causes the code to fail, but the dateFormatter was easily fixed by simply setting the locale to "un_US_POSIX" as suggested in the answer to that question (it was also suggested below by vadian - I did not try his code however). Thank you to everyone who contributed hints and leads!
How can I check if which side the currency symbol is at? For example, in the United States, the symbol would be like this: "$56.58", but in France it would be like this: "56.58€". So how would I be able to detect if it's on the right or left side?
NSNumberFormatter *currencyFormatter = [[[NSNumberFormatter alloc] init] autorelease];
[currencyFormatter setLocale:[NSLocale currentLocale]];
If you just want to format a number as currency, set the formatter's numberStyle to NSNumberFormatterCurrencyStyle and then use its stringFromNumber: method.
If, for some reason, you really want to know the position of the currency symbol in the format for the formatter's locale, you can ask the formatter for its positiveFormat and look for the character ¤ (U+00A4 CURRENCY SIGN).
NSNumberFormatter *f = [[NSNumberFormatter alloc] init];
f.numberStyle = NSNumberFormatterCurrencyStyle;
f.locale = [NSLocale localeWithLocaleIdentifier:#"en-US"];
NSLog(#"%# format=[%#] ¤-index=%lu", f.locale.localeIdentifier, f.positiveFormat,
(unsigned long)[f.positiveFormat rangeOfString:#"\u00a4"].location);
f.locale = [NSLocale localeWithLocaleIdentifier:#"fr-FR"];
NSLog(#"%# format=[%#] ¤-index=%lu", f.locale.localeIdentifier, f.positiveFormat,
(unsigned long)[f.positiveFormat rangeOfString:#"\u00a4"].location);
f.locale = [NSLocale localeWithLocaleIdentifier:#"fa-IR"];
NSLog(#"%# format=[%#] ¤-index=%lu", f.locale.localeIdentifier, f.positiveFormat,
(unsigned long)[f.positiveFormat rangeOfString:#"\u00a4"].location);
Result:
2015-06-10 21:27:09.807 commandline[88239:3716428] en-US format=[¤#,##0.00] ¤-index=0
2015-06-10 21:27:09.808 commandline[88239:3716428] fr-FR format=[#,##0.00 ¤] ¤-index=9
2015-06-10 21:27:09.808 commandline[88239:3716428] fa-IR format=[¤#,##0] ¤-index=1
Note that in the fa-IR case, the symbol is not the first or last character in the format string. The first character (at index zero) is invisible. It's U+200E LEFT-TO-RIGHT MARK.
Not sure if this is possible, but for my app I would like to get a locale based string that describes a number.
For example, if I had the number 10,000,000.
In english, I would expect the phrase "Ten Million". However, in Hindi, it would be One crore. Is there any properties in NSNumberFormatter, or NSLocale that could help me with this?
I have checked the docs (NSNumberFormatter, NSLocale), and havent found what I'm looking for yet. Obviously I could write some code to handle these two cases, but I'd like a way that could work for any locale.
Edit: Thanks to leo for the answer! Here is a small snippet of code that will get anyone looking for the same thing started:
NSNumberFormatter formatter = [[NSNumberFormatter alloc] init];
[self.formatter setNumberStyle:NSNumberFormatterSpellOutStyle];
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:#"hi_hi"];
[self.formatter setLocale:locale];
NSNumber * myNumber = [NSNumber numberWithInt:10000];
self.numberLabel.text = [self.formatter stringFromNumber:myNumber];
What you are looking for is the NSNumberFormatterSpellOutStyle style.
NSString* spelledOutString = [NSNumberFormatter localizedStringFromNumber:#10000000 numberStyle:NSNumberFormatterSpellOutStyle];
I need to get a string like en_US or es_ES in iOS.
The obvious method is to use [NSLocale currentLocale] and get the language info from there. However, that gives you the "region format" and not the actual device language, which means that if you have the device language in english but the region format as "spain", it'll erroneously report es_ES.
If you need the device language you must do this instead:
[[NSLocale preferredLanguages] objectAtIndex:0]
However, that only gives you the language, so you get something like en or es, without the country code at the end.
How would I get the country code correctly, like Safari does?
Try with this code:
NSString *locale = [[NSLocale currentLocale] localeIdentifier];
NSRange startRange = [locale rangeOfString:#"_"];
NSString *result = [locale stringByReplacingCharactersInRange:NSMakeRange(0, startRange.length+1) withString:[[NSLocale preferredLanguages] objectAtIndex:0]];
DebugLog(#"current locale: %#", result);
Ok, seeing getting the right info out of iOS is probably not possible, here's a hackish solution but which gives the output I needed. It's not complete and it won't give precise output in some cases (like for arabic), but it's the best I've been able to get.
const string lang = [[[NSLocale preferredLanguages] objectAtIndex:0] UTF8String];
// Search the language with country code in the langs map
static std::map<string, string> langMap;
static bool initialized = false;
if(!initialized) {
#define LANG(l, c) langMap.insert(std::make_pair(#l, #c))
LANG(en, en-us); LANG(es, es-es); LANG(fr, fr-fr); LANG(de, de-de);
LANG(ja, ja-jp); LANG(nl, nl-nl); LANG(it, it-it); LANG(pt, pt-br);
LANG(da, da-dk); LANG(fi, fi-fi); LANG(nb, nb-no); LANG(sv, sv-se);
LANG(ko, ko-kr); LANG(ru, ru-ru); LANG(pl, pl-pl); LANG(tr, tr-tr);
LANG(uk, uk-ua); LANG(hr, hr-hr); LANG(cs, cs-cz); LANG(el, el-gr);
LANG(he, he-il); LANG(ro, ro-ro); LANG(sk, sk-sk); LANG(th, th-th);
LANG(ca, ca-es); LANG(hu, hu-hu); LANG(vi, vi-vn);
LANG(zh-Hans, zh-cn); LANG(pt-PT, pt-pt); LANG(id, id); LANG(ms, ms);
LANG(zh-Hant, zh-tw); LANG(en-GB, en-gb); LANG(ar, ar);
#undef LANG
initialized = true;
}
map<string,string>::iterator it = langMap.find(lang);
if( it != langMap.end() ){
return it->second;
}
// Didn't find a country code, default to the lang name
return lang;
Check the below code:
NSArray *langs = [NSLocale preferredLanguages];
for (NSString *lang in langs) {
NSLog(#"%#: %# %#",lang, [NSLocale canonicalLanguageIdentifierFromString:lang], [[[NSLocale alloc] initWithLocaleIdentifier:lang] displayNameForKey:NSLocaleIdentifier value:lang]);
}
Swift
NSLocale.preferredLanguages()[0] as String
Output would be like
en-GB
Source
An alternative would be to reconstruct the locale from the components of the current locale, swapping in the language that you want.
For example, to get the current locale but modified to use the language which UIKit is currently using to display the app:
let languageCode = Bundle.main.preferredLocalizations.first ?? "en"
var components = Locale.components(fromIdentifier: Locale.current.identifier)
components[NSLocale.Key.languageCode.rawValue] = languageCode
let locale = Locale(identifier: Locale.identifier(fromComponents: components))
I am using this at the moment. I have run a few test cases and it seems ok, However I am not convinced that it is a robust solution. I am surprised not to find a clear answer to this simple problem. I would not recommend my solution but I hope it can generate discussion.
NSLocale *locale = [NSLocale currentLocale];
NSString *countryCode = [locale objectForKey: NSLocaleCountryCode];
NSString *language = [[NSLocale preferredLanguages] firstObject];
NSString *myLocale = [NSString stringWithFormat:#"%#_%#",language,countryCode];
NSLocale *userLocale = [[NSLocale alloc] initWithLocaleIdentifier:myLocale];
NSDate* now = [NSDate date];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
NSString *dateFormat = [NSDateFormatter dateFormatFromTemplate:#"E MMM d yyyy HH:mm" options:0 locale:userLocale];
Simply do this:
NSString *language = [[NSLocale preferredLanguages] objectAtIndex:0];
NSString *locale = [[NSLocale currentLocale] objectForKey: NSLocaleCountryCode];
NSString *formattedStr = [NSString stringWithFormat:#"%#_%#",language, locale];
NSLog(#"%#", formattedStr); // Display en_US for example
I'm using NSLocale quite a lot for Numbers or Currency formatting. For example I use it this way:
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
// Config the NSNumberFormatter ...
formatter.groupingSeparator = [[NSLocale currentLocale] objectForKey:NSLocaleGroupingSeparator];
The compiler always gives me the warning: Multiple methods named 'objectForKey:' found
This gets really annoying in larger projects (20+ warnings of this type). The only way I found to get rid of this warning is doing a type cast to NSDictionary:
formatter.groupingSeparator = [(NSDictionary *)[NSLocale currentLocale] objectForKey:NSLocaleGroupingSeparator];
This works but I'm not sure if this will lead to problems as [NSLocale currentLocale] seems not directly to be an NSDictionary ([[NSLocale currentLocale] class] returns __NSCFLocale).
Is there any better solution to this?
CMD+Click to your objectForKey statement. Xcode will find the method at NSDictionary.h.
Now change your code like
NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
...
NSLocale *currentLocale = [NSLocale currentLocale];
formatter.groupingSeparator = [currentLocale objectForKey:NSLocaleGroupingSeparator];
and CMD+Click again to this objectForKey statement. Xcode will go to correct place, NSLocale.h.
Or, as you suggested, you can just force-cast NSLocale like
formatter.groupingSeparator = [(NSLocale *)[NSLocale currentLocale] objectForKey:NSLocaleGroupingSeparator];