Post

Generate Word Count and Reading Time for Posts in Rails

A few weeks ago, I watched a video from Dean at Deanin on YouTube where he showed how to use StimulusJS to generate reading times for posts/articles in his demo application.

In his video, he used a split method to get the number of words in the article. He then divided that number by 265, which is an approximation for the number of words adult humans can read in one minute.

According to various websites, adult humans can read anywhere between 200 and 300 words per minute.

To calculate the reading time, the word count is divided by the WPM. Example: a 400 word post would equate to 1.09 minutes, or “about one minute”.

I’m not too comfortable with Stimulus just yet and wondered how I could make it work by storing the word count and the reading time on the record in the database.

Dean commented on my question and recommended the before_save callback. So, just like his videos, I fired up the terminal and generated a test Rails web application.

Let’s build the Rails web application

Generate a new rails application.

rails new word_count

Generate a Post model, controller, and views using the scaffold generator. Then, migrate the database.

1
2
3
rails g scaffold Post title:string body:text word_count:integer reading_time:integer`

rails db:migrate

Modify the Post model, adding the before_save callback. The before_save call back will call a new action that we’ll write called calculate_reading_time.

1
2
3
4
5
6
7
8
9
class Post < ApplicationRecord
  before_save :calculate_reading_time

  def calculate_reading_time
    wpm = 265
    self.word_count = self.body.split.length
    self.reading_time = word_count / wpm
  end
end

The calculate_reading_time action will:

  • split the post into words,
  • count those words,
  • divide the word count by 265, and
  • store the values on the record.

I’d recommend removing the word_count and reading_time parameters from the strong parameters action on the app > controllers > posts_controller.rb.

Sure it’s a simple application, but it’s a good practice to not permit extra parameters to be passed in if they aren’t needed.

It should look something like this when you’re done:

1
2
3
4
5
6
7
8
9
10
11
12
13
class PostsController < ApplicationController
...

private
...

# Only allow a list of trusted parameters through.

    def post_params
      params.require(:post).permit(:title, :body)
    end

end

Test the before_save callback action

I created a new post by using a simple title and adding filler text from hipsum.co for the body. The Word count and Reading time fields are left blank.

Before clicking on Create Post

Before clicking on Create Post

After clicking on the Create Post button, you can see that the Word count and Reading time fields are automatically updated by the before_save: calculate_reading_time callback.

Word count and Reading time fields were automatically updated upon saving

Update New View

Let’s go a step further and update the new view to remove the Word count and Reading time fields.

Open app > views > post > \_form.html.erb and remove the two div blocks of code for the word count and reading time fields.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<%= form_with(model: post) do |form| %>
  <% if post.errors.any? %>

    <div style="color: red">
      <h2><%= pluralize(post.errors.count, "error") %> prohibited this post from being saved:</h2>

      <ul>
        <% post.errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
      </ul>
    </div>

  <% end %>

  <div>
    <%= form.label :title, style: "display: block" %>
    <%= form.text_field :title %>
  </div>

  <div>
    <%= form.label :body, style: "display: block" %>
    <%= form.text_area :body %>
  </div>

  <div>
    <%= form.label :word_count, style: "display: block" %>
    <%= form.number_field :word_count %>
  </div>

  <div>
    <%= form.label :reading_time, style: "display: block" %>
    <%= form.number_field :reading_time %>
  </div>

  <div>
    <%= form.submit %>
  </div>
<% end %>

Update Show View

Finally, we’ll update the Post partial that gets rendered in the Show view (app > views > post > \_post.html.erb) with some logic for showing the reading time value near the top of the post.

If a post is short (less than 530 words), the reading time will show 0 for the reading time. We’ll work around this with a case statement that will display “less than a minute” if the reading_time value is 0 and “about 1 minute” if the reading_time is 1. All other values for reading_time will show “about # minutes”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div id="<%= dom_id post %>">
  <p>
    <strong>Reading Time: </strong>
    <% case post.reading_time %>
    <% when 0 %>
      less than 1 minute
    <% when 1 %>
      about 1 minute
    <% else %>
      about <%= post.reading_time %> minutes
    <% end %>
  </p>

  <p>
    <strong>Word count:</strong>
    <%= post.word_count %>
  </p>
  <p>
    <strong>Title:</strong>
    <%= post.title %>
  </p>

  <p>
    <strong>Body:</strong>
    <%= post.body %>
  </p>

</div>

Now when viewing posts, a better representation of the time it will take to read the article will be displayed.

Post with 996 words shows “about 3 minutes” for Reading Time

Post with 996 words shows "about 3 minutes" for Reading Time

Post with 133 words shows “less than 1 minute” for Reading Time

Post with 133 words shows "less than 1 minute" for Reading Time

Side Note on integer division

Like Dean, I originally chose to use the ruby ceil (ceiling) method that rounds the value up to the next whole number. However, since the field types in my Post model are integers, it’s an unnecessary addition. Since the field types are integers, the “floor” value gets stored in reading_time when the record is saved. Check out: Integer Division

Essentially, when the division is complete, the decimal values get dropped and you are left with the whole number. Examples below:

1
2
3
4
5
6
7
8
> (488/265).ceil # The ceil method does nothing with integers
> => 1

> 488.0/265.0 # Regular decimal math
> => 1.8415094339622642

> (488.0/265.0).ceil # The ceil command works here because of decimal math
> => 2

The reading_time calculation be engineered a bit more if you want to go that far by converting the field types to decimal and converting the fractions into seconds to get closer to a more accurate reading time, but I’m not going that far in this post.

Conclusion

In this post, we created a new Ruby on Rails web application that counts the number of words in a Post and then calculates the reading time based on the number of words an adult human can read per minute. Every time a Post is created or updated, a before_save callback is called to update the word_count and reading_time values. Finally, we updated the New and Show pages (views) so that better information

This post is licensed under CC BY 4.0 by the author.