A Tale of Two Dates
Recently, I discovered a curious thing about Dates in two large projects
I work on. Simply put, both projects receives, from various HTTP endpoints, the
same object component: a timestamp, and a duration. Combining these two pieces,
both projects derives two Foundation.Dates to represent a time range. So far,
so good.
However, project A uses Fonudation.DateInterval to represent this concept,
while project B uses Range<Date>. But why? Why represent the same component
differently? What a gigantic waste of brain power for everyone on both projects!
So I set out to unify this thing. Wherever a Range literal is used, I swap in
DateInterval.init(start:end:); Range.lowerBound becomes
DateInterval.start; Range.upperBound becomes DateInterval.end, etc. It
didn't take long to complete the conversion to DateInterval in project B,
now it builds and runs!
Oh, wait, why are some tests failing in project B? Shouldn't this just be an
mechanical change?
I spent time investigating. The failing tests are for some very specific business logic that I'm not familiar with. So things took a while to become clear. What felt like a long time later, I realized my mistake.
(I'm sorry if this has been obvious to you. You are a better Swift programmer!)
Somewhere in project B is the following:
struct Item {
let range: Range<Date>
}
struct Container {
let items: [Item]
}
func containerFactory(range: Range<Date>, items: [Item]) -> Container {
/// pretend there's more code here
return Conatiner(items: items.filter { $0.range.contains(range.lowerBound) })
}
And of course, after my "refactor", it became
struct Item {
let range: DateInterval
}
struct Container {
let items: [Item]
}
func containerFactory(range: DateInterval, items: [Item]) -> Container {
/// pretend there's more code here
return Conatiner(items: items.filter { $0.range.contains(range.start) })
}
Tests for containerFactory failed. And here's why: DateInterval.contains
is inclusive for its upper bound (.end), whereas Range.contains isn't! You
can see it plainly by running the following
import Foundation
let sooner = Date(timeIntervalSince1970: 0)
let later = sooner.addingTimeInterval(1000)
DateInterval(start: sooner, end: later).contains(later) // true
(sooner..<later).contains(later) // false
So here's what stumped me:
-
The 2 projects chose to interpret the same component differently, which I didn't not expect.
-
I didn't know how
Foundation.DateIntervalworks.
Well, today I learned.