简体   繁体   中英

Swift - Group an array of items by week of year over a large time frame

I'm currently working on a project that requires me to manipulate an array of objects that take a date and a double value. I am able to break it down in a dictionary of years and then those nested arrays down into weeks of that year. Unfortunately the first week of the year is often present in one of the years and the following.

What I want to achieve is an array or dictionary of all dates grouped down to each individual week and the sum of all amounts in that group.

I've already tried grouping them by .weekOfYear , how ever that doesn't take into consideration the year itself, and if the two dates were more than a year apart, I would always only have an array 52 entries long.

I've also looked into using the ordinality of the week by using Calendar.current.ordinality(of: .weekOfYear, in: .era, for: 'date') but it gets converted to an integer and I'm not sure how to then return it back to a date

Below is a copy of my code that I'm using in an xCode playground and it's output

import Foundation

let cal = Calendar.current

extension Date {
    var year: Int {
        return cal.component(.year, from: self)
    }
    var week: Int {
        return cal.component(.weekOfYear, from: self)
    }
    var weekAndYear: DateComponents {
        return cal.dateComponents([.weekOfYear, .year], from: self)
    }
}

struct Item {
    var date: Date
    var amount: Double
}

let startDate: Date = cal.date(from: DateComponents(year: 2018, month: 12, day: 2))!
let endDate: Date = cal.date(from: DateComponents(year: 2019, month: 1, day: 28))!

// Here I'm creating dummy items for each date between our date range
func setUpItems (startDate: Date, endDate: Date) -> [Item] {
    var date = startDate
    var inputArray: [Item] = []

    let dateCount = cal.dateComponents([.day], from: startDate, to: endDate).day
    let valuePerDay: Double = 1000 / Double(dateCount!)
    let lowerLim = valuePerDay - (valuePerDay / 2)
    let upperLim = valuePerDay + (valuePerDay / 2)

    while date < endDate {
        let input = Double.random(in: lowerLim...upperLim).rounded()
        inputArray.append(Item(
            date: date,
            amount: input
        ))
        date = cal.date(byAdding: .day, value: 1, to: date)!
    }

    return inputArray
}

let itemArray = setUpItems(startDate: startDate, endDate: endDate)


let grouppedByYear = Dictionary(grouping: itemArray, by: { $0.date.year })

let grouppedByYearThenWeek = grouppedByYear
    .mapValues{(yearArray: [Item]) -> Dictionary<Int, Double> in
        return Dictionary(grouping: yearArray, by: { $0.date.week })
            .mapValues{ (value: [Item]) in
                return value.map{ $0.amount }.reduce(0, +)
        }
    }

print("Grouped by week")
print(grouppedByYearThenWeek)

From this you can expect the output to look something like this

Grouped by week
[2018: [49: 127.0, 52: 125.0, 50: 119.0, 51: 113.0, 1: 39.0],
  2019: [3: 124.0, 4: 122.0, 5: 19.0, 1: 87.0, 2: 121.0]]

I would hope to end up with a dictionary that doesn't split the week up dependent on the year

[2018: [49: 127.0, 52: 125.0, 50: 119.0, 51: 113.0],
  2019: [3: 124.0, 4: 122.0, 5: 19.0, 1: 126.0, 2: 121.0]]

// or potentially

[["49 2018": 127.0, "52 2018": 125.0...],
  ["3 2019": 124.0, "4 2019": 122.0...]]

Any help would be greatly appreciated.

Unfortunately the first week of the year is often present in one of the years and the following.

Yes. But if you're retrieving the week of year, it makes no sense to retrieve that with .year . You want to use .yearForWeekOfYear in conjunction with .weekOfYear :

var weekAndYear: DateComponents {
    return cal.dateComponents([.weekOfYear, .yearForWeekOfYear], from: self)
}

var yearForWeekOfYear: Int {
    return cal.component(.yearForWeekOfYear, from: self)
}

And you might find it useful to have a “start of week” computed property:

var startOfWeek: Date {
    let components = cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: self)
    return cal.date(from: components)!
}

It comes down to how do you want do you want to treat 31 Dec 2018. Do you want it to appear in week 1 of 2019 (like the standard weekOfYear and yearForWeekOfYear does), or do you want to attribute it to “week 53 of 2018”. In short, do you want to treat the week starting Dec 30th 2018 as a single week (which the above will achieve), or do you want to split it up, treating the start of the week as week 53 of 2018 and the latter part of the week as week 1 of 2019?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM