JetForMe
(Rick M)
1
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.
sveinhal
(Svein Halvor Halvorsen)
2
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.
1 Like
Not exactly functional, but since the data is sorted, you could also find the pivots and slice them in order:
/// Make a handful of useful sections. “Today”, “Yesterday”, and “Earlier”.
/// Assumes `notifications` are sorted reverse-chronologically.
func sortIntoSections(notifications: [Notification], for now: Date = Date()) -> [NotificationSection] {
let calendar = Calendar.current, today = calendar.startOfDay(for: now)
var sections: [NotificationSection] = [], notifications = notifications[...]
/// Call in reverse chronological order
func slice(date: Date, title: String) -> NotificationSection {
let endIndex = notifications.firstIndex { date > $0.createdAt } ?? notifications.endIndex
defer { notifications = notifications[endIndex...] } // Do you need this?
return NotificationSection(title: title, startDate: date, notifications: .init(notifications[..<endIndex]))
}
sections.append(slice(date: today, title: "Today"))
if let yesterday = calendar.date(byAdding: .day, value: -1, to: today) {
sections.append(slice(date: yesterday, title: "Yesterday"))
}
sections.append(slice(date: Date.distantPast, title: "Earlier")) // Could use custom item here
return sections
}
Your code would put Today items in the Yesterday section, so maybe you don't need the defer block.
mayoff
(Rob Mayoff)
4
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...])
}
}
We can use it like this:
extension Collection where Element == Notification {
func sectioned(asOf now: Date) -> [NotificationSection] {
let cal = Calendar.current
let startOfToday = cal.startOfDay(for: now)
let startOfYesterday = cal.date(byAdding: .day, value: -1, to: startOfToday)
let (front: todayNotes, back: notTodayNotes) = self.firstSplit { $0.createdAt < startOfToday }
let yesterdayNotes: SubSequence, earlierNotes: SubSequence
if let startOfYesterday = startOfYesterday {
(front: yesterdayNotes, back: earlierNotes) = notTodayNotes.firstSplit { $0.createdAt < startOfYesterday }
} else {
yesterdayNotes = notTodayNotes[..<notTodayNotes.startIndex]
earlierNotes = notTodayNotes
}
func make(_ title: String, _ start: Date?, _ notes: SubSequence) -> NotificationSection? {
guard let start = start else { return nil }
return .init(title: title, startDate: start, notifications: .init(notes))
}
return [
make("Today", startOfToday, todayNotes),
make("Yesterday", startOfYesterday, yesterdayNotes),
make("Earlier", .distantPast, earlierNotes)
].compactMap { $0 }
}
}
3 Likes