Mhurd has submitted this change and it was merged.

Change subject: Fix NSDateFormatter issues
......................................................................


Fix NSDateFormatter issues

We were failing to parse timestamps when the device was set to Arabic on
iOS 6.  Also, the main date formatter wasn't thread-safe.

- Made main date formatter thread safe
- Refactored the "short time" formatter w/ isMainThread assertion
- Added unit tests for formatter validity across all locales and
  expected output
- Tweak in "main.m" which cuts off app start during unit testing (faster, more 
reliable tests)

Change-Id: Ibc9758578cb370565310140166c6a20d2781e273
---
M Wikipedia.xcodeproj/project.pbxproj
A WikipediaUnitTests/WMFDateFormatterTests.m
M wikipedia/Categories/NSDateFormatter+WMFExtensions.h
M wikipedia/Categories/NSDateFormatter+WMFExtensions.m
M wikipedia/View Controllers/PageHistory/PageHistoryResultCell.m
M wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
M wikipedia/main.m
7 files changed, 137 insertions(+), 47 deletions(-)

Approvals:
  Fjalapeno: Looks good to me, but someone else must approve
  Mhurd: Looks good to me, approved
  jenkins-bot: Verified



diff --git a/Wikipedia.xcodeproj/project.pbxproj 
b/Wikipedia.xcodeproj/project.pbxproj
index 71214a4..ce94344 100644
--- a/Wikipedia.xcodeproj/project.pbxproj
+++ b/Wikipedia.xcodeproj/project.pbxproj
@@ -233,6 +233,7 @@
                BC0FED751AAA026C002488D7 /* NSArray+BKIndexTests.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = BCB58F7B1A8D0C8E00465627 /* 
NSArray+BKIndexTests.m */; };
                BC0FED761AAA026C002488D7 /* NSString+WMFHTMLParsingTests.m in 
Sources */ = {isa = PBXBuildFile; fileRef = C983151B1AA5205700E25EE1 /* 
NSString+WMFHTMLParsingTests.m */; };
                BC0FED771AAA026C002488D7 /* WMFImageURLParsingTests.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BCBDE0AB1AA76EAC006BD29A /* 
WMFImageURLParsingTests.m */; };
+               BC23759E1AB8928600B0BAA8 /* WMFDateFormatterTests.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = BC23759D1AB8928600B0BAA8 /* 
WMFDateFormatterTests.m */; };
                BC2CBB8E1AA10F400079A313 /* UIView+WMFFrameUtils.m in Sources 
*/ = {isa = PBXBuildFile; fileRef = BC2CBB8D1AA10F400079A313 /* 
UIView+WMFFrameUtils.m */; };
                BC50C37F1A83C784006DC7AF /* WMFNetworkUtilities.m in Sources */ 
