twiz_ twiz_ - 1 month ago 6
Swift Question

Swift: Array map or reduce, enumerate with index

I have an array of values [CGFloat] and array of days [CGFloat] with which each value is associated (the time is also important so the days array is a decimal value).

Each day has between 0 and n values. (n typically less than 5 or 6)

I want to find the mean value for each day, so after the calculations I would like to have an array of means [CGFloat] and an array of days [CGFloat], or a dictionary of the two combined or an array of [CGPoint]. I am fairly certain this can be done with either a mapping or reducing or filter function, but I'm having trouble doing so.
For instance

the third day might look like [2.45, 2.75, 2.9]
with associated values [145.0, 150.0, 160.0]

And I would like to end with day[2] = 2.7
and value[2] = 151.7

or

[CGPoint(2.7, 151.7)] or [2.7 : 151.7]


Can anyone offer guidance?

Some code:

let xValues : [CGFloat] = dates.map{round((CGFloat($0.timeIntervalSinceDate(dateZero))/(60*60*24))*100)/100}
let yValues : [CGFloat] = valueDoubles.map{CGFloat($0)}
//xValues and yValues are the same length
var dailyMeans = [CGFloat]()
var xVals = [CGFloat]()

let index = Int(ceil(xValues.last!))

for i in 0..<index{

let thisDay = xValues.enumerate().filter{$0.element >= CGFloat(i) && $0.element < CGFloat(i+1)}
if thisDay.count > 0{
var sum : CGFloat = 0
var day : CGFloat = 0
for i in thisDay{
sum += yValues[i.index]
day += xValues[i.index]
}
dailyMeans.append(sum/CGFloat(thisDay.count))
xVals.append(day/CGFloat(thisDay.count))
}

}


The above code works, but also has to do that enumerate.filter function values.count * days.last times. So for 40 days and 160 readings.. like 6500 times. And I'm already using way too much processing power as it is. Is there a better way to do this?

edit: forgot a line of code defining index as the ceiling of xValues.last

This has been seen 1000 times so I thought I would update with my final solution:

var daySets = [Int: [CGPoint]]()
// points is the full array of (x: dayTimeInDecimal, y: value)
for i in points {
let day = Int(i.x)

daySets[day] = (daySets[day] ?? []) + [i]
}

let meanPointsEachDay = daySets.map{ (key, value) -> CGPoint in
let count = CGFloat(value.count)
let sumPoint = value.reduce(CGPoint.zero, {CGPoint(x: $0.x + $1.x, y: $0.y + $1.y)})
return CGPoint(x: sumPoint.x/count, y: sumPoint.y/count)
}

Answer
// must be sorted by 'day'!!!
let arrA0 = [2.45, 2.75, 2.9, 3.1, 3.2, 3.3]
// associated values
let arrA1 = [145.0, 150.0, 160.0, 245.0, 250.0, 260.0]

let arr = Array(zip(arrA0, arrA1))

// now i have an array of tuples, where tuple.0 is key and tuple.1 etc. is associated value
// you can expand the tuple for as much associated values, as you want
print(arr)
// [(2.45, 145.0), (2.75, 150.0), (2.9, 160.0), (3.1, 245.0), (3.2, 250.0), (3.3, 260.0)]




// now i can perform my 'calculations' the most effective way
var res:[Int:(Double,Double)] = [:]
// sorted set of Int 'day' values
let set = Set(arr.map {Int($0.0)}).sort()
// for two int values the sort is redundant, but
// don't be depend on that!

print(set)
// [2, 3]
var g = 0
var j = 0
set.forEach { (i) -> () in
    var sum1 = 0.0
    var sum2 = 0.0
    var t = true
    while t && g < arr.count {
        let v1 = arr[g].0
        let v2 = arr[g].1
        t = i == Int(v1)
        if t {
            g++
            j++
        } else {
            break
        }
        sum1 += v1
        sum2 += v2
    }
    res[i] = (sum1 / Double(j), sum2 / Double(j))
    j = 0
}
print(res)
// [2: (2.7, 151.666666666667), 3: (3.2, 251.666666666667)]

see, that every element of your data is process only once in the 'calculation', independent from size of 'key' set size

use Swift's Double instead of CGFloat! this increase the speed too :-)

and finally what you are looking for

if let (day, value) = res[2] {
    print(day, value) // 2.7 151.666666666667
}