Bucketizing time (e.g., 4 min => 0-5 min) Specification:

Given a string (e.g., the content of an article), print the estimated reading time by bucketizing time into increasingly large chunks in the form &quot;5-10 min&quot; or &quot;48 min-2 hr&quot;.

Notes:

Distribution of time into buckets is arbitrary, to make it &quot;look nice&quot; for the default 5-minute time chunks. This is okay.

60 minutes exactly is special-cased, so that it should be &quot;30-60 min&quot; instead of &quot;30 min-1 hr&quot;.

The &quot;real&quot; application of this code is inside a plugin for something else; rather than printing verbose information like in the code below, it actually returns $time_label.

I don't normally work in PHP, so any &quot;idiomatic&quot; feedback is also welcome.


Sample Input:
print_reading_time(&quot;This is a test.&quot;);

print_reading_time(str_repeat(&quot;test &quot;,   100));
print_reading_time(str_repeat(&quot;test &quot;,  1000));
print_reading_time(str_repeat(&quot;test &quot;, 10000));
print_reading_time(str_repeat(&quot;test &quot;, 20000));

print_reading_time(str_repeat(&quot;test &quot;, 10000), 8);

Sample Output:

estimated reading time: 0 min (0-5 min), based on 4 words at 180 WPM
estimated reading time: 1 min (0-5 min), based on 100 words at 180 WPM 
estimated reading time: 6 min (5-10 min), based on 1000 words at 180 WPM 
estimated reading time: 56 min (30-60 min), based on 10000 words at 180 WPM 
estimated reading time: 111 min (2 hr), based on 20000 words at 180 WPM
estimated reading time: 56 min (48 min-2 hr), based on 10000 words at 180 WPM

Original:
&lt;?php

function print_reading_time($content, $minimum_time_bucket = 5 /* minutes */) {
  $wpm = 180; // arbitrary value, based on adult averages
  $word_count = str_word_count($content);
  $minutes = round($word_count / $wpm);
  $time_label = minutes_to_time_label($minutes, $minimum_time_bucket);

  print(&quot;estimated reading time: $minutes min ($time_label), &quot; .
      &quot;based on $word_count words at $wpm WPM&quot;);
}

function minutes_to_time_label($minutes, $minimum_time_bucket) {
  // Buckets: &lt;5, 5-10, 10-15, 15-30, 30-60, then 1-hour increments,
  // for the default 5-minute minimum bucket size.
  $buckets = array(0, 1, 2, 3, 6, 12);
  
  for ($i = 0; $i &lt; count($buckets) - 1; $i++) {
    $lowerBound = $buckets[$i] * $minimum_time_bucket;
    $upperBound = $buckets[$i+1] * $minimum_time_bucket;
    
    if ($lowerBound &lt;= $minutes &amp;&amp; $minutes &lt; $upperBound) {
      $lowerOptions = array();
      // If the amounts are both in minutes or both in hours, don't display
      // the units for the first amount (ie, show &quot;5-10 min&quot;, not &quot;5 min-10 min&quot;.
      if (($lowerBound &lt;= 60 &amp;&amp; $upperBound &lt;= 60) || ($lowerBound &gt; 60 &amp;&amp; $upperBound &gt; 60)) {
        $lowerOptions[&quot;no_units&quot;] = true;
      }
      return minutes_to_time_str($lowerBound, $lowerOptions) . &quot;-&quot;
          . minutes_to_time_str($upperBound);
    }
  }
  
  // If we get to here, then the time is longer than our largest explicit bucket.
  // Divide everything beyond into hour-long buckets.
  return minutes_to_time_str($minutes, array(&quot;treat_60_min_as_an_hour&quot; =&gt; true));
}

function minutes_to_time_str($minutes, $options = array()) {
  $options = array_merge(default_options(), $options);
  $amount = $minutes;
  $units = &quot;min&quot;;
  
  if ($minutes &gt; 60 || ($minutes === 60 &amp;&amp; $options[&quot;treat_60_min_as_an_hour&quot;])) {
    $amount = round($minutes / 60);
    $units = &quot;hr&quot;;
  }
  
  return $amount . ($options[&quot;no_units&quot;] ? &quot;&quot; : &quot; $units&quot;);
}

function default_options() {
  return array(
    &quot;no_units&quot; =&gt; false,
    &quot;treat_60_min_as_an_hour&quot; =&gt; false,
  );
}

?&gt;
Here are some thoughts for you. I'm guessing other folks will (hopefully) tackle things from a variety of perspectives.

print_reading_time

How about changing this function to reading_time and returning nothing but the estimated time.  Perhaps change the declaration to:

reading_time($content, $wpm=180)


This function can be adjusted based on the reader and does not really need to know anything about the time buckets or how the output will be generated. You are mixing in additional concepts and that is likely to make it harder to reuse or extend the functionality.

minutes_to_time_label

Based on your descriptive language I'd probably go with something like:

minutes_to_time_bucket($minutes, $initial_bucket_size)


From here I think I'd immediately jump into a determination of whether or not the estimate is greater than 1 hour.

if ( $minutes &gt; 60 )
{
  $low = floor($minutes/60);
  $high = $low + 1;

  // return time label here
}


If we are below 60 minutes we can go into the particulars of how to scale the buckets (and do all of our work in minutes).

$bounds = array(0,5,10,15,30,60);
foreach ($bounds as $key =&gt; $val)
{
  if ( $minutes &gt;= $val &amp;&amp; $minutes &lt;= $bounds[$key+1] )
  {
    // generate label using $bounds[$key] and $bounds[$key+1]
  }
}

// If we get here there is an error, quit with error message


If you want to support bounds more dynamically, you could generate the bounds array based on the initial_bucket_size passed in until you end up with a value of 60 or greater. 

You could use an array of multipliers to do this. The multipliers using a bucket size of 5 would be 0, 1, 2, 3, 6, 12. If you optionally passed in the multipliers as an array you'd be able to change boundary definitions on the fly.

Note

I've assumed the options array and use of two functions related to label output was additional complexity to handle units. If you wanted that flexibility back you could add a parameter or two to the minutes_to_time_bucket function.

As an extension, you could perhaps support multiple languages by passing in the text to display for minutes and hours to override English defaults.

In short, my feedback would be to consider ways to keep your functions focused on a single concept. In this case we have a calculation of reading time and a method of generating a text time range based on a reading time. Some controlling code will mash those concepts together (not the functions themselves).

Beyond that, consider how to generalize those functions to support a wider variety of use within that focus. For example, an ability to use the readers own reading speed or output language doesn't change the focus of the function but will allow for use in a wider set of scenarios.