This article is also available in Chinese.

A side project I’m currently working needs an understanding of lots of different kinds of units. (I should probably be working on getting that off the ground instead of writing this blog post. Nevertheless.)

I’ve always found modeling units to be a fascinating programming problem. For time, for example, if you have an API that accepts a time, it’s probably going to accept seconds (or perhaps milliseconds! who can know!), but sometimes, you need to express a time like 2 hours. So instead of a magic number (7200, for the number of seconds in an hour), you write 2 * 60 * 60, perhaps adding spaces in between the operators to aid in “readability”.

7200, though, doesn’t mean anything. If you look at long enough and you have the freakish knack for manipulating mathematic symbols in your head, you might recognize it as two hours in seconds. If it weren’t a round number of hours, though, you never could.

And as that 7200 winds its way through the bowels of your application, it becomes less and less clear what units that mere integer is in.

A way to associate our integer with some metadata is what we need. Types have been described as units before, but can we bring that back to to units of measure, describing them with types? That can prevent us from adding 2 hours with 30 minutes and getting a meaningless result of 32.

(While it’s possible to handle this at the language level, most languages don’t have support for stuff like this.)

We still want to be able to add 2 hours to 30 minutes and get a meaningful result, so in our type system Time needs to be an entity, but Hours and Seconds do too.

Multiple things can be a Time, and each of those things must have a way to represented in seconds:

protocol Time {
    var inSeconds: Double { get }
}

Each unit of time will each be its own thing, but it will also be a Time.

struct Hours: Time {
    let value: Double
    
    var inSeconds: Double {
        return value * 3600
    }
}

struct Minutes: Time {
    let value: Double
    
    var inSeconds: Double {
        return value * 60
    }
}

We could add similar structs for Seconds, Days, Weeks, et cetera, understanding that we’ll lose some precision as we go up in scale.

Now that we have a shared understanding of how our units of measure can be represented, we can manipulate that unit.

func + (lhs: Time, rhs: Time) -> Time {
    return Seconds(value: lhs.inSeconds + rhs.inSeconds)
}

We can also add some handy conversions for ourselves:

extension Time {
    var inMinutes: Double {
        return inSeconds / 60
    }
    
    var inHours: Double {
        return inMinutes / 60
    }
}

And create a DSL-like extension onto Int, helpfully cribbed from ActiveSupport:

extension Int {
    var hours: Time {
        return Hours(value: Double(self))
    }
    
    var minutes: Time {
        return Minutes(value: Double(self))
    }
}

Which lets us write a short, simple, expressive line of code that leverages our type system.

let total = 2.hours + 30.minutes

(This result will of course be in Seconds so we will want some kind of presenter to reduce the units so that you can display this value in a meaningful way to the user. My side project has affordances for this. The side project is, unfortunately, in JavaScript, so no such type system fun will be had.)