好的,这将是一个冗长而复杂的答案(恭喜你!)。
总体上,你的想法是正确的,但有一些微妙的错误存在。
时间概念
现在已经相当明确(希望如此)Date
表示时间的瞬间。但反过来就不那么明确了。一个Date
表示一个时间点,那么什么表示日历值呢?
仔细思考,“天”、“小时”(甚至“月份”)都是一个范围。它们是代表从起始时刻到终止时刻之间所有可能瞬间的值。
因此,在处理日历值时,我发现将其视为范围最有帮助。换句话说,通过问“这一分钟/小时/天/周/月/年的范围是什么?”等问题来思考。
有了这个,你会发现理解正在发生的事情变得更容易了。你已经了解了Range<Int>
,对吧?所以Range<Date>
同样直观。
幸运的是,Calendar
上有获取范围的API。不幸的是,由于Objective-C遗留API的存在,我们不能完全使用Range<Date>
。但我们有NSDateInterval
,它实际上是相同的东西。
你可以通过请求包含特定瞬间的小时/天/周/月/年的范围来请求范围,就像这样:
let now = Date()
let calendar = Calendar.current
let today = calendar.dateInterval(of: .day, for: now)
有了这个,你可以获得几乎任何东西的范围:
let units = [Calendar.Component.second, .minute, .hour, .day, .weekOfMonth, .month, .quarter, .year, .era]
for unit in units {
let range = calendar.dateInterval(of: unit, for: now)
print("Range of \(unit): \(range)")
}
我写这篇文章的时候,它打印出如下内容:
Range of second: Optional(2018-10-04 19:50:10 +0000 to 2018-10-04 19:50:11 +0000)
Range of minute: Optional(2018-10-04 19:50:00 +0000 to 2018-10-04 19:51:00 +0000)
Range of hour: Optional(2018-10-04 19:00:00 +0000 to 2018-10-04 20:00:00 +0000)
Range of day: Optional(2018-10-04 06:00:00 +0000 to 2018-10-05 06:00:00 +0000)
Range of weekOfMonth: Optional(2018-09-30 06:00:00 +0000 to 2018-10-07 06:00:00 +0000)
Range of month: Optional(2018-10-01 06:00:00 +0000 to 2018-11-01 06:00:00 +0000)
Range of quarter: Optional(2018-10-01 06:00:00 +0000 to 2019-01-01 07:00:00 +0000)
Range of year: Optional(2018-01-01 07:00:00 +0000 to 2019-01-01 07:00:00 +0000)
Range of era: Optional(0001-01-03 00:00:00 +0000 to 139369-07-19 02:25:04 +0000)
您可以看到各个单位的范围。这里有两点需要指出:
此方法返回一个可选项 (DateInterval?
),因为可能会有奇怪的情况导致失败。我想不出任何问题,但日历是棘手的东西,所以请注意。
询问时代的范围通常是毫无意义的操作,即使时代对于正确构建日期至关重要。在某些日历之外(主要是日本日历),很难精确描述时代,因此那个值“公元纪年(CE或AD)始于公元1年1月3日”可能是错误的,但我们无法对此做任何事情。当然,我们也不知道当前时代何时结束,所以我们只能假设它永远存在。
打印Date
值
这带我来到下一个点,关于打印日期。我在你的代码中注意到了这一点,我认为这可能是你遇到问题的根源。我赞赏你努力解决夏令时带来的混乱,但我认为你被某些实际上并非错误的事情所困扰了,那就是:
日期
始终以UTC打印。
总是总是总是。
这是因为它们是时间的瞬间,无论您身在加利福尼亚、吉布提还是加德满都,它们都是相同的瞬间。一个日期
值没有时区。它实际上只是另一个已知时间点的偏移量。但是,将560375786.836208
打印为日期
的描述对人类来说似乎并不那么有用,因此API的创建者使其打印为格里高利日历相对于UTC时区的日期。
如果您想打印日期,即使是为了调试,您几乎肯定需要一个DateFormatter
。
DateComponents.date
这不是您代码的问题,而是提醒。 DateComponents
上的.date
属性不能保证返回与指定组件匹配的第一个瞬间。它也不能保证返回与指定组件匹配的最后一个瞬间。它所做的只是保证,如果可以找到答案,它将为您提供一个位于指定单位范围内的日期
。
我不认为您在技术上依赖此功能,但了解这一点是很好的,我曾经看到这种误解导致软件中出现错误。
考虑到所有这些问题,让我们来看看您想要做什么:
- 您想知道该年份的范围
- 您想知道该年份的所有月份
- 您想知道一个月的所有日期
- 您想知道包含该月第一天的星期
因此,根据我们对范围的了解,现在应该相当简单,有趣的是,我们可以在不需要单个DateComponents
值的情况下完成所有工作。在下面的代码中,我将强制展开值以简
let yearRange = calendar.dateInterval(of: .year, for: now)!
一年中的月份
这有点复杂,但不是太难。
var monthsOfYear = Array<DateInterval>()
var startOfCurrentMonth = yearRange.start
repeat {
let monthRange = calendar.dateInterval(of: .month, for: startOfCurrentMonth)!
monthsOfYear.append(monthRange)
startOfCurrentMonth = monthRange.end.addingTimeInterval(1)
} while yearRange.contains(startOfCurrentMonth)
基本上,我们将从一年的第一个瞬间开始,并向日历询问包含该瞬间的月份。一旦我们得到了那个月份,我们将获取该月份的最后一个瞬间,并在其基础上加一秒钟,以便(表面上)将其转移到下个月份。然后,我们将获得包含该瞬间的月份的范围,以此类推。重复此过程,直到我们获得一个不再是今年的值,这意味着我们已经汇总了初始年份中所有月份的范围。
当我在我的计算机上运行时,我得到了这个结果:
[
2018-01-01 07:00:00 +0000 to 2018-02-01 07:00:00 +0000,
2018-02-01 07:00:00 +0000 to 2018-03-01 07:00:00 +0000,
2018-03-01 07:00:00 +0000 to 2018-04-01 06:00:00 +0000,
2018-04-01 06:00:00 +0000 to 2018-05-01 06:00:00 +0000,
2018-05-01 06:00:00 +0000 to 2018-06-01 06:00:00 +0000,
2018-06-01 06:00:00 +0000 to 2018-07-01 06:00:00 +0000,
2018-07-01 06:00:00 +0000 to 2018-08-01 06:00:00 +0000,
2018-08-01 06:00:00 +0000 to 2018-09-01 06:00:00 +0000,
2018-09-01 06:00:00 +0000 to 2018-10-01 06:00:00 +0000,
2018-10-01 06:00:00 +0000 to 2018-11-01 06:00:00 +0000,
2018-11-01 06:00:00 +0000 to 2018-12-01 07:00:00 +0000,
2018-12-01 07:00:00 +0000 to 2019-01-01 07:00:00 +0000
]
需要再次指出的是,这些日期是以协调世界时(UTC)为准,尽管我位于山区时区。因此,在07:00和06:00之间小时会发生变化,因为山区时区与UTC相差7或6个小时,具体取决于我们是否观察夏令时。因此,这些数值对于我的日历时区是准确的。
一个月中的日期
这与先前的代码类似:
var daysInMonth = Array<DateInterval>()
var startOfCurrentDay = currentMonth.start
repeat {
let dayRange = calendar.dateInterval(of: .day, for: startOfCurrentDay)!
daysInMonth.append(dayRange)
startOfCurrentDay = dayRange.end.addingTimeInterval(1)
} while currentMonth.contains(startOfCurrentDay)
当我运行这段代码时,我得到了如下结果:
[
2018-10-01 06:00:00 +0000 to 2018-10-02 06:00:00 +0000,
2018-10-02 06:00:00 +0000 to 2018-10-03 06:00:00 +0000,
2018-10-03 06:00:00 +0000 to 2018-10-04 06:00:00 +0000,
... snipped for brevity ...
2018-10-29 06:00:00 +0000 to 2018-10-30 06:00:00 +0000,
2018-10-30 06:00:00 +0000 to 2018-10-31 06:00:00 +0000,
2018-10-31 06:00:00 +0000 to 2018-11-01 06:00:00 +0000
]
包含一个月初的星期
让我们回到之前创建的 monthsOfYear
数组。我们将使用它来确定每个月的星期:
for month in monthsOfYear {
let weekContainingStart = calendar.dateInterval(of: .weekOfMonth, for: month.start)!
print(weekContainingStart)
}
对于我的时区,这将打印:
2017-12-31 07:00:00 +0000 to 2018-01-07 07:00:00 +0000
2018-01-28 07:00:00 +0000 to 2018-02-04 07:00:00 +0000
2018-02-25 07:00:00 +0000 to 2018-03-04 07:00:00 +0000
2018-04-01 06:00:00 +0000 to 2018-04-08 06:00:00 +0000
2018-04-29 06:00:00 +0000 to 2018-05-06 06:00:00 +0000
2018-05-27 06:00:00 +0000 to 2018-06-03 06:00:00 +0000
2018-07-01 06:00:00 +0000 to 2018-07-08 06:00:00 +0000
2018-07-29 06:00:00 +0000 to 2018-08-05 06:00:00 +0000
2018-08-26 06:00:00 +0000 to 2018-09-02 06:00:00 +0000
2018-09-30 06:00:00 +0000 to 2018-10-07 06:00:00 +0000
2018-10-28 06:00:00 +0000 to 2018-11-04 06:00:00 +0000
2018-11-25 07:00:00 +0000 to 2018-12-02 07:00:00 +0000
你会发现这里的一件事是,这里将周的第一天设置为
星期日。例如,在你的截图中,2月1日所在的那一周从29日开始。
这种行为由
Calendar
上的
firstWeekday
属性控制。默认情况下,该值可能为
1
(根据您的区域设置而定),表示星期从星期日开始。如果你想让你的日历进行计算,其中一周从
星期一开始,那么你需要将该属性的值更改为
2
:
var weeksStartOnMondayCalendar = calendar
weeksStartOnMondayCalendar.firstWeekday = 2
for month in monthsOfYear {
let weekContainingStart = weeksStartOnMondayCalendar.dateInterval(of: .weekOfMonth, for: month.start)!
print(weekContainingStart)
}
现在当我运行该代码时,我看到包含2月1日的那一周“开始”于1月29日:
2018-01-01 07:00:00 +0000 to 2018-01-08 07:00:00 +0000
2018-01-29 07:00:00 +0000 to 2018-02-05 07:00:00 +0000
2018-02-26 07:00:00 +0000 to 2018-03-05 07:00:00 +0000
2018-03-26 06:00:00 +0000 to 2018-04-02 06:00:00 +0000
2018-04-30 06:00:00 +0000 to 2018-05-07 06:00:00 +0000
2018-05-28 06:00:00 +0000 to 2018-06-04 06:00:00 +0000
2018-06-25 06:00:00 +0000 to 2018-07-02 06:00:00 +0000
2018-07-30 06:00:00 +0000 to 2018-08-06 06:00:00 +0000
2018-08-27 06:00:00 +0000 to 2018-09-03 06:00:00 +0000
2018-10-01 06:00:00 +0000 to 2018-10-08 06:00:00 +0000
2018-10-29 06:00:00 +0000 to 2018-11-05 07:00:00 +0000
2018-11-26 07:00:00 +0000 to 2018-12-03 07:00:00 +0000
最后一点小建议
通过这篇文章,你已经拥有了构建类似Calendar.app界面所需的所有工具。
另外值得一提的是,我在这里发布的所有代码都可以 适用于任何日历、时区和语言环境。它可以用来构建日本日历、科普特日历、希伯来日历、ISO8601等各种类型的UI。而且它会适用于任何时区和语言环境。
此外,拥有所有这些 DateInterval
值很可能会使你的应用程序实现更加容易,因为当制作一个日历应用程序时,进行“范围包含”检查是你想要进行的检查之一。
唯一需要注意的是,在将这些值渲染成 String
时,请使用 DateFormatter
。请勿分离单个日期组件。
如果您有后续问题,请将其作为新问题发布,并在评论中@mention
我。
25/12/18 at 00:00 UTC
->26/12/18 00:00 UTC
,并且我想要在纽约用户在午夜之前查看日历时突出显示当前日期,那么将会突出显示错误的日期,因为他们仍然处于24/12/18,除非我进行某种转换。我认为在继续之前,我需要理解日历和时区的工作原理。 - Adam Waite