1

I am trying to write an extension for Array Types that sums the n-previous indexes in the index n.

let myArray = [1, 2, 3, 4, 5]
let mySumArray = myArray.sumNIndex()
print(mySumArray)
// returns [1,3,6,10,15]

I have tried various approaches which all failed at some point. For instance, the example hereafter triggers a compile error "Cannot invoke 'reduce' with an argument list of type '(Int, _)'":

extension Array {
    mutating func indexSum() {
        var tempArray = [Any]()
        for index in 1...self.count - 1 {
        self[index] += self[.prefix(index + 2).reduce(0, +)]
        }
    }
}

This other attempt triggers another compile error: "Binary operator '+=' cannot be applied to two 'Element' operands"

extension Array {
    mutating func indexSum() {
        var tempArray = [Any]()
        for index in 1...self.count - 1 {
        self[index] += self[index - 1]
        }
    }
}

Any idea is welcome! Thank you very much for your help!

EDIT: Many thanks to @Martin and @Carpsen who figured it out in 2 different ways

@Martin using map method:

extension Array where Element: Numeric {
    func cumulativeSum() -> [Element] {
        var currentSum: Element = 0
        return map {
            currentSum += $0
            return currentSum
        }
    }
}

@Carpsen using reduce method:

extension Array where Element: Numeric {
    func indexSum() -> [Element] {
        return self.reduce(into: [Element]()) {(acc, element) in
            return acc + [(acc.last ?? 0) + element]
        })
    }
}
Chris
  • 305
  • 1
  • 12

2 Answers2

1

The main problem is that the addition operator + is not defined for elements of arbitrary arrays. You need to restrict the extension method, e.g. to arrays of Numeric elements.

Also there is no need to use Any.

Here is a possible implementation as a non-mutating method:

extension Array where Element: Numeric {
    func cumulativeSum() -> [Element] {
        var currentSum: Element = 0
        return map {
            currentSum += $0
            return currentSum
        }
    }
}

Examples:

let intArray = [1, 2, 3, 4, 5]
print(intArray.cumulativeSum()) // [1, 3, 6, 10, 15]

let floatArray = [1.0, 2.5, 3.25]
print(floatArray.cumulativeSum()) [1.0, 3.5, 6.75]

In a similar fashion we can “cumulatively join” the elements of a string array. enumerated() is now used to provide the current element index together with the element, and that is used to decide whether to insert the separator or not:

extension Array where Element == String {
    func cumulativeJoin(separator: String) -> [Element] {
        var currentJoin = ""
        return enumerated().map { (offset, elem) in
            if offset > 0 { currentJoin.append(separator) }
            currentJoin.append(elem)
            return currentJoin
        }
    }
}

Examples:

let stringArray = ["a", "b", "c"]
print(stringArray.cumulativeJoin()) // ["a", "ab", "abc"]
print(stringArray.cumulativeJoin(separator: ":")) // ["a", "a:b", "a:b:c"]
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
  • Thanks for taking the time on this Martin. Your remarks make sense, and I thought I had to use a Protocol. However, 2 things: (1) the code does compile but returns the same array [1,2,3,4,5] and (2) I'd like the code to work on Strings as well, hence the Numeric Protocol might be too restrictive. Thanks again!!! – Chris Oct 02 '18 at 18:00
  • For `let intArray = [1, 2, 3, 4, 5]` , `print(intArray.cumulativeSum())` returns [1,2,3,4,5] instead of [1,2,6,10,15] – Chris Oct 02 '18 at 18:03
  • @Chris: Oops. Fixed now. – Martin R Oct 02 '18 at 18:05
  • Thanks a lot @Martin! It works like a charm! I think I understand the idea: you increment the currentSum property with the index value thanks to the map method, but what I don't really understand is the successive `returns` on the one hand and why you did not write self.map instead of map on the other hand. Last but not least, I'd like it to work with Strings as well. – Chris Oct 02 '18 at 18:16
  • @Chris: `map()` defaults to `self.map()` (as do all instance methods). `map()` calls the closure with each array element in turn, and returns the array with the results. – What should `["a","b"].cumulativeSum()` return? – Martin R Oct 02 '18 at 18:18
  • Indeed you are right about the implicit `self.map()` ! I am just not enough familiar with the code yet! `["a","b"].cumulativeSum()` throws an compile error *Type of expression is ambiguous without more context*. PS: I have edited the original post to show both @Carpsen and you figured it out and I cannot validate 2 answers. – Chris Oct 02 '18 at 18:30
  • @Chris: It is still unclear to me how you *define* the cumulative sum of a string array `["a","b"]`. What do you *want* the result to be? – Martin R Oct 02 '18 at 18:33
  • it isn't common indeed :) but ideally `["a","b",c"]` would return `["a","a b","a b c"]` using a separator which can be "", " ", /n... – Chris Oct 02 '18 at 18:48
  • Thanks @Martin! The closure is neat and clear thanks to your explanations. It helps me better understand how map, reduce and enumerated work which are fundamental for collections. – Chris Oct 02 '18 at 19:17
