2

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:

  • 10.5 apple
  • 20.0 lemon
  • 15.2 apple
  • 45

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

  • sum(value) group by name
  • delete last row because no have name
  • ordered by value

Expected result based on input:

  • 25.7 apple
  • 20.0 lemon
  • and nothing else

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?

sfjac
  • 7,119
  • 5
  • 45
  • 69
Ziggy
  • 121
  • 1
  • 9

3 Answers3

5

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)
vadian
  • 274,689
  • 30
  • 353
  • 361
  • I'm playing with your answer to understand. I wrote `let allKeys = Set (dataArray.filter({!$0.name.isEmpty}))` . I *intentionally* didn't write the `map{$0.name})` to see/understand what I get...but I get an error saying **ambigous refernce to member filter**. I looked [here](http://stackoverflow.com/questions/34368958/ambiguous-reference-to-member-map-when-attempting-to-append-replace-array-elem) but still couldn't solve it. What am I doing wrong? – mfaani Nov 17 '16 at 15:22
  • Without `map` you get `Set` – vadian Nov 17 '16 at 15:23
  • I get *that*. But right now, why am I getting that error if I only write that far into your answer. – mfaani Nov 17 '16 at 15:25
  • The type used in a `Set` must conform to `Hashable` which the simple struct does not. – vadian Nov 17 '16 at 15:30
  • I don't get it. If you can run a function( ie map) on something(ie `let allKeys = Set (dataArray.filter({!$0.name.isEmpty}))` , then that something should be independently available to the compiler. Perhaps the issue you mentioned is *another* issue—unrelated to the compiler error I am currently getting – mfaani Nov 17 '16 at 15:54
  • 1
    The error might be misleading. The ambiguity is either the type does not match – `Set` is annotated but the inferred type is `Set` or the creation of the set is not possible because `Item` is not hashable. – vadian Nov 17 '16 at 15:58
  • You are write. I just changed my code to `let mallKeys = Set (dataArray.filter({!$0.name.isEmpty}))`. The previous error goes away *now* I get error : **type 'Item' does not conform to protocol 'Hasahable'**. Thanks – mfaani Nov 17 '16 at 16:04
  • Thank you everybody, all are good solution but vadian's solution is the best, about me! – Ziggy Nov 18 '16 at 07:18
  • @vadian - hi I am trying to use this on a struct that is Hashable with an array of 17,000 items and it is quite slow it seems. It takes milliseconds to create the Set (struct has two properties that make up the unique key) and about 20 seconds to do the sum. Just wonder if there is any way to speed this up. – Duncan Groenewald May 21 '21 at 11:30
  • @vadian you're a man on fire! Can you do that with a struct grouped by two properties not just one /\ ? – Duncan Groenewald May 21 '21 at 11:55
  • I don’t know exactly what you mean. The keys of the dictionary are the values of the predicate respectively. – vadian May 21 '21 at 12:31
2

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)")
}
Jim Matthews
  • 1,181
  • 8
  • 16
1

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

twiz_
  • 1,178
  • 10
  • 16