Using NSDate to get date for Easter

1.3k views Asked by At

I'm working on an application that requires the use of getting dates for national holidays.

Below, I was able to get Memorial Day:

// Set the components for Memorial Day (last Monday of May)

let memorialDayComps = NSDateComponents()
memorialDayComps.weekday = 2
memorialDayComps.month = 5
memorialDayComps.year = currentYear

var mondaysOfMay = [NSDate]()

for var i = 1; i <= 5; i++ {
    memorialDayComps.weekdayOrdinal = i
    let monday = calendar.dateFromComponents(memorialDayComps)
    let components = calendar.components(.CalendarUnitMonth, fromDate: monday!)
    if components.month == 5 {
        mondaysOfMay.append(monday!)
    }
}
let memorialDayDate = mondaysOfMay.last

Because the dates are pretty well set, I am able to successfully create NSDate instances for the following holidays:

  • New Year's Day
  • Martin Luther King, Jr. Day
  • Presidents' Day
  • Memorial Day
  • Independence Day
  • Labor Day
  • Thanksgiving Day
  • Christmas Day

However, the only one that I am having difficulty figuring out how to get is Easter. It varies every year, so I'm curious as to whether anyone else has been able so successfully get the date for Easter via an API or other means.

5

There are 5 answers

1
Nick Kohrn On

I was able to find a gist on GitHub that has a solution that was accurate for calculating and returning an NSDate for Easter.

The code below is what the gist contains:

// Easter calculation in swift after Anonymous Gregorian algorithm
// Also known as Meeus/Jones/Butcher algorithm

func easter(Y : Int) -> NSDate {
  let a = Y % 19
  let b = Int(floor(Double(Y) / 100))
  let c = Y % 100
  let d = Int(floor(Double(b) / 4))
  let e = b % 4
  let f = Int(floor(Double(b+8) / 25))
  let g = Int(floor(Double(b-f+1) / 3))
  let h = (19*a + b - d - g + 15) % 30
  let i = Int(floor(Double(c) / 4))
  let k = c % 4
  let L = (32 + 2*e + 2*i - h - k) % 7
  let m = Int(floor(Double(a + 11*h + 22*L) / 451))
  let components = NSDateComponents()
  components.year = Y
  components.month = Int(floor(Double(h + L - 7*m + 114) / 31))
  components.day = ((h + L - 7*m + 114) % 31) + 1
  components.timeZone = NSTimeZone(forSecondsFromGMT: 0)
  let cal = NSCalendar(calendarIdentifier: NSGregorianCalendar)
  return cal.dateFromComponents(components)
}

println(easter(2014))  // "2014-04-20 00:00:00 +0000"
0
Damiaan Dufaux On

Here is a Swift 5 implementation of O'Beirne's algorithm with inline documentation.

The code is more compact than the implementations in the other provided answers because it makes use of Integer arithmetic and thus removes the need to explicitly round numbers and convert between Floats and Ints.

/// **How ten divisions lead to Easter** *by T. H. O'Beirne, New Scientist, march 30 1961 - Vol. 9,Nr. 228*
func easter(in year: Int) -> (day: Int, month: Int) {
    /// Identify the position of the `year` in a 19-year cycle, to use this later to determine the principal constituent of the changes of full-moon dates from year to year
    let a = year % 19

    /// Take note of the corrections which the Gregorian calendar introduces in century years
    let (b, c) = year.quotientAndRemainder(dividingBy: 100)

    /// Take account of the leap-year exceptions in century years
    let (d, e) = b.quotientAndRemainder(dividingBy: 4)

    /// Provide similarly for the century years auxiliary corrections to the new-moon and full-moon dates
    let g = (8*b + 13) / 25

    /// Determine the number of days between 21 March and the coincident or next full moon, if no special exceptions arise
    let h = (19*a + b - d - g + 15) % 30

    /// Determine the position of the year in the ordinary leap-year cycle of four years
    let (i, k) = c.quotientAndRemainder(dividingBy: 4)

    /// Determine number of days (between 0 and 6) until the Sunday *after* full moon
    let l = (2*e + 2*i - h - k + 32) % 7

    /// The exceptions which make a 29-day month interrupt the regularity of a simpler pattern need here be considered *only* when they transfer the full moon *from a Sunday to a Saturday*: the *Easter date* is unaffected in other cases. When appropriate — 1954 and 1981 are quite rare examples — we have m=1; otherwise m=0 : this permits the necessary correction (failing which the Easter date *would* be 26 April in 1981.
    let m = (a + 11*h + 19*l) / 433

    /// Determine days between March 22 and Easter
    let relativeDayCount = h + l - 7*m

    /// Convert relative day count into absolute month and day index
    let month = (relativeDayCount + 90) / 25
    return (day: (relativeDayCount + 33*month + 19) % 32, month)
}

