Skip to content
Advertisement

Is this code realiable for working out hours in Java, considering summer time changes?

could someone pls help me solve this confusion? I need to display the total work hours for each worker. There are different work shifts. The most complex one, in this case, is the dawn-shift when the person works from 21:00 on a day to 7:00 on the next day, considering day offsets, i.e. on 31th October there is a time change in Switzerland. I intend to apply the logic only within the country. Would the following code be a reliable solution for this scenario? I have tried to understand java.time and many posts for this specific issue, but they all got more confuse.

public String saveWorkTime(@Valid @ModelAttribute("workCalc") WorkCalculator workCalculator,
                               BindingResult bindingResult, ModelMap modelMap) throws ParseException {

        (...)

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");

        Date startDT = sdf.parse(workCalculator.getStartDate() + " " + workCalculator.getStartTime());
        Date endDT = sdf.parse(workCalculator.getEndDate() + " " + workCalculator.getEndTime());

        long timeDiffMilli = endDT.getTime() - startDT.getTime();
        long timeDiffMin = TimeUnit.MILLISECONDS.toMinutes(timeDiffMilli) % 60;
        long timeDiffH = TimeUnit.MILLISECONDS.toHours(timeDiffMilli) % 24;

        workCalculator.setTotalWorkH(timeDiffH);
        workCalculator.setTotalWorkMin(timeDiffMin);

        workCalculatorService.save(workCalculator);
        return "redirect:/index";
}

My entity model looks like:

 (...)

 @Id
 @GeneratedValue(strategy = GenerationType.IDENTITY)
 @Column(name = "id")
 private Integer id;

 @Column(name = "start_date")
 private String startDate;

 @Column(name = "end_date")
 private String endDate;

 @Column(name = "total_work_h")
 private long totalWorkH;

 @Column(name = "total_work_min")
 private long totalWorkMin;

 @Column(name = "end_time")
 private String endTime;

 @Column(name = "start_time")
 private String startTime;

 (...)

My Db schema looks like:

(...)

CREATE TABLE `work_calc` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `start_date` varchar(30) DEFAULT NULL,
    `end_date` varchar(30) DEFAULT NULL,
    `start_time` varchar(20) DEFAULT NULL,
    `end_time` varchar(20) DEFAULT NULL,
    `total_work_h` int(10) DEFAULT NULL,
    `total_work_min` int(10) DEFAULT NULL,

    PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=latin1; 

enter image description here

Thank you all for your efforts and time in advance.

Advertisement

Answer

java.time

It’s very simple. If you will just allow it to be. I recommend that you use java.time, the modern Java date and time API, for all of your date and time work.

// This should be your entity class
public class Work {
    
    private static final DateTimeFormatter FORMATTER
            = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm", Locale.ROOT);
    
    private OffsetDateTime start;
    private OffsetDateTime end;
    
    public Work(OffsetDateTime start, OffsetDateTime end) {
        this.start = start;
        this.end = end;
    }

    public Duration getTotalWorkTime() {
        return Duration.between(start, end);
    }

    @Override
    public String toString() {
        return start.format(FORMATTER) + " - " + end.format(FORMATTER);
    }
    
}

Let’s try it out. For constructing some Work objects I wrote the following helper method. It accepts strings for the dates and times.

/** Switzerland time */
private static final ZoneId ZONE = ZoneId.of("Europe/Zurich");

private static Work buildWork(String startDate, String startTime,
        String endDate, String endTime) {
    ZonedDateTime start = ZonedDateTime.of(
            LocalDate.parse(startDate), LocalTime.parse(startTime), ZONE);
    ZonedDateTime end = ZonedDateTime.of(
            LocalDate.parse(endDate), LocalTime.parse(endTime), ZONE);
    
    if (start.isAfter(end)) {
        throw new IllegalArgumentException("Start must not be after end");
    }
    
    return new Work(start.toOffsetDateTime(), end.toOffsetDateTime());
}

Demonstration:

    Work[] exampleEntities = {
            buildWork("2021-10-13", "07:00", "2021-10-14", "17:45"),
            buildWork("2021-10-15", "07:00", "2021-10-15", "21:00"),
            buildWork("2021-10-30", "21:00", "2021-10-31", "07:00")
    };

    for (Work work : exampleEntities) {
        Duration totalWork = work.getTotalWorkTime();
        System.out.format("%s: %-8s or %2d hours %2d minutes%n", work,
                totalWork, totalWork.toHours(), totalWork.toMinutesPart());
    }

Output:

2021-10-13 07:00 - 2021-10-14 17:45: PT34H45M or 34 hours 45 minutes
2021-10-15 07:00 - 2021-10-15 21:00: PT14H    or 14 hours  0 minutes
2021-10-30 21:00 - 2021-10-31 07:00: PT11H    or 11 hours  0 minutes

In a normal night we would expect the time from 21:00 to 7:00 to be 10 hours. You can see in the above output that in the night between October 30 and 31, where Europe turns its clocks back after summer time (DST), it’s 11 hours. Edit: If one is in doubt, here’s a way to see it: The worker first works from 21 to 3 = 6 hours. Then the clocks are turned back to 2. The worker then continues working from 2 to 7 = 5 hours. So the total time worked i 6 + 5 hours = 11 hours. So the code calculates the actual time elapsed (not the difference between the clock hours).

Further points I wanted to make

  1. Use proper date-time objects for storing date and time. You don’t store numbers and Boolean values in strings (I sure hope), you also should not store dates and times in strings. I had really wanted to use ZonedDateTime for a date and time with a time zone (like Europe/Zurich), but many SQL databases cannot store the value of a ZonedDateTime with a real time zone, so we may have to use OffsetDateTime as in the code above. An OffsetDateTime fits nicely into a timestamp with time zone column of common SQL databases, though variations between vendors are great. For an amount of time use the Duration class in Java (not separate numbers).
  2. Don’t store redundant information in your entity. The risk of some database update causing undetected data inconsistency is too great. Total work time can be calculated from start and end, so we simply calculate them on demand (which probably isn’t that often, but you know better).
  3. As MadProgrammer said in a comment, leave to the standard library methods to perform date and time math. It’s often more complicated than you think, so don’t do it by hand as in your own code.

Was your code reliable?

Giving just a partial answer to your question as asked. There isn’t much reliable about the old date and time classes from Java 1.0 and 1.1. Even if you get code to work with them, it will often be needlessly complicated code, and the risk of some fellow programmer introducing a bug later should not be overlooked.

I didn’t test your code with data from the nights of fall back (for example 30–31 October 2021) and spring forward (expected 26–27 March 2022). You can do that yourself.

You have got a very long work shift from 2021-10-13 07:00 to 2021-10-14 17:45. It may be an error in the data, but work time should be calculated correctly, which will also help the user notice if it is indeed an error. The correct duration is 34 hours 45 minutes. It seems from your screen shot that your code has calculated it as just 10 hours 45 minutes.

Link

Oracle tutorial: Date Time explaining how to use java.time.

Advertisement