DEV Community

dev.to staff
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!

Latest comments (19)

Collapse
 
coolshaurya profile image
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
}

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.

Collapse
 
brightone profile image
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
 
coreyja profile image
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!

Collapse
 
coreyja profile image
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());
    }
}
Collapse
 
coreyja profile image
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!

Collapse
 
rocketrobc profile image
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

Collapse
 
alexmacniven profile image
Alex Macniven

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

Collapse
 
willsmart profile image
willsmart

A functional style one in JS

that was fun 😋

Collapse
 
kerrishotts profile image
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";

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

Collapse
 
alvaromontoro profile image
Alvaro Montoro

This is great