How to Determine If a Year Has a Leap Month in Swift

Is there a way to determine if a year has a leap month in Swift?

You can check if the first day of the month in question, when set to leap, is a valid date or not:

func isLeap(month: Int, year: Int, era: Int, calendar: Calendar) -> Bool {
var components = DateComponents()
components.era = era
components.year = year
components.month = month
components.day = 1
components.isLeapMonth = true

return components.isValidDate(in: calendar)
}

// The Chinese year that begins in 2017
isLeap(month: 5, year: 34, era: 78, calendar: Calendar(identifier: .chinese)) // false
isLeap(month: 6, year: 34, era: 78, calendar: Calendar(identifier: .chinese)) // true

// The Chinese year that begins in 2020
isLeap(month: 3, year: 37, era: 78, calendar: Calendar(identifier: .chinese)) // false
isLeap(month: 4, year: 37, era: 78, calendar: Calendar(identifier: .chinese)) // true

According to this list, there are a few calendar systems that use leap month as the date correction mechanism. The Chinese calendar is the one I'm more familiar with. You can cross-reference it against the list of leap months in the Chinese calendar

What is the most performant way to determine if a year is a leap year in Swift?

There are a number of ways that whether a year is a leap year can be calculated, at least for the Gregorian calendar: using mathematical rules based on the current definition of leap years, and using Calendar-based methods.

In the Gregorian calendar, the base definition of leap years is a simple mathematical formula of the year, so the simplest way to get the answer could potentially not require any Date-related functions in Swift. Those leap year rules are:

  • A year is a leap year if it is divisible by 4...
  • Unless it is also divisible by 100, when it isn't a leap year,
  • Except when it is again also divisible by 400, then it is a leap year after all.

