iOS教學 – 日期時間處理(NSDate等物件使用)
取得日期聽來容易,不過實際上使用時很快就發現:哇!怎麼沒有想像的簡單?
實際上依個人經驗,無論什麼程式語言,甚至 MySQL ,日期與時間的處理從來都沒有簡單過,雖然不致於到十分困難,不過若是小看它隨便寫的話,最終一定會遇到問題。
… 話說回來,程式語言的發展也歷經半世紀以上的時光,為何到現在還無法統一日期時間的處理方式呢?新的語言雖然帶來了新的方法,必然有其優點,不過無法完全覆蓋舊的作法,結果也是導致又多了一個時間日期的處理方式罷了。
Github 目錄
本文中的程式碼皆可在範例 project 中找到:
https://github.com/orangedream/DateExampleiOS
NSDate
NSDate 物件單純只是表示一個特定的時間點。進一步的說,NSDate裡記錄的是自 2001/1/1 以來的時間,時區是 GMT+0。
要取得目前的時間十分簡單:
NSDate *currentDate=[NSDate date];
要印出來也還算簡單…:
NSLog(@"currentDate=%@",currentDate);
執行結果(模擬器):
currentDate=2016-02-07 04:18:50 +0000
執行結果(iPhone):
currentDate=2016-02-08 00:17:55 +0000
!?這時間看來不太對,仔細看可以看到最後面有個 +0000,這就是表示這個是以 GMT+0 的時區來列印的。簡單來說,這是美國時間,因為台灣時間是 GMT+8。所以說無法直接使用,要先作轉換。
印出當地時間 Local Time
直接用 NSLog() 列印 NSDate 的結果,會得到美國時間,若要取得當地時間,則要借助 NSDateFormatter 的幫忙作以下的轉換:
NSDate *currentDate=[NSDate date]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateStyle:NSDateFormatterShortStyle]; [formatter setTimeStyle:NSDateFormatterMediumStyle]; NSString *currentDateString = [formatter stringFromDate:currentDate]; NSLog(@"currentDate=%@", currentDateString);
執行結果(模擬器):
currentDate=2/8/16, 8:24:35 AM
執行結果(iPhone):
currentDate=2016/2/8 上午8:24:06
!?為何模擬器與 iPhone 的執行結果會不同?這跟地區( NSDateFormatter.locale )屬性初始值以及時區( NSDateFormatter.timeZone )屬性初始值有關。
用這段 code 來找出 NSDateFormatter.locale 與 NSDateFormatter.timeZone 為何:
NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; NSLog(@"NSDateFormatter.locale=%@",formatter.locale.localeIdentifier); NSLog(@"NSDateFormatter.timeZone=%@",formatter.timeZone);
執行結果(模擬器):
NSDateFormatter.locale=en_US NSDateFormatter.timeZone=Asia/Taipei (GMT+8) offset 28800
執行結果(iPhone):
NSDateFormatter.locale=zh_TW NSDateFormatter.timeZone=Asia/Taipei (GMT+8) offset 28800
印出台灣時間 Local Time
所以,若不管手機的地點為何,要列印出台灣時間的話,就可以這麼作:
NSDate *currentDate=[NSDate date]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateStyle:NSDateFormatterShortStyle]; [formatter setTimeStyle:NSDateFormatterMediumStyle]; [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_Hant_TW"]]; [formatter setTimeZone:[NSTimeZone timeZoneWithName:@"Asia/Taipei"]]; NSString *currentDateString = [formatter stringFromDate:currentDate]; NSLog(@"%@", currentDateString);
執行結果(模擬器):
Taiwan time=2016/2/8 上午8:52:14
執行結果(iPhone):
Taiwan time=2016/2/8 上午8:53:12
註:時區列表可以用 [NSTimeZone knownTimeZoneNames] 來取得,而地區列表可以用 [NSLocale availableLocaleIdentifiers] 來得到。
格式化日期輸出
前面的日期輸出使用了內建的格式:NSDateFormatterShortStyle與NSDateFormatterMediumStyle,如果我們要輸出像「2 月 8 日 (星期一)」這樣的格式,可以這麼作:
// setup date format NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_Hant_TW"]]; [formatter setTimeZone:[NSTimeZone timeZoneWithName:@"Asia/Taipei"]]; [formatter setDateFormat:@"M 月 d 日 (eeee)"]; // Date to string NSDate *now = [NSDate date]; NSString *currentDateString = [formatter stringFromDate:now]; NSLog(@"currentDate=%@", currentDateString);
台灣常見的幾個輸出格式範例:
2016年 2月 9日
YYYY年 M月 d日
2016年 2月 9日 23:09:00
YYYY年 M月 d日 HH:mm:ss
2016/2/9
YYYY/M/d
2016/02/09
YYYY/MM/dd
2/9 21:00
M/d HH:mm
2/9 9:00 下午
M/d h:mm a
註:格式詳列:Date Format Patterns
民國日期
如果我們想列出民國的年曆,像「民國105年 2月 9日」,就會需要轉換日曆(Calendar)物件才能輸出民國年份:
// setup date format NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_Hant_TW"]]; [formatter setTimeZone:[NSTimeZone timeZoneWithName:@"Asia/Taipei"]]; [formatter setDateFormat:@"民國yyy年 M月 d日 (eeee)"]; [formatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierRepublicOfChina]]; // Date to string NSDate *now = [NSDate date]; NSString *currentDateString = [formatter stringFromDate:now]; NSLog(@"currentDate=%@", currentDateString);
農曆
比照前例,我們可以將日期轉換為農曆
// setup date format NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_Hant_TW"]]; [formatter setTimeZone:[NSTimeZone timeZoneWithName:@"Asia/Taipei"]]; [formatter setDateFormat:@"UUU年 M月 d日"]; [formatter setCalendar:[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierChinese]]; // Date to string NSDate *now = [NSDate date]; NSString *currentDateString = [formatter stringFromDate:now]; NSLog(@"currentDate=%@", currentDateString);
輸出(模擬器):
丙申年 1月 2日
由字串轉換為 NSDate
要將串轉換為 NSDate 需要再度借助 NSDateFormatter 的幫助:
NSString *dateString = @"2016/2/9"; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; // Make sure it's matched format! [dateFormatter setDateFormat:@"yyyy/M/d"]; NSDate *date = [[NSDate alloc] init]; date = [dateFormatter dateFromString:dateString]; NSLog(@"date=%@", date);
輸出(模擬器):
date=2016-02-08 16:00:00 +0000
為何輸出不是 2016-02-09 00:00:00 呢?請參考前面,因為 Timezone 的不同,在 GMT+8 的地方的「2016-02-09 00:00:00」會等於 GMT+0的[2016-02-08 16:00:00]
2013/1/30~2016/2/9之間有幾天
要計算二個 NSDate 之間有幾天,就要借助NSDateComponents 與 NSCalendar 來計算。(注意:使用NSDate.timeIntervalSinceDate/86400 秒的作法在大多數情況下可行,然而遇到日光節約時間就會出錯!)
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy/M/d"]; NSDate *date1 = [dateFormatter dateFromString:@"2013/1/30"]; NSDate *date2 = [dateFormatter dateFromString:@"2016/2/9"]; NSDateComponents *components; NSInteger numberOfDays; components = [[NSCalendar currentCalendar] components: NSCalendarUnitDay fromDate: date1 toDate: date2 options: 0]; numberOfDays = [components day]; NSLog(@"Total days=%ld", numberOfDays);
輸出:
Total days=1105
“07:09” ~ “23:12” 之間有幾分鐘
同樣的,我們也可以延伸上例來計算二個時間的間隔:
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"HH:mm"]; NSDate *date1 = [dateFormatter dateFromString:@"07:09"]; NSDate *date2 = [dateFormatter dateFromString:@"23:12"]; NSDateComponents *components; NSInteger numberOfMinutes; components = [[NSCalendar currentCalendar] components: NSCalendarUnitMinute fromDate: date1 toDate: date2 options: 0]; numberOfMinutes = [components minute]; NSLog(@"Total minutes=%ld", numberOfMinutes);
取得今天一開始的時間
借由 NSCalendar.startOfDayForDate 可以取得一天一開始的時間
NSDate *now=[NSDate date]; NSCalendar *cal=[NSCalendar currentCalendar]; NSDate *beginOfToday= [cal startOfDayForDate:now]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy/M/d HH:mm:ss"]; NSLog(@"Begin of today=%@",[dateFormatter stringFromDate:beginOfToday]);
取得一個月一開始的時間
若要取得本月一開始的時間,就要借由 NSDateComponents 來處理,自現在時間只取 年/月/日 三個屬性,然後把日期設為1, 即可達成任務。
NSDate *now=[NSDate date]; NSCalendar *cal=[NSCalendar currentCalendar]; NSDateComponents *comp = [cal components:(NSCalendarUnitYear | NSCalendarUnitMonth | NSCalendarUnitDay) fromDate:now]; [comp setDay:1]; NSDate *firstDayOfMonthDate = [cal dateFromComponents:comp]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy/M/d HH:mm:ss"]; NSLog(@"Begin of this month=%@",[dateFormatter stringFromDate:firstDayOfMonthDate]);
換算自某個日期之後30天後的日期
同樣,要對 NSDate 作計算,NSDateComponents是十分重要的工具,透過NSCalendar.dateByAddingComponents 即可輕易完成任務。
NSDate *now=[NSDate date]; NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; [dateComponents setDay:30]; NSDate *thirtyDaysLatter = [[NSCalendar currentCalendar] dateByAddingComponents:dateComponents toDate:now options:0]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"yyyy/M/d HH:mm:ss"]; NSLog(@"Begin of this month=%@",[dateFormatter stringFromDate:thirtyDaysLatter]);
要注意的是,這裡並沒有重設幾時幾分,所以會變成30天前的同一時間。使用上依需求可能要作些調整。參考前面的例子應該可以組合出你所要的結果。
與 Unix(PHP) timestamp 互換
由於 iOS App 常與後台 API 作搭配,而無論是自行建立的後台伺服器或是使用現成的雲端平台,總免不了要作 timestamp 的處理。而 PHP(Unix)或是 MySQL 的 timestamp
不意外的,與 iOS 的 timestamp 並不相同。幸好,轉換的方式並不困難,因為一方是 1970 開始的 timestamp,而另一方則為 2001 年開始的 timestamp,所以只要這樣轉換即可:
NSDate *now=[NSDate date]; NSTimeInterval timeStamp = [now timeIntervalSince1970]; // NSTimeInterval is defined as double NSNumber *timeStampObj = [NSNumber numberWithDouble: timeStamp]; NSLog(@"Unix timestamp=%ld", [timeStampObj longValue]);
其它有關 NSDate 處理教學
有關 NSDate 的處理線上文章不少,不過多半過於零碎,以下這篇我覺得寫得不錯,值得推薦:
Date Programming