1

I have a duration integer in seconds that I want to display in a human readable using vanilla js. For example:

Duration Human readable
24 24 seconds
430 7 minutes and 10 seconds
600 10 minutes
3600 1 hour
7477 2 hours, 4 minutes and 37 seconds

I've looked at Intl.DateTimeFormat but it also prints the date parts. I've also looked at Intl.RelativeTimeFormat but it only supports 1 unit at a time.

code913
  • 43
  • 6

2 Answers2

4

function convert(x) {
  let h = ~~(x / 3600), m = ~~((x - h * 3600) / 60),
    s = x - h * 3600 - m * 60;
  let words = ['hour', 'minute', 'second'];
  return [h, m, s].map((x, i) => !x ? '' :
    `${x} ${words[i]}${x !== 1 ? 's' : ''}`)
    .filter(x => x).join(', ').replace(/,([^,]*)$/, ' and$1')
}

console.log([24, 430, 600, 3600, 7477].map(convert).join('\n'));

Explanation: Get the hours, minutes and seconds, put them in an array and map through adding the words hour(+ s if 1), minute(+s if 1) and seconds(+s if 0) - also make sure to return empty string if a value is zero. Filter out empty strings. Join on ', ' and then replace last ',' with 'and'.

What about ~~? It does the same as Math.floor() for positive numbers (and same as Math.ceil for negative numbers). If you feel it reduces readability replace it with Math.floor in this case.

Thomas Frank
  • 1,404
  • 4
  • 10
  • 1
    To a learner, you're aware that this `let h = ~~(x / 3600)` looks like black magic, or a black box, right? Could you explain what it's doing? To be clear, the answer works beautifully and I happily upvote, but I'm cautious that learners will struggle to learn from it without some explanatory comments. – David Thomas May 03 '23 at 12:53
  • It does the same as Math.floor for positive numbers and the same as Math.ceil for negative numbers. If you feel it reduces readability replace with Math.floor. – Thomas Frank May 03 '23 at 12:55
  • I personally understood what it was doing, but as I said: I was concerned that learners would (probably) not. But thank you for the explanation and the edit. :) – David Thomas May 03 '23 at 12:56
  • I have :) But then I been part of writing the software ;) – Thomas Frank May 03 '23 at 14:32
1

Code

function readableDuration(seconds) {
  const locale = "en-US";
  const wordMap = [
    [3600, "hour"],
    [60, "minute"],
    [1, "second"],
  ];
  const formatter = new Intl.ListFormat(locale, { style: "long", type: "conjunction" });

  function splitInterval(value, index = 0) {
    const [divisor, unit] = wordMap[index];
    const mod = value % divisor;
    const quot = Math.floor(value / divisor);
    const remaining = value - quot * divisor;
    let arr = [];
    if (quot >= 1) arr.push(`${quot} ${unit}${quot !== 1 ? "s" : ""}`);
    if ((quot < 1 || mod > 0) && (++index) < wordMap.length) arr.push(...splitInterval(Math.floor(remaining), index));

    return arr;
  }

  return formatter.format(splitInterval(seconds));
}

Explanation

The readableDuration function below accepts values in seconds (see notes below on how to change it from seconds) and returns a human readable string. The wordMap variable contains values for how many seconds an hour, minute etc. is, and 1 for second itself. It must be ordered from largest to smallest.

In the splitInterval function:

  1. Grab the seconds equivalent and word for the current unit
  2. Calculate the modulus (seconds % unit equivalent)
  3. Calculate and round-off the quotient (The 1 in 1 hour and 2 minutes)
  4. Get the remaining seconds after rounding off using the modulus
  5. Declare an array to store every unit & value in the final string
  6. When the quotient is greater than or equal to 1, add it to the array with the singular/plural form* of the unit
  7. Add a smaller unit when:
    • The current unit is not the end of wordMap, and
    • The quotient is smaller than 1 and would look strange to print with the current unit (0.25 hours vs 15 minutes)
    • Modulus is larger than 0 which means there are remaining seconds
  8. Finally use Intl.ListFormat to join the parts* and return the string.

Notes

  • The numbers in wordMap must be relative to one value e.g. 1 second but that value can be changed freely along with the words
  • The plural part in the code works by just adding s but you'll want to use a different method for i18n.
  • Change locale to en-UK if you want to remove the oxford comma
code913
  • 43
  • 6