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.
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
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
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
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:
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.com for the body. The Word count and Reading time fields are left blank.
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.
app > views > post > _form.html.erb and remove the two
div blocks of code for the word count and reading time fields.
<%= 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”.
<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.
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:
> (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
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.
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
Finally, we updated the New and Show pages (views) so that better information was displayed to the user.
I would definitely recommend checking out Dean’s video and all of his other videos.