Thursday, February 28, 2013

Java TimeUnit Conversion

Java TimeUnit Conversion

When Java 5 (J2SE 1.5/5.0) was released in 2004 it included Doug Lea's Java concurrency library. This library includes functionality for e.g. scheduling threads at particular instants in time. Some corners of this uses an enumeration object named java.util.concurrent.TimeUnit and which continues to be present in Java SE 6, 7 and 8.

The time unit object represents nanoseconds, microseconds, milliseconds, seconds, minutes, hours and days. In the context of the concurrency library, a time duration is represented as an integer of type long and refers to one of the time units. Conversion of time duration values can be done easily by using the method TimeUnit.convert(long,TimeUnit).

This method has an annoying property: It does not handle rounding. If a time duration of 800 microseconds is converted to milliseconds, the result becomes 0 milliseconds. This is not always preferable. In some cases it would be nice to have rounding applied and to end up with a result of 1 millisecond. Rounding is not offered by the time unit object.

In general rounding is easy to implement. To convert hours to days using truncation is just an integer division with 24. To do the same with conversion, rounding can be done by first adding 12 hours and then dividing with 24 hours/day giving a result in days. This is exactly the same which happens, when rounding floating points values to integers: First add 0.5 and then truncate the fraction leaving only the integer part. Simple.

For the time unit object, this functionality seems to be hard to obtain. If the source time duration has a coarser granularity than the target time duration then it is possible to perform the conversion by using convert() directly:

long targetDuration=targetUnit.convert(sourceDuration,sourceUnit);

Within the implementation, a simple multiplication is applied.

If the source time duration has a finer granularity than the target time duration then TimeUnit.convert() will apply division and truncate. To get rounding, it is possible to convert 1 from the target unit space to the source unit space, and then add half of this to the source time duration before truncating:

long targetToSourceFactor=sourceUnit.convert(1,targetUnit);
long targetDuration=targetUnit.convert(sourceDuration+targetToSourceFactor/2,
                                       sourceUnit);

The very nice thing about this is that it works for all combinations of time units.

When put into a context of more complete code, the end result may be this:

public class TimeUnitUtility
{
  public static long convertWithRounding(long sourceDuration,
                                         TimeUnit sourceUnit,
                                         TimeUnit targetUnit)
  {
    long res=0;
    
    {
      if (sourceUnit==targetUnit)
      {
        res=sourceDuration;
      }
      else
      {
        if (sourceDuration<0)
        {
          res=-convertWithRounding(-sourceDuration,sourceUnit,targetUnit);
        }
        else
        {
          int order=targetUnit.compareTo(sourceUnit);
      
          if (order<=0)
          {
            res=targetUnit.convert(sourceDuration,sourceUnit);
          }
          else
          {
            long targetToSourceFactor=sourceUnit.convert(1,targetUnit);
            res=targetUnit.convert(sourceDuration+targetToSourceFactor/2,
                                   sourceUnit);
          }
        }
      }
    }
    
    return res;
  }
}

There exists another way to do the same thing. Instead of using compareTo() it is possible to convert the time duration to the fineste time unit and then convert this duration to the target duration. This involves three convert() operations:

public class TimeUnitUtility
{
  public static long convertWithRounding2(long sourceDuration,
                                          TimeUnit sourceUnit,
                                          TimeUnit targetUnit)
  {
    long res=0;
    
    {
      if (sourceUnit==targetUnit)
      {
        res=sourceDuration;
      }
      else
      {
        if (sourceDuration<0)
        {
          res=-convertWithRounding2(-sourceDuration,sourceUnit,targetUnit);
        }
        else
        {
          TimeUnit finestUnit=TimeUnit.NANOSECONDS;
          long finestDuration=finestUnit.convert(sourceDuration,sourceUnit);

          long targetToFinestFactor=finestUnit.convert(1,targetUnit);
          res=targetUnit.convert(finestDuration+targetToFinestFactor/2,
                                 finestUnit);
        }
      }
    }
    
    return res;
  }
}

This static implementation does not make it possible to address an abstract convert() method with two possible implementations - one performing truncation and the other performing rounding. One solution is to declare the abstract method in the form of an interface -

public interface TimeUnitConverter
{
  long convert(long sourceDuration,
               TimeUnit sourceUnit,
               TimeUnit targetUnit);
}

- and then implement two different implementations, which can be instantiated and passed around:

public class TruncateTimeUnitConverter implements TimeUnitConverter
{
  @Override
  public long convert(long sourceDuration,
                      TimeUnit sourceUnit,
                      TimeUnit targetUnit)
  {
    return targetUnit.convert(sourceDuration,sourceUnit);
  }
}
public class RoundTimeUnitConverter implements TimeUnitConverter
{
  @Override
  public long convert(long sourceDuration,
                      TimeUnit sourceUnit,
                      TimeUnit targetUnit)
  {
    return TimeUnitUtility.convertWithRounding(sourceDuration,sourceUnit,
                                               targetUnit);
  }
}

However, it is also possible to make the conversion parametrized and implement strict rounding as defined by java.math.RoundingMode. The set of rounding modes includes FLOOR, CEILING, UP, DOWN, HALF_UP, HALF_DOWN, HALF_EVEN and UNNECESSARY.

public class TimeUnitUtility
{
  ...
  public static long convert(long sourceDuration,
                             TimeUnit sourceUnit,
                             TimeUnit targetUnit,
                             RoundingMode roundingMode)
  {
    long res=0;
    
    {
      if (roundingMode==null)
      {
        String message=
          "Failure to convert; rounding mode must be set!";
        throw new IllegalArgumentException(message);
      }
      else
      {
        switch (roundingMode)
        {
          case FLOOR:
          {
            res=convert_FLOOR(sourceDuration,sourceUnit,targetUnit);
            break;
          }
        
          case CEILING:
          {
            res=convert_CEILING(sourceDuration,sourceUnit,targetUnit);
            break;
          }
          
          case UP:
          {
            res=convert_UP(sourceDuration,sourceUnit,targetUnit);
            break;
          }
          
          case DOWN:
          {
            res=convert_DOWN(sourceDuration,sourceUnit,targetUnit);
            break;
          }
          
          case HALF_UP:
          {
            res=convert_HALF_UP(sourceDuration,sourceUnit,targetUnit);
            break;
          }
          
          case HALF_DOWN:
          {
            res=convert_HALF_DOWN(sourceDuration,sourceUnit,targetUnit);
            break;
          }
          
          case HALF_EVEN:
          {
            res=convert_HALF_EVEN(sourceDuration,sourceUnit,targetUnit);
            break;
          }
          
          case UNNECESSARY:
          {
            res=convert_UNNECESSARY(sourceDuration,sourceUnit,targetUnit);
            break;
          }
          
          default:
          {
            String message=
              "Failure to convert; rounding mode \""+
              roundingMode.name()+
              "\" can not be recognised!";
            throw new IllegalArgumentException(message);
          }
        }
      }
    }
    
    return res;
  }
  ...
}

For a complete context of these examples and including a test suite, please refer to the archive Yelstream-TimeUnitUtility_2013-02-28.zip. This archive contains an Eclipse Java project ready for import. All eight rounding modes are implemented for both positive and negative time durations and accompanied by unit test cases.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.