簡體   English   中英

獲取日歷月的第一個星期一

[英]Get first Monday in Calendar month

我被困在日歷/時區/日期組件/日期地獄中,我的頭腦在旋轉。

我正在創建一個日記應用程序,我希望它在全球范圍內工作(使用 iso8601 日歷)。

我正在嘗試在 iOS 上創建一個類似於 macOS 日歷的日歷視圖,以便我的用戶可以瀏覽他們的日記條目(注意每個月的 7x6 網格):

/Users/adamwaite/Desktop/Scr​​eenshot 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 計算日期來“修復”該問題。 我覺得在不同時區查看日歷視圖時,通過強制將其設置為 UTC,日期會變得無效。

真正的問題是:我如何獲得包含該月第一天的一周中的第一個星期一? 例如,我如何獲得 2 月 29 日星期一或 4 月 26 日星期一? (請參閱 macOS 屏幕截圖)。 要到月底,我是否只需從開始的 42 天開始添加? 還是那太天真了?

編輯

感謝當前的答案,但我們仍然被卡住了。

以下工作,直到您考慮夏令時:

幾乎

好的,這將是一個冗長而復雜的答案(萬歲!)。

總體而言,您走在正確的軌道上,但仍存在一些細微的錯誤。

概念化日期

現在(我希望) Date代表時間的瞬間已經非常成熟。 不太確定的是相反的情況。 Date代表一個瞬間,但什么代表日歷值?

當您考慮它時,“天”或“小時”(甚至“月”)是一個范圍 它們是表示開始時刻和結束時刻之間所有可能時刻的值。

出於這個原因,我發現在處理日歷值時考慮范圍最有幫助。 換句話說,通過詢問“這一分鍾/小時/天/周/月/年的范圍是多少?” 等等。

有了這個,我想你會發現更容易理解發生了什么。 你已經了解Range<Int>了吧? 所以Range<Date>同樣直觀。

幸運的是, Calendar上有 API 可以獲取范圍。 不幸的是,由於 Objective-C 時代的保留 API,我們不能完全使用Range<Date> 但是我們得到了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. 要求一個時代的范圍通常是一個荒謬的操作,即使時代對於正確構建日期至關重要。 在某些日歷(主要是日本日歷)之外的時代很難有太多的精確度,所以說“共同時代(公元或公元)開始於第一年的 1 月 3 日”的價值可能是錯誤的,但我們不能真的對此有所作為。 此外,當然,我們不知道當前時代何時結束,所以我們只是假設它永遠持續下去。

打印Date

這讓我想到下一點,關於打印日期。 我在您的代碼中注意到了這一點,我認為這可能是您的某些問題的根源。 我贊賞你為解釋夏令時這一令人憎惡的事情所做的努力,但我認為你被一些實際上不是錯誤的東西所吸引,但這是:

Dates總是以 UTC 打印。

永遠永遠永遠。

這是因為它們是時間上的瞬間,無論您是在加利福尼亞、吉布提、加德滿都還是其他任何地方,它們都是相同的瞬間 一個Date沒有一個時區。 它實際上只是與另一個已知時間點的偏移。 但是,將560375786.836208打印為Date's描述對人類來說似乎沒什么用,因此 API 的創建者將其打印為相對於 UTC 時區的公歷日期。

如果你想打印一個日期,即使是為了調試,你幾乎肯定需要一個DateFormatter

DateComponents.date

這不是你的代碼的問題,而是更多的提醒。 .date物業DateComponents並不能保證返回指定的組件匹配的第一個瞬間。 它不保證返回與組件匹配的最后時刻。 它所做的只是保證,如果它可以找到答案,它將在指定單位范圍內某處為您提供Date

我不認為您在技術上依賴於您的代碼中的這一點,但了解這一點是件好事,而且我已經看到這種誤解導致了軟件中的錯誤。


考慮到所有這些,讓我們來看看您想要做什么:

  1. 你想知道一年的范圍
  2. 你想知道一年中的所有月份
  3. 你想知道一個月的所有日子
  4. 您想知道包含該月第一天的那一周

因此,根據我們對范圍的了解,現在這應該非常簡單,而且有趣的是,我們可以在不需要單個DateComponents值的情況下完成所有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

您會在這里注意到的一件事是,這將星期日作為一周的第一天。 例如,在您的屏幕截圖中,您的一周包含 2 月 1 日,從 29 日開始。

該行為由Calendar上的firstWeekday屬性控制。 默認情況下,該值可能為1 (取決於您的語言環境),表示周從星期日開始。 如果您希望您的日歷在一周從Monday開始的地方進行計算,那么您需要將該屬性的值更改為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值可能會使實現您的應用程序更容易,因為進行“范圍包含”檢查是您在制作日歷應用程序時想要進行的那種檢查。

唯一的另一件事是確保在這些值呈現String時使用DateFormatter 請不要拉出單獨的日期組件。

如果您有后續問題,請將它們作為新問題發布,並在評論中@mention我。

查看我幾年前寫的一些代碼來做類似的事情,它就像......

  • 獲取當月第一天的日期
  • 獲取當天的dayNumber ( CalendarUnit.weekday )
  • 要顯示的前一周的天數 = dayNumber - 1
  • 從第一天減去它以獲得日歷開始日期

那對我有用...

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))
}

我有第一個問題的答案:

第一個問題:我現在如何獲得包含該月第一天的一周中的第一個星期一? 例如,我如何獲得 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
    }

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM