Ziggy
Ziggy

Reputation: 131

Swift 3: sum value with group by of an array of objects

I have this code in my viewController

var myArray :Array<Data> = Array<Data>()
for i in 0..<mov.count {    
   myArray.append(Data(...))
}

class Data {
    var value :CGFloat
    var name  :String=""
    init({...})
}

My input of Data is as:

Once I loop through, I would like return a new array as:

Expected result based on input:

I wrote many rows of code and it is too confused to post it. I'd find easier way, anyone has a idea about this?

Upvotes: 2

Views: 4786

Answers (3)

vadian
vadian

Reputation: 285072

First of all Data is reserved in Swift 3, the example uses a struct named Item.

struct Item {
    let value : Float
    let name  : String
}

Create the data array with your given values

let dataArray = [Item(value:10.5, name:"apple"), 
                 Item(value:20.0, name:"lemon"), 
                 Item(value:15.2, name:"apple"), 
                 Item(value:45, name:"")]

and an array for the result:

var resultArray = [Item]()

Now filter all names which are not empty and make a Set - each name occurs one once in the set:

let allKeys = Set<String>(dataArray.filter({!$0.name.isEmpty}).map{$0.name})

Iterate thru the keys, filter all items in dataArray with the same name, sum up the values and create a new Item with the total value:

for key in allKeys {
    let sum = dataArray.filter({$0.name == key}).map({$0.value}).reduce(0, +)
    resultArray.append(Item(value:sum, name:key))
}

Finally sort the result array by value desscending:

resultArray.sorted(by: {$0.value < $1.value})

---

Edit:

Introduced in Swift 4 there is a more efficient API to group arrays by a predicate, Dictionary(grouping:by:

var grouped = Dictionary(grouping: dataArray, by:{$0.name})
grouped.removeValue(forKey: "") // remove the items with the empty name

resultArray = grouped.keys.map { (key) -> Item in
    let value = grouped[key]!
    return Item(value: value.map{$0.value}.reduce(0.0, +), name: key)
}.sorted{$0.value < $1.value}

print(resultArray)

Upvotes: 5

twiz_
twiz_

Reputation: 1198

First change your data class, make string an optional and it becomes a bit easier to handle. So now if there is no name, it's nil. You can keep it as "" if you need to though with some slight changes below.:

class Thing {
    let name: String?
    let value: Double

    init(name: String?, value: Double){
        self.name = name
        self.value = value
    }

    static func + (lhs: Thing, rhs: Thing) -> Thing? {
        if rhs.name != lhs.name {
            return nil
        } else {
            return Thing(name: lhs.name, value: lhs.value + rhs.value)
        }
    }
}

I gave it an operator so they can be added easily. It returns an optional so be careful when using it.

Then lets make a handy extension for arrays full of Things:

extension Array where Element: Thing {

func grouped() -> [Thing] {

    var things = [String: Thing]()

    for i in self {
        if let name = i.name {
            things[name] = (things[name] ?? Thing(name: name, value: 0)) + i
        }
    }
    return things.map{$0.1}.sorted{$0.value > $1.value}
}
}

Give it a quick test:

let t1 = Thing(name: "a", value: 1)
let t2 = Thing(name: "b", value: 2)
let t3 = Thing(name: "a", value: 1)
let t4 = Thing(name: "c", value: 3)
let t5 = Thing(name: "b", value: 2)
let t6 = Thing(name: nil, value: 10)


let bb = [t1,t2,t3,t4,t5,t6]

let c = bb.grouped()

// ("b",4), ("c",3) , ("a",2)

Edit: added an example with nil for name, which is filtered out by the if let in the grouped() function

Upvotes: 1

Jim Matthews
Jim Matthews

Reputation: 1191

First of all, you should not name your class Data, since that's the name of a Foundation class. I've used a struct called MyData instead:

struct MyData {
    let value: CGFloat
    let name: String
}

let myArray: [MyData] = [MyData(value: 10.5, name: "apple"),
                         MyData(value: 20.0, name: "lemon"), 
                         MyData(value: 15.2, name: "apple"),
                         MyData(value: 45,   name: "")]

You can use a dictionary to add up the values associated with each name:

var myDictionary = [String: CGFloat]()
for dataItem in myArray {
    if dataItem.name.isEmpty {
        // ignore entries with empty names
        continue
    } else if let currentValue = myDictionary[dataItem.name] {
        // we have seen this name before, add to its value
        myDictionary[dataItem.name] = currentValue + dataItem.value
    } else {
       // we haven't seen this name, add it to the dictionary
        myDictionary[dataItem.name] = dataItem.value
    }
}

Then you can convert the dictionary back into an array of MyData objects, sort them and print them:

// turn the dictionary back into an array
var resultArray = myDictionary.map { MyData(value: $1, name: $0) }

// sort the array by value
resultArray.sort { $0.value < $1.value }

// print the sorted array
for dataItem in resultArray {
    print("\(dataItem.value) \(dataItem.name)")
}

Upvotes: 2

Related Questions