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.