Episode #229

Let's Build Activity++ - Part 7

Series: Let's Build Activity++!

13 minutes
Published on July 22, 2016

This video is only available to subscribers. Get access to this video and 582 others.

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))

This episode uses Nimble 4.1.0, Quick 0.9.2, Xcode 7.3, Swift 2.3.