com.github.s4ke.worktimegen.Main.java Source code

Java tutorial

Introduction

Here is the source code for com.github.s4ke.worktimegen.Main.java

Source

/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* <martinbraun123@aol.com> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return. Martin Braun
* ----------------------------------------------------------------------------
*/
package com.github.s4ke.worktimegen;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Row;

/**
 * Created by Martin on 09.03.2016.
 */
public class Main {

    private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

    //stackoverflow: http://stackoverflow.com/questions/11697581/check-if-a-list-of-dates-contains-a-particular-date
    private static class DateComparator implements Comparator<Date> {

        public int compare(Date d1, Date d2) {
            return DATE_FORMAT.format(d1).compareTo(DATE_FORMAT.format(d2));
        }

    }

    private static final String BUNDESLAND = "BY";

    private static final Gson GSON = new Gson();

    private static final String[][] ADDITIONAL_RANGES = { { "2014-12-24", "2015-01-06" },
            { "2015-12-24", "2016-01-06" }, { "2016-05-17" }, { "2016-12-24", "2017-01-06" }, };

    public static Set<Date> getHolidaysForYear(int year, String bundesland) throws IOException {
        String jsonFromWebsite = downloadHTMLFromWebsite(new URL("http://feiertage.jarmedia.de/api/?jahr=" + year));
        //hacky-hack-hack, but works
        Type type = new TypeToken<Map<String, Map<String, Info>>>() {
        }.getType();
        Map<String, Map<String, Info>> allInfo = GSON.fromJson(jsonFromWebsite, type);
        Set<Date> dates = allInfo.get(bundesland).entrySet().stream().map(entry -> entry.getValue().datum)
                .collect(Collectors.toSet());
        TreeSet<Date> ret = new TreeSet<>(new DateComparator());
        ret.addAll(dates);
        return ret;
    }

    public static Set<Date> getAdditionalExceptions() {
        Set<Date> ret = new TreeSet<>(new DateComparator());
        for (String[] tuple : ADDITIONAL_RANGES) {
            if (tuple.length == 2) {
                Date start = GSON.fromJson(tuple[0], Date.class);
                Date end = GSON.fromJson(tuple[1], Date.class);
                ret.add(start);
                ret.add(end);
                GregorianCalendar startCalendar = new GregorianCalendar();
                startCalendar.setTime(start);
                GregorianCalendar endCalendar = new GregorianCalendar();
                endCalendar.setTime(end);
                GregorianCalendar calendar = new GregorianCalendar();
                calendar.setTime(start);
                while (calendar.before(endCalendar)) {
                    ret.add(calendar.getTime());
                    calendar.add(Calendar.DAY_OF_YEAR, 1);
                }
            } else if (tuple.length == 1) {
                ret.add(GSON.fromJson(tuple[0], Date.class));
            } else {
                throw new AssertionError();
            }
        }
        return ret;
    }

    public static Set<Date> generateAllWorkingDatesForMonth(int year, int month) throws IOException {
        Set<Date> ret = new TreeSet<>(new DateComparator());
        Set<Date> holidays = getHolidaysForYear(year, BUNDESLAND);
        Set<Date> vorlesungsfrei = getAdditionalExceptions();
        GregorianCalendar gregorianCalendar = new GregorianCalendar();
        gregorianCalendar.set(year, month, 1, 0, 0, 0);
        GregorianCalendar end = new GregorianCalendar();
        int endYear = year;
        int endMonth = month + 1;
        if (endMonth == 12) {
            endMonth = 0;
            endYear = endYear + 1;
        }
        end.set(endYear, endMonth, 1, 0, 0, 0);
        System.out.println(end.getTime());
        while (gregorianCalendar.before(end)) {
            Date date = gregorianCalendar.getTime();
            int dayOfWeek = gregorianCalendar.get(GregorianCalendar.DAY_OF_WEEK);
            if (dayOfWeek != GregorianCalendar.SATURDAY && dayOfWeek != GregorianCalendar.SUNDAY) {
                if (!holidays.contains(date) && !vorlesungsfrei.contains(date)) {
                    ret.add(date);
                }
            }
            gregorianCalendar.add(GregorianCalendar.DAY_OF_YEAR, 1);
        }
        return ret;
    }