0

Try this:

let myArray = [1, 2, 3, 4, 5]

myArray.reduce([Int](), {accumulator, element in
    return accumulator + [(accumulator.last ?? 0) + element]
})
//[1, 3, 6, 10, 15]

What this reduce does is:

  • Start with an empty array
  • With each element from myArray it calculates its sum with the last element in the accumulator
  • Return the previous array plus the last sum

Here is a simpler, but longer version:

let myArray = [1, 2, 3, 4, 5]

let newArray = myArray.reduce([Int](), {accumulator, element in
    var tempo = accumulator
    let lastElementFromTheAccumulator = accumulator.last ?? 0
    let currentSum = element + lastElementFromTheAccumulator
    tempo.append(currentSum)
    return tempo
})

print(newArray)  //[1, 3, 6, 10, 15]

A more efficient solution, as suggested by Martin R in the comments, uses reduce(into:):

myArray.reduce(into: [Int]()) { (accumulator, element) in
    accumulator += [(accumulator.last ?? 0) + element]
}
//[1, 3, 6, 10, 15]

And you could have it as an extension:

extension Array where Element: Numeric {
    func indexSum() -> [Element] {
        return self.reduce([Element](), {acc, element in
            return acc + [(acc.last ?? 0) + element]
        })
    }
}

myArray.indexSum()  //[1, 3, 6, 10, 15]

Here a solution that will work with strings too:

extension Array where Element == String {
    func indexSum() -> [String] {
        return self.reduce(into: [String]()) {(accumulator, element) in
            accumulator += [(accumulator.last ?? "") + element]
        }
    }
}

["a", "b", "c", "d"].indexSum() //["a", "ab", "abc", "abcd"]

If you'd like to have a separator between the elements of the initial array elements, you could use this extension:

extension Array where Element == String {
    func indexSum(withSparator: String) -> [String] {
        return self.reduce(into: [String]()) {(accumulator, element) in
            var previousString = ""
            if let last = accumulator.last {
                previousString = last + " "
            }
            accumulator += [previousString +  element]
        }
    }
}

["a", "b", "c", "d"].indexSum(withSparator: " ") //["a", "a b", "a b c", "a b c d"]
ielyamani
  • 17,807
  • 10
  • 55
  • 90
  • Thanks Carpsen! It works, but would you mind explaining it a bit further so I can better understand how you wrote/found this closure please? – Chris Oct 02 '18 at 18:07
  • Thanks a lot @Carpsen! I am just starting with Swift so I read fine the longer version (thanks!!) but I still have troubles jumping from the longer version to the shorter. I need to figure out how come you return an Array and not the value in the short version. Besides would you know how to make it work with Strings? Many, many thanks! – Chris Oct 02 '18 at 18:22
  • This creates a lot of intermediate arrays. You should at least use `reduce(into:)` – Martin R Oct 02 '18 at 18:24
  • @Chris `accumulator` is an array, so `accumulator + [(accumulator.last ?? 0) + element]` is an array two. You get a longer array by adding two arrays using `+` – ielyamani Oct 02 '18 at 18:25
  • @MartinR Thank you, I'll add it to the answer – ielyamani Oct 02 '18 at 18:28
  • @Chris I'll update the answer with a general solution as soon as possible – ielyamani Oct 02 '18 at 18:28
  • @Chris I've updated the answer for an array of strings – ielyamani Oct 02 '18 at 18:47
  • @Carpsen @Martin, you're awesome guys! Thanks a ton! It also gives me food for thoughts, small things like `reduce(into)` takes me back to square one lol – Chris Oct 02 '18 at 18:55
  • @MartinR That depends entirely on the characteristics of the expected inputs. That's not a statement that can be made categorically, especially in response to an example of size `n = 5`. – Wayne Oct 02 '18 at 19:05
  • 1
    @WayneBurkett: Fair point. I'll reformulate it: `reduce(into:)` can be used to reduce(!) the number of intermediate arrays (and that's what it was made for: https://github.com/apple/swift-evolution/blob/master/proposals/0171-reduce-with-inout.md) – Martin R Oct 02 '18 at 19:13