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