The modulo operator % calculates the remainder when you divide one number by another. Therefore, when this remainder is 0, you have a number that is evenly divisible. The leap year rules are in the order that makes the most day-to-day sense (you rarely have to worry about the other two rules, but for our calculation we reverse the order to get the if–unless-except logic that we need built in.

    private func isLeapYearUsingModulo(_ targetYear: Int) -> Bool {
if targetYear % 400 == 0 { return true }
if targetYear % 100 == 0 { return false }
if targetYear % 4 == 0 { return true }
return false
}

Swift also has a built-in function to calculate if something is a multiple, isMultiple(of:) which could also provide the same outcome:

    private func isLeapYearUsingMultipleOf(_ targetYear: Int) -> Bool {
if targetYear.isMultiple(of: 400) { return true }
if targetYear.isMultiple(of: 100) { return false }
if targetYear.isMultiple(of: 4) { return true }
return false
}

These mathematical approaches do have potential limitations. They assume the rules for leap years will not change in the future, and perhaps more importantly treat past years as though they have had leap years even in cases where the rules were different or not in place at all.

A calendar-based approach might therefore be better. One approach that has been identified is to count the number of days in the target year, and see if it is 366 rather than 365:

    private func isLeapYearUsingDaysInYear(_ targetYear: Int) -> Bool {
let targetYearComponents = DateComponents(calendar: Calendar.current, year: targetYear)
let targetYearDate = Calendar.current.date(from: targetYearComponents)
return Calendar.current.range(of: .day, in: .year, for: targetYearDate!)!.count == 366
}

Alternatively, given we know leap days only fall in February in the Gregorian calendar, we could just count the number of days in February:

    private func isLeapYearUsingDaysInFebruary(_ targetYear: Int) -> Bool {
let targetYearFebruary = Calendar.current.range(of: .day, in: .month,
for: DateComponents(calendar: .current, year: targetYear, month: 2).date!)
return targetYearFebruary!.count == 29
}

The question here asks what is the most performant way to calculate a leap year. It would seem reasonable to speculate that pure mathematical approaches are likely to be more performant than methods that need to instantiate Calendar, Date and DateComponent instances. However, the best way to answer the question is through actual performance testing.

XCTest will automatically run performance tests of any code included in a self.measure block, running each measure block 10 times, averaging the results, and storing performance baselines for future regression testing.

In the case of these functions, we expect them to be fast, making single calls to these functions difficult to compare for performance testing. Therefore, we can embed a loop within the measure block, to call each function 1 million times. This test will be run through ten iterations, using ten million calls to each function to give us an average time each approach took to run 1 million times:

func testA1_mathematical_usingModulo_leapYearPerformance() throws {
self.measure {
for _ in 1...1_000_000 {
let targetYearInt = Int.random(in: 0...4000)
let result: Bool = isLeapYearUsingModulo(targetYearInt)
}
}
}

func testA2_mathematical_usingIsMultipleOf_leapYearPerformance() throws {
self.measure {
for _ in 1...1_000_000 {
let targetYearInt = Int.random(in: 0...4000)
let result: Bool = isLeapYearUsingMultipleOf(targetYearInt)
}
}
}

func testB1_date_usingDaysInYear_leapYearPerformance() throws {
self.measure {
for _ in 1...1_000_000 {
let targetYearInt = Int.random(in: 0...4000)
let result: Bool = isLeapYearUsingDaysInYear(targetYearInt)
}
}
}

func testB2_date_usingDaysInFebruary_leapYearPerformance() throws {
self.measure {
for _ in 1...1_000_000 {
let targetYearInt = Int.random(in: 0...4000)
let result: Bool = isLeapYearUsingDaysInFebruary(targetYearInt)
}
}
}

The results are instructive:

Test results for the four test methods outlined: modulo averaged 0.501 seconds for 1 million runs, isMultipleOf averaged 0.598 seconds, while usingDaysInFebruary took 10 seconds and usingDaysInYear took 38 seconds for this same 1 million runs.

Modulo was the fastest of the functions, taking on average 0.501 seconds to calculate whether 1 million integers represented leap years.

While isMultipleOf would seem likely to simply call modulo in its own implementation, it was found to be about 20% slower taking on average 0.598 seconds for the same 1 million iterations.

Date-based methods were significantly slower. Counting the number of days in February took on average 10 seconds for the same 1 million runs—20 times slower than the mathematical methods. Meanwhile, counting the number of days in a year took on average 38 seconds, so was 75 times slower than the mathematical methods.

Calendar-based approaches are certainly going to be wholly accurate, and for many applications will be the right way to proceed as they are fully informed on the complexity of calendars, and also are able to be used with non-Gregorian calendars. However, for uncomplicated applications where performance matters at all, all approaches are relatively fast and so may be functionally as good as each other, but it is clear mathematical approaches do have a significant performance advantage.

There is potential for further optimisation, however. In a comment elsewhere, Anthony noted that simply examining whether a year can be divided by 4 will eliminate 75% of years as not being leap years, without further comparisons being required, since while not all years divisible by 4 are leap years, all leap years are divisible by four. A more optimized algorithm therefore would be:

private func isLeapYearUsingOptimizedModulo(_ targetYear: Int) -> Bool {
if targetYear % 4 != 0 { return false }
if targetYear % 400 == 0 { return true }
if targetYear % 100 == 0 { return false }
return true
}

func testA3_mathematical_usingOptimizedModulo_leapYearPerformance() throws {
self.measure {
for _ in 1...1_000_000 {
let targetYearInt = Int.random(in: 0...4000)
let result: Bool = isLeapYearUsingOptimizedModulo(targetYearInt)
}
}
}

This does indeed run slightly faster—averaging 0.488 seconds for 1 million calls. However, this is not as much of a speed increase as would be expected for reducing by 2/3 the number of comparisons being made in 75% of cases.

That draws attention to the potential performance of the shared component of all the performance tests: computing random Integers for the target year. We can test the time that portion of the tests takes in isolation:

func test00_randomInteger_portionOfPerformance() throws {
self.measure {
for _ in 1...1_000_000 {
let targetYearInt = Int.random(in: 0...4000)
}
}
}

This test runs on average in 0.482 seconds, representing about 95% of the execution time of the performance tests:

Test results showing random integer test averaging 0.482s, modulo and optimised modulo approaches taking 0.495 and 0.488 seconds respectively, and date tests again taking around 10 and 38 seconds to run

Results vary slightly for previous tests on re-running, but show the same pattern. More significantly, if we subtract the 0.482 seconds of random integer calculation portion of the time from each test, we find the performance differences between mathematical and Calendar-based are even more stark:

Average execution, subtracting random integer execution time:

  • Mathematical—optimized modulo approach: 0.006 seconds
  • Mathematical—modulo approach: 0.013 seconds (2.1x slower)
  • Mathematical—isMultipleOf approach: 0.105 seconds (17.5x slower)
  • Date—count days in February: 9.818 seconds (1,636x slower)
  • Date—count days in year: 37.518 seconds (6,253x slower)

If this approach of subtracting the time taken to calculate the random integers is valid, it suggests an optimized modulo approach is 6,253 times faster than a Calendar approach counting the days in the target year.

Here I've implemented it as a computed variable that is an extension on Int, so for any integer you can just ask 2024.isALeapYear and you'll get back a Bool: true or false. You could obviously instead put the same logic in a function elsewhere.

extension Int {
var isALeapYear: Bool {
if self % 4 != 0 { return false }
if self % 400 == 0 { return true }
if self % 100 == 0 { return false }
return true
}
}

How to find out whether current year leap year or not in iphone

You can do the following:

- (BOOL)isYearLeapYear:(NSDate *) aDate {
NSInteger year = [self yearFromDate:aDate];
return (( year%100 != 0) && (year%4 == 0)) || year%400 == 0;
}

- (NSInteger)yearFromDate:(NSDate *)aDate {
NSDateFormatter *dateFormatter = [NSDateFormatter new];
dateFormatter.dateFormat = @"yyyy";
NSInteger year = [[dateFormatter stringFromDate:aDate] integerValue];
return year;
}

Count the number of Leap Year Days in a temporal difference in xcode

Ok, well, you are overcomplicating this. I think this is what you want:

NSUInteger leapYearsInTimeFrame(NSDate *startDate, NSDate *endDate)
{
// check to see if it's possible for a leap year (e.g. endDate - startDate > 1 year)
if ([endDate timeIntervalSinceDate:startDate] < 31556926)
return 0;

// now we go year by year
NSUInteger leapYears = 0;
NSUInteger startYear = [[NSCalendar currentCalendar] components:NSYearCalendarUnit fromDate:startDate].year;
NSUInteger numYears = [[NSCalendar currentCalendar] components:NSYearCalendarUnit fromDate:endDate].year - startYear;

for (NSUInteger currentYear = startYear; currentYear <= (startYear + numYears); currentYear++) {
if (currentYear % 400 == 0)
// divisible by 400 is a leap year
leapYears++;
else if (currentYear % 100 == 0)
/* not a leap year, divisible by 100 but not 400 isn't a leap year */
continue;
else if (currentYear % 4 == 0)
// divisible by 4, and not by 100 is a leap year
leapYears++;
else
/* not a leap year, undivisble by 4 */
continue;
}

return leapYears;
}

How do I calculate the number of days in this year in Objective C

I finally came up with a solution that works. What I do is first calculate the number of months in the year and then for each month calculate the number of days for that month.

The code looks like this:

NSUInteger days = 0;
NSCalendar *calendar = [NSCalendar currentCalendar];
NSDate *today = [NSDate date];
NSDateComponents *components = [calendar components:NSYearCalendarUnit fromDate:today];
NSUInteger months = [calendar rangeOfUnit:NSMonthCalendarUnit
inUnit:NSYearCalendarUnit
forDate:today].length;
for (int i = 1; i <= months; i++) {
components.month = i;
NSDate *month = [calendar dateFromComponents:components];
days += [calendar rangeOfUnit:NSDayCalendarUnit
inUnit:NSMonthCalendarUnit
forDate:month].length;
}

return days;

It is not as neat as I would have hoped for but it will work for any calendar such as the ordinary gregorian one or the islamic one.

first and last day of the current month in swift

You get the first day of the month simply with

let components = calendar.components([.Year, .Month], fromDate: date)
let startOfMonth = calendar.dateFromComponents(components)!
print(dateFormatter.stringFromDate(startOfMonth)) // 2015-11-01

To get the last day of the month, add one month and subtract one day:

let comps2 = NSDateComponents()
comps2.month = 1
comps2.day = -1
let endOfMonth = calendar.dateByAddingComponents(comps2, toDate: startOfMonth, options: [])!
print(dateFormatter.stringFromDate(endOfMonth)) // 2015-11-30

Alternatively, use the rangeOfUnit method which gives you
the start and the length of the month:

var startOfMonth : NSDate?
var lengthOfMonth : NSTimeInterval = 0
calendar.rangeOfUnit(.Month, startDate: &startOfMonth, interval: &lengthOfMonth, forDate: date)

For a date on the last day of month, add the length of the month minus one second:

let endOfMonth = startOfMonth!.dateByAddingTimeInterval(lengthOfMonth - 1)

Updated for Swift5:

extension Date {
var startOfDay: Date {
return Calendar.current.startOfDay(for: self)
}

var startOfMonth: Date {

let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents([.year, .month], from: self)

return calendar.date(from: components)!
}

var endOfDay: Date {
var components = DateComponents()
components.day = 1
components.second = -1
return Calendar.current.date(byAdding: components, to: startOfDay)!
}

var endOfMonth: Date {
var components = DateComponents()
components.month = 1
components.second = -1
return Calendar(identifier: .gregorian).date(byAdding: components, to: startOfMonth)!
}

func isMonday() -> Bool {
let calendar = Calendar(identifier: .gregorian)
let components = calendar.dateComponents([.weekday], from: self)
return components.weekday == 2
}
}


Related Topics



Leave a reply



Submit