NSCalendar Additions

翻译自# NSCalendar Additions
日期,一个很普通的时间和它的实现间往往有着巨大的差异,里面隐藏的多方面的复杂性。其中包括亚秒级的精度,重叠单元,不同的地理位置的时区边界,语言和语法上的本地化差异,以及为了夏令时的转换和闰年调整,而在标准时间中添加删除整块的时间等等,里面有太多的东西需要处理。

再开始进行任何日期计算相关的任务前,我们有必要深入了解一下我们手中已有的工具。相比写上千个版本的dateIsTomorrow,我觉得更好的办法是使用Foundation方法。你有在用NSdateComponents吗?你有指定正确的日历单元吗?你的代码在2100年2月28号还能正常工作吗?

但事实上:大家一直都在使用那些已经非常熟悉了的APIs,很少有人注意到,NSCalendar 已经添加了一系列功能十分强大的方法去操作计算日期

let calendar = NSCalendar.currentCalendar()

从全新的日期组件存取与日期比较方法,到强大的日期插值与枚举方法,有太多的东西被我们忽视了。接下来让我们抽点时间来了解一下吧。

便利的日期组件存取

哇, NSDateComponents 真是既实用又灵活,但当我只是想知道间隔的小时数时,它用起来感觉又太麻烦了。不要慌, NSCalendar 来救你了!

let hour = calendar.component(.CalendarUnitHour, fromDate: NSDate())

这样就好多了。NSCalendar,你还有哪些本事?

  • getEra(_:year:month:day:fromDate:):根据传入的日期引用返回纪元,年,月,日。不需要的参数可以传入 nil/NULL。
    -getEra(_:yearForWeekOfYear:weekOfYear:weekday:fromDate:): 根据传入的日期引用返回纪元,年,当年第几周,星期几。不需要的参数可以传入 nil/NULL。
  • getHour(_:minute:second:nanosecond:fromDate:): 根据传入的日期引用返回时间信息,然后 nil/NULL 巴拉巴拉, 你懂的。

NSDateComponents,刚才我是逗你玩呢,我收回前面吐槽你的话。下面还有不少属于你的方法:

  • componentsInTimeZone(_:fromDate:): 根据传入的的日期和时区返回一个 NSDateComponents 实例。
  • components(_:fromDateComponents:toDateComponents:options:): 返回两个 NSDateComponents 实例间的差异。如果有未赋值的组件,该方法会使用默认值,所以我们传入的实例至少得设置了年属性。options参数暂时没有用,传 nil/0 就行。

日期比较

虽然直接比较 NSDate 是件挺简单的事,但一些更有意义的比较可能变得惊人的复杂。两个 NSDate 实例是同一天?同一小时?亦或是同一周?

现在没必要发愁了,NSCalendar 提供了大量的比较方法:

