Makaille
Makaille

Reputation: 1666

Get next value on a map?

I'm trying to compare element to next element in a collection.

For example :

let array: [(Double, String)]= [(2.3, "ok"),
                                (1.4, "ok"),
                                (5.1, "notOk")]

I need a returned array who will summary element where the string is the same. So my result will be :

new array = [(3.7, "ok"), (5.1, "notOk")]

I need to do it functional if possible. i tried to get next element in a map but can't found how.

Something like this (this is just for logic, this code isn't working.

let newArray = array.map {(element, nextElement) in 
    if element.1 == nextElement.1 {
        return element.0 + nextElement.0 
    }
} 

Upvotes: 4

Views: 3005

Answers (4)

DrummerB
DrummerB

Reputation: 40211

In a more functional way:

let array: [(Double, String)]= [(2.3, "ok"),
                                (1.4, "ok"),
                                (5.1, "notOk")]
let keys = Set(array.map{$0.1})            // find unique keys
let result = keys.map { key -> (Double, String) in   
    let sum = array.filter {$0.1 == key}   // find all entries with the current key
                   .map {$0.0}             // map them to their values
                   .reduce(0, +)           // sum the values
    return (sum, key)
}
print(result)

Output:

[(5.0999999999999996, "notOk"), (3.6999999999999997, "ok")]

Alternatively (suggested by @dfri):

let keys = Set(array.map{$0.1})            // find unique keys
let result = keys.map { key -> (Double, String) in   
    let sum = array.reduce(0) { $0 + ($1.1 == key ? $1.0 : 0) }
    return (sum, key)
}

Upvotes: 7

Rob Napier
Rob Napier

Reputation: 299545

I like alexburtnik's answer. It's basically word for word how I wrote my first pass of this. It's straightforward, clear, and efficient. It is excellent Swift.

But functional programming can help us think more deeply about problems and create better, reusable tools. So let's think functionally.

dfri's solution appears beautiful, but is O(m*n) (in the worst case, O(n^2)). It loops through the entire array for every unique key. This gets back the old adage by Alan Perlis: "A Lisp programmer knows the value of everything and the cost of nothing." But functional programming doesn't have to be inefficient.

The point of functional programming is to break down complex problems into simpler problems, make those simpler problems generic, and then recombine them. It's not about filters and flatMaps.

So let's break down this problem. We want to group by key, and then sum the values for each key. Grouping by key is going to be a lot easier if we sort by key first:

let result = array
    .sorted(by: { $0.1 < $1.1 })

Now, we wish we could group them with something like this:

let result = array
    .sorted(by: { $0.1 < $1.1 })
    .grouped(by: { $0.1 == $1.1 })

I wish I had that grouped(by:). Wish fulfillment is the heart of functional programming, so let's write it. Well, a group is a sequence of elements that are all "equal" for some value of "equal." We could build that this way:

extension Array {
    func grouped(by equal: (Element, Element) -> Bool) -> [[Element]] {
        guard let firstElement = first else { return [] }
        guard let splitIndex = index(where: { !equal($0, firstElement) } ) else { return [self] }
        return [Array(prefix(upTo: splitIndex))] + Array(suffix(from: splitIndex)).grouped(by: equal)
    }

That said, I don't really like this code. It's not very Swifty. That [Array(prefix(...)] + is a good indication of how much Swift hates us doing it this way. And it can be very expensive due to copying (probably getting us back to O(n^2). The Swiftier solution would be an Sequence:

struct GroupedSequence<Element>: Sequence, IteratorProtocol {
    var elements: [Element]
    let equal: (Element, Element) -> Bool
    private var nextIndex = 0
    init(of elements: [Element], by equal: @escaping (Element, Element) -> Bool) {
        self.elements = elements
        self.equal = equal
    }

    mutating func next() -> ArraySlice<Element>? {
        guard nextIndex < elements.endIndex else { return nil }
        let first = elements[nextIndex]
        let splitIndex = elements[nextIndex..<elements.endIndex].index(where: { !equal($0, first) } ) ?? elements.endIndex
        defer { nextIndex = splitIndex }
        return elements[nextIndex..<splitIndex]
    }
}

extension Array {
    func grouped(by equal: @escaping (Element, Element) -> Bool) -> GroupedSequence<Element> {
        return GroupedSequence(elements: self, equal: equal)
    }
}

Yes, it mutates and it's a little more code, but it's also lazy (which is a key tool from functional programming), it's better Swift, and very reusable. I like it. But you can use the recursive, pure version if you like.

OK, so now we have an array of arrays that are equivalent. We want to map over those and reduce each element to its sum. So we'll have a reduce inside a map. But this is not O(n^2) because each reduce is only over a single slice. We're going to walk every element just one time. To take care of one impossible corner case (an empty group, which grouped(by:) will never actually create), we'll use flatMap, but it's really just a map. You might be tempted to jump to this, but don't do it:

let result: [(Double, String)] = array
    .sorted(by: { $0.1 < $1.1 })
    .grouped(by: { $0.1 == $1.1 })
    .flatMap { group in
        guard let key = group.first?.1 else { return nil }
        return (group.reduce(0, { $0 + $1.0 }), // Sum of our values
                key)
}

Why? That's horribly unreadable. This is what gives functional programming a bad name. What the heck is that last piece doing? No, we want functional composition, not just functional tools. So we extract a function:

func sumEach(pairGroup: ArraySlice<(Double, String)>) -> (Double, String)? {
    guard let key = pairGroup.first?.1 else { return nil }
    return (pairGroup.reduce(0, { $0 + $1.0 }), // Sum of our values
        key)
}

Now, we can have our nice functional approach without sacrificing comprehension:

let result = array
    .sorted(by: { $0.1 < $1.1 })
    .grouped(by: { $0.1 == $1.1 })
    .flatMap(sumEach(pairGroup:))

And in the process we've created a new tool, grouping, that we can use to compose other solutions. I think that's pretty nice.

But I'd still probably do it alexburtnik's way.

Upvotes: 4

user3581248
user3581248

Reputation: 1014

I guess that a solution that makes a good use of Swift's features would be to combine filter and reduce:

    let array: [(String, Double)] = [("ok", 2.4),
                                     ("ok", 1.3),
                                     ("not ok", 4.4),
                                     ("very not ok", 99.0)]
    let key = "ok"
    let result = array.filter({$0.0 != key}) + [array.filter({ $0.0 == key }).reduce((key, 0.0), { (key, $0.1 + $1.1) })]
    print(result)

And then the result would be

[("not ok", 4.4000000000000004), ("very not ok", 99.0), ("ok", 3.7000000000000002)]

Which I assume is what you wanted to achieve.

EDIT:

To reduce all tuples you could simply wrap the solution inside of a function:

    func reduceAllTuples(tupleArray: [(String, Double)]) -> [(String, Double)]{
        var array = tupleArray
        for (key, _) in tupleArray {
            array = array.filter({$0.0 != key}) + [array.filter({ $0.0 == key }).reduce((key, 0.0), { (key, $0.1 + $1.1) })]
        }
        return array
    }

Upvotes: 1

alexburtnik
alexburtnik

Reputation: 7741

You can iterate over every tupple in your input array and save a sum in a dictionary like this:

let array: [(Double, String)] = [(1.0,"notok"),(2.0,"ok"),(3.0,"ok"),(4.0,"ok"),(5.0,"ok"),(6.0,"ok"), (7.0,"notok")]

var dict = [String: Double]()
for (value, key) in array {
    dict[key] = (dict[key] ?? 0) + value
}
print ("dict: \(dict)")

Output:

dict: ["notok": 8.0, "ok": 20.0]

If you really need to get an array of tuples, use this:

let result = dict.map { (key, value) in (value, key) }
print ("result: \(result)")

Output:

result: [(8.0, "notok"), (20.0, "ok")]

Upvotes: 3

Related Questions