Apa cara terbaik untuk berurusan dengan "feechur" lokal NSDateFormatter?

168

Tampaknya NSDateFormattermemiliki "fitur" yang menggigit Anda secara tak terduga: Jika Anda melakukan operasi format "tetap" sederhana seperti:

NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyyMMddHHmmss"];
NSString* dateStr = [fmt stringFromDate:someDate];
[fmt release];

Kemudian berfungsi dengan baik di AS dan sebagian besar lokal SAMPAI ... seseorang dengan telepon mereka diatur ke wilayah 24 jam, menetapkan sakelar 12/24 jam di pengaturan menjadi 12. Kemudian yang di atas mulai menempelkan "AM" atau "PM" ke akhir string yang dihasilkan.

(Lihat, misalnya, NSDateFormatter, apakah saya melakukan sesuatu yang salah atau ini bug? )

(Dan lihat https://developer.apple.com/library/content/qa/qa1480/_index.html )

Rupanya Apple telah menyatakan ini sebagai "BURUK" - Rusak Seperti Dirancang, dan mereka tidak akan memperbaikinya.

Pengelakan ini tampaknya untuk mengatur lokal formatter tanggal untuk wilayah tertentu, umumnya AS, tetapi ini agak berantakan:

NSLocale *loc = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
[df setLocale: loc];
[loc release];

Tidak terlalu buruk dalam onsies-twosies, tapi saya berurusan dengan sekitar sepuluh aplikasi yang berbeda, dan yang pertama saya lihat memiliki 43 contoh skenario ini.

Jadi ada ide cerdas untuk kelas makro / yang ditimpa / apa pun untuk meminimalkan upaya untuk mengubah segalanya, tanpa membuat kode untuk mengaburkan? (Insting pertama saya adalah menimpa NSDateFormatter dengan versi yang akan mengatur lokal dalam metode init. Memerlukan perubahan dua baris - baris alokasi / init dan impor tambahan.)

Ditambahkan

Inilah yang saya buat sejauh ini - tampaknya bekerja di semua skenario:

@implementation BNSDateFormatter

-(id)init {
static NSLocale* en_US_POSIX = nil;
NSDateFormatter* me = [super init];
if (en_US_POSIX == nil) {
    en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
[me setLocale:en_US_POSIX];
return me;
}

@end

Karunia!

Saya akan memberikan hadiah untuk saran / kritik terbaik (sah) yang saya lihat pada pertengahan hari Selasa. [Lihat di bawah - batas waktu diperpanjang.]

Memperbarui

Proposal Re OMZ, inilah yang saya temukan -

Ini adalah versi kategori - file h:

#import <Foundation/Foundation.h>


@interface NSDateFormatter (Locale)
- (id)initWithSafeLocale;
@end

Kategori file m:

#import "NSDateFormatter+Locale.h"


@implementation NSDateFormatter (Locale)

- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX = nil;
self = [super init];
if (en_US_POSIX == nil) {
    en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX.description, [en_US_POSIX localeIdentifier]);
[self setLocale:en_US_POSIX];
return self;    
}

@end

Kode:

NSDateFormatter* fmt;
NSString* dateString;
NSDate* date1;
NSDate* date2;
NSDate* date3;
NSDate* date4;

fmt = [[NSDateFormatter alloc] initWithSafeLocale];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];  
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"]; 
NSLog(@"date4 = %@", date4.description);
[fmt release];

