2. Leap

Jun 21, 2020 | 21:58
Stored vs computed properties, brevity vs readability.

Listen and/or subscribe via:

Hello everyone, today we're looking at the Leap exercise found at exercism.io. More specifically we're comparing two common approaches to solving this problem and discussing the benefits and drawbacks of each. Let's get started.

Exercise

Given a year, report if it is a leap year according to the following rules. A leap year in the Gregorian calendar occurs:

  1. On every year that is evenly divisible by 4.
  2. Except every year that is evenly divisible by 100.
  3. Unless the year is also evenly divisible by 400.

Here are the tests:

class LeapTests: XCTestCase {
    func testVanillaLeapYear() {
        let year = Year(calendarYear: 1996)
        XCTAssertTrue(year.isLeapYear)
    }

    func testAnyOldYear() {
        let year = Year(calendarYear: 1997)
        XCTAssertFalse(year.isLeapYear)
    }

    func testCentury() {
        let year = Year(calendarYear: 1900)
        XCTAssertFalse(year.isLeapYear)
    }

    func testExceptionalCentury() {
        let year = Year(calendarYear: 2400)
        XCTAssertTrue(year.isLeapYear)
    }
}

So we need a type Year which can be initialized with a calendarYear: Int argument and who's instances have a isLeapYear: Bool property.

Stored vs computed properties

There are two common approaches to solving this problem.

  1. Implement isLeapYear as a stored property and compute it's value in the initializer.
  2. Store the calendarYear argument as a stored property and implement isLeapYear as a computed property.
// 1. Stored property
struct Year {
    let isLeapYear: Bool

    init(calendarYear: Int) {
        self.isLeapYear = calendarYear.isMultiple(of: 4) 
            && !calendarYear.isMultiple(of: 100) 
            || calendarYear.isMultiple(of: 400)
    }
}

// 2. Computed property
struct Year {
    let calendarYear: Int

    var isLeapYear: Bool {
        return calendarYear.isMultiple(of: 4) 
            && !calendarYear.isMultiple(of: 100) 
            || calendarYear.isMultiple(of: 400)
    }
}

So which approach should we use? In this case I'm going with the stored property version for the following reasons.

Immutability

isLeapYear will always return the same value because calendarYear can not be changed after initialization. There is really no benefit to re-computing the value on each access. The only effect is increased CPU usage which decreases battery life on battery powered devices.

This would be different if calendarYear could be changed after initialization. In that case a computed property would be useful by ensuring the correct value is always returned even as calendarYear is changed.

Speed

Returning the pre-computed isLeapYear value from memory will be faster than re-computing it on each access. Having said that, I'm not sure the difference will be perceivable in this case because the logic is simple math that modern processors can do very fast.

Memory

I haven't actually tested this, but I imagine storing the isLeapYear: Bool will use less memory than storing the provided calendarYear: Int. I'm assuming a Bool uses 1 bit whereas an Int probably uses 64 bits. I could be wrong here.

So that's my thinking, but what do you think? Stored or computed property?

Brevity vs readability

I realize readability is quite subjective. So rather than think of it as "how easy is it for me to read this?", I'm thinking of readability as "how complex or simple is this?". The simpler the code, the more readable I consider it. For example, let's look at our existing isLeapYear logic:

self.isLeapYear = calendarYear.isMultiple(of: 4) 
    && !calendarYear.isMultiple(of: 100) 
    || calendarYear.isMultiple(of: 400)

There are at least five separate concepts happening in this one statement:

  1. Property assignment into isLeapYear property.
  2. Calculation of results from isMultiple(of:).
  3. Inversion of second isMultiple(of:) result via ! operator.
  4. Combining of returned values with && and || operators.
  5. Order of operations of the && and || operators.

Personally I find this too complex and prefer limiting each statement to two or three concepts. Let's break this up into multiple statements using descriptively named variables to serve as documentation and parenthesis to clarify order of operations.

let isEvenlyDivisbleBy4 = calendarYear.isMultiple(of: 4)
let isNotEvenlyDivisibleBy100 = calendarYear.isMultiple(of: 100) == false
let isEvenlyDivisibleBy400 = calendarYear.isMultiple(of: 400)

self.isLeapYear = isEvenlyDivisbleBy4 
    && (isNotEvenlyDivisibleBy100 || isEvenlyDivisibleBy400)

This code is certainly more verbose but also less complex. Each statement now has at most three concepts.

What do you think? Brevity or readability?

Next episode

Next episode we'll be looking at the Gigasecond exercise found at exercism.io. Feel free to complete that exercise before the next episode and we can compare our answers.

Take care everyone.