【问题标题】:Get first Monday in Calendar month获取日历月的第一个星期一
【发布时间】:2019-03-09 13:35:11
【问题描述】:

我被困在日历/时区/日期组件/日期地狱中,我的思绪在旋转。

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

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

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

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 计算日期,可以“修复”该问题。在不同时区查看日历视图时,我觉得好像通过将其强制为 UTC 日期变得无效。

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

编辑

感谢当前的答案,但我们仍然陷入困境。

在您考虑夏令时之前,以下方法有效:

【问题讨论】:

  • 嗨。我正在寻找要在我的应用程序中使用的健全的日历小部件,但其中很多都是错误的或过时的。我终于找到并使用了this one。代码很好地分为逻辑部分。在这里你参与了日期操作,它可以帮助你JTDateHelper
  • 请查看苹果Calendar docs中的“获取日历信息”和“计算间隔”部分。
  • 嗯...夏令时错误让我很困惑。您是否可以检查您当前是否处于 DST 并加/减一个小时?我不确定这是否可行……当然,您必须检查您所在的国家/地区才能获得正确的 DST……该死的。
  • @user10176969 是的,这太难了。我担心如果我弄乱了 DST,那么在呈现用户的当前日期时,每个单元格的日期将不正确。
  • @MattJohnson 我想我想说的是:如果日历中的每一天单元格都按日期间隔返回,例如25/12/18 at 00:00 UTC -> 26/12/18 00:00 UTC,我想在午夜前一小时向纽约用户突出显示当前日期,然后将突出显示错误的日期,因为它们仍将在 24/12/18除非我进行某种转换。我想我需要先了解一下日历和时区是如何工作的,然后再继续。

标签: ios swift date calendar timezone


【解决方案1】:

查看我几年前写的一些代码来做类似的事情,它就像……

  • 获取当月第一天的日期
  • 获取当天的dayNumber (CalendarUnit.weekday)
  • 显示前一周的天数 = dayNumber - 1
  • 从第一天减去它得到日历开始日期

【讨论】:

  • 为什么需要前一周的天数?如果小于或等于 4,您只需减去 dayNumber 天数,否则添加 7-dayNumber 天数。这将使您到达最近的星期一。
  • 差不多。夏令时可以解决这个问题。
【解决方案2】:

我有第一个问题的答案:

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

