3

I'm trying to downsample a long collection by decimating or extracting every nth element.

Here's what I got for my array extension:

func downsampled(to threshold: Int) -> [T] {
    // Validate that threshold falls in valid range
    guard !isEmpty, 1...count ~= threshold else { return Array(self) }
    
    let skip = (count / threshold) + 1
    var index = 0
    
    var items = [T]()
    while index < count {
        items.append(self[index])
        index += skip
    }
    
    return items
}

I'm expecting 50-100k items in the original array and will probably downsample to the native bounds width of the screen (500-1k points).

Is there a more concise or efficient way of doing this?

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
TruMan1
  • 33,665
  • 59
  • 184
  • 335

1 Answers1

6
extension RangeReplaceableCollection {
    func every(from: Index? = nil, through: Index? = nil, nth: Int) -> Self { .init(stride(from: from, through: through, by: nth)) }
}

extension Collection {
    func stride(from: Index? = nil, through: Index? = nil, by: Int) -> AnySequence<Element> {
        var index = from ?? startIndex
        let endIndex = through ?? self.endIndex
        return AnySequence(AnyIterator {
            guard index < endIndex else { return nil }
            defer { index = self.index(index, offsetBy: by, limitedBy: endIndex) ?? endIndex }
            return self[index]
        })
    }
}

Playground testing

let array = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
for element in array.stride(by: 3) {
    print(element)
}
array.stride(by: 3).forEach {
    print($0)
}
let nth = array.every(nth: 3)  // [1, 4, 7, 10, 13]

let str = "0123456789"
for character in str.stride(by: 2) {
    print(character)
}
str.stride(by: 2).forEach {
    print($0)
}
let even = str.every(nth: 2)   // "02468"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Just for fun: `return enumerated().filter { $0.0 % nth == 0 }.map { $0.1 }` will make it a bit shorter, but probably worse in efficiency. – Eendje Dec 08 '16 at 02:16
  • Why not call `result.reserveCapacity()`? – Jeffery Thomas Dec 08 '16 at 03:04
  • Yeah, move those goalposts. :) – matt Dec 08 '16 at 05:19
  • Not at all. The use of `map` vs. your appending to an original empty array was exactly the source of my approach's advantage. I was careful to take a completely different approach. Now you're just taking my ideas and running with them. Nothing wrong with that, but it's not fun for me any more. I'm deleting my answer. – matt Dec 08 '16 at 05:24