Java timespan string
The other day I had the need to print out a string which represented the span of time between two points. I had a Calendar object and assumed there would be a function on the Calendar API to handle such a situation. After spending a few minutes, it turned out I was wrong… So I had a look on the net and as usual, Joda-Time was the main search result. But I didn’t want to add Joda-Time to the project as all I wanted to do was print a string for the timespan between two Calendar objects.
I’ve written this utility to format Calendar objects, Date objects or even longs to create a string representing timespan between the objects.
TimeSpanFormat.java
/**
* <p>Copyright 2011 Victoria Scales</p>
* <p>This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.</p>
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p>
* <p>You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.</p>
*/
package uk.co.vsf.utilities.time;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import uk.co.vsf.utilities.exception.VSFUtilitiesException;
/**
* <p>
* Can format a Calendar, Date or long values to create a string representation of the time span between the two values entered. The
* possible options to supply for the format are:
* <ul>
* <li>Y (years)</li>
* <li>D (hours)</li>
* <li>H (days)</li>
* <li>M (minutes)</li>
* <li>S (seconds)</li>
* <li>N (milliseconds)</li>
* </ul>
* E.g. If you passed in two calendar objects which had a span from 0L to 73173704000L and the format specified was "ydhms", the returned
* formatted string will be: "2 years, 116 days, 22 hours, 1 minute, 44 seconds".
* </p>
* <p>
* This formatter is not necessarily completely correct, but does provide a rough guide which should be suitable in most situations.
* </p>
*
* @author Victoria
* @date 2011-05-30
* @version 0.1
*/
public class TimeSpanFormat
{
private static final String EMPTY = "";
private static final String SPACE = " ";
private static final String SEPARATOR = ", ";
/**
* Handy enum for determining the values that the person has requested and also storing the multipliers and milliseconds.
*/
private enum TimeSpanFormatOption
{
Y(365l, "years", "year"),
D(24l, "days", "day"),
H(60l, "hours", "hour"),
M(60l, "minutes", "minute"),
S(1000l, "seconds", "second"),
N(0l, "milliseconds", "millisecond");
private long milliseconds;
private long multiplier;
private String textPlural;
private String text;
static
{
for (TimeSpanFormatOption option : TimeSpanFormatOption.values())
{
option.milliseconds = multiplier(option);
}
}
private TimeSpanFormatOption(long multiplier, String textPlural, String text)
{
this.multiplier = multiplier;
this.textPlural = textPlural;
this.text = text;
}
private static long multiplier(TimeSpanFormatOption option)
{
long value = 0l;
switch (option)
{
case Y:
value = addMultiplier(value, Y);
case D:
value = addMultiplier(value, D);
case H:
value = addMultiplier(value, H);
case M:
value = addMultiplier(value, M);
case S:
value = addMultiplier(value, S);
}
return value;
}
private static long addMultiplier(long value, TimeSpanFormatOption index)
{
if (value == 0l)
{
return index.multiplier;
}
return value * index.multiplier;
}
}
/**
* Comparator class for ordering the formats according to highest denomination first.
*/
private class TimeSpanFormatOptionComparator implements Comparator<TimeSpanFormatOption>
{
@Override
public int compare(TimeSpanFormatOption o1, TimeSpanFormatOption o2)
{
return o1.ordinal() - o2.ordinal();
}
}
private final String format;
private final LinkedHashMap<TimeSpanFormatOption, Integer> optionsAsRequested = new LinkedHashMap<TimeSpanFormatOption, Integer>();
private final List<TimeSpanFormatOption> optionsOrdered = new ArrayList<TimeSpanFormatOption>();
/**
* Specify the format that you would like applied to create the text string. For example, "ydhms".
*
* @param format
* to apply
*/
public TimeSpanFormat(final String format)
{
if (format == null || format.trim().equals(EMPTY))
{
throw new VSFUtilitiesException("Format must be supplied");
}
this.format = format;
processFormat();
}
/**
* Format the calendar objects according to the specified format given to the constructor.
*
* @param past
* E.g. yesterday
* @param future
* E.g. tomorrow
* @return formatted string
*/
public String format(final Calendar past, final Calendar future)
{
final long thenMillis = past.getTimeInMillis();
final long nowMillis = future.getTimeInMillis();
return format(thenMillis, nowMillis);
}
/**
* Format the date objects according to the specified format given to the constructor.
*
* @param past
* E.g. yesterday
* @param now
* E.g. tomorrow
* @return formatted string
*/
public String format(final Date past, final Date future)
{
final long thenMillis = past.getTime();
final long nowMillis = future.getTime();
return format(thenMillis, nowMillis);
}
/**
* Format the longs according to the specified format given to the constructor.
*
* @param past
* E.g. yesterday
* @param now
* E.g. tomorrow
* @return formatted string
*/
public String format(final long past, final long future)
{
final long timeSpan = future - past;
if (timeSpan < 0l)
{
throw new VSFUtilitiesException("The future date must be greater than or equal to the past date");
}
return buildFormattedString(timeSpan);
}
/**
* Check each value is correct by converting from char to enum value.
*/
private void processFormat()
{
for (final char letter : format.toCharArray())
{
try
{
final TimeSpanFormatOption option = TimeSpanFormatOption.valueOf((EMPTY + letter).toUpperCase());
optionsAsRequested.put(option, null);
}
catch (IllegalArgumentException e)
{
throw new VSFUtilitiesException("Value: " + letter + " is not valid");
}
}
optionsOrdered.addAll(optionsAsRequested.keySet());
Collections.sort(optionsOrdered, new TimeSpanFormatOptionComparator());
}
private String buildFormattedString(long timeSpan)
{
for (final TimeSpanFormatOption option : optionsOrdered)
{
timeSpan = determineUnitValue(timeSpan, option);
}
final StringBuilder stringBuilder = new StringBuilder();
for (TimeSpanFormatOption option : optionsAsRequested.keySet())
{
if (stringBuilder.length() > 0)
{
stringBuilder.append(SEPARATOR);
}
final int unitValue = optionsAsRequested.get(option);
stringBuilder.append(unitValue);
stringBuilder.append(SPACE + (unitValue == 1 ? option.text : option.textPlural));
}
return stringBuilder.toString();
}
private long determineUnitValue(final long timeSpan, final TimeSpanFormatOption otion)
{
final int unit = (otion.equals(TimeSpanFormatOption.N) ? (int) timeSpan : (int) (timeSpan / otion.milliseconds));
optionsAsRequested.put(otion, unit);
return (otion.equals(TimeSpanFormatOption.N) ? 0l : timeSpan % otion.milliseconds);
}
}
VSFUtilitiesException.java
/**
* <p>Copyright 2011 Victoria Scales</p>
* <p>This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.</p>
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p>
* <p>You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.</p>
*/
package uk.co.vsf.utilities.exception;
public class VSFUtilitiesException extends RuntimeException
{
private static final long serialVersionUID = 1189940090944241539L;
public VSFUtilitiesException(String message)
{
super(message);
}
}
TimeSpanFormatTest.java
/**
* <p>Copyright 2011 Victoria Scales</p>
* <p>This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.</p>
* <p>This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.</p>
* <p>You should have received a copy of the GNU General Public License along with this program; if not, see <http://www.gnu.org/licenses/>.</p>
*/
package uk.co.vsf.utilities.time;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import uk.co.vsf.utilities.exception.VSFUtilitiesException;
public class TimeSpanFormatTest
{
@Test
public void constructOK()
{
TimeSpanFormat tsp = new TimeSpanFormat("d");
assertNotNull(tsp);
}
@Test(expectedExceptions = {VSFUtilitiesException.class}, dataProvider = "constructNotOKDataProvider")
public void constructNotOK(String format)
{
new TimeSpanFormat(format);
}
@DataProvider(name = "constructNotOKDataProvider")
public Object[][] constructNotOKDataProvider()
{
return new Object[][] { {" "}, {""}, {null}, {"e"}};
}
@Test(dataProvider = "formatTimeSpanOK")
public void formatTimeSpanOK(String format, long then, long now, String expectedTimeSpan)
{
TimeSpanFormat tsp = new TimeSpanFormat(format);
assertEquals(tsp.format(then, now), expectedTimeSpan);
}
@DataProvider(name = "formatTimeSpanOK")
public Object[][] formatTimeSpanOK()
{
return new Object[][] { {"ydhms", 0l, 73173704000l, "2 years, 116 days, 22 hours, 1 minute, 44 seconds"},
{"ydhms", 0l, 41637704000l, "1 year, 116 days, 22 hours, 1 minute, 44 seconds"},
{"mds", 0l, 42163304000l, "1 minute, 488 days, 44 seconds"}, {"smd", 0l, 42163304000l, "44 seconds, 1 minute, 488 days"},
{"ns", 0l, 788l, "788 milliseconds, 0 seconds"}};
}
@Test(expectedExceptions = {VSFUtilitiesException.class})
public void formatTimeSpanNotOK()
{
TimeSpanFormat tsp = new TimeSpanFormat("ydhms");
tsp.format(73173704000l, 0l);
}
@Test
public void formatTimeSpanOK2()
{
TimeSpanFormat tsp = new TimeSpanFormat("ydhms");
assertEquals(tsp.format(0l, 73173704000l), "2 years, 116 days, 22 hours, 1 minute, 44 seconds");
assertEquals(tsp.format(0l, 41637660000l), "1 year, 116 days, 22 hours, 1 minute, 0 seconds");
}
@Test
public void formatTimeSpanOKCalendars()
{
Calendar past = new GregorianCalendar(2010, 05, 14, 22, 44, 1);
Calendar future = new GregorianCalendar(2011, 05, 14, 22, 44, 1);
TimeSpanFormat tsp = new TimeSpanFormat("ydhms");
assertEquals(tsp.format(past, future), "1 year, 0 days, 0 hours, 0 minutes, 0 seconds");
// two leap years in between!
future = new GregorianCalendar(2017, 05, 14, 22, 44, 1);
assertEquals(tsp.format(past, future), "7 years, 2 days, 0 hours, 0 minutes, 0 seconds");
}
@Test
public void formatTimeSpanOKDatess()
{
Date past = new Date(2010, 05, 14, 22, 44, 1);
Date future = new Date(2011, 05, 14, 22, 44, 1);
TimeSpanFormat tsp = new TimeSpanFormat("ydhms");
assertEquals(tsp.format(past, future), "1 year, 0 days, 0 hours, 0 minutes, 0 seconds");
// two leap years in between!
future = new Date(2017, 05, 14, 22, 44, 1);
assertEquals(tsp.format(past, future), "7 years, 2 days, 0 hours, 0 minutes, 0 seconds");
}
}
Please enable the Disqus feature in order to add comments