    private static String downloadHTMLFromWebsite(URL url) throws IOException {
        StringBuilder builder = new StringBuilder();
        try (InputStream is = url.openStream(); BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
            String line;
            while ((line = br.readLine()) != null) {
                builder.append(line);
            }
        }
        return builder.toString();
    }

    public static class Work {
        private String date;

        private int startHours;
        private int startMinutes;
        private int endHours;
        private int endMinutes;

        public int minutes() {
            return (endHours - startHours) * 60 + (endMinutes - startMinutes);
        }

        @Override
        public String toString() {
            return "Work{" + "date='" + date + '\'' + ", startHours=" + startHours + ", startMinutes="
                    + startMinutes + ", endHours=" + endHours + ", endMinutes=" + endMinutes + '}';
        }
    }

    public static class Info {
        private Date datum;
        private String hinweis;

        public String getHinweis() {
            return hinweis;
        }

        public void setHinweis(String hinweis) {
            this.hinweis = hinweis;
        }

        public Date getDatum() {
            return datum;
        }

        public void setDatum(Date datum) {
            this.datum = datum;
        }

        @Override
        public String toString() {
            return "Info{" + "datum='" + datum + '\'' + ", hinweis='" + hinweis + '\'' + '}';
        }
    }

    private static final double MINIMAL_WORK_TIME_PER_DAY = 15 / 60;

    private static final int MIN_START_HOURS = 8;
    private static final int MAX_END_HOURS = 19;
    private static final int MAX_HOURS_PER_DAY = 6;

    private static final Random random = new Random();

    public static List<Work> scheduleWork(int year, int month, double hours, double maxHoursPerWeek)
            throws IOException {
        List<Work> ret = new ArrayList<>();

        Set<Date> workingDaysInMonth = generateAllWorkingDatesForMonth(year, month);
        //split into weeks
        Map<Integer, Set<Date>> splitIntoWeeks = new HashMap<>();
        for (Date date : workingDaysInMonth) {
            GregorianCalendar calendar = new GregorianCalendar();
            calendar.setTime(date);
            int weekOfMonth = calendar.get(GregorianCalendar.WEEK_OF_MONTH);
            splitIntoWeeks.computeIfAbsent(weekOfMonth, (_$) -> new TreeSet<>(new DateComparator())).add(date);
        }

        //schedule the general workload over the days
        double dayCount = workingDaysInMonth.size();
        double perDay = hours / dayCount;

        double maxPerDay = 2.2 * perDay;
        double diffPerDay = maxPerDay - perDay;

        int maxStartHours = MAX_END_HOURS - (int) Math.ceil(perDay);
        int diffStartHours = maxStartHours - MIN_START_HOURS;

        if (perDay > MAX_HOURS_PER_DAY) {
            throw new AssertionError("perDay > MAX_HOURS_PER_DAY");
        }

        if (perDay > maxHoursPerWeek) {
            throw new AssertionError("perDay > maxHoursPerWeek");
        }

        if (maxPerDay > maxHoursPerWeek) {
            throw new AssertionError("maxPerDay > maxHoursPerWeek");
        }

        if (maxPerDay > MAX_HOURS_PER_DAY) {
            throw new AssertionError("maxPerPay > maxHoursPerDay");
        }

        if (5 * perDay > maxHoursPerWeek) {
            throw new AssertionError("5 * perDay > maxHoursPerWeek");
        }

        int totalMinutes = 0;

        for (Map.Entry<Integer, Set<Date>> entry : splitIntoWeeks.entrySet()) {
            //calculate the budget for this week
            double budget = entry.getValue().size() * perDay;
            //shuffle the dates so we get arbitrary shuffling
            //of dates that are more work-intensive
            List<Date> dates = new ArrayList<>();
            dates.addAll(entry.getValue());
            Collections.shuffle(dates);

            for (Date date : dates) {
                double additionalWork = random.nextDouble() * diffPerDay;
                double workHours = perDay + additionalWork;
                boolean shouldBreak = false;
                if (workHours > budget) {
                    workHours = budget;
                    shouldBreak = true;
                }

                budget -= workHours;

                //we don't want days with a too low working time
                if (budget < MINIMAL_WORK_TIME_PER_DAY && workHours < maxPerDay) {
                    workHours += budget;
                    budget = 0;
                    shouldBreak = true;
                }

                Work workObj = new Work();
                workObj.date = DATE_FORMAT.format(date);

                workObj.startHours = MIN_START_HOURS + random.nextInt(diffStartHours);
                int endHours = workObj.startHours + (int) Math.floor(workHours);

                workObj.startMinutes = random.nextInt(60);

                int endMinutes = workObj.startMinutes + (int) Math.round((workHours - Math.floor(workHours)) * 60);
                if (endMinutes >= 60) {
                    endMinutes -= 60;
                    endHours += 1;
                }
                workObj.endHours = endHours;
                workObj.endMinutes = endMinutes;

                totalMinutes += workObj.minutes();

                System.out.println("working " + workHours + " hours (" + workObj.minutes() + " minutes ) on " + date
                        + ", workObj: " + workObj);

                ret.add(workObj);

                if (shouldBreak) {
                    break;
                }
            }
        }

        if (Math.abs(totalMinutes / 60d - hours) > 0.1) {
            throw new AssertionError("zu groe Abweichung vom Vertrag!");
        }
        System.out.println(totalMinutes / 60d);

        Collections.sort(ret, (o1, o2) -> o1.date.compareTo(o2.date));
        return ret;
    }

