Swift 中的 DateComponents 返回奇怪的值。为啥?

Posted

技术标签:

【中文标题】Swift 中的 DateComponents 返回奇怪的值。为啥?【英文标题】:DateComponents in Swift returns weird values. Why?Swift 中的 DateComponents 返回奇怪的值。为什么? 【发布时间】:2016-10-29 16:18:57 【问题描述】:

我正在计算两个日期之间的时间差。我有一段代码:

func timeAgoSinceDate(_ date:Date, numericDates:Bool) -> String 
    let calendar = Calendar.current
    let now = Date()
    let components:DateComponents = (calendar as NSCalendar).components([NSCalendar.Unit.minute , NSCalendar.Unit.hour , NSCalendar.Unit.day , NSCalendar.Unit.weekOfYear , NSCalendar.Unit.month , NSCalendar.Unit.year , NSCalendar.Unit.second], from: now, to: date, options: NSCalendar.Options())
    print(now)
    print(date)
    print(components.month)

这些是我在控制台中看到的一些示例:

2016-10-29 16:12:34 +0000
2017-03-29 16:03:13 +0000
Optional(4) //I think it should return 5?


2016-10-29 16:12:34 +0000
2017-01-29 17:03:13 +0000
Optional(2) //again, why 2 instead of 3?

2016-10-29 16:12:34 +0000
2015-09-30 15:54:20 +0000
Optional(0) //why 0 since it's almost whole year?


2016-10-29 16:12:34 +0000
2016-05-29 14:09:31 +0000
Optional(-5) //this seems to be fine

等等...这里有什么问题?

我遇到的问题是日期计算。在我正在做的代码中:

if (components.month! >= 2) 
        return "in \(components.month!) months"
     else if (components.month! >= 1)
        if (numericDates)
            return "in 1 month"
         else 
            return "in one month"
        
    

    else if (components.month! <= -2) 
        return "\(components.month!) months ago"
     else if (components.month! <= -1)
        if (numericDates)
            return "1 month ago"
         else 
            return "one month ago"
        
    

然后,由于我的计算,当前当一个日期是2016-10-29 16:32:54 +0000,另一个是2017-01-29 17:03:13 +0000,用户看到in 2 months。那是因为整个print(components) 返回:

year: 0 month: 2 day: 2 hour: 23 minute: 30 second: 18 weekOfYear: 4 isLeapMonth: false 

但在现实生活中它应该返回 3,因为它是 10 月和 1 月之间的差异。有没有比一堆if 将所有内容四舍五入的语句更好的方法来计算这种差异?

【问题讨论】:

你在哪个时区?日期以 UTC/GMT 打印,但日历计算在您的本地日历/时区完成。 在您的第一个示例中,给定日期之间 小于 5 个月,因此结果 4 是正确的。 hm 但我认为在months 的情况下时区不应该影响计算...无论如何,我在GMT +1,有什么方法可以独立于时区进行计算? 您可以将日历时区设置为 GMT,但即便如此,在第一个示例中您会得到 4 而不是 5,在第三个示例中会得到 0,因为差异是 less 一年多。试试print(components) 看看实际的区别。 几乎在编程方面是不是;-) 【参考方案1】:
2016-10-29 16:12:34 +0000
2017-03-29 16:03:13 +0000
Optional(4) //I think it should return 5?

由于 16:03 是

2016-10-29 16:12:34 +0000
2017-01-29 17:03:13 +0000
Optional(2) //again, why 2 instead of 3?

夏令时是 2016 年 11 月 6 日。您损失了一个小时。我很确定这就是为什么它认为只有 2 个月的时间。

增加一个小时:

2016-10-29 16:12:34 +0000
2017-01-29 18:03:13 +0000
Optional(3)

你得到'3'

更改月份但保持原来的时间:

2016-7-29 16:12:34 +0000
2016-10-29 17:03:13 +0000
Optional(3)

你也会得到'3'

2016-10-29 16:12:34 +0000
2015-09-30 15:54:20 +0000
Optional(0) //why 0 since it's almost whole year?

日期组件返回您在组件定义中包含的最高单位的差值。在您的情况下,您已包含NSCalendar.Unit.year。因此components.month 将返回月份单位,而不是月份的总差值。所以这个计算中的月份单位是0个月。如果要在日期上加上 25 年零 5 个月,它将返回“5”。但是,如果您要删除 NSCalendar.Unit.year,那么它将使用月份作为最高单位并返回 '305'

你可以像这样测试一些日期:

let startString = "2016-7-30 16:12:34 +0000"
let endString = "2016-10-30 17:03:13 +0000"

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss Z"
let now = formatter.date(from: startString)!
let date = formatter.date(from: endString)!

【讨论】:

【参考方案2】:

