Checking the local season

I am trying to trigger specific behaviour based on the season of the year. Eventually I will need this to be localized, but am presently trying to circumvent the issue of the year value being hard coded, trying to always get the current year, but for some reason it will not work:

let currentDate = Date()
let formatter = DateFormatter()
let formatter2 = DateFormatter()
formatter.dateFormat = "yyyy/MM/dd"
let currentYear = formatter.string(from: currentDate)   //get current year
let yearDataInStringFormat = String(currentYear)
var globalSeasons: [String : [String : Bool]]   = [:]


let someDate1 = formatter.date(from: "2020/September/08") ?? Date()
let someDate2 = formatter.date(from: "2020/October/08") ?? Date()
let checkDate = (someDate1 ... someDate2).contains(Date())


globalSeasons["Europe"] = ["Summer" : ((formatter.date(from: "\(yearDataInStringFormat)/June/01") ?? Date()) ...
            (formatter.date(from: "\(yearDataInStringFormat)/August/27") ?? Date())).contains(Date()),
"Autumn" : ((formatter.date(from: "\(yearDataInStringFormat)/August/28") ?? Date()) ...
            (formatter.date(from: "\(yearDataInStringFormat)/November/05") ?? Date())).contains(Date())]

I thought it best to keep the data in a nested dictionary, but to perform the bool checks inside it a little cumbersome, and I don't want to commit them to values as that would be a little verbose.

The above code will work with hardcoded values with "checkDate", but I am looking to remove the dependency on the year by substituting yyyy with the dynamic "currentYear" in the dictionary, but it does not work (defaulting to the current date).

Ultimately I would like to check the local season when the app starts.

nb. while it is summer in middle of the year in Europe, it is summer in December in Japan

Using a DateFormatter for this sort of thing is a really bad idea. See QA1480 NSDateFormatter and Internet Dates for an explanation as to why.

If you want to get the year, month and day from a date you should use calendar. For example:

import Foundation

func ymdForDate(_ date: Date) -> (year: Int, month: Int, day: Int) {
    let c = Calendar(identifier: .gregorian)
    let dc = c.dateComponents([.year, .month, .day], from: date)
    return (dc.year!, dc.month!, dc.day!)
}

print(ymdForDate(Date()))
// prints: (year: 2020, month: 10, day: 1)

IMPORTANT I’m hard wiring the Gregorian calendar here. Do not use the default calendar (Calendar.current or Calendar.autoupdatingCurrent) for this because a non-Gregorian calendar can yield unexpected results. I gave a specific example of this in QA1480 but there are many more. For example, not all calendars have 12 months.

As to mapping that to a season, that’s a tricky problem, one that’s very dependent on the locale. For example:

  • Seasons are reversed in the southern hemisphere.

  • Locations near the equator don’t have seasons as they do in Europe. In some places there’s a wet season and dry season, and in others there’s simply no concept of seasons.

  • Even in places that do have seasons, the exact start and end dates vary (some folks consider them to change on the solstice and equinox, some folks consider them to change at the month boundaries).

It would be great if Foundation gave you an API to get season information but, AFAIK, it does not. A full solution to this problem will require a lot of work on your part.

it is summer in December in Japan

Ah, um, no its not. Japan is in the northern hemisphere and shares the same seasons as Europe (may be not the exact start and end dates but, as I mentioned above, those are subject to debate anyway). A better example would be where I grew up, Australia, where Christmas is indeed in summer.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

5 Likes

Eskimo to the rescue once more, it must be boredom that compels you, stuck in an Igloo. Why ever did you leave Australia ?

I have been tinkering with the system I had, and found that I need only jump to the start of each season, rather than have an end date, but in doing so, calculating for spring the start date can be greater than the end, or, when calculating on just integer values e.g. the day, it runs into minus numbers and just wasn't working with dates, but number values.

Checking online I found that I could hack together something that did using the calendar, but I still cannot check between dates and must perform month and day checks separately:

func ymdForDate(_ date: Date) -> (year: Int, month: Int, day: Int) {
    let c = Calendar(identifier: .gregorian)
    let dc = c.dateComponents([.year, .month, .day], from: date)
    return (dc.year!, dc.month!, dc.day!)
}

