loading...

Daily Challenge #21 - Human Readable Time

thepracticaldev profile image dev.to staff ・1 min read

Today's challenge is from davazp on CodeWars.

The function will accept an input of non-negative integers. If it is zero, it just returns "now". Otherwise, the duration is expressed as a combination of years, days, hours, minutes, and seconds, in that order.

The resulting expression is made of components like 4 seconds, 1 year, etc. The unit of time is used in plural if the integer is greater than 1. The components are separated by a comma and a space (", "), except the last component which is separated by " and ", just like it would be written in English. For the purposes of this challenge, a year is 365 days and a day is 24 hours. Note that spaces are important.

The challenge is much easier to understand through example:
format_duration(62) # returns "1 minute and 2 seconds"
format_duration(3662) # returns "1 hour, 1 minute and 2 seconds"

Definitely a useful bit of code to have. It's much easier to work in seconds sometimes while coding, but easier to read human time in actual use.

Good luck!


Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge for a future post? Email yo+challenge@dev.to with your suggestions!

Posted on by:

thepracticaldev profile

dev.to staff

@thepracticaldev

The hardworking team behind dev.to ❤️

Discussion

markdown guide
 

Javascript, but with functional stuff

const quotientFunctions = {
  year: time => Math.floor(time / (365 * 24 * 60 * 60)),
  day: time => Math.floor((time / (24 * 60 * 60)) % 365),
  hour: time => Math.floor((time / (60 * 60)) % 24),
  minute: time => Math.floor((time / 60) % 60),
  second: time => Math.floor(time % 60),
};
const pluralize = ({
  str,
  quotient
}) => quotient > 1 ? `${quotient} ${str}s` : `${quotient} ${str}`;
const humanReadable = time => Object.entries(quotientFunctions)
  .map(([str, quotient]) => ({
    str,
    quotient: quotient(time),
  }))
  .filter(({ quotient }) => quotient >= 1) // prevent '0 year 0 day...' polluting the final string
  .map(pluralize).join(' and ');
 

Ya this answer ruined me 😆
Once I read it's the only way to 'cleanly' solve it I could think of!

I opted to force myself to go with a different iterativ-ish solution but this one gets my vote here!

 

You could think about the comma/and issue that I was too lazy to make

And thanks for the kind words :)

You could think about the comma/and issue that I was too lazy to make

Ahh ya I didn't notice that on my first quick read through. I did get the commas right, I believe, so I got that going for me!

The part of this that I really like is just defining the conversion functions, it reads much better than the solution that I came up with in terms of how the conversions all work!

 
 

JavaScript

const format_duration = number => {
  if (parseInt(number) && number > 0) {
    // breakpoints: 60 seconds in a minute, 3600 seconds in an hour, etc.
    const bp = [60, 3600, 86400, 31536000, Number.MAX_VALUE];
    const units = ["second", 'minute', 'hour', 'day', 'year'];
    let solution = [];
    for (let x = 0; x < bp.length; x++) {
      if (number % bp[x] !== 0) {
        const value = Math.floor((number % bp[x]) / (bp[x-1] || 1));
        solution.push(`${value} ${units[x]}${value > 1 ? 's' : ''}`);
        number -= number % bp[x];
      }
    }

    // edge case: the value was too large and the modulus was considered 0
    if (number > 0) solution.push(`${number} years`)

    // add the "and" for the last element (because Oxford comma and cheating)
    solution[0] = "and " + solution[0];

    return solution.reverse().join(', ');
  }
  return "Invalid number";
}

With a little bit of cheating: instead of replacing the last comma with an "and", I just add the "and" to the last element and claim that everyone should be using Oxford comma to avoid misunderstandings.

Live demo on CodePen.

 

This feels overcomplicated. It probably can be done easier with a map or a reduce. I'll check later with more time.

 

Hello Alvaro. Did u get a chance to work on this?🤓

Thats fine Alvaro. Are u on discord??

Not really. I have only used once or twice.

Cool.i like the way you explain things man. Just want to be in touch with you to ask some basic doubta as im a beginner. Is there any other way i can be in touch. Do u have wssap? I wont bother u much.dont be scared😅

I'm not going to lie: I've had bad experiences with this in the past. If you have questions and you post them here or in StackOverflow, I'll be happy to look at them and answer if I know the answer.

 
#!/usr/bin/perl
use warnings;
use strict;

my @UNITS;
unshift @UNITS, $_ * ($UNITS[0] || 1) for 1, 60, 60, 24, 365;
my @NAMES = qw( year day hour minute second );

sub format_duration {
    my ($s) = @_;
    my @out;
    for (@UNITS) {
        push @out, int($s / $_);
        $s = $s % $_;
    }
    @out = map $out[$_] ? "$out[$_] $NAMES[$_]"
        . ("", 's')[ $out[$_] > 1 ]: (), 0 .. @NAMES;
    return join ' and ', join(', ', @out[0 .. $#out - 1]) || (), $out[-1];
}


use Test::More tests => 5;

is format_duration(62), '1 minute and 2 seconds';
is format_duration(3662), '1 hour, 1 minute and 2 seconds';
is format_duration(66711841),
    '2 years, 42 days, 3 hours, 4 minutes and 1 second';
is format_duration(31539601), '1 year, 1 hour and 1 second';
is format_duration(120), '2 minutes';
 

Easy to muck up, that's for sure. For awhile, I had this thing trying to say that 3600 was equal to several days (instead of an hour).

Quite enjoyable. Turns out that Wolfram Alpha stops being terribly useful if you give it a large number of seconds, so had to go "sure, that looks right" a couple times.

const notEmpty = i => i !== "";
const singularize = str => str.substr(0, str.length - 1);
const englishJoin = (str, part, idx, arr) => str + ((idx === arr.length - 1 && arr.length > 1) ? " and " : ( str && ", ")) + part;
const spellInterval = (seconds = 0) => 
    Object.entries({
        years: 365 * 24 * 60 * 60,
        days: 24 * 60 * 60,
        hours: 60 * 60,
        minutes: 60,
        seconds: 1 
    }).reduce(({secondsRemaining, spelling}, [units, place]) => {
        const v = Math.floor(secondsRemaining / place);
        return {
            secondsRemaining: secondsRemaining % place,
            spelling: [...spelling, v ? `${v} ${v === 1 ? singularize(units) : units}` : ""]
        };
    }, { secondsRemaining: seconds, spelling: []})
    .spelling
    .filter(notEmpty)
    .reduce(englishJoin, "")
    .trim() || "now";

Gist: gist.github.com/kerrishotts/e655d6...

 

Javascript (no idea whether this works or not :D )

const quoAndRem = (divd,div) => {
let rem = divd % div
let quo = (divd - rem)/div

return [quo,rem]
}

const formatter = (...vals) => {
  const output = vals.map((itm,indx) => {
    let unit
    switch (indx) {
      case 0:
        unit = "year"
        break;
      case 1:
        unit = "day"
        break;
      case 2:
        unit = "hour"
        break;
      case 3:
        unit = "minute"
        break;
      case 4:
        unit = "second"
        break;
    }

    if (indx !== 1) {
      unit += "s"
    }

    return [itm,unit]
  }).filter(itm => itm[0] !== 0).reduce((accm,curr,indx,arr) => {
    if (indx < arr.length - 2) {
      return `${accm}${curr[0]} ${curr[1]}, `
    } else if (indx === arr.length - 2) {
      return `${accm}${curr[0]} ${curr[1]} and `
    } else {
      return `${accm}${curr[0]} ${curr[1]}`
    }
  },"");

  return output
}

function readableTime(seconds) {
const yearInSecs = 365*24*60*60
const dayInSecs = 24*60*60
const hourInSecs = 60*60
const minInSecs = 60
const secInSecs = 1

let yearVal = quoAndRem(seconds, yearInSecs)
let dayVal = quoAndRem(yearVal[1], dayInSecs)
let hourVal = quoAndRem(dayVal[1], hourInSecs)
let minVal = quoAndRem(hourVal[1], minInSecs)
let secVal = quoAndRem(minVal[1], secInSecs)

output = formatter(yearVal[0],dayVal[0],hourVal[0],minVal[0],secVal[0])

return output
}

console.log(readableTime(60))

I may change the formatter function to be more elegant and less crappy using slice and arrays and idontknow.

 

(Late) Rust Solution

pub fn format_duration(seconds: u64) -> String {
    if seconds == 0 {
        "now".to_string()
    } else {
        let units = [
            ("second", Some(60)),
            ("minute", Some(60)),
            ("hour", Some(24)),
            ("day", Some(365)),
            ("year", None),
        ];
        let mut cur = seconds;
        let mut times = vec![];
        for (label, number_before_next) in units.iter() {
            let remainder = match number_before_next {
                Some(x) => cur % x,
                None => cur,
            };
            cur = match number_before_next {
                Some(x) => cur / x,
                None => 0,
            };
            let maybe_plural_label = if remainder != 1 {
                format!("{}s", label)
            } else {
                label.to_string()
            };
            if remainder > 0 {
                times.push(format!("{} {}", remainder, maybe_plural_label));
            }
        }

        let mut iter = times.iter().cloned();
        let last_unit = iter.next().unwrap();
        let rest_units = iter.rev().collect::<Vec<_>>().join(", ");

        if rest_units.len() > 0 {
            format!("{} and {}", rest_units, last_unit)
        } else {
            last_unit
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::format_duration;

    #[test]
    fn it_for_zero() {
        assert_eq!(format_duration(0), "now".to_string());
    }

    #[test]
    fn it_for_the_small_example() {
        assert_eq!(format_duration(62), "1 minute and 2 seconds".to_string());
    }

    #[test]
    fn it_for_the_large_example() {
        assert_eq!(
            format_duration(3662),
            "1 hour, 1 minute and 2 seconds".to_string()
        );
    }

    #[test]
    fn it_works_for_e_chorobas_examples() {
        assert_eq!(
            format_duration(31539601),
            "1 year, 1 hour and 1 second".to_string()
        );
        assert_eq!(
            format_duration(66711841),
            "2 years, 42 days, 3 hours, 4 minutes and 1 second".to_string()
        );
        assert_eq!(format_duration(120), "2 minutes".to_string());
    }
}
 

Python3

Solution

DIVS = [86400 * 365, 86400, 3600, 60]


def eval_time(a, b, c):
    if a >= DIVS[b]:
        c.append(a // DIVS[b])
    else:
        c.append(0)
    remain = a % DIVS[b]
    if (remain > 0) and (b < 3):
        eval_time(remain, b + 1, c)
    else:
        c.append(remain)

def pretty_output(a):
    names = [" year", " day", " hour", " minute", " second"]
    r = ""
    for i in range(0, len(a)):
        if a[i] >= 1:
            r += str(a[i]) + names[i]
            if a[i] > 1:
                r += "s"
            r += ", "
    r = r.strip(", ")
    s_items = r.rsplit(",")
    if len(s_items) > 1:
        print(",".join(s_items[:-1]) + " and" + s_items[-1])
    elif s_items[0] == "":
        print("now")
    else:
        print(r)

def format_duration(n):
    time_vals = []
    eval_time(n, 0, time_vals)
    pretty_output(time_vals)


format_duration(62)
format_duration(3662)
format_duration(66711841)
format_duration(31539601)
format_duration(120)
format_duration(0) 

Output

1 minute and 2 seconds
1 hour, 1 minute and 2 seconds
2 years, 42 days, 3 hours, 4 minutes and 1 second
1 year, 1 hour and 1 second
2 minutes
now

The pretty_output method was more challenging than I first thought, I did get a bit lazy with the if/elif/else statements

 

Ruby

SECONDS = 1
MINUTES = 60
HOURS = 3600
DAYS = 86400
YEARS = 31536000

NAMES = %w[year day hour minute second]

def calculate_durations(seconds)
  [YEARS, DAYS, HOURS, MINUTES, SECONDS].map do |period|
    next if period > seconds
    quantity = seconds / period
    seconds -= (quantity * period)
    quantity
  end
end

def trim_durations(durations)
  while durations[-1].nil?
    durations.pop
  end
  durations
end

def format_duration(seconds)
  durations = trim_durations(calculate_durations(seconds))
  output = ''
  NAMES.each.with_index do |name, i|
    next if durations[i].nil?
    name += 's' if durations[i] > 1
    unless i == durations.size - 1
      output += (durations[i].to_s + ' ' + name)
      output += ', ' if i < durations.size - 2
    else
      output += (' and ' + durations[i].to_s + ' ' + name)
    end
  end
  output
end

puts "4500 seconds: #{format_duration(4500)}"
puts "2000487 seconds: #{format_duration(2000487)}"
puts "42000487 seconds: #{format_duration(42000487)}"
puts "1563739200 seconds: #{format_duration(1563739200)}"

4500 seconds: 1 hour and 15 minutes
2000487 seconds: 23 days, 3 hours, 41 minutes and 27 seconds
42000487 seconds: 1 year, 121 days, 2 hours, 48 minutes and 7 seconds
1563739200 seconds: 49 years, 213 days and 20 hours

 

Elixir:

defmodule ReadableTime do
  @minute 60
  @hour @minute * 60
  @day @hour * 24
  @year @day * 365

  @spec from_seconds(non_neg_integer) :: String.t()
  def from_seconds(0), do: "now"

  def from_seconds(time) do
    [
      year: &div(&1, @year),
      day: &(&1 |> div(@day) |> rem(365)),
      hour: &(&1 |> div(@hour) |> rem(24)),
      minute: &(&1 |> div(@minute) |> rem(60)),
      second: &rem(&1, 60)
    ]
    |> Enum.map(fn {word, quotient} -> {quotient.(time), pluralize(word, quotient.(time))} end)
    |> Enum.filter(fn {quotient, _} -> quotient > 0 end)
    |> Enum.map(fn {quotient, word} -> "#{quotient} #{word}" end)
    |> to_sentence()
  end

  @spec pluralize(String.t(), non_neg_integer) :: String.t()
  defp pluralize(word, 1), do: word
  defp pluralize(word, n) when n >= 0, do: "#{word}s"

  @spec to_sentence([String.t()]) :: String.t()
  defp to_sentence([elem]), do: elem

  defp to_sentence(list) do
    (list |> Enum.slice(0..-2) |> Enum.join(", ")) <>
      " and " <> List.last(list)
  end
end
 

A functional style one in JS

that was fun 😋