Problem

I’ve finally got a solution for a “weekly streak” bucketing system I’ve been working on but unfortunately it’s resulted in a very large switch statement.

I’m running through a handful of dates and determining by a range of seconds (the length of a week) which bucket that date belongs to.

It’s obvious to me that this could be refactored in a programmatic way because of the amount of repetition but I’m not sure where to start. Dictionary? Some kind of sorting algorithm? Recursion?

```
open func weeklyStreakCount(weeklyGoal target: Int) -> Int {
let endDate = Date()
let startDate = endDate.startOfWeek!
let startDateInterval = Double(startDate.timeIntervalSinceNow)
var workoutsPerWeek = [Int: Int]()
let userWorkouts: [UserWorkoutEntity] = self.userWorkouts(completed: true)
var numberOfGoodBuckets = 0
for i in 0...100 {
workoutsPerWeek.updateValue(0, forKey: i)
}
// calculate the time from now to seconds in a week and round to the nearest hundreths to create a bucket for that week
for userWorkout in userWorkouts {
let workoutTimeInterval = Double((userWorkout.completionDate?.timeIntervalSinceNow)!)
let rawBucket = (startDateInterval - workoutTimeInterval) / numberOfSecondsInAWeek
let bucket = Int(rawBucket * 1000)
let abbrNumberOfSecondsInAWeek = 604
switch bucket {
case 0 ... abbrNumberOfSecondsInAWeek:
workoutsPerWeek[0]! += 1
case abbrNumberOfSecondsInAWeek ... (abbrNumberOfSecondsInAWeek * 2):
workoutsPerWeek[1]! += 1
case abbrNumberOfSecondsInAWeek * 2 ... (abbrNumberOfSecondsInAWeek * 3):
workoutsPerWeek[2]! += 1
case abbrNumberOfSecondsInAWeek * 3 ... (abbrNumberOfSecondsInAWeek * 4):
workoutsPerWeek[3]! += 1
case abbrNumberOfSecondsInAWeek * 4 ... (abbrNumberOfSecondsInAWeek * 5):
workoutsPerWeek[4]! += 1
case abbrNumberOfSecondsInAWeek * 5 ... (abbrNumberOfSecondsInAWeek * 6):
workoutsPerWeek[5]! += 1
case abbrNumberOfSecondsInAWeek * 6 ... (abbrNumberOfSecondsInAWeek * 7):
workoutsPerWeek[6]! += 1
case abbrNumberOfSecondsInAWeek * 7 ... (abbrNumberOfSecondsInAWeek * 8):
workoutsPerWeek[7]! += 1
case abbrNumberOfSecondsInAWeek * 8 ... (abbrNumberOfSecondsInAWeek * 9):
workoutsPerWeek[8]! += 1
case abbrNumberOfSecondsInAWeek * 9 ... (abbrNumberOfSecondsInAWeek * 10):
workoutsPerWeek[9]! += 1
default:
break
}
}
// Run through each bucket and see how many times the user hit their goal
for i in 0...10 {
if(workoutsPerWeek[i]! > target) {
numberOfGoodBuckets += 1
} else {
break
}
}
return numberOfGoodBuckets
}
```

Solution

First, I would replace the dictionary

```
var workoutsPerWeek = [Int: Int]()
```

by an array. A dictionary *could* be memory-saving if you have “few” keys in a “large range”,

e.g. 1, 20, 300, 4000, as a “sparse-array” emulation. But that is not the case here:

The bucket numbers range from 0 to 9, so that an array is more appropriate,

easier to initialize, and easier to access (no forced unwraps needed).

It is unclear in your code how many entries are needed: You

initialize entries for `0...100`

, update entries for `0...9`

, and finally

evaluate entries `0...10`

. If you want to collect the data for the last 10 weeks then it would be

```
var workoutsPerWeek = Array(repeating: 0, count: 10)
```

Now you can use the computed bucket number as an index into the array and

replace the switch statement by

```
let rawBucket = Int((startDateInterval - workoutTimeInterval) / numberOfSecondsInAWeek)
if rawBucket >= 0 && rawBucket < workoutsPerWeek.count {
workoutsPerWeek[rawBucket] += 1
}
```

The range check for the bucket number can also be done as

```
if workoutsPerWeek.indices.contains(rawBucket) { ... }
```

There are more things which can be improved: In

```
let workoutTimeInterval = Double((userWorkout.completionDate?.timeIntervalSinceNow)!)
```

the conversion to `Double`

is not needed. And what if `completionDate`

is `nil`

?

Your code would crash in that case.

But my main point of criticism is *how* the bucket is computed.

**A day does not necessarily have 24 hours.** In regions with daylight saving time,

a day can have 23 or 25 hours if the clocks are adjusted one hour back or forward.

Applied to your code: A week does not necessarily have 604,800 seconds.

The proper way to compute calendar differences is to use the `Calendar`

methods

and `DateComponents`

:

```
for userWorkout in userWorkouts {
// Ensure that completionDate is set, otherwise ignore this entry
guard let completionDate = userWorkout.completionDate else { continue }
// Compute #of weeks between completionDate and startDate
let weeksAgo = Calendar.current.dateComponents([.weekOfMonth], from: completionDate, to: startDate).weekOfMonth!
// Update corresponding bucket
if workoutsPerWeek.indices.contains(weeksAgo) {
workoutsPerWeek[weeksAgo] += 1
}
}
```

Finally, the calculation of the number of consecutive weeks where the goal has been reached can be simplified to

```
let numberOfGoodBuckets = workoutsPerWeek.prefix(while: { $0 >= target }).count
```