从 2016-10-29 16:12:34 到 2017-03-29 16:03:13 有 4 个月 28 天 23 小时 50 分 39 秒 这就是为什么 components.month 在您的第一个示例中计算为 4 的原因。

如果您打算计算与 2016 年 10 月的差值(以月为单位) 到 2017 年 3 月,那么您必须计算两者之间的差异 月初两个日期:

let startOfFirstMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: firstDate))!
let startOfSecondMonth = calendar.date(from: calendar.dateComponents([.year, .month], from: secondDate))!

let comps = calendar.dateComponents([.month], from: startOfFirstMonth, to: startOfSecondMonth)
print(comps.month)

【讨论】:

我不同意第一个陈述,因为如果您在不使用 HH:mm:ss 的情况下评估“从 2016-10-29 到 2017-03-29”,那么它实际上评估为 5 个月。但是 OP 的示例包含的确切时间使其接近 5 个月。 (不是我的反对票) @Frankie:你说得对,缩写太多了,我现在已经修好了,谢谢你的反馈! – 然而,建议的解决方案应该适用于所有问题的情况(我从更新中了解到)。【参考方案3】:

此答案旨在为 Frankie 的答案提供额外信息。

func calendarTest(_ from: String, _ to: String) 
    var calendar = Calendar.current
    calendar.timeZone = TimeZone(identifier: "Europe/Paris")! //<- GMT+1
    let inFormatter = DateFormatter()
    inFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    inFormatter.timeZone = TimeZone(abbreviation: "UTC")!
    guard
        let now = inFormatter.date(from: from),
        let date = inFormatter.date(from: to)
    else return
    //Removing "weeks" for clarity
    let components = (calendar as NSCalendar).components([.minute, .hour, .day/*, .weekOfYear*/, .month, .year, .second], from: now, to: date, options: [])
    let outFormatter = DateFormatter()
    outFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    outFormatter.timeZone = calendar.timeZone
    print("UTC time                  Localtime")
    print(now, outFormatter.string(from: now))
    print(date, outFormatter.string(from: date))
    print(components)

使用上面的代码:

calendarTest("2016-10-29 16:12:34","2017-03-29 16:03:13")
calendarTest("2016-10-29 16:12:34","2017-03-29 16:12:34")

输出:

UTC time                  Localtime
2016-10-29 16:12:34 +0000 2016-10-29 18:12:34
2017-03-29 16:03:13 +0000 2017-03-29 18:03:13
year: 0 month: 4 day: 28 hour: 23 minute: 50 second: 39 isLeapMonth: false 
UTC time                  Localtime
2016-10-29 16:12:34 +0000 2016-10-29 18:12:34
2017-03-29 16:12:34 +0000 2017-03-29 18:12:34
year: 0 month: 5 day: 0 hour: 0 minute: 0 second: 0 isLeapMonth: false 

calendarTest("2016-10-29 16:12:34","2017-01-29 17:03:13")
calendarTest("2016-10-29 16:12:34","2017-01-29 17:12:34")

输出:

UTC time                  Localtime
2016-10-29 16:12:34 +0000 2016-10-29 18:12:34
2017-01-29 17:03:13 +0000 2017-01-29 18:03:13
year: 0 month: 2 day: 30 hour: 23 minute: 50 second: 39 isLeapMonth: false 
UTC time                  Localtime
2016-10-29 16:12:34 +0000 2016-10-29 18:12:34
2017-01-29 17:12:34 +0000 2017-01-29 18:12:34
year: 0 month: 3 day: 0 hour: 0 minute: 0 second: 0 isLeapMonth: false 

calendarTest("2016-10-29 16:12:34","2015-09-30 15:54:20")

输出:

UTC time                  Localtime
2016-10-29 16:12:34 +0000 2016-10-29 18:12:34
2015-09-30 15:54:20 +0000 2015-09-30 17:54:20
year: -1 month: 0 day: -29 hour: 0 minute: -18 second: -14 isLeapMonth: false 

calendarTest("2016-10-29 16:12:34","2016-05-29 14:09:31")

输出:

UTC time                  Localtime
2016-10-29 16:12:34 +0000 2016-10-29 18:12:34
2016-05-29 14:09:31 +0000 2016-05-29 16:09:31
year: 0 month: -5 day: 0 hour: -2 minute: -3 second: -3 isLeapMonth: false 

请记住 Calendar.current 使用当地时区的日期和时间计算月份。


顺便说一句,在两个Dates 之间获取DateComponents 时,您无需使用NSCalendar

let components = calendar.dateComponents([.minute, .hour, .day/*, .weekOfYear*/,.month, .year, .second], from: now, to: date)

【讨论】:

以上是关于Swift 中的 DateComponents 返回奇怪的值。为啥?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Swift 中将 NSDateComponents 转换为 DateComponents

swift DateComponents

swift DateComponents

swift DateComponents

swift DateComponents

Swift 3 中的日期扩展