0

I have an array of Objects fetched from an API. An example would be:

static let previewPriceData: [PriceData] = [
    PriceData(date: "2022-04-19", close: 41.45),
    PriceData(date: "2022-04-20", close: 41.45),
    PriceData(date: "2022-04-21", close: 41.45),
    PriceData(date: "2022-04-22", close: 41.45),
    PriceData(date: "2022-04-23", close: 41.45),
    PriceData(date: "2022-04-24", close: 41.45),
    PriceData(date: "2022-04-25", close: 41.45),
    PriceData(date: "2022-04-26", close: 41.45),
    PriceData(date: "2022-04-27", close: 41.45),
    PriceData(date: "2022-04-28", close: 41.45),
    PriceData(date: "2022-04-29", close: 41.45),
    PriceData(date: "2022-04-30", close: 41.45),
    PriceData(date: "2022-05-01", close: 45.45),
    PriceData(date: "2022-05-02", close: 43.45),
    PriceData(date: "2022-05-03", close: 42.45),
    PriceData(date: "2022-05-04", close: 45.45),
    PriceData(date: "2022-05-05", close: 43.45),
    PriceData(date: "2022-05-06", close: 47.45),
    PriceData(date: "2022-05-07", close: 46.45),
    PriceData(date: "2022-05-08", close: 37.45),
    PriceData(date: "2022-05-09", close: 35.45),
    PriceData(date: "2022-05-10", close: 32.45),
    PriceData(date: "2022-05-11", close: 29.45),
  -->  //  Part of May missing
    PriceData(date: "2022-06-01", close: 45.45),
    PriceData(date: "2022-06-02", close: 43.45),
    PriceData(date: "2022-06-03", close: 42.45),
    PriceData(date: "2022-06-04", close: 45.45),
    PriceData(date: "2022-06-05", close: 43.45),
    PriceData(date: "2022-06-06", close: 47.45),
    PriceData(date: "2022-06-07", close: 46.45),
    PriceData(date: "2022-06-08", close: 37.45),
    PriceData(date: "2022-06-09", close: 35.45),
    PriceData(date: "2022-06-10", close: 32.45),
    PriceData(date: "2022-06-11", close: 29.45),
    PriceData(date: "2022-06-12", close: 35.45),
    PriceData(date: "2022-06-13", close: 48.45),
    PriceData(date: "2022-06-14", close: 52.45),
    PriceData(date: "2022-06-15", close: 51.45),
    PriceData(date: "2022-06-16", close: 49.45),
    PriceData(date: "2022-06-17", close: 46.45),
    PriceData(date: "2022-06-18", close: 45.45),
    PriceData(date: "2022-06-19", close: 44.45),
    PriceData(date: "2022-06-20", close: 43.45),
    PriceData(date: "2022-06-21", close: 45.45),
    PriceData(date: "2022-06-22", close: 46.45),
    PriceData(date: "2022-06-23", close: 49.45),
    PriceData(date: "2022-06-24", close: 51.45),
    PriceData(date: "2022-06-25", close: 50.45),
    PriceData(date: "2022-06-26", close: 48.45),
    PriceData(date: "2022-06-27", close: 43.45),
    PriceData(date: "2022-06-28", close: 39.45),
    PriceData(date: "2022-06-29", close: 34.45),
    PriceData(date: "2022-06-30", close: 32.45),
    PriceData(date: "2022-07-01", close: 45.45),
    PriceData(date: "2022-07-02", close: 43.45),
    PriceData(date: "2022-07-03", close: 42.45),
    PriceData(date: "2022-07-04", close: 45.45),
    PriceData(date: "2022-07-05", close: 43.45)
]

This array contains price data on a daily basis for an interval of 5 years, But it returns not every time every day. There can be gaps like here in May 2022. I use the data to display It inside a chart. To not fill the chart with extraordinary much data I want to change the daily data from this 5 years interval to a monthly. This is not possible with the API, unfortunately.

My approach was this:

