import java.text.DecimalFormat; import java.time.LocalTime; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; import java.util.stream.Collectors; public class LoadingBar { private static final String TIME_FORMAT = "HH:mm"; private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern(TIME_FORMAT); private static final Pattern TIME_PATTERN = Pattern.compile("(?>[01]\\d|2[0-4]):[0-5]\\d"); private static final Pattern LUNCH_DURATION_PATTERN = Pattern.compile("\\d+"); private static final Pattern OFFSET_PATTERN = Pattern.compile("[+-]\\d+"); private static final DecimalFormat PERCENTAGE_FORMAT = new DecimalFormat("00.00"); private static final int MIN_LUNCH_DURATION = 30; private static final LocalTime LATEST_LUNCH_TIME = LocalTime.of(13, 30); private static final long DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH = 5L * 60; private static final int MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH = 6 * 60; private static final long MAX_NUMBER_WORK_MINS = 8L * 60; private static final int MINS_PER_HOUR = 60; private static final int LINE_LENGTH = 100; private enum DaySection { MITTAG("-m", "Mittag"), ZAPFENSTREICH("-z", "Zapfenstreich"); private final String param; private final String description; DaySection(String param, String description) { this.param = param; this.description = description; } public static DaySection byParam(String param) { return Arrays.stream(DaySection.values()).filter((DaySection section) -> section.getParam().equals(param)).findFirst().orElse(null); } public String getParam() { return param; } public String getDescription() { return description; } } public static void main(String[] args) { if (args.length > 0 && Objects.equals(args[0], "--help")) { printHelp(); return; } verifyMinimumNumberOfArgs(args); String nextArg = args[0]; verifyTimeFormat(nextArg, "Erstes Argument"); var startTime = LocalTime.parse(nextArg, TIME_FORMATTER); nextArg = args[1]; var section = DaySection.byParam(nextArg); verifyDaySection(section, nextArg); if (section == DaySection.MITTAG) { handleMittagspause(args, startTime); } else { handleZapfenstreich(args, startTime); } } private static void handleMittagspause(String[] args, LocalTime startTime) { if (args.length == 2) { showLoadingBarMittagspause(startTime); return; } String nextArg = args[2]; if (OFFSET_PATTERN.matcher(nextArg).matches()) { showLoadingBarMittagspause(startTime, Integer.parseInt(nextArg)); return; } verifyTimeFormat(nextArg, "Argument nach " + DaySection.MITTAG.getParam()); var maxMittagspause = LocalTime.parse(nextArg, TIME_FORMATTER); showLoadingBarMittagspause(startTime, maxMittagspause); } private static void handleZapfenstreich(String[] args, LocalTime startTime) { Integer lunchDuration = null; if (args.length == 2) { showLoadingBarZapfenstreich(startTime); return; } String nextArg = args[2]; LocalTime maxZapfenstreich = null; int endTimeOffset = 0; if (TIME_PATTERN.matcher(nextArg).matches()) { maxZapfenstreich = LocalTime.parse(nextArg, TIME_FORMATTER); } else if (OFFSET_PATTERN.matcher(nextArg).matches()) { endTimeOffset = Integer.parseInt(nextArg); } else { verifyDurationFormat(nextArg, "Argument nach " + DaySection.ZAPFENSTREICH.getParam(), true); // FSFIXME erweitere Fehlermeldung lunchDuration = Integer.parseInt(nextArg); } if (args.length == 3) { if (maxZapfenstreich == null && endTimeOffset == 0) { showLoadingBarZapfenstreich(startTime, lunchDuration); } else if (maxZapfenstreich == null) { showLoadingBarZapfenstreich(startTime, lunchDuration, endTimeOffset); } else { showLoadingBarZapfenstreich(startTime, lunchDuration, maxZapfenstreich); } return; } nextArg = args[3]; if (lunchDuration == null) { System.out.println("Letztes Argument darf nur auf Mittagspausendauer folgen."); System.exit(1); } if (maxZapfenstreich == null && !OFFSET_PATTERN.matcher(nextArg).matches()) { verifyTimeFormat(nextArg, "Letztes Argument nach " + DaySection.ZAPFENSTREICH.getParam() + " und Mittagspausendauer"); maxZapfenstreich = LocalTime.parse(nextArg, TIME_FORMATTER); showLoadingBarZapfenstreich(startTime, lunchDuration, maxZapfenstreich); return; } verifyOffsetFormat(nextArg, "Letztes Argument nach " + DaySection.ZAPFENSTREICH.getParam() + " und Enduhrzeit"); endTimeOffset = Integer.parseInt(nextArg); showLoadingBarZapfenstreich(startTime, lunchDuration, endTimeOffset); } private static void verifyMinimumNumberOfArgs(String[] args) { if (args.length >= 2) { return; } System.out.println("Mindestens 2 Argumente müssen gegeben sein."); printHelp(); System.exit(1); } private static void verifyTimeFormat(String param, String errMsgPrefix) { verifyInputFormat(TIME_PATTERN, param, errMsgPrefix, true, false); } private static void verifyDurationFormat(String param, String errMsgPrefix, boolean timeInputPossible) { verifyInputFormat(LUNCH_DURATION_PATTERN, param, errMsgPrefix, false, timeInputPossible); } private static void verifyOffsetFormat(String param, String errMsgPrefix) { verifyInputFormat(OFFSET_PATTERN, param, errMsgPrefix, false, false); } private static void verifyInputFormat(Pattern pattern, String param, String errMsgPrefix, boolean timeInput, boolean timeInputPossible) { if (pattern.matcher(param).matches()) { return; } // FSFIXME fine tune message -> HH:mm, mm, -+mm var firstInputPart = timeInput ? "Uhrzeitformat (" + TIME_FORMAT + ")" : "Minutenanzahl (" + LUNCH_DURATION_PATTERN + ")"; var possibleTimeInputPart = !timeInput && timeInputPossible ? " oder Uhrzeitformat (" + TIME_FORMAT + ")" : ""; System.out.println(errMsgPrefix + " \"" + param + "\" muss " + firstInputPart + possibleTimeInputPart + " entsprechen."); System.exit(1); } private static void verifyDaySection(DaySection section, String param) { if (section != null) { return; } List sectionDescs = Arrays.stream(DaySection.values()).map((DaySection ds) -> ds.getDescription() + " (" + ds.getParam() + ")") .collect(Collectors.toList()); String sectionDescsJoined = String.join(" oder ", sectionDescs); System.out.println("Argument nach Startzeit \"" + param + "\" muss Angabe für " + sectionDescsJoined + " sein."); System.exit(1); } private static void printHelp() { System.out.println("Mögliche Argumente für LoadingBar:\n" + "Normalfall Vormittag (Mittagspause <= " + LATEST_LUNCH_TIME + ")\n" + TIME_FORMAT + " " + DaySection.MITTAG.getParam() + "\n" + "Vormittag mit expliziter Mittagspause (<= " + LATEST_LUNCH_TIME + ")\n" + TIME_FORMAT + " " + DaySection.MITTAG.getParam() + " " + TIME_FORMAT + "\n" + "Vormittag mit abweichender Minutenanzahl Arbeitszeit\n" + TIME_FORMAT + " " + DaySection.MITTAG.getParam() + " -+mm\n" + "Normalfall Nachmittag (Mittagspause " + MIN_LUNCH_DURATION + " min)\n" + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + "\n" + "Nachmittag mit expliziter Länge Mittagspause (Mittagspause unter " + MIN_LUNCH_DURATION + " min wird auf " + MIN_LUNCH_DURATION + " min korrigiert)\n" + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " mm\n" + "Nachmittag mit explizitem Feierabend (Mittagspause je nach Minimum (s.u.))\n" + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " " + TIME_FORMAT + "\n" + "Nachmittag mit abweichender Minutenanzahl Arbeitszeit\n" + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " -+mm\n" + "Nachmittag mit explizitem Feierabend u. expliziter Länge Mittagspause (Mittagspause unter Minimum (s.u.) wird auf Minimum korrigiert)\n" + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " mm " + TIME_FORMAT + "\n" + "Nachmittag mit explizitem Feierabend u. abweichender Minutenanzahl Arbeitszeit\n" + TIME_FORMAT + " " + DaySection.ZAPFENSTREICH.getParam() + " " + TIME_FORMAT + " -+mm\n\n" + "Mittagspause minimum in Minuten:\n" + " - bis " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min (" + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH / MINS_PER_HOUR + " std): 0\n" + " - bis " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min + " + MIN_LUNCH_DURATION + " min: Arbeitszeit - " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min\n" + " - ab " + MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH + " min + " + MIN_LUNCH_DURATION + " min: " + MIN_LUNCH_DURATION + " min\n" ); } private static void showLoadingBarMittagspause(LocalTime startTime) { showLoadingBarMittagspause(startTime, null); } private static void showLoadingBarMittagspause(LocalTime startTime, int endTimeOffset) { LocalTime endTime = startTime.plusMinutes(DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH + endTimeOffset); LocalTime trueEndTime = endTime.isAfter(LATEST_LUNCH_TIME) ? LATEST_LUNCH_TIME : endTime; showLoadingBar(startTime, trueEndTime); } private static void showLoadingBarMittagspause(LocalTime startTime, LocalTime manualEndTime) { LocalTime endTime = manualEndTime != null ? manualEndTime : startTime.plusMinutes(DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH); LocalTime trueEndTime = endTime.isAfter(LATEST_LUNCH_TIME) ? LATEST_LUNCH_TIME : endTime; showLoadingBar(startTime, trueEndTime); } private static void showLoadingBarZapfenstreich(LocalTime startTime) { showLoadingBarZapfenstreich(startTime, -1, 0); } private static void showLoadingBarZapfenstreich(LocalTime startTime, Integer manualLunchDuration) { showLoadingBarZapfenstreich(startTime, manualLunchDuration, 0); } private static void showLoadingBarZapfenstreich(LocalTime startTime, Integer manualLunchDuration, int endTimeOffset) { long minLunchDuration = getMinLunchDuration(endTimeOffset); long realLunchDuration = getRealLunchDuration(manualLunchDuration, minLunchDuration); LocalTime trueEndTime = startTime.plusMinutes(MAX_NUMBER_WORK_MINS + realLunchDuration + endTimeOffset); realShowLoadingBarZapfenstreich(startTime, realLunchDuration, trueEndTime); } private static void showLoadingBarZapfenstreich(LocalTime startTime, Integer manualLunchDuration, LocalTime manualEndTime) { LocalTime trueEndTime = manualEndTime; long minLunchDuration = getMinLunchDuration(startTime, trueEndTime); long realLunchDuration = getRealLunchDuration(manualLunchDuration, minLunchDuration); if (trueEndTime == null) { trueEndTime = startTime.plusMinutes(MAX_NUMBER_WORK_MINS + realLunchDuration); } realShowLoadingBarZapfenstreich(startTime, realLunchDuration, trueEndTime); } private static long getMinLunchDuration(int endTimeOffset) { if (endTimeOffset == 0) { return MIN_LUNCH_DURATION; } long totalDuration = MAX_NUMBER_WORK_MINS + endTimeOffset; long effectiveLunchDuration = totalDuration - MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH; return getMinLunchDuration(effectiveLunchDuration); } private static long getMinLunchDuration(LocalTime startTime, LocalTime endTime) { if (endTime == null) { return MIN_LUNCH_DURATION; } long totalDuration = startTime.until(endTime, ChronoUnit.MINUTES); int effectiveLunchDuration = ((int) totalDuration) - MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH; return getMinLunchDuration(effectiveLunchDuration); } private static long getMinLunchDuration(long effectiveLunchDuration) { if (effectiveLunchDuration < 0) { effectiveLunchDuration = 0; } return Math.min(effectiveLunchDuration, MIN_LUNCH_DURATION); } private static long getRealLunchDuration(Integer manualLunchDuration, long minLunchDuration) { return manualLunchDuration != null && manualLunchDuration >= minLunchDuration ? manualLunchDuration : minLunchDuration; } private static void realShowLoadingBarZapfenstreich(LocalTime startTime, long manualLunchDuration, LocalTime endTime) { if (manualLunchDuration > 0) { var totalWorkTime = LocalTime.MIDNIGHT.plusMinutes(startTime.until(endTime, ChronoUnit.MINUTES) - manualLunchDuration); System.out.print("Arbeitszeit: " + TIME_FORMATTER.format(totalWorkTime) + "; "); } showLoadingBar(startTime, endTime); } private static void showLoadingBar(LocalTime startTime, LocalTime endTime) { long initialMinutes = startTime.until(endTime, ChronoUnit.MINUTES); System.out.print(minutesToTimeString(initialMinutes) + " gesamt; Endzeit: " + TIME_FORMATTER.format(endTime) + "\n"); long passedMinutes = startTime.until(LocalTime.now(), ChronoUnit.MINUTES); if (passedMinutes > initialMinutes) { passedMinutes = initialMinutes; } else if (passedMinutes < 0) { System.out.println(fillLoadingBar(initialMinutes, 0, false)); return; } while (passedMinutes < initialMinutes) { System.out.print(fillLoadingBar(initialMinutes, passedMinutes, true)); try { var now = LocalTime.now(); var oneMinuteLater = now.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES); // +1 second to adjust for ignored milliseconds as it is better to switch between 00 and 01 as between 59 and 00 TimeUnit.SECONDS.sleep(now.until(oneMinuteLater, ChronoUnit.SECONDS) + 1); // TimeUnit.SECONDS.sleep(1L); // DEBUG } catch (InterruptedException ie) { throw new RuntimeException(ie); } passedMinutes++; } System.out.println(fillLoadingBar(initialMinutes, passedMinutes, false)); } private static String fillLoadingBar(long initialMinutes, long passedMinutes, boolean progressive) { double wholePercentage = ((double) passedMinutes / initialMinutes) * 100; long remainingMinutes = initialMinutes - passedMinutes; int numberOfEquals = (int) wholePercentage; var sb = new StringBuilder("["); for (int i = 0; i < LINE_LENGTH; i++) { if (i < numberOfEquals) { sb.append("="); } else { sb.append("-"); } } sb.append("] ").append(PERCENTAGE_FORMAT.format(wholePercentage)).append("% ") .append(minutesToTimeString(passedMinutes)).append(" - ").append(minutesToTimeString(remainingMinutes)); if (progressive) { sb.append("\r"); } return sb.toString(); } private static String minutesToTimeString(long minutes) { return LocalTime.of((int) minutes / MINS_PER_HOUR, (int) minutes % MINS_PER_HOUR).format(TIME_FORMATTER); } }