Human Readable Duration Format in Java


The challenge

Your task in order to complete this challenge is to write a function which formats a duration, given as a number of seconds, in a human-friendly way.

The function must accept a non-negative integer. If it is zero, it just returns "now". Otherwise, the duration is expressed as a combination of yearsdayshoursminutes and seconds.

It is much easier to understand with an example:

//returns "1 minute and 2 seconds"
TimeFormatter.formatDuration(62)

//returns "1 hour, 1 minute and 2 seconds"
TimeFormatter.formatDuration(3662)

For the purpose of this challenge, a year is 365 days and a day is 24 hours.

Note that spaces are important.

Detailed rules

The resulting expression is made of components like 4 seconds1 year, etc. In general, a positive integer and one of the valid units of time, separated by a space. 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.

A more significant units of time will occur before than a least significant one. Therefore, 1 second and 1 year is not correct, but 1 year and 1 second is.

Different components have different unit of times. So there is not repeated units like in 5 seconds and 1 second.

A component will not appear at all if its value happens to be zero. Hence, 1 minute and 0 seconds is not valid, but it should be just 1 minute.

A unit of time must be used “as much as possible”. It means that the function should not return 61 seconds, but 1 minute and 1 second instead. Formally, the duration specified by of a component must not be greater than any valid more significant unit of time.

The solution in Java code

Option 1:

import java.util.Arrays;
import java.util.stream.*;
public class TimeFormatter {    
    public static String formatDuration(int seconds) {            
        return seconds == 0 ? "now" : 
                Arrays.stream(
                  new String[]{
                       formatTime("year",  (seconds / 31536000)),
                       formatTime("day",   (seconds / 86400)%365),
                       formatTime("hour",  (seconds / 3600)%24),
                       formatTime("minute",(seconds / 60)%60),
                       formatTime("second",(seconds%3600)%60)})
                      .filter(e->e!="")
                      .collect(Collectors.joining(", "))
                      .replaceAll(", (?!.+,)", " and ");
    }
    public static String formatTime(String s, int time){
      return time==0 ? "" : Integer.toString(time)+ " " + s + (time==1?"" : "s");
    }
}

Option 2:

public class TimeFormatter {
    
    public static String formatDuration(int seconds) {
        String res = "";
        int[] units = new int[] {31536000, 86400, 3600, 60, 1};
        String[] labels = new String[] {"year", "day", "hour", "minute", "second"};
        
        if (seconds == 0) return "now";
        
        for (int i = 0; i < 5; i++) {
          if (seconds >= units[i]) {
            int q = seconds / units[i];
            seconds = seconds % units[i];
            res += (res.equals("") ? "" : (seconds == 0 ? " and " : ", "))
                   + q + " " + labels[i] + (q > 1 ? "s" : "");              
          }
        }
        return res;
    }
}

Option 3:

import java.util.*;
public class TimeFormatter {
    private static final String[] TIME_WORDS = 
      {
        " years",
        " days",
        " hours",
        " minutes",
        " seconds"
      };    
    
    public static String formatDuration(int seconds) {
      if(seconds == 0)
        return "now";
    
      int[] timeUnits = 
          {
            seconds/(60*60*24*365),
            (seconds/(60*60*24))%365,
            (seconds/(60*60))%24,
            (seconds/60)%60,
            seconds%60
          };
          
      List<String> timeElements = new ArrayList<>();
      for(int i=0; i<timeUnits.length; i++) {
        if(timeUnits[i] != 0)
          timeElements.add(timeUnits[i] + getTimeWord(timeUnits[i], TIME_WORDS[i]));
      }
        
      StringBuilder result = new StringBuilder();
      for(int i=0; i<timeElements.size(); i++) {
        result.append(timeElements.get(i) + getSeparator(i, timeElements.size()));
      }
        
      return result.toString();
    }
    
    private static String getTimeWord(int timeUnit, String timeWord) {
      if(timeUnit == 1)
        return timeWord.substring(0, timeWord.length()-1);
        
      return timeWord;
    }
    
    private static String getSeparator(int idx, int sz) {
      String separator = "";
      
      if(idx < sz-2)
        separator = ", ";
      else if(idx == sz-2)
        separator = " and ";
      
      return separator;
    }
}

Test cases to validate our solution

import org.junit.Test;
import static org.junit.Assert.*;

public class TimeFormatterTest {
    @Test
    public void exampleTests() {
        assertEquals("1 second", TimeFormatter.formatDuration(1));
        assertEquals("1 minute and 2 seconds", TimeFormatter.formatDuration(62));
        assertEquals("2 minutes", TimeFormatter.formatDuration(120));
        assertEquals("1 hour", TimeFormatter.formatDuration(3600));
        assertEquals("1 hour, 1 minute and 2 seconds", TimeFormatter.formatDuration(3662));
    }
}