dev.to staff

Posted on

# Daily Challenge #21 - Human Readable Time

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!

Alvaro Montoro

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.

Alvaro Montoro

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

nishu1343

Hello Alvaro. Did u get a chance to work on this?š¤

Alvaro Montoro

LOL. I didn't šš¬

nishu1343

Thats fine Alvaro. Are u on discord??

Alvaro Montoro

Not really. I have only used once or twice.

nishu1343

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š

Alvaro Montoro

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.

E. Choroba
``````#!/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';
``````

Kerri Shotts

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";
``````

Shaurya

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
}

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
}

``````

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

Corey Alexander

(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());
}
}
``````

Alex Macniven

## 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

Rob Cornish • Edited

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

Oleksii Filonenko

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
``````

Corey Alexander

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!

willsmart

A functional style one in JS

that was fun š

Corey Alexander

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!

Alvaro Montoro

This is great