The Per Rewrite Diary: Day 3

This post is part of a series about rewriting my iOS app, Per. Per is a price per unit comparison app with a bunch of neat convenience figures, but it hasn’t been updated in years, so I’m rewriting it from scratch to eliminate a bunch of technical debt. Just because it’s not an open-source app doesn’t mean I can’t share what I learn as I go!

See the rest of the series here.

What’s that smell?

So yesterday, I posted about making Per’s Product model conform to Comparable, so that I can compare between Product objects — something that’s pretty important in a price-comparison app.

Comparing the actual price-per-unit is pretty straightforward:

static func <(lhs: Self, rhs: Self) -> Bool {
    return lhs.pricePerUnit < rhs.pricePerUnit
}

static func ==(lhs: Self, rhs: Self) -> Bool {
    return lhs.pricePerUnit == rhs.pricePerUnit
}

We can’t just leave it at that, though, because of Per’s built-in unit conversion — you can input one product’s quantity in pounds and another product’s quantity in grams, and Per will figure out the best-value option automatically. This means that Per’s Product model needs to understand how units compare; you can’t compare the price per unit between one thing sold by the kilogram, and something else sold by the quart.

The Product model stores units as an optional Unit, and we’re going to create the initializer such that it’ll set that property as either some flavour of UnitMass (for items sold by weight) or UnitVolume (for items sold by volume), or otherwise nil (for dimensionless units). I started with an ugly nested-conditional mess for figuring out whether lhs.units and rhs.units can be compared. It works, but it feels like a code smell:

static func <(lhs: Self, rhs: Self) -> Bool {
    if let lhsUnitType = lhs.units {
        // lhs.units is not nil
        if let rhsUnitType = rhs.units {
            // rhs.units is not nil
            if (type(of: lhsUnitType) == type(of: rhsUnitType)) {
                return lhs.pricePerUnit < rhs.pricePerUnit
            } else {
                return false
            }
        } else {
            // rhs.units is nil
            return false
        }
    } else {
        // lhs.units is nil
        if rhs.units != nil {
            // rhs.units is not nil
            return false
        } else {
            // rhs.units is nil
            return lhs.pricePerUnit < rhs.pricePerUnit
        }
    }
}

We can make this much, much cleaner:

static func ==(lhs: Self, rhs: self) -> Bool {
    // If both lhs and rhs units nil, then evaluate the boolean expression:
    if (lhs.units == nil && rhs.units == nil) { return lhs.pricePerUnit == rhs.pricePerUnit }

    // If they're not BOTH nil, but any ONE is nil, return false:
    guard let lhsUnitType = lhs.units else { return false }
    guard let rhsUnitType = rhs.units else { return false }

    // If neither is nil, but they're of the same type, evaluate the boolean expression:
    if (type(of: lhsUnitType) == type(of: rhsUnitType)) { return lhs.pricePerUnit == rhs.pricePerUnit }

    // If we get to this point, they're not of the same type, so return false:
    return false
}

That feels much cleaner. Tomorrow, let’s explore how that initializer reasons about unit types!

Angelo Stavrow

Montreal, Canada
Email me

Mobile/full-stack developer. Montrealer. Internet gadabout. Your biggest fan.