14

Problem

Given an array of values how can I split it into sub-arrays made of elements that are equal?

Example

Given this array

let numbers = [1, 1, 1, 3, 3, 4]

I want this output

[[1,1,1], [3, 3], [4]]

What I am NOT looking for

A possible way of solving this would be creating some sort of index to indicate the occurrences of each element like this.

let indexes = [1:3, 3:2, 4:1]

And finally use the index to rebuild the output array.

let subsequences = indexes.sort { $0.0.0 < $0.1.0 }.reduce([Int]()) { (res, elm) -> [Int] in
    return res + [Int](count: elm.1, repeatedValue: elm.0)
}

However with this solution I am losing the original values. Of course in this case it's not a big problem (an Int value is still and Inteven if recreated) but I would like to apply this solution to more complex data structures like this

struct Starship: Equatable {
    let name: String
    let warpSpeed: Int
}

func ==(left:Starship, right:Starship) -> Bool {
    return left.warpSpeed == right.warpSpeed
}

Final considerations

The function I am looking for would be some kind of reverse of flatten(), infact

let subsequences: [[Int]] = [[1,1,1], [3, 3], [4]]
print(Array(subsequences.flatten())) // [1, 1, 1, 3, 3, 4]

I hope I made myself clear, let me know should you need further details.

0

4 Answers 4

34
 // extract unique numbers using a set, then
 // map sub-arrays of the original arrays with a filter on each distinct number

 let numbers = [1, 1, 1, 3, 3, 4]

 let numberGroups = Set(numbers).map{ value in return numbers.filter{$0==value} }

 print(numberGroups)

[EDIT] changed to use Set Initializer as suggested by Hamish

[EDIT2] Swift 4 added an initializer to Dictionary that will do this more efficiently:

 let numberGroups = Array(Dictionary(grouping:numbers){$0}.values)

For a list of objects to be grouped by one of their properties:

 let objectGroups = Array(Dictionary(grouping:objects){$0.property}.values)
Sign up to request clarification or add additional context in comments.

5 Comments

Why use reduce to create the Set? You could just use the Set initialiser directly: Set(numbers).map...
You are right Hamish, I wasn't sure the set initializer would tolerate duplications but it does work and is much clearer. However, since the OP intended to use internal properties of a Struct, either a .map() or the original reduce I had would eventually be needed.
@AlainT. I have an array of custom objects which i need to split up into sub arrays based off one property. I tried to modify your code slightly to achieve this but I got duplicates. My change looked like: let itemsGrouped = Set(items!).map{ value in return items!.filter{$0.custProp == value.custProp} } How might this need to change to get the desired outcome?
Set(items!.map{$0.custProp}).map{ value in return items!.filter{$0.custProp == value} } assuming custProp is hashable
In Swift 4 : Array(Dictionary(grouping:items){$0.custProp}.values)
6

If you could use CocoaPods/Carthage/Swift Package Manager/etc. you could use packages like oisdk/SwiftSequence which provides the group() method:

numbers.lazy.group()
// should return a sequence that generates [1, 1, 1], [3, 3], [4].

or UsrNameu1/TraverSwift which provides groupBy:

groupBy(SequenceOf(numbers), ==)

If you don't want to add external dependencies, you could always write an algorithm like:

func group<S: SequenceType where S.Generator.Element: Equatable>(seq: S) -> [[S.Generator.Element]] {
    var result: [[S.Generator.Element]] = []
    var current: [S.Generator.Element] = []
    for element in seq {
        if current.isEmpty || element == current[0] {
            current.append(element)
        } else {
            result.append(current)
            current = [element]
        }
    }
    result.append(current)
    return result
}

group(numbers)
// returns [[1, 1, 1], [3, 3], [4]].

1 Comment

I think adding a whole dependency just for using a simple grouping function is a bit too much. +1 for the algorithmic solution, that's the approach I used by implementing it into an extension
2

Let's assume that you have an unsorted array of items. You will need to sort the initial array then you will have something like this: [1, 1, 1, 3, 3, 4]

After that you will initialize two arrays: one for storing arrays and another one to use it as a current array.

Loop through the initial array and:

  • if the current value isn't different from the last one, push it to the current array
  • otherwise push the current array to the first one then empty the current array.

Hope it helps!

Comments

0

Worth mentioning, using Swift Algorithms this is now a one-liner:

import Algorithms
let numbers = [1, 1, 1, 3, 3, 4]
let chunks: [[Int]] = numbers.chunked(by: ==).map { .init($0) }
print(chunks) // [[1, 1, 1], [3, 3], [4]]

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.