UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

A useful exercise in summing arrays

Forums > SwiftUI

I needed to sum a number of elements in an integer array for an app that I am writing, but only for the first x numbers. As I couldn't find anything in the standard SwiftUI that fitted the bill (it doesn't mean that it doesn't exist, just that I did a quick search and couldn't find anything suitable), I wrote my own function.

This made me think, are there other related functions that could also be useful (in probably only a handful of cases), and could I make them generic, at least for numbers. I came up with four general scenarios.

  • sum the first x numbers in the array.
  • sum the last y numbers, starting after the first x number in the array.
  • sum the numbers between position x and y, inclusive, in the array.
  • sum the numbers in the array outside of the range of postion x to position (so excluding x and y values, and those inbetween).

Position selected that are outside the array need to be handled, as well as if in some of the function calls the Start and End position values are reversed.

I also made a simplistic view, to be able to see the result, and to set the start and end index, for the various summation functions.

Here is my code for you to use, if you need it. I am sure that there improvements to be made.

func sumNumberListTo<T: Numeric>(_ upto: Int, numberList: [T]) -> T {
    guard !numberList.isEmpty else { return 0 }

    guard upto > 0 else { return 0 }
    guard upto < numberList.count else { return numberList.reduce(0, +) }

    return numberList
        .dropLast(numberList.count - upto)
        .reduce(0, +)
}

func sumNumberListFrom<T: Numeric>(_ from: Int, numberList: [T]) -> T {
    guard !numberList.isEmpty else { return 0 }

    guard from > 0 else { return numberList.reduce(0, +) }
    guard from <= numberList.count else { return 0 }

    return numberList
        .dropFirst(from - 1)
        .reduce(0, +)
}

func sumNumberListBetween<T: Numeric>(_ from: Int, _ to: Int, numberList: [T]) -> T {
    guard !numberList.isEmpty else { return 0 }

    guard from > 0 else { return sumNumberListTo(to, numberList: numberList) }
    guard from <= numberList.count else { return sumNumberListFrom(to, numberList: numberList) }

    // At this point 'from' is within the range
    guard to > 0 else { return sumNumberListTo(from, numberList: numberList) }
    guard to <= numberList.count else { return sumNumberListFrom(from, numberList: numberList) }

    // At this point 'to' is also within the range
    if from == to {
        return numberList[from - 1]
    } else if from > to {   // handle swapped boundary positions
        return numberList
            .dropLast(numberList.count - from)   // work on the back of the array first
            .dropFirst(to - 1)
            .reduce(0, +)
    } else {
        return numberList
            .dropLast(numberList.count - to)   // work on the back of the array first
            .dropFirst(from - 1)
            .reduce(0, +)
    }
}

func sumNumberListExcept<T: Numeric>(_ from: Int, _ to: Int, numberList: [T]) -> T {

    // It is debateable whether this `let` should be defined to improves clarity and make the code smaller,
    // versus if the speed of operation is more important where you would write the code repeatedly, where needed,
    // at the expense of code size.

    let sumAllNumberList = numberList.reduce(0, +)

    guard !numberList.isEmpty else { return 0 }

    guard from > 0 else { return sumNumberListFrom(to + 1, numberList: numberList) }
    guard from <= numberList.count else { return sumAllNumberList - sumNumberListFrom(to, numberList: numberList) }

    // At this point 'from' is within the range

    guard to > 0 else { return sumNumberListFrom(from + 1, numberList: numberList) }
    guard to <= numberList.count else { return sumNumberListTo(from - 1, numberList: numberList) }

    // At this point 'to' is also within the range

    if from == to {
        return sumAllNumberList - numberList[from - 1]
    } else if from > to {   // handle swapped boundary positions
        return sumAllNumberList - (numberList
            .dropLast(numberList.count - from)   // work on the back of the array first
            .dropFirst(to - 1)
            .reduce(0, +))
    } else {
        return sumAllNumberList - (numberList
            .dropLast(numberList.count - to)   // work on the back of the array first
            .dropFirst(from - 1)
            .reduce(0, +))
    }
}

struct ContentView: View {

    let numberList = [7, 4, 38, 21, 16, 15, 12, -33, 31, 49]
    let emptyList: [Int] = []

    @State private var startFrom = 1
    @State private var endHere = 1

    var body : some View {
        let totalSumNumbersTo = numberList.reduce(0, +)
        let lowIndexSum = sumNumberListTo(endHere, numberList: numberList)
        let highIndexSum = sumNumberListFrom(startFrom, numberList: numberList)
        let emptySum = sumNumberListFrom(startFrom, numberList:  emptyList)
        let betweenIndexSum = sumNumberListBetween(startFrom, endHere, numberList: numberList)
        let exceptIndexSum = sumNumberListExcept(startFrom, endHere, numberList: numberList)

        VStack(alignment: .leading) {

            HStack{
                ForEach(numberList, id: \.self) { value in
                    Text("\(value) ")
                }
            }
            .padding([.trailing, .leading, .bottom])

            Stepper("Start from \(startFrom) (range -5 to 15)", value: $startFrom, in: -5...15)
                .padding([.trailing, .leading, .bottom])

            Stepper("End here \(endHere) (range -5 to 15)", value: $endHere, in: -5...15)
                .padding([.trailing, .leading, .bottom])

            Text("Total for the first \(endHere < 0 ? 0 : endHere) numbers is \(lowIndexSum)")
                .padding([.trailing, .leading, .bottom])

            Text("Total from position \(startFrom) for \((numberList.count - startFrom + 1) < 0 ? 0 : startFrom < 0 ? numberList.count : numberList.count - startFrom + 1) numbers is \(highIndexSum)")
                .padding([.trailing, .leading, .bottom])

            Text("Total between position \(startFrom) to position \(endHere) is \(betweenIndexSum)")
                .padding([.trailing, .leading, .bottom])

            Text("Except for the range between position \(startFrom) to position \(endHere), total is \(exceptIndexSum)")
                .padding([.trailing, .leading, .bottom])

            Text("The empty list sum is \(emptySum)")
                .padding([.trailing, .leading, .bottom])
        }
    }
}