let calendar = Calendar(identifier: .iso8601)
    func firstOfMonth(month: Int, year: Int) -> Date {



        var firstOfMonthComponents = DateComponents()
        firstOfMonthComponents.calendar = calendar
        firstOfMonthComponents.year = year
        firstOfMonthComponents.month = month
        firstOfMonthComponents.day = 01

        return firstOfMonthComponents.date!
    }

    var aug01 = firstOfMonth(month: 08, year: 2018)
    let dayOfWeek = calendar.component(.weekday, from: aug01)
    if dayOfWeek <= 4 { //thursday or earlier
         //take away dayOfWeek days from aug01
    else {
         //add 7-dayOfWeek days to aug01
    }

【讨论】:

    【解决方案3】:

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

    总的来说,你在正确的轨道上,但有几个细微的错误正在发生。

    概念化日期

    现在(我希望)已经很确定Date 代表了一个瞬间。反之亦然。 Date 代表瞬间,但什么代表日历值?

    仔细想想,“天”或“小时”(甚至“月”)都是一个范围。它们是代表开始瞬间和结束瞬间之间所有可能瞬间的值。

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

    有了这个,我想你会发现更容易理解发生了什么。您已经了解Range&lt;Int&gt;,对吗?所以Range&lt;Date&gt; 也同样直观。

    幸运的是,Calendar 上有 API 可以获取范围。不幸的是,我们不能完全使用Range&lt;Date&gt;,因为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)
    

    您可以查看显示的各个单位的范围。这里有两点需要指出:

    1. 此方法返回一个可选 (DateInterval?),因为它可能会在一些奇怪的情况下失败。我什么也想不出来,但是日历是变化无常的东西,所以请注意这一点。

    2. 询问纪元的范围通常是一种荒谬的操作,尽管纪元对于正确构建日期至关重要。很难对某些日历(主要是日本日历)之外的时代进行精确的计算,所以说“共同时代(CE 或 AD)开始于第一年的 1 月 3 日”的价值可能是错误的,但我们不能真的对此做任何事情。另外,当然,我们不知道当前时代何时结束,所以我们只是假设它会永远持续下去。

    打印Date

    这将我带到下一点,关于打印日期。我在您的代码中注意到了这一点,我认为这可能是您的一些问题的根源。我赞赏你为解释夏令时这一令人憎恶的问题所做的努力,但我认为你正在陷入实际上不是错误的事情,但这是:

    Dates 总是以 UTC 打印。

    永远永远永远。

    这是因为它们是时间上的瞬间,并且它们是同一瞬间,无论您是在加利福尼亚、吉布提还是哈德满都或其他任何地方。 Date 值确实没有有时区。这实际上只是与另一个已知时间点的偏移。但是,将 560375786.836208 打印为 Date's 描述对人类来说似乎没有多大用处,因此 API 的创建者将其打印为相对于 UTC 时区的公历日期。

    如果你想打印一个日期,即使是为了调试,你几乎肯定需要DateFormatter

    DateComponents.date

    这不是你的代码的问题,而是更多的提醒。 DateComponents 上的 .date 属性不保证返回与指定组件匹配的第一个瞬间。它不保证返回与组件匹配的最后时刻。它所做的只是保证,如果它可以找到答案,它会给你一个Date在指定单位范围内的某个地方

    我不认为您在代码中从技术上讲依赖于这一点,但要注意这一点是一件好事,而且我已经看到这种误解会导致软件出现错误。


    考虑到所有这些,让我们来看看你想要做什么:

    1. 你想知道一年的范围
    2. 您想知道一年中的所有月份
    3. 您想知道一个月中的所有日子
    4. 您想知道包含该月第一天的星期

    因此,根据我们对范围的了解,现在这应该非常简单,有趣的是,我们可以在不需要单个 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 小时,这取决于我们是否观察到 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
    

    您会在这里注意到的一件事是 this 以 星期日 作为一周的第一天。例如,在您的屏幕截图中,从 29 日开始包含 2 月 1 日这一周。

    该行为由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 显示的 UI 相同的 UI 所需的所有工具。

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

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

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

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

    【讨论】:

    • 这是一个了不起的答案!只是在firstWeekday 上的一个简短说明:我不确定 OP 是否真的想强制将星期一作为第一列 无论用户当前使用什么日历 - 如果是这种情况,你的建议设置 firstWeekday = 2 确实是正确的——或者如果 OP 只是不知道并非所有日历都从星期一开始几周,而有些日历则从星期日开始……然后现在他们知道了,也许他们更喜欢尊重日历设置并使用其firstWeekday 来确定第一列而不是将其硬编码为星期一;-)
    • 是的,感谢@AliSoftware 的澄清。设置firstWeekday 的目的是使底层DateInterval 与UI 中显示的内容相匹配。但是您绝对正确,更改 firstWeekday 可能不是文化上正确的事情。
    【解决方案4】:

    这对我有用...

    let calendar = Calendar.autoupdatingCurrent
    let date = Date().description(with: Locale.current)
    
    func firstDayOfMonth(month:Int, year:Int) -> Date {
    
        var firstOfMonthComponents = DateComponents()
        firstOfMonthComponents.calendar = calendar
        firstOfMonthComponents.year = year
        firstOfMonthComponents.month = month
        firstOfMonthComponents.day = 01
        
        return firstOfMonthComponents.date!
    }
    
    func firstMonday(month:Int, year:Int) -> Date {
        
        let first = firstDayOfMonth(month: month, year: year)
    
        let dayNumber = calendar.component(.weekday, from: first)
        let daysToAdd = ((calendar.firstWeekday + 7) - dayNumber) % 7
        return calendar.date(byAdding: .day, value: daysToAdd, to: first)!
    
    }
    
    (1...12).forEach {
        print(firstMonday(month: $0, year: 2021).description(with: Locale.current))
    }
    

    【讨论】:

      猜你喜欢
      • 2014-07-21
      • 1970-01-01
      • 1970-01-01
      • 2014-02-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-05
      • 1970-01-01
      相关资源
      最近更新 更多