Part of Swift with Copilot

Swift: Is 2023 a leap year?

Sandy LaneSandy Lane

Video: Swift: Is 2023 a leap year? by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Swift with Copilot: Is 2023 a Leap Year?

Leap year rule: divisible by 4 AND (not divisible by 100 OR divisible by 400). 2023 is not a leap year. The cleanest Swift implementation: (year % 4 == 0 && year % 100 != 0) || year % 400 == 0.

A staple of programming exercises — and the rule has more edge cases than people remember.

The Copilot prompt

// Check if 2023 is a leap year

Copilot completes:

func isLeapYear(_ year: Int) -> Bool {
  return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

print(isLeapYear(2023))   // false
print(isLeapYear(2024))   // true
print(isLeapYear(2000))   // true
print(isLeapYear(1900))   // false

The leap year rule (full)

A year is a leap year if:

  1. Divisible by 4, AND
  2. Not divisible by 100, UNLESS
  3. Divisible by 400.

So: - 2024 — divisible by 4, not by 100. Leap. - 1900 — divisible by 4, divisible by 100, not by 400. Not leap. - 2000 — divisible by 4, by 100, and by 400. Leap.

The full rule was added to handle the small drift in the Gregorian calendar. The "every 4 years" simplification works for most lifetimes but breaks at century boundaries.

Implementations

One-liner

func isLeapYear(_ year: Int) -> Bool {
  (year % 4 == 0 && year % 100 != 0) || year % 400 == 0
}

Implicit return for single expressions in Swift.

Verbose if/else

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

Reads top-down: check the most-specific rule first, fall through to general.

Foundation's Calendar

import Foundation

func isLeapYear(_ year: Int) -> Bool {
  var components = DateComponents()
  components.year = year
  components.month = 2
  components.day = 29

  let calendar = Calendar(identifier: .gregorian)
  return calendar.date(from: components) != nil
}

"Try to construct Feb 29; if it succeeds, it's a leap year." More verbose than the math, but uses the canonical calendar logic — matches whatever the OS thinks.

For the Gregorian calendar (what we usually mean), the math is fine.

Other calendars

The Gregorian leap year rule is specific to Gregorian. Different calendars have different rules:

  • Julian: divisible by 4. Period. (Hence the small drift the Gregorian fixes.)
  • Islamic (Hijri): different system.
  • Hebrew: 7 in every 19 years.
  • Persian: complex algorithm.

Calendar handles them all:

let cal = Calendar(identifier: .hebrew)
// ... cal.date(from: components) ...

For business code dealing with multiple cultures, prefer Calendar over hand-rolled math.

Days in February

The simplest practical use of isLeapYear:

func daysInFebruary(_ year: Int) -> Int {
  isLeapYear(year) ? 29 : 28
}

Or via Calendar:

import Foundation

func daysInFebruary(_ year: Int) -> Int {
  let cal = Calendar(identifier: .gregorian)
  var c = DateComponents(); c.year = year; c.month = 2
  let date = cal.date(from: c)!
  return cal.range(of: .day, in: .month, for: date)!.count
}

range(of:in:for:) returns the range of valid values. For days in a month, that's 1..<29 or 1..<30 depending on the month. .count gives the size.

This works for any month, not just February.

A range of years

let leapYearsThisCentury = (2000...2099).filter(isLeapYear)
print(leapYearsThisCentury.count)   // 25 — every 4 years

Sequence.filter accepts any (Element) -> BoolisLeapYear matches.

Avoiding integer overflow

For Int, no concerns. For huge years (Int.max), % 400 is always safe. No risk.

Common stumbles

Forgetting the 400 rule. "Every 4 years" gets 1900 wrong. Use the full rule.

% with negatives. Swift's % returns negative for negative dividend: -7 % 4 == -3. For year math, all years are positive, so safe.

Calendar approach for non-Gregorian. Don't use (y % 4 == 0 && ...) for Hebrew or Islamic dates — different rules. Use Calendar.

Year 0. Gregorian has no year 0; year 1 BCE (or -1 astronomically) is preceded by year 1 CE. Most code uses astronomical numbering (year 0 exists). For business code: just ignore.

isLeapYear(2023.5). Won't compile — Int parameter. Cast or pass an Int.

What's next

Episode 9: Capitalize and decapitalize a list. map with string methods.

Recap

Leap year: divisible by 4 AND (not by 100 OR by 400). Implementation: (y % 4 == 0 && y % 100 != 0) || y % 400 == 0. For non-Gregorian calendars, use Calendar.date(from:) and check for nil. Calendar.range(of:in:for:) gives days in a month, accounting for leap years automatically.

Next episode: capitalize / decapitalize.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.