0

I know that my title is slightly confusing but let me explain: I have been at this for a while and I can't seem to figure this out.

First of all here is some of my code:

struct CalorieLog {

  var date: Date
  var calories: Int

} 

  var logs: [CalorieLog] = []

  func logCalories() {

  //... calculate calories to log

  let currentDate: Date = Date()
  let calories: Int = calculatedCalories

  logs.append(CalorieLog(date: currentDate, calories: calculatedCalories))

}

Now how do I group the CalorieLog items in the logs array by day and get the sum of all the calories logged per day? And maybe sort them into an array of dictionaries? e.g. Dictionary(String: Int) so that (Day: Total Calories)

Please don't be harsh I am still a novice developer. Thanks in advance.

3 Answers3

1

A lot of what you're attempting to do can be accomplished using Swift's map, sorted, filtered, and reduce functions.

struct CalorieLog {

    var date: Date
    var calories: Int

}

var logs: [CalorieLog] = []

// I changed your method to pass in calculatedCalories, we can make that random just for learning purposes. See below
func logCalories(calculatedCalories: Int) {

    let currentDate: Date = Date()

    logs.append(CalorieLog(date: currentDate, calories: calculatedCalories))

}

// This is a method that will calculate dummy calorie data n times, and append it to your logs array
func addDummyCalorieData(n: Int, maxRandomCalorie: Int) {

    for _ in 1...n {
        let random = Int(arc4random_uniform(UInt32(maxRandomCalorie)))

        logCalories(calculatedCalories: random)
    }

}

// Calculate 100 random CalorieLog's with a max calorie value of 1000 calories
addDummyCalorieData(n: 100, maxRandomCalorie: 1000)

// Print the unsorted CalorieLogs
print("Unsorted Calorie Data: \(logs)")

// Sort the logs from low to high based on the individual calories value. 
let sortedLowToHigh = logs.sorted { $0.calories < $1.calories }

// Print to console window
print("Sorted Low to High: \(sortedLowToHigh)")

// Sort the CalorieLogs from high to low
let sortedHighToLow = logs.sorted { $1.calories < $0.calories }

// Print to console window
print("Sorted High to Low: \(sortedHighToLow)")

// Sum
// This will reduce the CaloreLog's based on their calorie values, represented as a sum
let sumOfCalories = logs.map { $0.calories }.reduce(0, +)

// Print the sum
print("Sum: \(sumOfCalories)")

If you wanted to map your CalorieLogs as an array of dictionaries you could do something like this:

let arrayOfDictionaries = logs.map { [$0.date : $0.calories] }

However that's kind of inefficient. Why would you want an array of dictionaries? If you just wanted to track the calories consumed/burned for a specific date, you could just make one dictionary where the date is your key, and an array of Int is the value which represents all the calories for that day. You probably would only need one dictionary, i.e.

var dictionary = [Date : [Int]]()

Then you could find all the calories for a date by saying dictionary[Date()]. Although keep in mind that you would have to have the exact date and time. You may want to change the key of your dictionary to be something like a String that just represents a date like 2/19/2017, something that could be compared easier. That will have to be taken into account when you design your model.

Pierce
  • 3,148
  • 16
  • 38
  • Thank you for your answer. How would I get the sum of each day's calories not the overall total of calories? And then have an array with the day as key and sum of that day as the value? –  Feb 19 '17 at 18:08
  • @He1nr1ch - See Matt's answer below. I think that will help you achieve what you're asking – Pierce Feb 19 '17 at 18:16
1

A great deal depends on what you mean by a "day". So, in this highly simplified example, I simply use the default definition, that is, a day as the Calendar defines it for a particular Date (not taking time zone realities into account).

Here's some basic data:

struct CalorieLog {
    var date: Date
    var calories: Int
}
var logs: [CalorieLog] = []
logs.append(CalorieLog(date:Date(), calories:150))
logs.append(CalorieLog(date:Date(), calories:140))
logs.append(CalorieLog(date:Date()+(60*60*24), calories:130))

Now we construct a dictionary where the key is the ordinality of the date's Day, and the value is an array of all logs having that day as their date:

var dict = [Int:[CalorieLog]]()
for log in logs {
    let d = log.date
    let cal = Calendar(identifier: .gregorian)
    if let ord = cal.ordinality(of: .day, in: .era, for: d) {
        if dict[ord] == nil {
            dict[ord] = []
        }
        dict[ord]!.append(log)
    }
}

Your CalorieLogs are now clumped into days! Now it's easy to run through that dictionary and sum the calories for each day's array-of-logs. I don't know what you ultimately want to do with this information, so here I just print it, to prove that our dictionary organization is useful:

for (ord,logs) in dict {
    print(ord)
    print(logs.reduce(0){$0 + $1.calories})
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Nice use of ordinality in `.era`! Note that the in-place mutation of the array values (`dict[ord]!.append(log)`) in `dict` can be a performance killer (disregarding the implications of premature optimization ...) due to a temporary shared ownership of the array(s) triggering copy-on-write, as covered [e.g in this Q&A](http://stackoverflow.com/questions/41079687/). Using remove-mutate-replace in the body of the optional binding `if` statement above could be used as an alternative to avoid this (`var ords = dict.removeValue(forKey: ord) ?? []` / `ords.append(log)` / `dict[ord] = ords`). – dfrib Feb 19 '17 at 18:55
  • ... although in this case the size of the arrays should be small (unless we're dealing with a really big eater :) with light weight elements, so I can't see this ever becoming an issue here. I leave the comment above nonetheless, as it might be useful for someone stumbling upon this particular Q&A and applying it to an application where performance could become an issue. – dfrib Feb 19 '17 at 19:14
  • 1
    @dfri The point is a welcome one; I was already thinking of replacing that line with a mutate-and-replace. Actually I was surprised that this sort of append-in-place worked at all; I would have expected `dict[ord]` to be a `let` value. – matt Feb 19 '17 at 19:31
0

To get the logs sorted by date, you can simply do:

logs.sorted(by: { $0.date < $1.date })

To get a dictionary that maps a day to a sum of calories on that day you can do this:

let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "dd MMM yyyy"

var calorieCalendar = [String: Int]()
for log in logs {
    let date = dateFormatter.string(from: log.date)
    if let _ = calorieCalendar[date] {
        calorieCalendar[date]! += log.calories
    } else {
       calorieCalendar[date] = log.calories
    }
}

For logs setup like this

logs.append(CalorieLog(date: Date(), calories: 1))
logs.append(CalorieLog(date: Date.init(timeIntervalSinceNow: -10), calories: 2))
logs.append(CalorieLog(date: Date.init(timeIntervalSinceNow: -60*60*24*2), calories: 3))

The code above will produce a dictionary like this:

["17 Feb 2017": 3, "19 Feb 2017": 3]
Stephen
  • 172
  • 2
  • 8