= {isa = PBXBuildFile; fileRef = BC50C37E1A83C784006DC7AF /* 
WMFNetworkUtilities.m */; };
                BC50C3871A83CBDA006DC7AF /* MWKImageInfoResponseSerializer.m in 
Sources */ = {isa = PBXBuildFile; fileRef = BC50C3861A83CBDA006DC7AF /* 
MWKImageInfoResponseSerializer.m */; };
@@ -725,6 +726,7 @@
                17A2F22335C5256576CEDBDD /* 
Pods-WikipediaUnitTests.release.xcconfig */ = {isa = PBXFileReference; 
includeInIndex = 1; lastKnownFileType = text.xcconfig; name = 
"Pods-WikipediaUnitTests.release.xcconfig"; path = "Pods/Target Support 
Files/Pods-WikipediaUnitTests/Pods-WikipediaUnitTests.release.xcconfig"; 
sourceTree = "<group>"; };
                357504E50DA104E39C6ACFEB /* Pods.release.xcconfig */ = {isa = 
PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = 
Pods.release.xcconfig; path = "Pods/Target Support 
Files/Pods/Pods.release.xcconfig"; sourceTree = "<group>"; };
                8CE61C6963F825760822A28A /* libPods-WikipediaUnitTests.a */ = 
{isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; 
path = "libPods-WikipediaUnitTests.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+               BC23759D1AB8928600B0BAA8 /* WMFDateFormatterTests.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= WMFDateFormatterTests.m; sourceTree = "<group>"; };
                BC2CBB8C1AA10F400079A313 /* UIView+WMFFrameUtils.h */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = 
"UIView+WMFFrameUtils.h"; sourceTree = "<group>"; };
                BC2CBB8D1AA10F400079A313 /* UIView+WMFFrameUtils.m */ = {isa = 
PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path 
= "UIView+WMFFrameUtils.m"; sourceTree = "<group>"; };
                BC4273521A7C736800068882 /* WikipediaUnitTests.xctest */ = {isa 
= PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; 
path = WikipediaUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -1986,6 +1988,7 @@
                                BCBDE0AB1AA76EAC006BD29A /* 
WMFImageURLParsingTests.m */,
                                BCB8487A1AAAADF90077EC24 /* 
WMFRoundingUtilitiesTests.m */,
                                BCDB75C31AB0E8300005593F /* 
WMFSubstringUtilsTests.m */,
+                               BC23759D1AB8928600B0BAA8 /* 
WMFDateFormatterTests.m */,
                        );
                        path = WikipediaUnitTests;
                        sourceTree = "<group>";
@@ -2816,6 +2819,7 @@
                                BC0FED731AAA026C002488D7 /* 
NSMutableDictionary+MaybeSetTests.m in Sources */,
                                048830D31AB775E3005BF3A1 /* 
UIScrollView+WMFScrollsToTop.m in Sources */,
                                BC0FED6E1AAA0268002488D7 /* MWKImageListTests.m 
in Sources */,
+                               BC23759E1AB8928600B0BAA8 /* 
WMFDateFormatterTests.m in Sources */,
                                04D686F51AB2949C0009B44A /* MenuButton.m in 
Sources */,
                                BC0FED701AAA026C002488D7 /* 
NSArray+PredicateTests.m in Sources */,
                                04D686F91AB2949C0009B44A /* PaddedLabel.m in 
Sources */,
@@ -3309,6 +3313,7 @@
                        buildSettings = {
                                BUNDLE_LOADER = "$(TEST_HOST)";
                                CLANG_WARN_UNREACHABLE_CODE = YES;
+                               CODE_SIGN_IDENTITY = "iPhone Developer";
                                ENABLE_STRICT_OBJC_MSGSEND = YES;
                                FRAMEWORK_SEARCH_PATHS = (
                                        
"$(SDKROOT)/Developer/Library/Frameworks",
@@ -3334,6 +3339,7 @@
                        buildSettings = {
                                BUNDLE_LOADER = "$(TEST_HOST)";
                                CLANG_WARN_UNREACHABLE_CODE = YES;
+                               CODE_SIGN_IDENTITY = "iPhone Developer";
                                ENABLE_STRICT_OBJC_MSGSEND = YES;
                                FRAMEWORK_SEARCH_PATHS = (
                                        
"$(SDKROOT)/Developer/Library/Frameworks",
diff --git a/WikipediaUnitTests/WMFDateFormatterTests.m 
b/WikipediaUnitTests/WMFDateFormatterTests.m
new file mode 100644
index 0000000..bfbb7da
--- /dev/null
+++ b/WikipediaUnitTests/WMFDateFormatterTests.m
@@ -0,0 +1,67 @@
+//
+//  WMFDateFormatterTests.m
+//  Wikipedia
+//
+//  Created by Brian Gerstle on 3/17/15.
+//  Copyright (c) 2015 Wikimedia Foundation. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+#import <XCTest/XCTest.h>
+#import "NSDateFormatter+WMFExtensions.h"
+
+#define HC_SHORTHAND 1
+#import <OCHamcrest/OCHamcrest.h>
+
+@interface WMFDateFormatterTests : XCTestCase
+
+@end
+
+@implementation WMFDateFormatterTests
+
+- (void)testIso8601Example {
+    NSString* testTimestamp   = @"2015-02-10T10:31:27Z";
+    NSDate* decodedDate = [[NSDateFormatter wmf_iso8601Formatter] 
dateFromString:testTimestamp];
+    assertThat(decodedDate, is(notNilValue()));
+    assertThat([[NSDateFormatter wmf_iso8601Formatter] 
stringFromDate:decodedDate], is(equalTo(testTimestamp)));
+}
+
+- (void)testShortTimeFormatterIsValidForAllLocales {
+    NSString* testTimestamp = @"2015-02-10T10:31:27Z";
+    NSArray* availableLocaleIDs = [NSLocale availableLocaleIdentifiers];
+
+    dispatch_apply(availableLocaleIDs.count, 
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t i) {
+        NSString* localeID = availableLocaleIDs[i];
+        NSLocale* locale = [NSLocale localeWithLocaleIdentifier:localeID];
+
+        // need to parse date using the "regular" formatter
+        NSDate* decodedDate = [[NSDateFormatter wmf_iso8601Formatter] 
dateFromString:testTimestamp];
+        NSParameterAssert(decodedDate);
+
+        // TODO: check for "AM" for corresponding time locales
+        assertThat([[NSDateFormatter wmf_shortTimeFormatterWithLocale:locale] 
stringFromDate:decodedDate],
+                   describedAs(@"expected non-nil for locale: %0 from 
timestamp %1",
+                               notNilValue(),
+                               localeID,
+                               testTimestamp,
+                               nil));
+    });
+}
+
+- (void)testShortTimeFormatterExamples {
+    NSString* testTimestamp = @"2015-02-10T14:31:27Z";
+    NSDate* decodedDate = [[NSDateFormatter wmf_iso8601Formatter] 
dateFromString:testTimestamp];
+    NSParameterAssert(decodedDate);
+
+    NSDateFormatter* usFormatter =
+        [NSDateFormatter wmf_shortTimeFormatterWithLocale:[NSLocale 
localeWithLocaleIdentifier:@"en_US"]];
+
+    NSDateFormatter* gbFormatter =
+         [NSDateFormatter wmf_shortTimeFormatterWithLocale:[NSLocale 
localeWithLocaleIdentifier:@"en_GB"]];
+
+    assertThat([usFormatter stringFromDate:decodedDate], is(equalTo(@"2:31 
PM")));
+
+    assertThat([gbFormatter stringFromDate:decodedDate], 
is(equalTo(@"14:31")));
+}
+
+@end
diff --git a/wikipedia/Categories/NSDateFormatter+WMFExtensions.h 
b/wikipedia/Categories/NSDateFormatter+WMFExtensions.h
index 28d54a7..7815191 100644
--- a/wikipedia/Categories/NSDateFormatter+WMFExtensions.h
+++ b/wikipedia/Categories/NSDateFormatter+WMFExtensions.h
@@ -4,10 +4,33 @@
 @interface NSDateFormatter (WMFExtensions)
 
 /**
- *  Standard formatter. Cached for performance
+ * Formatter which can be used to parse timestamps from the mediawiki API.
  *
- *  @return The formatter
+ * @note It is safe to call this method from any thread.
+ *
+ * @return Singleton @c NSDateFormatter for transcoding WMF timestamps.
  */
 + (NSDateFormatter*)wmf_iso8601Formatter;
 
+
+/**
+ * Formatter which can be used to present a short time string for a given date.
+ *
+ * @warning Do not attempt to parse raw timestamps from the mediawiki API 
using this method. Use the unstyled
+ *          @c +wmf_iso8601Formatter method instead.
+ *
+ * @note    This method is not thread safe, as it is intended to only be used 
by code which presents text to the user.
+ *
+ * @see +[NSDateFormatter wmf_iso8601Formatter]
+ *
+ * @return Singleton @c NSDateFormatter for displaying times to the user.
+ */
++ (NSDateFormatter*)wmf_shortTimeFormatter;
+
+/**
+ * Create an short-style time formatter with the given locale.
+ * @warning This method is exposed for testing only, use @c 
+wmf_shortTimeFormatter instead.
+ */
++ (NSDateFormatter*)wmf_shortTimeFormatterWithLocale:(NSLocale*)locale;
+
 @end
diff --git a/wikipedia/Categories/NSDateFormatter+WMFExtensions.m 
b/wikipedia/Categories/NSDateFormatter+WMFExtensions.m
index ac21e25..43e7da5 100644
--- a/wikipedia/Categories/NSDateFormatter+WMFExtensions.m
+++ b/wikipedia/Categories/NSDateFormatter+WMFExtensions.m
@@ -1,19 +1,40 @@
 
 #import "NSDateFormatter+WMFExtensions.h"
 
+static NSString* const WMF_ISO8601_FORMAT = 
@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'";
+
 @implementation NSDateFormatter (WMFExtensions)
 
 + (NSDateFormatter*)wmf_iso8601Formatter {
-    static NSDateFormatter* _formatter = nil;
+    static NSDateFormatter* iso8601Formatter = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        // need to use "en" locale, otherwise the timestamp will fail to parse 
when the current locale is arabic on iOS 6
+        iso8601Formatter = [NSDateFormatter new];
+        iso8601Formatter.timeZone = [NSTimeZone timeZoneWithName:@"UTC"];
+        iso8601Formatter.dateFormat = WMF_ISO8601_FORMAT;;
+        iso8601Formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en"];
+    });
+    return iso8601Formatter;
+}
 
-    if (!_formatter) {
-        // See: https://www.mediawiki.org/wiki/Manual:WfTimestamp
-        _formatter = [[NSDateFormatter alloc] init];
-        [_formatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
-        [_formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
++ (NSDateFormatter*)wmf_shortTimeFormatter {
+    NSParameterAssert([NSThread isMainThread]);
+    static NSDateFormatter* shortTimeFormatter = nil;
+    if (!shortTimeFormatter) {
+        shortTimeFormatter = [self wmf_shortTimeFormatterWithLocale:[NSLocale 
currentLocale]];
     }
+    return shortTimeFormatter;
+}
 
-    return _formatter;
++ (NSDateFormatter*)wmf_shortTimeFormatterWithLocale:(NSLocale*)locale {
+    NSDateFormatter* shortTimeFormatter = [NSDateFormatter new];
+    shortTimeFormatter.timeZone          = [NSTimeZone 
timeZoneWithName:@"UTC"];
+    shortTimeFormatter.dateStyle         = NSDateFormatterNoStyle;
+    shortTimeFormatter.timeStyle         = NSDateFormatterShortStyle;
+    shortTimeFormatter.formatterBehavior = NSDateFormatterBehavior10_4;
+    shortTimeFormatter.locale            = locale;
+    return shortTimeFormatter;
 }
 
 @end
diff --git a/wikipedia/View Controllers/PageHistory/PageHistoryResultCell.m 
b/wikipedia/View Controllers/PageHistory/PageHistoryResultCell.m
index f2d1597..2b928f4 100644
--- a/wikipedia/View Controllers/PageHistory/PageHistoryResultCell.m
+++ b/wikipedia/View Controllers/PageHistory/PageHistoryResultCell.m
@@ -7,6 +7,7 @@
 #import "Defines.h"
 #import "NSString+Extras.h"
 #import "UIFont+WMFStyle.h"
+#import "NSDateFormatter+WMFExtensions.h"
 
 @interface PageHistoryResultCell ()
 
@@ -21,22 +22,6 @@
 
 @implementation PageHistoryResultCell
 
-+ (NSDateFormatter*)dateFormatter {
-    static NSDateFormatter* _formatter = nil;
-
-    if (!_formatter) {
-        // See: https://www.mediawiki.org/wiki/Manual:WfTimestamp
-        _formatter = [[NSDateFormatter alloc] init];
-        [_formatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
-        [_formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
-        _formatter.dateStyle         = NSDateFormatterNoStyle;
-        _formatter.timeStyle         = NSDateFormatterShortStyle;
-        _formatter.formatterBehavior = NSDateFormatterBehavior10_4;
-    }
-
-    return _formatter;
-}
-
 - (void)setName:(NSString*)name
            time:(NSString*)time
           delta:(NSNumber*)delta
@@ -45,7 +30,7 @@
       separator:(BOOL)separator {
     self.nameLabel.text = name;
 
-    self.timeLabel.text = [[[self class] dateFormatter] 
stringForObjectValue:[time getDateFromIso8601DateString]];
+    self.timeLabel.text = [[NSDateFormatter wmf_shortTimeFormatter] 
stringFromDate:[time getDateFromIso8601DateString]];
 
     self.deltaLabel.text =
         [NSString stringWithFormat:@"%@%@", (delta.integerValue > 0) ? @"+" : 
@"", delta.stringValue];
diff --git a/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m 
b/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
index 8cd3401..d2b0960 100644
--- a/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
+++ b/wikipedia/View Controllers/PageHistory/PageHistoryViewController.m
@@ -13,6 +13,7 @@
 #import "Defines.h"
 #import "PaddedLabel.h"
 #import "UITableView+DynamicCellHeight.h"
+#import "NSDateFormatter+WMFExtensions.h"
 
 #define TABLE_CELL_ID @"PageHistoryResultCell"
 
@@ -24,22 +25,6 @@
 @end
 
 @implementation PageHistoryViewController
-
-+ (NSDateFormatter*)dateFormatter {
-    static NSDateFormatter* _formatter = nil;
-
-    if (!_formatter) {
-        // See: https://www.mediawiki.org/wiki/Manual:WfTimestamp
-        _formatter = [[NSDateFormatter alloc] init];
-        [_formatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
-        [_formatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
-        _formatter.dateStyle         = NSDateFormatterLongStyle;
-        _formatter.timeStyle         = NSDateFormatterNoStyle;
-        _formatter.formatterBehavior = NSDateFormatterBehavior10_4;
-    }
-
-    return _formatter;
-}
 
 - (NavBarMode)navBarMode {
     return NAVBAR_MODE_X_WITH_LABEL;
@@ -209,9 +194,7 @@
     NSNumber* daysAgo = sectionDict[@"daysAgo"];
     NSDate* date      = [NSDate dateWithDaysBeforeNow:daysAgo.integerValue];
 
-    NSString* formattedDate = [[[self class] dateFormatter] 
stringForObjectValue:date];
-
-    label.text = formattedDate;
+    label.text = [[NSDateFormatter wmf_shortTimeFormatter] 
stringFromDate:date];
 
     [view addSubview:label];
 
diff --git a/wikipedia/main.m b/wikipedia/main.m
index 202bcd0..798ddc4 100644
--- a/wikipedia/main.m
+++ b/wikipedia/main.m
@@ -5,9 +5,14 @@
 
 #import "AppDelegate.h"
 
-int main(int argc, char* argv[]){
+int main(int argc, char* argv[]) {
+    // disable app when unit testing to allow tests to run in isolation (w/o 
side effects)
+    BOOL const isUnitTesting = NSClassFromString(@"XCTestCase") != nil;
     @autoreleasepool {
-        return UIApplicationMain(argc, argv, nil, 
NSStringFromClass([AppDelegate class]));
+        return UIApplicationMain(argc,
+                                 argv,
+                                 nil,
+                                 isUnitTesting ? nil : 
NSStringFromClass([AppDelegate class]));
     }
 }
 

-- 
To view, visit https://gerrit.wikimedia.org/r/197387
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings

Gerrit-MessageType: merged
Gerrit-Change-Id: Ibc9758578cb370565310140166c6a20d2781e273
Gerrit-PatchSet: 3
Gerrit-Project: apps/ios/wikipedia
Gerrit-Branch: master
Gerrit-Owner: Bgerstle <[email protected]>
Gerrit-Reviewer: Dr0ptp4kt <[email protected]>
Gerrit-Reviewer: Fjalapeno <[email protected]>
Gerrit-Reviewer: Mhurd <[email protected]>
Gerrit-Reviewer: jenkins-bot <>

_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits

Reply via email to