在iOS開發中,經常會遇到各種各樣的時間問題,8小時時差,時間戳,求時間間隔,農曆等等。解決辦法網上比比皆是,但大多零零散散,很多資料並沒有說明其中問題。這裏集中總結一下,以便於以後查閱和供大家參考。有我自己的理解,錯漏之處請大家吐槽。
NSDate的8小時問題
-
NSDate轉字符串時間
初始化一個NSDate時間[NSDate date],獲取的是零時區的時間(格林尼治的時間: 年-月-日 時:分:秒: +時區),而北京時間是東八區時間,因爲時區不同,所以打印的時間相差了8小時。此刻表示的時間是一樣的。
1
2
3
4
5
6
7
|
NSDate *date = [NSDate date]; NSLog(@ "date時間 = %@" , date); NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@ "yyyy-MM-dd HH:mm:ss Z" ]; NSString *dateStr = [formatter stringFromDate:date]; NSLog(@ "字符串時間 = %@" , dateStr); |
打印結果:
1
2
|
2016-12-07 10:44:24.470 timeTest[32743:2995134] date時間 = 2016-12-07 02:44:24 +0000 2016-12-07 10:44:24.471 timeTest[32743:2995134] 字符串時間 = 2016-12-07 10:44:24 +0800 |
打印結果前面的時間是北京時間:2016-12-07 10:44:24.470。而date打印出來的時間顯示少了8小時,因爲它表示的是零時區(+0000)時間02:44:24。此刻對應東八區的北京時間就是10:44:24。只是時區不同,表示的時間點是一樣的。好比1公斤和2斤,重量是一樣的。[NSDate date]獲取的時間單位是零時區(+0000),我們所要的北京時間的單位是東八區(+0800)。
系統會默認[NSDate date]獲取的時間爲零時區時間,而經過NSDateFormatter轉化爲字符串時間就是當前所在時區的準確時間,並沒有8小時誤差。
-
轉字符串時間的時區設定
上文中NSDate時間轉爲字符串時間並沒有設置NSDateFormatter的timeZone。不設置會默認使用當前所在的時區,與設置系統時區formatter.timeZone = [NSTimeZone systemTimeZone]的效果是一樣的。
也可以設置時區,獲取指定時區的字符串時間
1
2
3
4
5
6
|
NSDate *date = [NSDate date]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@ "yyyy-MM-dd HH:mm:ss" ]; formatter.timeZone = [NSTimeZone timeZoneWithName:@ "Asia/Shanghai" ]; //東八區時間 NSString *dateStr = [formatter stringFromDate:date]; NSLog(@ "字符串時間 = %@" , dateStr); |
這時獲取的時間就是東八區時間,哪怕手機拿到零時區的格林尼治,獲取的也是東八區的時間,因爲這裏指定時區了。也有如下時區指定:
1
2
|
formatter.timeZone = [NSTimeZone timeZoneWithName:@ "Asia/Tokyo" ]; //東九區時間 formatter.timeZone = [NSTimeZone timeZoneWithName:@ "GMT" ]; //零區時間 |
通過下面方法可得到系統支持的時區對應的字符串常量:
1
2
3
4
|
NSArray *zones = [NSTimeZone knownTimeZoneNames]; for (NSString *zone in zones) { NSLog(@ "時區名 = %@" , zone); } |
時區對照表
-
字符串時間轉NSDate
字符串時間轉爲NSDate時間也會有時區問題。也會遇到有所謂的8小時誤差,其實就是時區不同。比如下面的例子:
1
2
3
4
|
NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@ "yyyy-MM-dd HH:mm:ss Z" ]; NSDate *newDate = [formatter dateFromString:@ "2016-12-07 14:06:24 +0800" ]; NSLog(@ "newDate = %@" , newDate); |
打印結果:
1
|
2016-12-07 14:12:17.468 timeTest[34279:3155380] newDate = 2016-12-07 06:06:24 +0000 |
NSDateFormatter的指定格式是:@"yyyy-MM-dd HH:mm:ss Z"。這裏面的Z指的是時區。要轉化的字符串時間格式必須和這個格式匹配,上面給定的字符串時間是:@"2016-12-07 14:06:24 +0800",是一個東八區時間,轉化爲NSDate後是零區時間2016-12-07 06:06:24 +0000,字面顯示上少了8小時,其實時間一樣。
其實如果上面給定的字符串時間爲@"2016-12-07 14:06:24 +0000",轉化出來的NSDate時間會完全一樣,因爲字符串時間爲零時區時間,不存在時區誤差。大家可以試一下。
當不指定字符串時間的時區時,即沒有後面的+0800,同時要把NSDateFormatter時間格式裏的Z去掉,保證格式匹配。系統會認爲字符串時間是系統所在時區的時間,轉化爲NSDate時間是零時區時間。
同樣,也可以使用formatter.timeZone = [NSTimeZone timeZoneWithName:@"GMT"];這種方式指定字符串時間的時區,和用Z對應+0000是一樣的。
-
NSDate轉當前時區的NSDate時間
因爲[NSDate date]得出的時間是零時區時間,當我們要獲取當前所在時區的NSDate時間時,通常會用以下方法:
1
2
3
4
5
|
NSDate *date = [NSDate date]; NSTimeZone *zone = [NSTimeZone systemTimeZone]; NSInteger interval = [zone secondsFromGMTForDate:date]; NSDate *localDate = [date dateByAddingTimeInterval:interval]; NSLog(@ "localDate = %@" ,localDate); |
打印結果:
1
|
2016-12-07 14:49:03.777 timeTest[34519:3183548] localDate = 2016-12-07 14:49:03 +0000 |
上面代碼中zone是當前時區,interval是當前時區和零時區時間的差值,最後結果localDate是零時區時間date加上這個差值interval,得到當前時區的NSDate時間。更有甚者,在開發中直接加8*60*60或28800這樣的值,因爲相差8小時嘛。這樣在東八區沒問題,在其他時區時間就錯了。
其實這種做法是不科學的,因爲得到的最終時間還是零時區時間,時間後面明顯是+0000,在使用中一般不顯示時區,所以認爲當做當前時區的時間使用也未嘗不可。此爲大坑!
坑1:這時如果轉爲字符串時間,又會增加8小時。因爲做時間轉換的時候,系統會認爲這個NSDate是零時區,得到的字符串時間是東八區的。
解決辦法是:將錯就錯,字符串時間也設置爲零時區的字符串時間。從深坑跌入更深的坑!
1
2
3
4
5
6
7
8
9
10
|
NSDate *date = [NSDate date]; NSTimeZone *zone = [NSTimeZone systemTimeZone]; NSInteger interval = [zone secondsFromGMTForDate:date]; NSDate *localDate = [date dateByAddingTimeInterval:interval]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@ "yyyy-MM-dd HH:mm:ss" ]; formatter.timeZone = [NSTimeZone timeZoneWithName:@ "UTC" ]; NSString *dateStr = [formatter stringFromDate:localDate]; NSLog(@ "字符串時間 = %@" , dateStr); |
這裏的@"UTC"是指世界標準時間,也是現在用的時間標準,東八區比這個時間也是快8小時,這裏填@"GMT"也是可以的。
坑2:在與後臺交互時,有時需要+0000時區,這時只能手動拼接字符串更改這個時區字段,改爲正確的時區。
所以,在開發中儘量不要這麼做,當時間要求顯示、存儲或與後臺交互的時候,使用字符串時間!不要使用轉化的NSDate。
時間換算,時間戳的概念
-
當前時間轉時間戳
時間戳是指1970年1月1日0時0分0秒到當前時間的秒數。注意:這裏的當前時間是指零時區的NSDate時間。
1
2
3
|
NSDate *date = [NSDate date]; NSTimeInterval timeIn = [date timeIntervalSince1970]; NSLog(@ "時間戳 = %.0f" , timeIn); |
打印結果:
2016-12-07 15:41:04.000 timeTest[34994:3232390] 時間戳 = 1481096464
-
時間戳轉當前時間
1
2
3
4
5
6
7
|
NSDate *date = [NSDate date]; NSTimeInterval timeIn = [date timeIntervalSince1970]; NSDate *newDate = [NSDate dateWithTimeIntervalSince1970:timeIn]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@ "yyyy-MM-dd HH:mm:ss Z" ]; NSString *newTime = [dateFormatter stringFromDate:newDate]; NSLog(@ "初始化時間 = %@,時間戳=%.0f,時間戳轉爲NSDate時間 = %@,轉爲字符串時間 = %@" , date, timeIn, newDate, newTime); |
打印結果:
1
|
2016-12-07 16:11:56.146 timeTest[35186:3253589] 初始化時間 = 2016-12-07 08:11:56 +0000,時間戳=1481098316,時間戳轉爲NSDate時間 = 2016-12-07 08:11:56 +0000,轉爲字符串時間 = 2016-12-07 16:11:56 +0800 |
注意時間戳使用的NSDate時間是當前零時區的時間!當前零時區時間!當前零時區時間!重要的事情說三遍!不要進行NSDate轉當前時區的NSDate時間,再轉時間戳。下面是驗證:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
NSDate *date = [NSDate date]; NSLog(@ "系統零時區NSDate時間 = %@" , date); NSTimeInterval timeIn = [date timeIntervalSince1970]; NSLog(@ "系統零時區NSDate時間轉化爲時間戳 = %.0f" , timeIn); NSTimeZone *zone = [NSTimeZone systemTimeZone]; NSInteger interval = [zone secondsFromGMTForDate:date]; NSDate *localDate = [date dateByAddingTimeInterval:interval]; NSLog(@ "轉化爲本地NSDate時間 = %@" , localDate); NSTimeInterval timeIn2 = [localDate timeIntervalSince1970]; NSLog(@ "本地NSDate時間轉化爲時間戳 = %.0f" , timeIn2); NSDate *detaildate = [NSDate dateWithTimeIntervalSince1970:timeIn]; NSDate *detaildate2 = [NSDate dateWithTimeIntervalSince1970:timeIn2]; NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@ "yyyy-MM-dd HH:mm:ss Z" ]; NSString *newTime = [dateFormatter stringFromDate:detaildate]; NSString *newTime2 = [dateFormatter stringFromDate:detaildate2]; NSLog(@ "最終轉爲字符串時間1 = %@, 時間2 = %@" , newTime, newTime2); |
打印結果:
1
2
3
4
5
|
2016-12-07 16:13:57.834 timeTest[35211:3255842] 系統零時區NSDate時間 = 2016-12-07 08:13:57 +0000 2016-12-07 16:13:57.834 timeTest[35211:3255842] 系統零時區NSDate時間轉化爲時間戳 = 1481098438 2016-12-07 16:13:57.835 timeTest[35211:3255842] 轉化爲本地NSDate時間 = 2016-12-07 16:13:57 +0000 2016-12-07 16:13:57.835 timeTest[35211:3255842] 本地NSDate時間轉化爲時間戳 = 1481127238 2016-12-07 16:13:57.836 timeTest[35211:3255842] 最終轉爲字符串時間1 = 2016-12-07 16:13:57 +0800, 時間2 = 2016-12-08 00:13:57 +0800 |
問題解釋詳見上文的NSDate轉當前時區的NSDate時間。
時間操作與比較
-
時間初始化和比較方法
幾個時間初始化方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//初始化當前時間,返回零時區時間 NSDate *date = [NSDate date]; //以當前時間爲準,正數超前指定秒數,負數延後指定秒數 NSDate *laterDate = [NSDate dateWithTimeIntervalSinceNow:60]; //以2001-01-01 00:00:00 +0000爲基準,正數超前指定秒數,負數延後指定秒數 NSDate *newDate = [NSDate dateWithTimeIntervalSinceReferenceDate:60]; //以1970-01-01 00:00:00 +0000爲基準,正數超前指定秒數,負數延後指定秒數 NSDate *newDate1 = [NSDate dateWithTimeIntervalSince1970:60]; //實例方法,以指定時間爲基準,正數超前指定秒數,負數延後指定秒數 NSDate *newDate2 = [date dateByAddingTimeInterval:60]; //很久以後的某一天 NSDate *newDate3 = [NSDate distantFuture]; //很久以前的某一天 NSDate *newDate4 = [NSDate distantPast]; |
幾個時間比較方法:
1
2
3
4
5
6
7
8
|
//比較兩個時間是否相等 - (BOOL)isEqualToDate:(NSDate *)otherDate; //兩個時間比較,返回較早時間 - (NSDate *)earlierDate:(NSDate *)anotherDate; //兩個時間比較,返回較晚時間 - (NSDate *)laterDate:(NSDate *)anotherDate; //兩個時間比較,返回枚舉類型 - (NSComparisonResult)compare:(NSDate *)other; |
幾個計算時間間隔的方法:
1
2
3
4
5
6
7
8
9
10
|
//返回實例時間與refDate時間間隔秒數 - (NSTimeInterval)timeIntervalSinceDate:(NSDate *)refDate; //返回實例時間與當前時間間隔秒數 - (NSTimeInterval)timeIntervalSinceNow; //返回實例時間的時間戳 - (NSTimeInterval)timeIntervalSince1970; //返回實例時間和2001-01-01 00:00:00 +0000的間隔秒數 - (NSTimeInterval)timeIntervalSinceReferenceDate; //返回當前時間和2001-01-01 00:00:00 +0000的間隔秒數 + (NSTimeInterval)timeIntervalSinceReferenceDate; |
-
獲取年月日時分秒周時區
OC裏的時間坑太多,根本沒辦法像其他語言那樣直接time.year就能獲取年份。要想獲取NSDate的年月日需要使用日曆對象NSCalendar。
1
2
3
4
5
|
NSDate *date = [NSDate date]; NSCalendar *cal = [NSCalendar currentCalendar]; NSDateComponents *dateComps = [cal components:NSCalendarUnitYear|NSCalendarUnitMonth|NSCalendarUnitDay|NSCalendarUnitHour|NSCalendarUnitMinute|NSCalendarUnitSecond|NSCalendarUnitWeekday|NSCalendarUnitWeekOfMonth|NSCalendarUnitWeekOfYear|NSCalendarUnitTimeZone fromDate:date]; NSLog(@ "時間 = %@" , date); NSLog(@ "年=%ld,月=%ld,日=%ld,時=%ld,分=%ld,秒=%ld,周=%ld,本月第%ld周,本年第%ld周,時區=%@" , dateComps.year, dateComps.month, dateComps.day, dateComps.hour, dateComps.minute, dateComps.second, dateComps.weekday, dateComps.weekOfMonth, dateComps.weekOfYear, dateComps.timeZone.name); |
打印結果:
1
2
|
2016-12-07 17:20:41.639 timeTest[35734:3311752] 時間 = 2016-12-07 09:20:41 +0000 2016-12-07 17:20:41.640 timeTest[35734:3311752] 年=2016,月=12,日=7,時=17,分=20,秒=41,周=4,本月第2周,本年第50周,時區=Asia/Shanghai |
NSDateComponents創建方法中添加的枚舉NSCalendarUnit,是後面要獲取的年月日時分秒必須對應添加的。比如要獲取年dateComps.year,就需要添加枚舉NSCalendarUnitYear。
可以看到,[NSDate date]時間可以使用NSCalendar直接獲取當前時區的時分秒,打印的時和時區即可看出。這是[NSCalendar currentCalendar]日曆對象初始化的原因,也可以用[[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian]指定Identifier的方式初始化陽曆日曆。可以試試指定Identifier爲NSCalendarIdentifierChinese,打印的是中國農曆。
dateComps.weekOfMonth是今天屬於本月的第幾周。
dateComps.weekOfYear是今天屬於本年的第幾周。
dateComps.weekday是星期,這個和日常使用有些不同。上述程序打印的是周=4,但2016-12-07是週三。這裏weekday的對應關係是:週日-1,週一-2,週二-3,週三-4,週四-5,週五-6,週六-7。畢竟國外慣例週日是每週的第一天。
農曆
獲取農曆的工具方法,可根據需求添加農曆節日和二十四節氣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
+ (NSString *)LunarForSolarYear:(int)wCurYear Month:(int)wCurMonth Day:(int)wCurDay { //農曆日期名 NSArray *cDayName = [NSArray arrayWithObjects:@ "*" ,@ "初一" ,@ "初二" ,@ "初三" ,@ "初四" ,@ "初五" ,@ "初六" ,@ "初七" ,@ "初八" ,@ "初九" ,@ "初十" ,@ "十一" ,@ "十二" ,@ "十三" ,@ "十四" ,@ "十五" ,@ "十六" ,@ "十七" ,@ "十八" ,@ "十九" ,@ "二十" ,@ "廿一" ,@ "廿二" ,@ "廿三" ,@ "廿四" ,@ "廿五" ,@ "廿六" ,@ "廿七" ,@ "廿八" ,@ "廿九" ,@ "三十" ,nil]; //農曆月份名 NSArray *cMonName = [NSArray arrayWithObjects:@ "*" ,@ "正月" ,@ "二月" ,@ "三月" ,@ "四月" ,@ "五月" ,@ "六月" ,@ "七月" ,@ "八月" ,@ "九月" ,@ "十月" ,@ "冬月" ,@ "臘月" ,nil]; //公曆每月前面的天數 const int wMonthAdd[12] = {0,31,59,90,120,151,181,212,243,273,304,334}; //農曆數據 const int wNongliData[100] = {2635,333387,1701,1748,267701,694,2391,133423,1175,396438 ,3402,3749,331177,1453,694,201326,2350,465197,3221,3402 ,400202,2901,1386,267611,605,2349,137515,2709,464533,1738 ,2901,330421,1242,2651,199255,1323,529706,3733,1706,398762 ,2741,1206,267438,2647,1318,204070,3477,461653,1386,2413 ,330077,1197,2637,268877,3365,531109,2900,2922,398042,2395 ,1179,267415,2635,661067,1701,1748,398772,2742,2391,330031 ,1175,1611,200010,3749,527717,1452,2742,332397,2350,3222 ,268949,3402,3493,133973,1386,464219,605,2349,334123,2709 ,2890,267946,2773,592565,1210,2651,395863,1323,2707,265877}; static int nTheDate,nIsEnd,m,k,n,i,nBit; //計算到初始時間1921年2月8日的天數:1921-2-8(正月初一) nTheDate = (wCurYear - 1921) * 365 + (wCurYear - 1921) / 4 + wCurDay + wMonthAdd[wCurMonth - 1] - 38; if ((!(wCurYear % 4)) && (wCurMonth > 2)) nTheDate = nTheDate + 1; //計算農曆天干、地支、月、日 nIsEnd = 0; m = 0; while (nIsEnd != 1) { if (wNongliData[m] < 4095) k = 11; else k = 12; n = k; while (n>=0) { //獲取wNongliData(m)的第n個二進制位的值 |