Let's Build Activity++ - Part 7

Episode #229 | 13 minutes | published on July 22, 2016 | Uses Nimble-4.1.0, Quick-0.9.2, Xcode-7.3, swift-2.3
Subscribers Only
We build our activity streak detection algorithm, testing it along the way with Quick and Nimble.

Episode Links

Defining the Streak Data Structure

First we need to define a structure to hold our streak data.

enum MetricType {
    case Movement
    case Exercise
    case Standing
}

struct Streak {
    let metric: MetricType
    var numberOfDays: Int
}

Now we need a way of computing the streaks, given the activity logs we were provided.

Computing Streaks

  lazy var streaks: [Streak] = {
        var streaks: [Streak] = []

        var potentialStreaks: [MetricType: Int] = [:]

        var lookup: [MetricType : (ActivityLog) -> Bool] = [
            .Movement : { $0.activityProgress >= 1.0 },
            .Exercise : { $0.exerciseProgress >= 1.0 },
            .Standing : { $0.standProgress >= 1.0 }
        ]

        for activity in self.activities {
            for metric in lookup.keys {
                let hasCompletedMetric = lookup[metric]!(activity)
                if hasCompletedMetric {
                    let numberOfDays = (potentialStreaks[metric] ?? 0) + 1
                    potentialStreaks[metric] = numberOfDays
                } else {
                    if let numberOfDays = potentialStreaks[metric] where numberOfDays > 1 {
                        streaks.append(Streak(metric: metric, numberOfDays: numberOfDays))
                    }
                    potentialStreaks[metric] = nil
                }
            }
        }

        return streaks
    }()

Testing the Algorithm

   describe("streak detection") {

            var samples: [ActivityLog]!
            let calendar = NSCalendar.currentCalendar()
            var streaks: [Streak]!


            context("no streaks") {
                beforeEach {
                    samples = FakeHealthData.randomSamplesWithNoStreaks(calendar: calendar, startingWithDate: NSDate())
                    var datasource = ActivityDataSource(calendar: calendar, activities: samples)
                    streaks = datasource.streaks
                }

                it("returns no streaks") {
                    expect(streaks).to(beEmpty())
                }
            }

            context("known streaks") {
                beforeEach {
                    samples = FakeHealthData.randomSampleWithStreaks(calendar: calendar, startingWithDate: NSDate())
                    var datasource = ActivityDataSource(calendar: calendar, activities: samples)
                    streaks = datasource.streaks
                }

                it("should return 2 streaks") {
                    expect(streaks).to(haveCount(2))
                }

                it("should have a movement streak of 4 days") {
                    let movementStreak = streaks.filter { $0.metric == .Movement }.first
                    expect(movementStreak).notTo(beNil())
                    expect(movementStreak!.numberOfDays).to(equal(4))
                }

                it("should have a standing streak of 5 days") {
                    let standingStreak = streaks.filter { $0.metric == .Standing }.first
                    expect(standingStreak).notTo(beNil())
                    expect(standingStreak!.numberOfDays).to(equal(5))
                }
            }

Adding the Date

In the video I neglected to add an important piece: the date of the streak! The easiest solution would be to have the streak's date be the starting date of the streak:

struct Streak {
  let metricType: MetricType
  let startingDate: NSDate
  let numberOfDays: Int
}

Then when we create the streak, we can use the date of the current log, which we've already guaranteed will be in reverse chronological order -- in other words the last element in a streak will be the earliest date. We can then create the streak like this:

streaks.append(Streak(metric: metric, startingDate: activity.date, numberOfDays: numberOfDays))
blog comments powered by Disqus