func easterDate(in year: Int) -> Date {
    let (day, month) = easter(in: year)
    let components = DateComponents(
        timeZone: TimeZone(secondsFromGMT: 0),
        year: year, month: month, day: day
    )
    return Calendar(identifier: .gregorian).date(from: components)!
}
0
coolcool1994 On

OBJECTIVE-C!

-(void) easterMonthAndDayForYear: (NSInteger) Y {
        NSInteger a = Y % 19;
        NSInteger b = (int) (floor( ((double)Y) / 100.0));
        NSInteger c = Y % 100;
        NSInteger d = (int)(floor(((double)b) / 4.0));
        NSInteger e = b % 4;
        NSInteger f = (int)(floor(((double)(b+8)) / 25.0));
        NSInteger g = (int)(floor(((double)(b-f+1)) / 3.0));
        NSInteger h = (19*a + b - d - g + 15) % 30;
        NSInteger i = (int)(floor(((double)c) / 4.0));
        NSInteger k = c % 4;
        NSInteger L = (32 + 2*e + 2*i - h - k) % 7;
        NSInteger m = (int)(floor(((double)(a + 11*h + 22*L)) / 451.0));
        NSDateComponents *components = [[NSCalendar currentCalendar] components:NSCalendarUnitDay | NSCalendarUnitMonth | NSCalendarUnitYear |NSCalendarUnitTimeZone  fromDate:[NSDate date]];
        components.year = Y;
        components.month = (int)(floor((double)(h + L - 7*m + 114) / 31.0));
        components.day = ((h + L - 7*m + 114) % 31) + 1;
        components.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];

        self.easterDayCache[@(Y)] = @{@"month": @(components.month), @"day":@(components.day)
};
1
David Thorsrud On

That Easter algorithm works great! Using with Swift 4.0 and pattern matching. Pattern matching made it easier for me to add other days based on month, day, weekday, weekdayOrdinal.

extension Date {

    var isUSHoliday: Bool {

        let components = Calendar.current.dateComponents([.year, .month, .day, .weekday, .weekdayOrdinal], from: self)
        guard let year = components.year,
            let month = components.month,
            let day = components.day,
            let weekday = components.weekday,
            let weekdayOrdinal = components.weekdayOrdinal else { return false }

        let easterDateComponents = Date.dateComponentsForEaster(year: year)
        let easterMonth: Int = easterDateComponents?.month ?? -1
        let easterDay: Int = easterDateComponents?.day ?? -1
        let memorialDay = Date.dateComponentsForMemorialDay(year: year)?.day ?? -1

        // weekday is Sunday==1 ... Saturday==7
        // weekdayOrdinal is nth instance of weekday in month

        switch (month, day, weekday, weekdayOrdinal) {
        case (1, 1, _, _): return true                      // Happy New Years
        case (1, 0, 2, 3): return true                      // MLK - 3rd Mon in Jan
        case (2, 0, 2, 3): return true                      // Washington - 3rd Mon in Feb
        case (easterMonth, easterDay, _, _): return true    // Easter - rocket science calculation
        case (5, memorialDay, _, _): return true            // Memorial Day
        case (7, 4, _, _): return true                      // Independence Day
        case (9, 0, 2, 1): return true                      // Labor Day - 1st Mon in Sept
        case (10, 0, 2, 2): return true                     // Columbus Day - 2nd Mon in Oct
        case (11, 11, _, _): return true                    // Veterans Day
        case (11, 0, 5, 4): return true                     // Happy Thanksgiving - 4th Thurs in Nov
        case (12, 25, _, _): return true                    // Happy Holidays
        case (12, 31, _, _): return true                    // New years Eve
        default: return false
        }

    }