var summerBegins = DateComponents()
summerBegins.year = ymdForDate(Date()).year
summerBegins.month = 7
summerBegins.day = 11
summerBegins.timeZone = TimeZone(abbreviation: "GMT")

var autumnBegins = DateComponents()
autumnBegins.year = ymdForDate(Date()).year
autumnBegins.month = 10
autumnBegins.day = 7
autumnBegins.timeZone = TimeZone(abbreviation: "GMT")

let userCalendar = Calendar.current // user calendar
let startDate = userCalendar.date(from: summerBegins)!
let endDate = userCalendar.date(from: autumnBegins)!

let calendar = Calendar.current
let dateValue = NSDate()
let currentDate = dateValue as Date
let beginSummer = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: startDate)
let beginAutumn = calendar.date(bySettingHour: 0, minute: 0, second: 0, of: endDate)

if currentDate >= beginSummer! && currentDate <= beginAutumn! {
    print("welcome summer")
}

I am fortunate in that my checks do not have to be extremely accurate, nor comprehensive, as I'm just looking region by region, but it is still a lot of work, is there any way to cut back on the code do you think ?

You really want to avoid Calendar.current. Consider what happens if a user in Thailand is using the Buddhist calendar. It’s 543 years ahead of Gregorian, so userCalendar will interpret the year component in summerBegins that way, which is not what you want.

If you’re dealing with date components in arbitrary locales it’s best to set the time to midday rather than leaving it blank. That’s because midnight doesn’t exist on certain days in certain locales. For an example of how this can cause problems, see my Parsing Dates Without Times post on DevForums.

Once you have the middle of the day it’s easy to get its start.

Also, if you want to work in GMT, set the time zone on the calendar rather than on the date components. Having said that, I don’t think you want to work in GMT because, presumably, you want the start of the reason in local time.

Finally, don’t forget nextDate(after:matching:matchingPolicy:repeatedTimePolicy:direction:), which lets you search backwards and forwards for a date matching specific components.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes

Thank you for the advice, I am not entirely sure if I understood it correctly, but have made the following changes:

∙ Calendar.current -> Calendar.autoupdatingCurrent
∙ Set time to mid day, but not sure how to use that data
∙ Using nextDate function to get date for conditional checks

let calendar = Calendar.autoupdatingCurrent //listens for user changes

//not sure how to use this data with my current setup
let summerDayBeginning = calendar.startOfDay(for: beginSummer ?? Date())
let autumnDayBeginning = calendar.startOfDay(for: beginAutumn ?? Date())

//I don't know if this is what you meant, or why this is better than before
let checkSummerStart = Calendar.current.nextDate(after: Date(), matching: summerBegins, matchingPolicy: .nextTime, repeatedTimePolicy: .first, direction: .backward)

let checkAutumnStart = Calendar.current.nextDate(after: Date(), matching: autumnBegins, matchingPolicy: .nextTime, repeatedTimePolicy: .first, direction: .forward)

//checks do work
if currentDate > checkSummerStart ?? Date() && currentDate < checkAutumnStart ?? Date() {
    print("summer starts")
}

A day here or there doesn't matter for my purposes, so I don't need the result of the startOfDay, but I couldn't fathom how to include it.

I don't know why I can't simply call upon a function to check between two dates, there is an option to, but not for DateComponents, only Date, which doesn't take into consideration local times and calendars.

What do you think?

Calendar.current -> Calendar.autoupdatingCurrent

You’re really missing the point here )-: The days and months that you use to define the seasons only make sense in the context of the Gregorian calendar. If you use current or autoupdatingCurrent you will get nonsensical results if the user has configured the machine to use a different calendar.

Consider this:

let c = Calendar(identifier: .islamic)
let dc = DateComponents(year: 2020, month: 10, day: 7)
let d = c.date(from: dc)!
print(d)    // 2582-03-03 00:00:00 +0000

The Islamic calendar is strictly lunar, meaning that its years don’t line up with the solar years you’re used to.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

2 Likes