2

I am looking for a way to split an array into chunks with a max value, but can't seem to find a solution.

Lets say we have the following code:

struct FooBar {
    let value: Int
}

let array: [FooBar] = [
    FooBar(value: 1),
    FooBar(value: 2),
    FooBar(value: 1),
    FooBar(value: 1),
    FooBar(value: 1),
    FooBar(value: 2),
    FooBar(value: 2),
    FooBar(value: 1)
]

And we want to split this into chunks where the maxSize of FooBar.value doesn't exceed 3. The end result should be something like:

let ExpectedEndResult: [[FooBar]] = [
    [
        FooBar(value: 1),
        FooBar(value: 2)
    ],
    [
        FooBar(value: 1),
        FooBar(value: 1),
        FooBar(value: 1)
    ],
    [
        FooBar(value: 2),
    ],
    [
        FooBar(value: 2),
        FooBar(value: 1)
    ]
]

I've written this so far, but there is an issue when a 3rd item could be added, also... I believe there must be simpler way but I just can't think of one right now:

extension Array where Element == FooBar {

    func chunked(maxValue: Int) -> [[FooBar]] {
        var chunks: [[FooBar]] = []
        var chunk: [FooBar] = []

        self.enumerated().forEach { key, value in
            chunk.append(value)
            if self.count-1 > key {
                let next = self[key+1]

                if next.value + value.value > maxValue {
                    chunks.append(chunk)
                    chunk = []
                }
            } else {
                chunks.append(chunk)
            }
        }

        return chunks
    }
}

Any suggestions?

Paul Peelen
  • 10,073
  • 15
  • 85
  • 168
  • I think the question needs greater detail. Are you looking for the minimum number of chunks, to have as many chunks as possible with the max value, or some other criteria. Otherwise, at the extreme, each item could be its own chunk and still meet the condition of having a value < 3. – flanker Sep 30 '22 at 00:40
  • You say “I am looking for a way to split an array into chunks with a max value”. Do you mean you want to split your array into chunks where the size of each chunk is ≥ some size? So if that size is 3, then you want to create an array of arrays where each sub-array contains 3 or less items? Is that what you mean by “max value”? – Duncan C Sep 30 '22 at 02:21
  • Sorry for my unclarity. What I meant what chunks where the combined (total) value of `FooBar.value` for the items in each chuck does not exceed X. – Paul Peelen Sep 30 '22 at 06:29

2 Answers2

3

I would use reduce(into:) for this

let maxValue = 3 //limit
var currentValue = 0 // current total value for the last sub array
var index = 0 // index of last (current) sub array
let groups = array.reduce(into: [[]]) {
    if $1.value > maxValue || $1.value + currentValue > maxValue {
        $0.append([$1])
        currentValue = $1.value
        index += 1
    } else {
        $0[index].append($1)
        currentValue += $1.value
    }
}

To make it more universal, here is a generic function as an extension to Array that also uses a KeyPath for the value to chunk over

extension Array {
    func chunk<ElementValue: Numeric & Comparable>(withLimit limit: ElementValue, 
                                                   using keyPath: KeyPath<Element, ElementValue>) -> [[Element]] {
        var currentValue = ElementValue.zero
        var index = 0

        return self.reduce(into: [[]]) {
            let value = $1[keyPath: keyPath]
            if value > limit || value + currentValue > limit {
                $0.append([$1])
                currentValue = value
                index += 1
            } else {
                $0[index].append($1)
                currentValue += value
            }
        }
    }
}

Usage for the sample

let result = array.chunk(withLimit: 3, using: \.value)
Joakim Danielson
  • 43,251
  • 5
  • 22
  • 52
  • Very nice solution. Thank you! Just what I was looking for; I knew there was a less complicated way, just couldn't think of it :P – Paul Peelen Sep 30 '22 at 10:02
1

Something like:

extension Array where Element == FooBar {
    
    func chunked(maxValue: Int) -> [[FooBar]] {
        var chunks: [[FooBar]] = []
        var chunk: [FooBar] = []
        
        let filtered = self.filter({ item in
            item.value <= maxValue
        })
        
        filtered.enumerated().forEach { index, foo in
            
                let currentTotal = chunk.reduce(0, { sum, nextFoo in sum + nextFoo.value })
                
                let newValue = currentTotal + foo.value
                
                if newValue < maxValue {
                    chunk.append(foo)
                } else if newValue == maxValue {
                    chunk.append(foo)
                    chunks.append(chunk)
                    chunk = []
                } else {
                    chunks.append(chunk)
                    chunk = [foo]
                }
            }
        
        return chunks
    }
}

It could be interesting to write something that goes looking in the array for the perfect groups. The problem with the sequential approach is that one can end up with groups are very low in value when there are perfectly good foos that could fit in the chunk, but they just aren't the next item.

Edit: Added a filter for values above maxValue ... just in case.

carlynorama
  • 166
  • 7
  • Of course! Thank you! Yes, that is a "problem", but only one where compactness is priority over order. In my case these are for "boxes" in an UI that need to come in a certain order. – Paul Peelen Sep 30 '22 at 06:27
  • Makes sense! Order preservation over compactness seems like a good trade off. I agree Joakim's answer is very tight. – carlynorama Sep 30 '22 at 19:49