7
\$\begingroup\$

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 "5-10 min" or "48 min-2 hr".

Notes:

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

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

  • The "real" 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 "idiomatic" feedback is also welcome.

Sample Input:

print_reading_time("This is a test.");

print_reading_time(str_repeat("test ",   100));
print_reading_time(str_repeat("test ",  1000));
print_reading_time(str_repeat("test ", 10000));
print_reading_time(str_repeat("test ", 20000));

print_reading_time(str_repeat("test ", 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:

<?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("estimated reading time: $minutes min ($time_label), " .
      "based on $word_count words at $wpm WPM");
}

function minutes_to_time_label($minutes, $minimum_time_bucket) {
  // Buckets: <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 < count($buckets) - 1; $i++) {
    $lowerBound = $buckets[$i] * $minimum_time_bucket;
    $upperBound = $buckets[$i+1] * $minimum_time_bucket;

    if ($lowerBound <= $minutes && $minutes < $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 "5-10 min", not "5 min-10 min".
      if (($lowerBound <= 60 && $upperBound <= 60) || ($lowerBound > 60 && $upperBound > 60)) {
        $lowerOptions["no_units"] = true;
      }
      return minutes_to_time_str($lowerBound, $lowerOptions) . "-"
          . 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("treat_60_min_as_an_hour" => true));
}

function minutes_to_time_str($minutes, $options = array()) {
  $options = array_merge(default_options(), $options);
  $amount = $minutes;
  $units = "min";

  if ($minutes > 60 || ($minutes === 60 && $options["treat_60_min_as_an_hour"])) {
    $amount = round($minutes / 60);
    $units = "hr";
  }

  return $amount . ($options["no_units"] ? "" : " $units");
}

function default_options() {
  return array(
    "no_units" => false,
    "treat_60_min_as_an_hour" => false,
  );
}

?>
\$\endgroup\$
  • \$\begingroup\$ Just sharing a thought, this would look nicer inside of a PHP class... \$\endgroup\$ – Chococroc Jul 20 '15 at 6:45
  • \$\begingroup\$ This is only a snippet extracted from an existing real-world project, where I need to match how the rest of the codebase already works. But you're right in general, this would be nicer in a class, and is a good comment for others reading this thread in the future. :) \$\endgroup\$ – Arthaey Jul 20 '15 at 18:14
1
\$\begingroup\$

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 > 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 => $val)
{
  if ( $minutes >= $val && $minutes <= $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.

\$\endgroup\$
  • \$\begingroup\$ So the PHP itself is reasonably idiomatic? This was my first post to this community, so apologies for not getting things quite right. Some suggestions (separating reading_time vs printing, renaming functions) are how I actually do it in my real project, but I thought I'd simplify a little for posting here. Seems that wasn't the right tack after all. I also have a Javasrcipt version of this same algorithm (for a separate project), and that one does expose the bucket size to the user, and so uses a more flexible multiplier array like you suggest. :) Good call on checking for >60min first. \$\endgroup\$ – Arthaey Jul 20 '15 at 18:22
  • 1
    \$\begingroup\$ @Arthaey Angosii, it's all good. Since it's a review site we can really only go by what you post - however, if you are already doing all the stuff suggested then I don't have much value to add. ;) \$\endgroup\$ – Max Haaksman Jul 20 '15 at 18:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.