func extractData(_ dataArray: [PriceData]) -> [PriceData] {    
    /// Date / Date-Formatter
    let calendar = Calendar.current
    let dateFormatter = DateFormatter()
    dateFormatter.dateFormat = "yyyy-MM-dd"   
    /// First price Value
    guard let firstValue = dataArray.first, let firstDate = firstValue.date else {
        return []
    }
    /// Set starter Month based on Firest price Value
    guard var starterMonthDate = dateFormatter.date(from: firstDate) else {
        return []
    }
    // Extracted Data with firstValue
    var extractedData: [PriceData] = [firstValue]
    var preFilterArray: [PriceData] = []
    var preArray: [PriceData] = []
    var emptyMonthAndYear: [DateComponents] = []
    for data in dataArray.dropFirst() {
        /// Convert String-Date to Date
        guard let dateString = data.date, let date = dateFormatter.date(from: dateString) else { continue }
        /// Fill array with fitting data
        var starterMonthComponents = calendar.dateComponents([.year, .month], from: starterMonthDate)
        let dataDateComponents = calendar.dateComponents([.year, .month], from: date)
        if dataDateComponents != starterMonthComponents {
            starterMonthComponents = dataDateComponents
        }
        if let dateApproved = getDateWithMonthAndYear(date),
           date == dateApproved,
           dataDateComponents == starterMonthComponents {
            extractedData.append(data)
            starterMonthDate = incrementMonth(of: starterMonthDate) ?? starterMonthDate
            removeAllOccurrences(of: dataDateComponents, from: &emptyMonthAndYear)
            continue
        } else if let dateApproved = getDateWithMonthAndYear(date), date != dateApproved, dataDateComponents == starterMonthComponents {
            if !emptyMonthAndYear.contains(dataDateComponents) {
                emptyMonthAndYear.append(dataDateComponents)
            }
        }
    }
    for emptyDateComponents in emptyMonthAndYear {
        for data in dataArray.dropFirst() {
            guard let dateString = data.date, let date = dateFormatter.date(from: dateString) else { continue }
            let dataDateComponents = calendar.dateComponents([.year, .month], from: date)
            if dataDateComponents == emptyDateComponents {
                preArray.append(data)
            }
        }
        if !preArray.isEmpty {
            let median = preArray.sorted(by: { $0.close < $1.close })[preArray.count / 2]
            extractedData.append(median)
        }
    }
    if let lastValue = dataArray.last {
        extractedData.append(lastValue)
    }
    // print(extractedData)
    return extractedData
}

with this function I append the first and last value of the array. Then I try to append for each month between this first and last value one value. here I wanted to take yesterday and add that day's value for each month:

if first value = PriceData(date: "2022-04-19", close: 41.45)
if last value = PriceData(date: "2022-07-21", close: 43.45)

I take the 2022-07-20 and append it, then go one month backwards and append the next one (2022-06-20) and so on.

My Problem was how to handle if on this specific day is no data available like here on the month May where is no data for 2022-05-20.

I would be grateful for any help

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • Not sure what is your goal. What is the expected result ? – Leo Dabus Jul 22 '23 at 23:21
  • @LeoDabus The goal is to get a new array: [PriceData] that just has just one Price per month and not 30. This way the array.count is much less on a 5 year period and the chart in the UI less laggy. The one price per month could either be the median or the value like I tried to get e.g. every 20th day of a month. – Social Processing Jul 22 '23 at 23:33

1 Answers1

1

What you need is to first group your data by month. Once you have your price data grouped all you need is to take the average of each subsequence. Something like:

Check this Group Array by Month/Year and sum

let grouped: [[PriceData]] = previewPriceData
    // .sorted(using: KeyPathComparator(\.date)) // this assumes your data is already sorted otherwise just uncomment this line
    .reduce(into: []) {
        if $0.last?.last?.date.yyyyMMdd?.startOfMonth == $1.date.yyyyMMdd?.startOfMonth {
            $0[$0.indices.last!].append($1)
        } else {
            $0.append([$1])
        }
    }

let monthAverage: [PriceData] = grouped.compactMap { month in
    guard let startOfMonth = month.first?.date.yyyyMMdd?.startOfMonth else { return nil }
    return .init(date: startOfMonth.yyyyMMdd, close: month.average(\.close))
}

print(monthAverage)

This will print:

[
PriceData(date: "2022-04-01", close: 41.449999999999996),
PriceData(date: "2022-05-01", close: 40.813636363636355),
PriceData(date: "2022-06-01", close: 43.58333333333336),
PriceData(date: "2022-07-01", close: 44.05)
]

You will need those helpers:

extension Formatter {
    static let yyyyMMdd: DateFormatter = {
        let dateFormatter = DateFormatter()
        dateFormatter.calendar = .init(identifier: .iso8601)
        dateFormatter.locale = .init(identifier: "en_US_POSIX")
        dateFormatter.dateFormat = "yyyy-MM-dd"
        return dateFormatter
    }()
}
extension String {
    var yyyyMMdd: Date? {
        Formatter.yyyyMMdd.date(from: self)
    }
}
extension Date {
    var yyyyMMdd: String {
        Formatter.yyyyMMdd.string(from: self)
    }
    var startOfMonth: Date { Calendar(identifier: .iso8601).dateComponents([.calendar, .year, .month], from: self).date! }
}

Check this Making my function calculate average of array Swift

extension Sequence  {
    func sum<T: AdditiveArithmetic>(_ predicate: (Element) -> T) -> T {
        reduce(.zero) { $0 + predicate($1) }
    }
}
extension Collection {
    func average<T: BinaryFloatingPoint>(_ predicate: (Element) -> T) -> T {
        sum(predicate) / T(count)
    }
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571