fmt = [[BNSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];  
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"]; 
NSLog(@"date4 = %@", date4.description);
[fmt release];

Hasil:

2011-07-11 17:44:43.243 DemoApp[160:307] Category's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.257 DemoApp[160:307] dateString = 2011-07-11 05:44:43 PM
2011-07-11 17:44:43.264 DemoApp[160:307] date1 = (null)
2011-07-11 17:44:43.272 DemoApp[160:307] date2 = (null)
2011-07-11 17:44:43.280 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.298 DemoApp[160:307] date4 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.311 DemoApp[160:307] Extended class's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.336 DemoApp[160:307] dateString = 2011-07-11 17:44:43
2011-07-11 17:44:43.352 DemoApp[160:307] date1 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.369 DemoApp[160:307] date2 = 2001-05-06 03:34:56 AM +0000
2011-07-11 17:44:43.380 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.392 DemoApp[160:307] date4 = (null)

Telepon [menjadikan iPod Touch] diatur ke Britania Raya, dengan sakelar 12/24 diatur ke 12. Ada perbedaan yang jelas dalam dua hasil, dan saya menilai versi kategorinya salah. Perhatikan bahwa log dalam versi kategori sedang dieksekusi (dan berhenti ditempatkan dalam kode dipukul), jadi itu bukan hanya kasus kode yang entah bagaimana tidak digunakan.

Pembaruan hadiah:

Karena saya belum mendapatkan balasan yang berlaku, namun saya akan memperpanjang batas waktu hadiah untuk satu atau dua hari lagi.

Bounty berakhir dalam 21 jam - itu akan jatuh ke siapa pun yang paling berusaha untuk membantu, bahkan jika jawabannya tidak benar-benar berguna dalam kasus saya.

Pengamatan yang aneh

Mengubah penerapan kategori sedikit:

#import "NSDateFormatter+Locale.h"

@implementation NSDateFormatter (Locale)

- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX2 = nil;
self = [super init];
if (en_US_POSIX2 == nil) {
    en_US_POSIX2 = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX2.description, [en_US_POSIX2 localeIdentifier]);
[self setLocale:en_US_POSIX2];
NSLog(@"Category's object: %@ and object's locale: %@ %@", self.description, self.locale.description, [self.locale localeIdentifier]);
return self;    
}

@end

Pada dasarnya hanya mengubah nama variabel lokal statis (kalau-kalau ada beberapa konflik dengan statis dinyatakan dalam subkelas) dan menambahkan NSLog tambahan. Tapi lihat apa yang dicetak NSLog:

2011-07-15 16:35:24.322 DemoApp[214:307] Category's locale: <__NSCFLocale: 0x160550> en_US_POSIX
2011-07-15 16:35:24.338 DemoApp[214:307] Category's object: <NSDateFormatter: 0x160d90> and object's locale: <__NSCFLocale: 0x12be70> en_GB
2011-07-15 16:35:24.345 DemoApp[214:307] dateString = 2011-07-15 04:35:24 PM
2011-07-15 16:35:24.370 DemoApp[214:307] date1 = (null)
2011-07-15 16:35:24.378 DemoApp[214:307] date2 = (null)
2011-07-15 16:35:24.390 DemoApp[214:307] date3 = (null)
2011-07-15 16:35:24.404 DemoApp[214:307] date4 = 2001-05-05 05:34:56 PM +0000

Seperti yang Anda lihat, setLocale tidak. Lokal formatter masih en_GB. Tampaknya ada sesuatu yang "aneh" tentang metode init dalam suatu kategori.

Jawaban akhir

Lihat jawaban yang diterima di bawah ini.

Hot Licks
sumber
5
Moshe, saya tidak tahu mengapa Anda memilih untuk mengedit judul. "Feechur" adalah istilah yang sah dalam bidang ini (dan telah berlangsung selama 30 tahun atau lebih), yang berarti aspek atau fitur dari beberapa perangkat lunak yang dianggap kurang baik untuk dianggap sebagai bug, meskipun penulis menolak untuk mengakuinya.
Hot Licks
1
saat mengonversi string menjadi tanggal, string tersebut harus sama persis dengan deskripsi formatter - ini adalah masalah tangensial dengan lokalitas Anda.
bshirley
Berbagai string tanggal ada untuk menguji berbagai konfigurasi yang mungkin, benar dan salah. Saya tahu bahwa beberapa di antaranya tidak valid, mengingat string pemformatan.
Hot Licks
sudahkah Anda bereksperimen dengan nilai yang berbeda - (NSDateFormatterBehavior)formatterBehavior?
bshirley
Belum pernah bereksperimen dengannya. Spesifikasi ini bertentangan dengan apakah spesifikasi itu bahkan dapat diubah di iOS. Deskripsi utama mengatakan "iOS Note: iOS hanya mendukung perilaku 10.4+", sedangkan bagian NSDateFormatterBehavior mengatakan kedua mode tersedia (tetapi mungkin hanya berbicara tentang konstanta).
Hot Licks

Jawaban:

67

Duh !!

Terkadang Anda memiliki "Aha !!" sesaat, kadang-kadang lebih dari "Duh !!" Ini yang terakhir. Dalam kategori untuk initWithSafeLocale"super" initdiberi kode sebagai self = [super init];. Ini inits superclass dari NSDateFormattertetapi tidak inityangNSDateFormatter obyek itu sendiri.

Rupanya ketika inisialisasi ini dilewati, setLocale"memantul", mungkin karena beberapa struktur data yang hilang di objek. Mengubah initke self = [self init];menyebabkan NSDateFormatterinisialisasi terjadi, dansetLocale bahagia lagi.

Berikut adalah sumber "final" untuk .m kategori:

#import "NSDateFormatter+Locale.h"

@implementation NSDateFormatter (Locale)

- (id)initWithSafeLocale {
    static NSLocale* en_US_POSIX = nil;
    self = [self init];
    if (en_US_POSIX == nil) {
        en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
    }
    [self setLocale:en_US_POSIX];
    return self;    
}

@end
Hot Licks
sumber
apa yang akan menjadi formatter tanggal untuk "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Agen Chocks.
@ Aggent - Carilah: unicode.org/reports/tr35/tr35-31/…
Hot Licks
@tbag - Bukankah seharusnya pertanyaan Anda tentang NSDateFormatter?
Hot Licks
@ HotLicks ya saya yang buruk. Saya daging NSDateFormatter.
tbag
@tbag - Apa yang dikatakan oleh spec?
Hot Licks
41

Alih-alih subklasifikasi, Anda bisa membuat NSDateFormatterkategori dengan penginisialisasi tambahan yang mengatur penempatan lokal dan mungkin juga format string, sehingga Anda akan memiliki formatter yang siap digunakan segera setelah menginisialisasi.

@interface NSDateFormatter (LocaleAdditions)

- (id)initWithPOSIXLocaleAndFormat:(NSString *)formatString;

@end

@implementation NSDateFormatter (LocaleAdditions)

- (id)initWithPOSIXLocaleAndFormat:(NSString *)formatString {
    self = [super init];
    if (self) {
        NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
        [self setLocale:locale];
        [locale release];
        [self setFormat:formatString];
    }
    return self;
}

@end

Maka Anda dapat menggunakan NSDateFormatterdi mana saja dalam kode Anda hanya dengan:

NSDateFormatter* fmt = [[NSDateFormatter alloc] initWithPOSIXLocaleAndFormat:@"yyyyMMddHHmmss"];

Anda mungkin ingin membuat awalan metode kategori Anda entah bagaimana untuk menghindari konflik nama, kalau-kalau Apple memutuskan untuk menambahkan metode seperti itu di versi OS masa depan.

Jika Anda selalu menggunakan format tanggal yang sama, Anda juga bisa menambahkan metode kategori yang mengembalikan instance tunggal dengan konfigurasi tertentu (seperti +sharedRFC3339DateFormatter). Berhati-hatilah karena itu NSDateFormatterbukan thread-safe dan Anda harus menggunakan kunci atau @synchronizedblok saat Anda menggunakan instance yang sama dari banyak utas.

omz
sumber
Apakah memiliki NSLocale statis (seperti dalam saran saya) berfungsi dalam suatu kategori?
Hot Licks
Ya, itu juga harus bekerja dalam suatu kategori. Saya meninggalkannya untuk membuat contoh lebih sederhana.
omz
Anehnya, pendekatan kategori tidak berhasil. Metode kategori dieksekusi, dan mendapatkan Lokal yang persis sama dengan versi lainnya (saya mengeksekusi mereka kembali ke belakang, versi kategori pertama). Entah bagaimana setLocale ternyata tidak "mengambil".
Hot Licks
Akan menarik untuk mengetahui mengapa pendekatan ini tidak berhasil. Jika tidak ada yang menemukan sesuatu yang lebih baik, saya akan memberikan hadiah untuk penjelasan terbaik dari bug ini.
Hot Licks
Yah, saya memberikan hadiah kepada OMZ, karena dia adalah satu-satunya yang melakukan upaya nyata dalam hal ini.
Hot Licks
7

Bolehkah saya menyarankan sesuatu yang sama sekali berbeda karena jujur ​​semua ini agak berlubang.

Anda harus menggunakan satu NSDateFormatterdengan dateFormatset dan localedipaksa en_US_POSIXuntuk menerima tanggal (dari server / API).

Maka Anda harus menggunakan yang berbeda NSDateFormatteruntuk UI yang akan Anda atur timeStyle/ dateStyleproperti - dengan cara ini Anda tidak memiliki dateFormatset yang eksplisit sendiri, dengan demikian secara salah mengasumsikan bahwa format akan digunakan.

