Search code examples
javadatetimesimpledateformat

Adding time doesn't add up


Sorry, but I come to you with a problem that should have been answered already. Alas, my google-foo is weak and I humbly come to ask for your guidance.

WHY DOES AN EXTRA 59 minutes appear??

Ok, so I got a "Clock In" thing going and my calculations go wrong when the persons out minutes are the same as the in minutes. I remember hearing about this problem before, but with no solution. Just a "Well, that's how it is. Good luck, brah." I've tried adding and subtracting minutes during calculation, but that just pushes the problem +/- the time added/subtracted. I've also tried calculating to the second (not shown below), but that also didn't help.

Obligatory, here's my code:

//calculateTotalHours() 
public static String calculateTotalHours(Cell inTime, Cell outTime, Cell breakStart, Cell breakEnd)
{
    System.out.println(LOG + "calculateTotalHours");
    SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm");
    Date in, out, start, end;       
    
    if(null != inTime && null != outTime && null != breakStart && null != breakEnd)
    {   
        try
        {
            in = timeFormat.parse(inTime.getStringCellValue());
            out = timeFormat.parse(outTime.getStringCellValue());
            start = timeFormat.parse(breakStart.getStringCellValue());
            end = timeFormat.parse(breakEnd.getStringCellValue());

            long lunchTotal = end.getTime() - start.getTime();
            long totalWork = out.getTime() - in.getTime();
            long totalTime = totalWork - lunchTotal;            
            long diffHours = totalTime/HOURS % 24;
            totalTime -= diffHours;
            long diffMinutes = totalTime/MINUTES % 60;
            totalTime -= diffMinutes;       
            
            return String.format("%02d:%02d", diffHours, diffMinutes);
        } 
        catch (ParseException e) {e.printStackTrace();}
    }
    else
    {
        try
        {
            if(null == breakStart || null == breakEnd)              {
                in = timeFormat.parse(inTime.getStringCellValue());
                out = timeFormat.parse(outTime.getStringCellValue());           
                
                long totalTime = out.getTime() - in.getTime();              
                long diffHours = totalTime/HOURS % 24;
                totalTime -= diffHours;
                long diffMinutes = totalTime/MINUTES % 60;
                totalTime -= diffMinutes;
                
                return String.format("%02d:%02d", diffHours, diffMinutes);
            }
            else if(null == inTime.getStringCellValue())
            {
                System.out.println(LOG + "inTime is blank");
                return "-1";
            }
        } 
        catch (ParseException e) {e.printStackTrace();}
    }
    return "-1";
}   

My sincere apologies for the mess I call my code. And let me know if calculating by the second or millisecond is the way to go. I may have overlooked something when trying it that way.

2020 UPDATE:

I wanted to update my code and what I should have done. First, I should have separated the cells, strings, and time more. Second, I should have broken it down to more methods for clarity.

I'll just show getTotalHours(startTimeString, endTimeString):

    DateTimeFormatter format = DateTimeFormatter.ofPattern("HH:mm");
    LocalTime end = LocalTime.parse(endTimeString.trim(), format);
    LocalTime start = LocalTime.parse(startTimeString.trim(), format);
    
    return   Duration.between(start, end).toHoursPart()%24 
           + ":"
           + Duration.between(start, end).toMinutes()%60;

Solution

  • private static final String defaultHours = "-1";
    
    public static String calculateTotalHours(Cell inTime, Cell outTime, Cell breakStart, Cell breakEnd)
    {
        try {
            LocalTime inLocalTime = LocalTime.parse(inTime.getStringCellValue());
            LocalTime outLocalTime = LocalTime.parse(outTime.getStringCellValue());
            LocalTime breakStartTime = LocalTime.parse(breakStart.getStringCellValue());
            LocalTime breakEndTime = LocalTime.parse(breakEnd.getStringCellValue());
    
            Duration lunch = Duration.between(breakStartTime, breakEndTime);
            Duration present = Duration.between(inLocalTime, outLocalTime);
            Duration worked = present.minus(lunch);
    
            return String.format("%02d:%02d", worked.toHours(), worked.toMinutesPart());
    
        } catch (DateTimeParseException dtpe) {
            System.out.println(dtpe);
            return defaultHours;
        }
    }
    

    The above code uses the toMinutesPart method that was introduced in Duration in Java 9. Assuming you are using ThreeTenABP (details below) or an earlier Java version, it’s:

            long hours = worked.toHours();
            long minutes = worked.minusHours(hours).toMinutes();
            return String.format("%02d:%02d", hours, minutes);
    

    For clarity I have omitted the null checks, I am sure you can handle those.

    As said in the comments, leave time math to well-tested library methods. It’s not only less error-prone, it also gives clearer code. I am using java.time, the modern Java date and time API. It also saves us from specifying an explicit formatter since LocalTime.parse() parses your format of HH:mm as its default (it’s ISO 8601 format).

    What went wrong in your code?

    As kshetline in the other answer I believe that totalTime is in milliseconds and diffHours is hours. So when you do totalTime -= diffHours;, you subtract hours from milliseconds and you’re bound to get an incorrect result (same with totalTime -= diffMinutes;, by the way, but you’re not using the result of the latter subtraction, so this error doesn’t show).

    Question: Can I use java.time on Android?

    Yes, java.time works nicely on older and newer Android devices. It just requires at least Java 6.

    • In Java 8 and later and on newer Android devices (from API level 26, I’m told) the modern API comes built-in.
    • In Java 6 and 7 get the ThreeTen Backport, the backport of the new classes (ThreeTen for JSR 310; see the links at the bottom).
    • On (older) Android use the Android edition of ThreeTen Backport. It’s called ThreeTenABP. And make sure you import the date and time classes from org.threeten.bp with subpackages.

    Links