Progress can be hard to measure with anything you do. That’s why having a process for becoming better at something is so helpful. You don’t always see results right away, but if you practice enough, things will start coming together. Being able to keep yourself accountable for sticking to a process is much easier when you can easily measure it.

When I started building Verba, I tried to think of what would help to motivate me to write every day. I took a cue from Github that tracking a streak of days would work. I wrote an earlier post on how Github’s streak feature helps me commit code (almost) every day.

My goal for Verba was to be able to track the number of days in a row that a given user had written a post. This metric would be displayed to each user.

When I thought about how I could implement a streak feature, I determined that there were two different ways I could build it:

  1. The User class has a streak integer column that gets checked and possibly changed every time a post is created.
class Post < ActiveRecord::Base
  #...
  after_create :check_streak

  def check_streak
    # Somehow check the last post, and the post before that, and so on.
    # Increment the user's streak column.
  end
end
  1. The User class has an instance method #streak that calculates the streak of posts every time it is called.
class User < ActiveRecord::Base
  #...

  def streak
    # Get all of this user's posts.
    # Determine how many days long it's streak is.
  end
end

There are several tradeoffs here that gave me a bit of trouble. With option one, the Post class will have to know about the concept of a streak, check whether there was a post before it, and so on. Plus, changing other objects in a callback probably isn’t the best decision, as it introduces tight coupling. It doesn’t cost much in performance, since it only increments a column, but the complexity of the system on my end is high.

With option two, performance will most likely take a hit. Every time #streak is called on a user, all of it’s associated posts need to be retrieved from the database and we have to calculate a streak based on that returned collection on posts. That being said, this makes it much simpler for me. All I have to worry about is the calculation.

I ended choosing the second option for a couple reasons. It only lives in the User class, which I think is better because the concept of a streak means nothing to a single post. Only users care about their streak. Also, this way makes it easier makes extending the behavior much easier. I’ll explain why later.

Let’s get into the implementation:

Here, I wrote a simple spec to make sure I’m getting the implementations right, and it will make it easier to refactor later.

require 'rails_helper'

describe User do
  context '#streak' do
    it "returns three for three posts in a row" do
      user = User.create(name: "Garrett")
      user.posts.create(content: "hello", created_at: 2.days.ago) 
      user.posts.create(content: "hello", created_at: 1.day.ago)
      user.posts.create(content: "hello", created_at: DateTime.current)

      expect(user.streak).to eq(3)
    end
  end
end

First, I’m going to write some pseudo-code in a comment to try to think through the calculation I need to perform.

class User < ActiveRecord::Base
  has_many :posts

  def streak
    # Get all of the post records associated with a given user.

    # Order them descending by created_at.

    # Iterate over each date, checking if the next one in line is
    # an adjacent day. Increment a streak counter for every day in a streak.

    # Return the length of the streak.
  end
end

By having this clear path to getting to the answer I want, writing the code is much easier. Here’s what I came up with:

class User < ActiveRecord::Base
  # ...
  def streak
    # Exit the method if the user has no posts.
    return if posts.blank?

    # Get the user's posts in descending order
    @posts = posts.order('created_at DESC')

    # Get the dates for each, and make sure there aren't any duplicates.
    days = @posts.pluck(:created_at).map(&:to_date).uniq

    # If the most recent post was today, the streak is one, otherwise, it is zero.
    streak = days.first == Date.current ? 1 : 0

    days.each_with_index |day, index|
      # Exit loop unless there was a post written today.
      # This means that the streak will remain at zero.
      break unless days.first == Date.current

      # Checks if the next day in the list is yesterday.
      if days[index+1] == day.yesterday
        streak += 1
      else
        # Exit the loop so the streak can be returned.
        break
      end
    end

    # Return the value of the streak.
    streak
  end
end

This method is works, but it’s getting long and hard to understand. We can extract some methods to clean it up:

class User < ActiveRecord::Base
  # ...
  def streak
    return if posts.blank?

    # Break up responsibilities into different methods.
    # This makes it much easier to understand what the streak method is
    # actually doing, and it eliminates the need for commenting the code.
    days = get_days
    determine_consecutive_days(days)
  end

  private

  def get_days
    self.posts.order("created_at DESC").pluck(:created_at).map(&:to_date).uniq
  end

  def determine_consecutive_days(days)
    streak = first_day_in_collection_is_today?(days) ? 1 : 0
    days.each_with_index do |day, index|
      break unless first_day_in_collection_is_today?(days)
      if days[index+1] == day.yesterday
        streak += 1
      else
        break
      end
    end
    streak
  end

  def first_day_in_collection_is_today?(days)
    days.first == Date.current
  end

end

Building this functionality took a bunch of trial, error. After thinking through how to correctly tackle the problem (and TDDing it), I finally got it to a point to where I am happy with how it works. I thought this might be useful in other projects, so I packaged it up as a gem.