2      

hi @Green,

i always enjoy these thought exercises, especially if you can introduce generics.

i'd suggest first adding something as an extension on Array that i'll call takeFirst, as in take the first N elements of the array. it looks like this:

extension Array {
    func takeFirst(_ howMany: Int) -> [Element] {
        let numberToTake = Swift.min(Swift.max(0, howMany), self.count)
        return self.dropLast(self.count - numberToTake)
    }
}

the min-max stuff just makes sure that you can always take a meaningful number of elements from the array, for any integer parameter value (producing an empty array if N <= 0 and the entire array if N exceeds the number of elements in the array).

so, for example, this sequence

let myArray = [5,6,9,2,1,7]
print(myArray.takeFirst(-3))
print(myArray.takeFirst(0))
print(myArray.takeFirst(4))
print(myArray.takeFirst(23))

produces this output:

[]

[]

[5, 6, 9, 2]

[5, 6, 9, 2, 1, 7]

now you have

func sumNumberListTo<T: Numeric>(_ upto: Int, numberList: [T]) -> T {
    numberList.takeFirst(upto + 1).reduce(0, +)
}
  • the reason for upto + 1 is that you specify perhaps adding "up to index 3," which is really the same as taking the first 4 elements (in an Array) and then adding.

could takeLast be next? sure, why not?

extension Array {
    func takeLast(_ howMany: Int) -> [Element] {
        return self.reversed().takeFirst(howMany).reversed()
    }
}

that should make sumNumberListFrom a lot easier as well (or, you can use takeFirst directly after reversing the array and then adding).

regards,

DMG

1      

Here's how I would do it:

extension Array where Element: Numeric {
    func sum<T: RangeExpression>(between range: T) -> Self.Element where T.Bound == Int {
        //convert our RangeExpression into a Range
        let rng = range.relative(to: self)
        //convert our Range into a ClosedRange and then
        //make sure we don't overflow our array bounds
        let includeRange = ClosedRange(rng).clamped(to: startIndex...endIndex)

        return self.enumerated().reduce(Self.Element.zero) { result, element in
            //if this item is WITHIN our includeRange, add it to the sum
            if includeRange.contains(element.offset) {
                return result + element.element
            } else {
                return result
            }
        }
    }

    //does not include end
    func sum(upTo end: Int) -> Self.Element {
        sum(between: ..<end)
    }

    //includes end
    func sum(through end: Int) -> Self.Element {
        sum(between: ...end)
    }

    func sum(from start: Int) -> Self.Element {
        sum(between: start...)
    }

    //this one is really the same as sum(between:) but allows us to
    //indicate the start and end as integers rather than a range
    //includes end
    func sum(from start: Int, through end: Int) -> Self.Element {
        sum(between: start...end)
    }

    //does not include end
    func sum(from start: Int, upTo end: Int) -> Self.Element {
        sum(between: start..<end)
    }

    func sum<T: RangeExpression>(excluding range: T) -> Self.Element where T.Bound == Int {
        self.reduce(Self.Element.zero, +) - sum(between: range)
    }

    //and just for the heck of it...
    func sum(with items: IndexSet) -> Self.Element {
        return self.enumerated().reduce(Self.Element.zero) { result, element in
            if items.contains(element.offset) {
                return result + element.element
            } else {
                return result
            }
        }

        //or, if you don't mind some ugly casting and bridging
        //to NSArray...
        //let includedItems = ((self as NSArray).objects(at: items)) as! [Element]
        //return includedItems.reduce(Self.Element.zero, +)
    }

    //using the above, you could also do sum(excluding:) like this...
    //func sum(excluding range: ClosedRange<Int>) -> Self.Element {
    //    let allItems = IndexSet(integersIn: startIndex...endIndex)
    //    let removeItems = IndexSet(integersIn: range)
    //    return sum(with: allItems.subtracting(removeItems))
    //}

    //more fun...
    func sum(without items: IndexSet) -> Self.Element {
        return self.enumerated().reduce(Self.Element.zero) { result, element in
            if items.contains(element.offset) {
                return result
            } else {
                return result + element.element
            }
        }
    }
}

let myArray = [5,6,9,2,1,7]
print(myArray)
//[5, 6, 9, 2, 1, 7]
print(myArray.sum(upTo: 4))
//22
print(myArray.sum(through: 4))
//23
print(myArray.sum(from: 3))
//10
print(myArray.sum(from: 1, through: 4))
//18
print(myArray.sum(from: 1, upTo: 4))
//17
print(myArray.sum(from: -13, through: 12))
//30
print(myArray.sum(between: 1...8))
//25
print(myArray.sum(between: 1..<4))
//17
print(myArray.sum(between: 3...))
//10
print(myArray.sum(between: ..<2))
//11
print(myArray.sum(excluding: 2...4))
//18
print(myArray.sum(with: [0, 2, 4]))
//15
print(myArray.sum(without: [0, 1, 5]))
//12
print(myArray.sum(with: IndexSet(integersIn: 1...3)))
//17

And I keep thinking of ways to improve this...

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.