Home > front end >  Swift split array into chunks based on total value
Swift split array into chunks based on total value

Time:09-30

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?

CodePudding user response:

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)

CodePudding user response:

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.

  • Related