isDateInToday(_:): 如果传入的日期是当天返回 true 。
isDateInTomorrow(_:): 如果传入的日期是明天返回 true 。
isDateInYesterday(_:): 如果传入的日期是昨天返回 true 。
isDateInWeekend(_:): 如果传入的日期是周末返回 true 。
isDate(_:inSameDayAsDate:): 如果两个 NSDate 实例在同一天返回 true - 没必要再去获取日期部件进行比较了。
isDate(_:equalToDate:toUnitGranularity:): 如果传入的日期在同一指定单位内返回 true 。这意味着,两个在同一周的日期实例调用calendar.isDate(tuesday, equalToDate: thursday, toUnitGranularity: .CalendarUnitWeekOfYear) 方法时会返回 true ,就算他们不在同一个月也是如此。
compareDate(_:toDate:toUnitGranularity:): 返回一个 NSComparisonResult,当做和任何指定区间内的日期相等。date(_:matchesComponents:)`: 如果日期匹配指定的部件返回 true 。

日期插值

接下来讲一些根据起始点寻找下一日期的方法。你可以基于一个NSDateComponents实例,一个指定的日期组建,或者特定的时分秒,去找到下一个(或上一个)日期。所有这些方法都需要一个NSCalendarOptions参数去提供更加精细的控制,特别是一开始我们没能找到准确的匹配的时候,痛可以帮我们确定如何选定下一个日期。

NSCalendarOptions

对于所有的方法NSCalendarOptions来说.SearchBackwards,最简单的选择是颠倒每个搜索的方向。向后搜索被构造为返回类似于向前搜索的日期。例如,向后搜索hour11 的前一个日期会给你11点,而不是11点59分,即使11:59在技术上会在“11点之前”向后搜索。事实上,向后搜索是直观的,直到你思考它,然后直到你想得更多,才会是直觉的。这.SearchBackwards是容易的部分应该给你一些前瞻的想法。

接下来的 NSCalendarOptions 选项能够帮助我们处理那些 “消失” 的时间。举个最直观的例子来说,当你进行一个短时窗搜索时碰到夏令时调整,时间提前了一个小时。或者搜索时遇到类似 2 月 或者 4 月 31 号,它都能帮我们跳过这些缺失的时间。

当遇到缺失的时间时,如果我们设置了 NSCalendarOptions.MatchStrictly,相关方法会根据传入的组件寻找一个 精确 的匹配。如果没有设置的话,那么必须提供 .MatchNextTime, .MatchNextTimePreservingSmallerUnits, 和 .MatchPreviousTimePreservingSmallerUnits 中的任一项。这些选项决定了如何处理我们请求时遇到的时间缺失问题。

这种情况,往往一例胜千言:

// 2015 年情人节,早上 9 点
let valentines = cal.dateWithEra(1, year: 2015, month: 2, day: 14, hour: 9, minute: 0, second: 0, nanosecond: 0)!

// 为了找到月的最后一天, 我设置一个日期组件然后把 `day` 设成 31:
let components = NSDateComponents()
components.day = 31

使用精确匹配会在三月找到下个 31 号,如下:

calendar.nextDateAfterDate(valentines, matchingComponents: components, options: .MatchStrictly)
// Mar 31, 2015, 12:00 AM

不使用精确匹配的话,nextDateAfterDate 方法会在找到匹配的指定天数前就在二月底停了下来,然后在下个月继续寻找。 可见,你所提供的选项决定了最终返回的具体日期。举例来说,使用 .MatchNextTime 选项找到下一个合适的日子:

calendar.nextDateAfterDate(valentines, matchingComponents: components, options: .MatchNextTime)
// Mar 1, 2015, 12:00 AM

类似的,当使用 .MatchNextTimePreservingSmallerUnits 选项时会找到下一天,但是所有比指定单元 NSCalendarUnitDay 要小的单元会被保留下来:

calendar.nextDateAfterDate(valentines, matchingComponents: components, options: .MatchNextTimePreservingSmallerUnits)
// Mar 1, 2015, 9:00 AM

最后, 使用 .MatchPreviousTimePreservingSmallerUnits 选项会在 另一个 方向上解决缺失的时间问题, 和前面一样,保留较小的单元,然后找到匹配的前一天:

calendar.nextDateAfterDate(valentines, matchingComponents: components, options: .MatchPreviousTimePreservingSmallerUnits)
// Feb 28, 2015, 9:00 AM

除了这里的 NDateComponents 外,还值得注意的是 nextDateAfterDate 方法有两种变化:

// 匹配指定的日历单元
cal.nextDateAfterDate(valentines, matchingUnit: .CalendarUnitDay, value: 31, options: .MatchStrictly)
// March 31, 2015, 12:00 AM

// 匹配时,分,秒
cal.nextDateAfterDate(valentines, matchingHour: 15, minute: 30, second: 0, options: .MatchNextTime)
// Feb 14, 2015, 3:30 PM

枚举插值日期

NSCalendar 提供了一个API去枚举日期, 所以大家没有必要反复的调用 nextDateAfterDate 方法。enumerateDatesStartingAfterDate(_:matchingComponents:options:usingBlock:) 方法根据提供的日期组件和选项,依次获取匹配的日期。可以将 stop 属性设为 true 去停止枚举。

来试试这个 NSCalendarOptions 的新方法吧,下面展示了一种获取随后50个闰年的方法

let leapYearComponents = NSDateComponents()
leapYearComponents.month = 2
leapYearComponents.day = 29

var dateCount = 0
cal.enumerateDatesStartingAfterDate(NSDate(), matchingComponents: leapYearComponents, options: .MatchStrictly | .SearchBackwards)
{ (date: NSDate!, exactMatch: Bool, stop: UnsafeMutablePointer<ObjCBool>) in
println(date)

if ++dateCount == 50 {
    // .memory 用来获取一个 UnsafeMutablePointer 属性的值
    stop.memory = true
}

}
// 2012-02-29 05:00:00 +0000
// 2008-02-29 05:00:00 +0000
// 2004-02-29 05:00:00 +0000
// 2000-02-29 05:00:00 +0000
// ...

处理周末

要想找周末的话,记住下面两个 NSCalendar 方法就行:

  • nextWeekendStartDate(_:interval:options:afterDate): 根据传入的前两个参数返回下个周末的开始时间和长度。如果当前的地区和日历未提供对周末属性的支持,该方法会返回 false 。唯一相关的属性是 .SearchBackwards。(例子在下面。)
  • rangeOfWeekendStartDate(_:interval:containingDate): 根据传入的前两个参数返回 包含 该日期的周末。如果传入的日期并不在周末或者当前的地区和日历未提供对周末属性的支持,该方法会返回 false 。

本地化的日历符号

就好像所有的新功能还不够,NSCalendar还可以访问一整套正确本地化的日历符号,从而可以快速访问月份名称,一周中的几天等等。每一组符号进一步列举在两个轴上:(1)符号的长度和(2)它作为独立的名词或作为日期的一部分使用。

理解这个第二个属性对于本地化是非常重要的,因为一些语言,尤其是斯拉夫语言,对不同的上下文使用不同的名词用例。例如,日历需要standaloneMonthSymbols为其标题使用其中一种变体,而不是monthSymbols用于格式化特定日期的变体。

为了您的细读,下面NSCalendar是俄文专栏中独立符号的不同值的所有可用符号表

en_US ru_RU
monthSymbols January, February, March… января, февраля, марта…
shortMonthSymbols Jan, Feb, Mar… янв., февр., марта…
veryShortMonthSymbols J, F, M, A… Я, Ф, М, А…
standaloneMonthSymbols January, February, March… Январь, Февраль, Март…
shortStandaloneMonthSymbols Jan, Feb, Mar… Янв., Февр., Март…
veryShortStandaloneMonthSymbols J, F, M, A… Я, Ф, М, А…
weekdaySymbols Sunday, Monday, Tuesday, Wednesday… воскресенье, понедельник, вторник, среда…
shortWeekdaySymbols Sun, Mon, Tue, Wed… вс, пн, вт, ср…
veryShortWeekdaySymbols S, M, T, W… вс, пн, вт, ср…
standaloneWeekdaySymbols Sunday, Monday, Tuesday, Wednesday… Воскресенье, Понедельник, Вторник, Среда…
shortStandaloneWeekdaySymbols Sun, Mon, Tue, Wed… Вс, Пн, Вт, Ср…
veryShortStandaloneWeekdaySymbols S, M, T, W… В, П, В, С…
AMSymbol AM AM
PMSymbol PM PM
quarterSymbols 1st quarter, 2nd quarter, 3rd quarter, 4th quarter 1-й квартал, 2-й квартал, 3-й квартал, 4-й квартал
shortQuarterSymbols Q1, Q2, Q3, Q4 1-й кв., 2-й кв., 3-й кв., 4-й кв.
standaloneQuarterSymbols 1st quarter, 2nd quarter, 3rd quarter, 4th quarter 1-й квартал, 2-й квартал, 3-й квартал, 4-й квартал
shortStandaloneQuarterSymbols Q1, Q2, Q3, Q4 1-й кв., 2-й кв., 3-й кв., 4-й кв.
eraSymbols BC, AD до н. э., н. э.
longEraSymbols Before Christ, Anno Domini до н.э., н.э.

注: 这些符号在 NSDateFormatter 中也可以使用。

小知识

这里给大家介绍一个非常好用的 NSCalendar 扩展集( 点 我 ),有了它我们使用访问日期组件和搜索周末方法时,可以不用把值传进又传出。比如,获取指定的日期组件就变得简单的多:

// built-in
var hour = 0
var minute = 0
calendar.getHour(&hour, minute: &minute, second: nil, nanosecond: nil, fromDate: NSDate())

// Swift化
let (hour, minute, _, _) = calendar.getTimeFromDate(NSDate())

获取下一个周末的日期范围:

// built-in
var startDate: NSDate?
var interval: NSTimeInterval = 0
let success = cal.nextWeekendStartDate(&startDate, interval: &interval, options: nil, afterDate: NSDate())
if success, let startDate = startDate {
    println("start: \(startDate), interval: \(interval)")
}

// Swift化
if let nextWeekend = cal.nextWeekendAfterDate(NSDate()) {
    println("start: \(nextWeekend.startDate), interval: \(nextWeekend.interval)")
}

这下复杂的日历计算吓不到你们了。有了 NSCalendar 提供的这些新功能,你可以很快的解决你碰到的问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,869评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,716评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,223评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,047评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,089评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,839评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,516评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,410评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,920评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,052评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,179评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,868评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,522评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,070评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,186评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,487评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,162评论 2 356

推荐阅读更多精彩内容

  • ######先说下需求:选择日期弹出日历(跟途牛,携程等差不多就行。。。行) 初识NSCalendar到写完日历的...
    只是个少年阅读 1,068评论 0 0
  • 之前一篇文章写的是时间与日期处理的简单模式,这篇文章就算是拓展的模式吧。 上一篇文章的地址是——IOS开发之时间与...
    这位网友阅读 2,224评论 8 1
  • 不该想的不要想,这是什么色彩的人来着?绿色?做一件决定不论大小,思前想后,怕别人的看法,怕这怕那。不能理性地对...
    福尔摩斯是学霸阅读 112评论 0 0
  • 一天天过去了,很多时候都会停留每一个昨天。曾经 一起肩并肩往前的 伙伴们,在那举杯 祝福后都走了、散了,只是记得...
    520小白阅读 342评论 1 1
  • 家庭与生活报 2017.04.25年第B08版:医周刊 月经周期是非常重要的一个时期,在这一过程中,要多加留意我们...
    25dda7dcde39阅读 283评论 0 1