Grouping Array of Objects by Custom Time Intervals

Viewed 66

I have an array of posts which I currently group by day. I use the below functions to do so;

private func splitDay(from date: Date) -> Date {
    let calendar = Calendar.current
    let components = calendar.dateComponents([.year, .month, .day], from: date)
    return calendar.date(from: components)!
}

private func sectionPosts(posts: [Post]) {
    let groups = Dictionary(grouping: posts) { (posts) in
        return splitDay(from: Date(timeIntervalSince1970: posts.createdOn))
    }
    self.sections = groups.map { (date, posts) in
        return PostSection(date: date, posts: posts)
    }
}

However, i'd like to implement a custom grouping which would be the following;

  1. Today
  2. Yesterday
  3. This Week
  4. This Month
  5. Older

How would I build this into my grouping function? My section struct is like such;

struct PostSection {
    var date: Date
    var posts: [Post]
}
2 Answers

Since you are trying to group your data into groups such as "today", "yesterday", "this week" and "this month", you should first create a type to represent these groups:

enum PostGroup {
    case today
    case yesterday
    case thisWeek
    case thisMonth
    case older
    case coming // added "coming" group so that the groups cover all possible dates
}

Then your PostSection struct would have a PostGroup property, rather than a Date property:

struct PostSection {
    let group: PostGroup
    let posts: [Post]
}

Now we just need a function that goes (Post) -> PostGroup that we can pass to Dictionary(grouping:by:). This can be implemented just by comparing the date components of the post date with that of today:

func group(for post: Post) -> PostGroup {
    let today = Date()
    let calendar = Calendar.current
    let postDateComponents = calendar.dateComponents([.year, .month, .day], from: post.createdOn)
    let todayDateComponents = calendar.dateComponents([.year, .month, .day], from: today)
    if postDateComponents == todayDateComponents {
        return .today
    }
    let daysDifference = calendar.dateComponents([.day], from: postDateComponents, to: todayDateComponents)
    if daysDifference.day == 1 {
        return .yesterday
    }
    let postWeekComponents = calendar.dateComponents([.weekOfYear, .yearForWeekOfYear], from: post.createdOn)
    let todayWeekComponents = calendar.dateComponents([.weekOfYear, .yearForWeekOfYear], from: today)
    if postWeekComponents == todayWeekComponents {
        return .thisWeek
    }
    if postDateComponents.year == todayDateComponents.year &&
        postDateComponents.month == todayDateComponents.month {
        return .thisMonth
    }
    if post.createdOn < today {
        return .older
    } else {
        return .coming
    }
}

To finish it off:

private func sectionPosts(posts: [Post]) {
    let groups = Dictionary(grouping: posts, by: group(for:))
    self.sections = groups.map { (group, posts) in
        return PostSection(group: group, posts: posts)
    }
}

This is not an elegant solution, but it solves the problem, please feel free to optimize this function, specially since date calculations can be slow.

private func preSplitProcess(from date: Date) -> Date {
    let calendar = Calendar.current
    let isToday = calendar.isDateInToday(date)
    
    if isToday {
        return date
    } else {
        let wasYesterday = calendar.isDateInYesterday(date)
        
        if wasYesterday {
            return date
        } else {
            let bowComponents = calendar.dateComponents([.weekOfYear], from: date)
            let beginningOfWeek = calendar.date(from: bowComponents)! // Handle errors, please
            
            if beginningOfWeek < date {
                return date
            } else {
                let bomComponents = calendar.dateComponents([.month], from: date)
                let beginningsOfMonth = calendar.date(from: bomComponents)! // Handle errors, please
                
                if beginningsOfMonth < date {
                    return date
                } else {
                    
                    var components = DateComponents()
                    components.year = 1970
                    components.month = 01
                    components.day = 01
                    let oldDate = calendar.date(from: components)!
                    
                    return oldDate
                }
            }
        }
    }
}

Add this before you call your splitDay function, since this doesn't reduce the return dates to the same date.

Related