gpt4 book ai didi

ios - 获取日历月的第一个星期一

转载 作者:行者123 更新时间:2023-12-02 17:14:08 27 4
gpt4 key购买 nike

我被困在日历/时区/日期组件/日期 hell 中,我的头脑在旋转。

我正在创建一个日记应用程序,我希望它在全局范围内工作(使用 iso8601 日历)。

我正在尝试在 iOS 上创建一个类似于 macOS 日历的日历 View ,以便我的用户可以浏览他们的日记条目(注意每个月的 7x6 网格):

/Users/adamwaite/Desktop/Screenshot 2018-10-04 at 08.22.24.png

要获得本月的第一天,我可以执行以下操作:

func firstOfMonth(month: Int, year: Int) -> Date {

let calendar = Calendar(identifier: .iso8601)

var firstOfMonthComponents = DateComponents()
// firstOfMonthComponents.timeZone = TimeZone(identifier: "UTC") // <- fixes daylight savings time
firstOfMonthComponents.calendar = calendar
firstOfMonthComponents.year = year
firstOfMonthComponents.month = month
firstOfMonthComponents.day = 01

return firstOfMonthComponents.date!

}

(1...12).forEach {

print(firstOfMonth(month: $0, year: 2018))

/*

Gives:

2018-01-01 00:00:00 +0000
2018-02-01 00:00:00 +0000
2018-03-01 00:00:00 +0000
2018-03-31 23:00:00 +0000
2018-04-30 23:00:00 +0000
2018-05-31 23:00:00 +0000
2018-06-30 23:00:00 +0000
2018-07-31 23:00:00 +0000
2018-08-31 23:00:00 +0000
2018-09-30 23:00:00 +0000
2018-11-01 00:00:00 +0000
2018-12-01 00:00:00 +0000
*/

}

这里有一个关于夏令时的直接问题。可以通过取消注释注释行并强制以 UTC 计算日期来“修复”该问题。我觉得在不同时区查看日历 View 时,好像通过强制它为 UTC 日期变得无效。

真正的问题是:我如何获得包含该月第一天的一周中的第一个星期一?例如,我如何获得 2 月 29 日星期一或 4 月 26 日星期一? (请参阅 macOS 屏幕截图)。要到月底,我是否只需从开始算起增加 42 天?还是那太天真了?

编辑

感谢当前的答案,但我们仍然被卡住了。

以下工作,直到您考虑夏令时:

almost

最佳答案

好的,这将是一个冗长而复杂的答案(万岁!)。

总体而言,您走在正确的轨道上,但仍存在一些细微的错误。

概念化日期

现在(我希望)已经非常完善 Date代表一个瞬间。不太确定的是相反的情况。一个 Date代表一个瞬间,但什么代表日历值?

当您考虑它时,“天”或“小时”(甚至“月”)是一个范围。它们是表示开始时刻和结束时刻之间所有可能时刻的值。

出于这个原因,我发现在处理日历值时考虑范围最有帮助。换句话说,通过询问“这一分钟/小时/天/周/月/年的范围是多少?”等等。

有了这个,我想你会发现更容易理解发生了什么。你懂的Range<Int>已经,对吧?所以一个 Range<Date>同样直观。

幸运的是,在 Calendar 上有 API得到范围。不幸的是,我们还不能完全使用 Range<Date>因为 Objective-C 时代的保留 API。但是我们得到 NSDateInterval ,这实际上是同一件事。

您可以通过询问 Calendar 来请求范围。对于包含特定时刻的小时/天/周/月/年范围,如下所示:

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? ),因为它可能会在奇怪的情况下失败。我想不出任何事情,但日历是变化无常的东西,所以请注意这一点。
  • 要求一个时代的范围通常是一个荒谬的操作,即使时代对于正确构建日期至关重要。在某些日历(主要是日本日历)之外的时代很难有太多的精确度,所以说“共同时代(公元或公元)开始于第一年的 1 月 3 日”的值(value)可能是错误的,但我们不能真的对此有所作为。此外,当然,我们不知道当前时代何时结束,所以我们只是假设它永远持续下去。

  • 打印 Date值(value)观

    这让我想到下一点,关于打印日期。我在您的代码中注意到了这一点,我认为这可能是您的某些问题的根源。我赞赏你为解释夏令时这一令人憎恶的事情所做的努力,但我认为你被一些实际上不是错误的东西所吸引,但这是:
    Dates始终以 UTC 打印。

    永远永远永远。

    这是因为它们是时间上的瞬间,无论您是在加利福尼亚、吉布提、加德满都还是其他地方,它们都是相同的瞬间。一个 Date值没有时区。它实际上只是与另一个已知时间点的偏移。然而,打印 560375786.836208作为 Date's description 对人类似乎没有那么有用,因此 API 的创建者将其打印为相对于 UTC 时区的公历日期。

    如果你想打印一个日期,即使是为了调试,你几乎肯定需要一个 DateFormatter .
    DateComponents.date
    这不是你的代码的问题,而是更多的提醒。 .date属性(property)在 DateComponents不保证返回与指定组件匹配的第一个瞬间。它不保证返回与组件匹配的最后时刻。它所做的只是保证,如果它能找到答案,它会给你一个 Date在指定单位范围内的某处。

    我不认为您在代码中在技术上依赖于这一点,但意识到这一点是件好事,而且我已经看到这种误解导致了软件中的错误。

    考虑到所有这些,让我们来看看您想要做什么:
  • 你想知道年份的范围
  • 您想知道一年中的所有月份
  • 您想知道一个月中的所有天数
  • 您想知道包含该月第一天的那一周

  • 因此,根据我们对范围的了解,现在这应该非常简单,而且有趣的是,我们可以在不需要单个 DateComponents 的情况下完成所有操作。值(value)。在下面的代码中,为了简洁起见,我将强制展开值。在您的实际代码中,您可能希望有更好的错误处理。

    年的范围

    这很简单:
    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 小时,这取决于我们是否观察到 DST。所以这些值对于我日历的时区是准确的。

    一个月的日子

    这就像之前的代码:
    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 日开始。

    该行为受 firstWeekday 的约束。属性(property)在 Calendar .默认情况下,该值可能是 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 所示相同的 UI 所需的所有工具。

    作为额外的奖励,无论您的日历、时区和语言环境如何,我在此处发布的所有代码都可以运行。它可以为日本日历、科普特日历、希伯来日历、ISO8601 等构建 UI。它可以在任何时区工作,也可以在任何语言环境中工作。

    此外,拥有所有 DateInterval像这样的值可能会使您的应用程序更容易实现,因为进行“范围包含”检查是您在制作日历应用程序时想要进行的那种检查。

    唯一的另一件事是确保您使用 DateFormatter将这些值渲染为 String 时.请不要拉出单独的日期组件。

    如果您有后续问题,请将它们作为新问题发布和 @mention我在评论中。

    关于ios - 获取日历月的第一个星期一,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/52641981/

    27 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com