Ini berarti UI didorong oleh preferensi pengguna (am / pm vs 24 jam, dan string tanggal diformat dengan benar dengan pilihan pengguna - dari pengaturan iOS), sedangkan tanggal yang "datang ke" aplikasi Anda selalu menjadi "diurai" benar untuk sebuah NSDateuntuk Anda gunakan.

Daniel
sumber
Terkadang skema ini berhasil, kadang tidak. Salah satu bahaya adalah bahwa metode Anda mungkin perlu mengubah format tanggal formatter dan, dalam melakukannya, mengubah format yang ditetapkan oleh kode yang memanggil Anda, ketika itu di tengah-tengah operasi format tanggal. Ada skenario lain di mana zona waktu harus diubah berulang kali.
Hot Licks
Saya tidak tahu mengapa mengubah timeZonenilai formatter akan menghalangi skema ini, bisakah Anda menguraikannya? Juga harus jelas Anda akan abstain dari mengubah format. Jika Anda perlu melakukannya maka ini akan terjadi pada formatter "impor", jadi formatter terpisah.
Daniel
Setiap kali Anda mengubah keadaan objek global, itu berbahaya. Mudah lupa bahwa orang lain juga menggunakannya.
Hot Licks
3

Berikut adalah solusi untuk masalah itu dalam versi cepat. Dengan cepat kita dapat menggunakan ekstensi alih-alih kategori. Jadi, Di sini saya telah membuat ekstensi untuk DateFormatter dan di dalamnya initWithSafeLocale mengembalikan DateFormatter dengan Lokal yang relevan, Di sini dalam kasus kami yaitu en_US_POSIX, Selain itu juga disediakan beberapa metode pembentukan tanggal.

  • Cepat 4

    extension DateFormatter {
    
    private static var dateFormatter = DateFormatter()
    
    class func initWithSafeLocale(withDateFormat dateFormat: String? = nil) -> DateFormatter {
    
        dateFormatter = DateFormatter()
    
        var en_US_POSIX: Locale? = nil;
    
        if (en_US_POSIX == nil) {
            en_US_POSIX = Locale.init(identifier: "en_US_POSIX")
        }
        dateFormatter.locale = en_US_POSIX
    
        if dateFormat != nil, let format = dateFormat {
            dateFormatter.dateFormat = format
        }else{
            dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
        }
        return dateFormatter
    }
    
    // ------------------------------------------------------------------------------------------
    
    class func getDateFromString(string: String, fromFormat dateFormat: String? = nil) -> Date? {
    
        if dateFormat != nil, let format = dateFormat {
            dateFormatter = DateFormatter.initWithSafeLocale(withDateFormat: format)
        }else{
            dateFormatter = DateFormatter.initWithSafeLocale()
        }
        guard let date = dateFormatter.date(from: string) else {
            return nil
        }
        return date
    }
    
    // ------------------------------------------------------------------------------------------
    
    class func getStringFromDate(date: Date, fromDateFormat dateFormat: String? = nil)-> String {
    
        if dateFormat != nil, let format = dateFormat {
            dateFormatter = DateFormatter.initWithSafeLocale(withDateFormat: format)
        }else{
            dateFormatter = DateFormatter.initWithSafeLocale()
        }
    
        let string = dateFormatter.string(from: date)
    
        return string
    }   }
  • deskripsi penggunaan:

    let date = DateFormatter.getDateFromString(string: "11-07-2001”, fromFormat: "dd-MM-yyyy")
    print("custom date : \(date)")
    let dateFormatter = DateFormatter.initWithSafeLocale(withDateFormat: "yyyy-MM-dd HH:mm:ss")
    let dt = DateFormatter.getDateFromString(string: "2001-05-05 12:34:56")
    print("base date = \(dt)")
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    let dateString = dateFormatter.string(from: Date())
    print("dateString = " + dateString)
    let date1 = dateFormatter.date(from: "2001-05-05 12:34:56")
    print("date1 = \(String(describing: date1))")
    let date2 = dateFormatter.date(from: "2001-05-05 22:34:56")
    print("date2 = \(String(describing: date2))")
    let date3 = dateFormatter.date(from: "2001-05-05 12:34:56PM")
    print("date3 = \(String(describing: date3))")
    let date4 = dateFormatter.date(from: "2001-05-05 12:34:56 PM")
    print("date4 = \(String(describing: date4))")
Tech
sumber