    static func dateComponentsForMemorialDay(year: Int) -> DateComponents? {
        guard let memorialDay = Date.memorialDay(year: year) else { return nil }
        return NSCalendar.current.dateComponents([.year, .month, .day, .weekday, .weekdayOrdinal], from: memorialDay)
    }

    static func memorialDay(year: Int) -> Date? {
        let calendar = Calendar.current
        var firstMondayJune = DateComponents()
        firstMondayJune.month = 6
        firstMondayJune.weekdayOrdinal = 1  // 1st in month
        firstMondayJune.weekday = 2 // Monday
        firstMondayJune.year = year
        guard let refDate = calendar.date(from: firstMondayJune) else { return nil }
        var timeMachine = DateComponents()
        timeMachine.weekOfMonth = -1
        return calendar.date(byAdding: timeMachine, to: refDate)
    }

    static func easterHoliday(year: Int) -> Date? {
        guard let dateComponents = Date.dateComponentsForEaster(year: year) else { return nil }
        return Calendar.current.date(from: dateComponents)
    }

    static func dateComponentsForEaster(year: Int) -> DateComponents? {
        // Easter calculation from Anonymous Gregorian algorithm
        // AKA Meeus/Jones/Butcher algorithm
        let a = year % 19
        let b = Int(floor(Double(year) / 100))
        let c = year % 100
        let d = Int(floor(Double(b) / 4))
        let e = b % 4
        let f = Int(floor(Double(b+8) / 25))
        let g = Int(floor(Double(b-f+1) / 3))
        let h = (19*a + b - d - g + 15) % 30
        let i = Int(floor(Double(c) / 4))
        let k = c % 4
        let L = (32 + 2*e + 2*i - h - k) % 7
        let m = Int(floor(Double(a + 11*h + 22*L) / 451))
        var dateComponents = DateComponents()
        dateComponents.month = Int(floor(Double(h + L - 7*m + 114) / 31))
        dateComponents.day = ((h + L - 7*m + 114) % 31) + 1
        dateComponents.year = year
        guard let easter = Calendar.current.date(from: dateComponents) else { return nil } // Convert to calculate weekday, weekdayOrdinal
        return Calendar.current.dateComponents([.year, .month, .day, .weekday, .weekdayOrdinal], from: easter)
    }

}
0
Hendra Kusumah On

Swift 4:

func easter(Y : Int) -> Date {
    let a = Y % 19
    let b = Int(floor(Double(Y) / 100))
    let c = Y % 100
    let d = Int(floor(Double(b) / 4))
    let e = b % 4
    let f = Int(floor(Double(b+8) / 25))
    let g = Int(floor(Double(b-f+1) / 3))
    let h = (19*a + b - d - g + 15) % 30
    let i = Int(floor(Double(c) / 4))
    let k = c % 4
    let L = (32 + 2*e + 2*i - h - k) % 7
    let m = Int(floor(Double(a + 11*h + 22*L) / 451))
    var components = DateComponents()
    components.year = Y
    components.month = Int(floor(Double(h + L - 7*m + 114) / 31))
    components.day = ((h + L - 7*m + 114) % 31) + 1
    components.timeZone = TimeZone(secondsFromGMT: 0)
    return Calendar.autoupdatingCurrent.date(from: components)!
}

print(easter(Y: 2018)) // "2018-04-01 00:00:00 +0000"