Today I had to write some code to sort notifications into three bins: Today, Yesterday, and Earlier. I thought I wrote some nice code but in the end got bitten by value semantics. So I started thinking about what a "more functional" solution would look like. I came up with one, but it has the downside of traversing the input array once for each bin, which my original solution didn't have to do, but failed in the face of the value semantics of the bin struct.
I figure this must have a nice, elegant, and purely-functional answer. Seems like a common-enough operation. Here’s the less-efficient way I came up with:
struct
NotificationSection
{
var title : String
var startDate : Date
var notifications : [Notification]
}
struct
Notification
{
var message : String
var createdAt : Date
}
// Assumes inNotifications are sorted reverse-chron…
func
sortIntoSections(notifications inNotifications: [Notification], for inDate: Date = Date())
-> [NotificationSection]
{
// Make a handful of useful sections. “Today”, “Yesterday”, and “Earlier”…
var sections = [NotificationSection]()
let cal = Calendar.current
let now = inDate
let startOfToday = cal.startOfDay(for: now)
var section = NotificationSection(title: "Today",
startDate: startOfToday,
notifications: [Notification]())
sections.append(section)
if let startOfYesterday = cal.date(byAdding: .day, value: -1, to: startOfToday) // If this fails, we just won’t have a Yesterday section
{
section = NotificationSection(title: "Yesterday",
startDate: startOfYesterday,
notifications: [Notification]())
sections.append(section)
}
section = NotificationSection(title: "Earlier",
startDate: Date.distantPast,
notifications: [Notification]())
sections.append(section)
sections = sections.map
{ inSection in
var section = inSection
section.notifications = inNotifications.filter { section.startDate <= $0.createdAt }
return section
}
return sections
}
Because the notifications come in in order, it should be possible to stop traversing when the first one is found that does not belong to the current section, and then start traversing again from that point. I tried various ways of composing prefix() and suffix() and dropFirst() but none were particularly nice.
I figured I'm just not very good at thinking functionally, so I'm probably overlooking an elegant solution.
You can use Dictionary.init(grouping:by:) to group a sequence of items into buckets. You pass it your list of notifications, and a closure that converts a notification into one some hashable bucket key. You can then Dictionary.map over that to turn a key-value sequence of bucket keys and notifications into a list of NotificationSections.
Suppose we had a function that splits a Collection into two SubSequences at the first Element satisfying a predicate. Then we could split the array of Notifications at the first element older than today, giving us today's notes and not-today's notes. Then we could split not-today's notes at the first element older then yesterday, giving us yesterday's notes and not-yesterday's notes.
Here's the splitter:
extension Collection {
func firstSplit(
where predicate: (Element) throws -> Bool
) rethrows -> (
front: SubSequence, // none of these satisfy `predicate`
back: SubSequence // the first of these (if it exists) satisfies `predicate`
) {
let index = try firstIndex(where: predicate) ?? endIndex
return (front: self[..<index], back: self[index...])
}
}