    public static void generateExcelSheet(int year, int month, List<Work> workObjs) throws IOException {
        try (InputStream is = Main.class.getResourceAsStream("/template_urlaub.xls")) {
            HSSFWorkbook workbook = new HSSFWorkbook(is);
            HSSFSheet sheet = workbook.getSheetAt(0);

            GregorianCalendar calendar = new GregorianCalendar();
            calendar.set(year, month - 1, 1);
            sheet.getRow(7).getCell(2).setCellValue(DATE_FORMAT.format(calendar.getTime()));
            calendar.set(year, month - 1, calendar.getActualMaximum(Calendar.DAY_OF_MONTH));
            sheet.getRow(7).getCell(4).setCellValue(DATE_FORMAT.format(calendar.getTime()));

            int startRow = 11;
            int endRow = 33;
            if (workObjs.size() > (endRow - startRow)) {
                throw new AssertionError("template has too few rows");
            }
            int curRow = startRow;
            for (Work work : workObjs) {
                Row row = sheet.getRow(curRow);
                row.getCell(0).setCellValue(work.date);
                row.getCell(1).setCellValue(pad(work.startHours) + ":" + pad(work.startMinutes));
                row.getCell(2).setCellValue(pad(work.endHours) + ":" + pad(work.endMinutes));
                ++curRow;
            }

            try (FileOutputStream fos = new FileOutputStream(
                    new File("zeiterfassung_braun_" + year + "_" + month + ".xls"))) {
                workbook.write(fos);
            }
        }
    }

    public static String pad(int num) {
        if (num < 10) {
            return "0" + num;
        } else {
            return String.valueOf(num);
        }
    }

    public static void main(String args[]) throws IOException {
        List<Work> workObjs;
        int year = 2015;
        for (int month = 3; month < 8; ++month) {
            System.out.println(workObjs = scheduleWork(year, month, 36.5, 20));
            generateExcelSheet(year, month + 1, workObjs);
        }
    }

}