import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.IOException; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.nio.charset.StandardCharsets; import java.text.DecimalFormat; import java.time.format.DateTimeFormatter; import java.time.LocalTime; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.concurrent.TimeUnit; import java.util.List; import java.util.Objects; 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 int MINS_PER_HOUR = 60; private static final BigDecimal MINS_PER_HOUR_BD = BigDecimal.valueOf(MINS_PER_HOUR); private static final long DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH = 5L * MINS_PER_HOUR; private static final int MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH = 6 * MINS_PER_HOUR; private static final long MAX_NUMBER_WORK_MINS = 8L * MINS_PER_HOUR; private static final int LINE_LENGTH = 100; private static final MathContext MC_INTEGER = new MathContext(1, RoundingMode.HALF_EVEN); private static final BigDecimal ONE_HUNDRED_PERCENT = BigDecimal.valueOf(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; } } private final LocalTime startTime; private LocalTime endTime; private long totalMinutes; private BigDecimal totalMinutesBD; private LoadingBar(LocalTime startTime) { this.startTime = startTime; } private void setEndTime(LocalTime endTime) { this.endTime = endTime; this.totalMinutes = startTime.until(endTime, ChronoUnit.MINUTES); this.totalMinutesBD = BigDecimal.valueOf(totalMinutes); } public static void main(String[] args) throws IOException { if (args.length == 0) { askParametersAndRun(); } else { parseParametersAndRun(args); } } private static void askParametersAndRun() throws IOException { var br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8)); print("Ankunftszeit: "); var startTime = LocalTime.parse(br.readLine(), TIME_FORMATTER).truncatedTo(ChronoUnit.MINUTES); var lb = new LoadingBar(startTime); if (lb.hasMittagspauseArrived()) { handleMittagspause(br, lb); lb.showLoadingBar(); } handleZapfenstreich(br, lb); lb.showLoadingBar(); } private static void println(Object o) { System.out.println(o); } private static void print(Object o) { System.out.print(o); } private static void handleMittagspause(BufferedReader br, LoadingBar lb) throws IOException { print("Mittagspause verschieben um (optional): "); String mittagspauseOffsetRaw = br.readLine(); if (mittagspauseOffsetRaw != null && !mittagspauseOffsetRaw.isBlank()) { var mittagspauseOffset = Integer.parseInt(mittagspauseOffsetRaw); lb.initMittagspause(mittagspauseOffset); return; } print("Mittagspause um (optional): "); String manualMittagspauseRaw = br.readLine(); if (manualMittagspauseRaw != null && !manualMittagspauseRaw.isBlank()) { var manualMittagspause = LocalTime.parse(manualMittagspauseRaw, TIME_FORMATTER).truncatedTo(ChronoUnit.MINUTES); lb.initMittagspause(manualMittagspause); } else { lb.initMittagspause(); } } private static void handleZapfenstreich(BufferedReader br, LoadingBar lb) throws IOException { print("Mittagspause hat gedauert (optional): "); String mittagspauseDurationRaw = br.readLine(); Integer mittagspauseDuration = null; if (mittagspauseDurationRaw != null && !mittagspauseDurationRaw.isBlank()) { mittagspauseDuration = Integer.valueOf(mittagspauseDurationRaw); } LocalTime vorlaeufigeEndzeit = lb.startTime.plusMinutes(MAX_NUMBER_WORK_MINS) .plusMinutes(mittagspauseDuration != null ? mittagspauseDuration : MIN_LUNCH_DURATION); println("Endzeit: " + TIME_FORMATTER.format(vorlaeufigeEndzeit)); print("Feierabend verschieben um (optional): "); String zapfenstreichOffsetRaw = br.readLine(); Integer zapfenstreichOffset = null; if (zapfenstreichOffsetRaw != null && !zapfenstreichOffsetRaw.isBlank()) { zapfenstreichOffset = Integer.valueOf(zapfenstreichOffsetRaw); lb.initZapfenstreich(mittagspauseDuration, zapfenstreichOffset); return; } print("Manuelle Uhrzeit Feierabend (optional): "); String manualZapfenstreichRaw = br.readLine(); LocalTime manualZapfenstreich = null; if (manualZapfenstreichRaw != null && !manualZapfenstreichRaw.isBlank()) { manualZapfenstreich = LocalTime.parse(manualZapfenstreichRaw, TIME_FORMATTER).truncatedTo(ChronoUnit.MINUTES); lb.initZapfenstreich(mittagspauseDuration, manualZapfenstreich); } else { lb.initZapfenstreich(mittagspauseDuration); } } private static void parseParametersAndRun(String[] args) { LoadingBar lb = getLoadingBarFromCLI(args); lb.showLoadingBar(); } private static LoadingBar getLoadingBarFromCLI(String[] args) { String nextArg = args[0]; if ("--help".equals(nextArg)) { printHelp(); System.exit(1); } verifyMinimumNumberOfArgs(args); verifyTimeFormat(nextArg, "Erstes Argument"); var startTime = LocalTime.parse(nextArg, TIME_FORMATTER).truncatedTo(ChronoUnit.MINUTES); nextArg = args[1]; var section = DaySection.byParam(nextArg); verifyDaySection(section, nextArg); return section == DaySection.MITTAG ? getLoadingBarMittagspause(args, startTime) : getLoadingBarZapfenstreich(args, startTime); } private static LoadingBar getLoadingBarMittagspause(String[] args, LocalTime startTime) { var lb = new LoadingBar(startTime); if (args.length == 2) { lb.initMittagspause(); return lb; } String nextArg = args[2]; if (OFFSET_PATTERN.matcher(nextArg).matches()) { lb.initMittagspause(Integer.parseInt(nextArg)); return lb; } verifyTimeFormat(nextArg, "Argument nach " + DaySection.MITTAG.getParam()); var manualMittagspause = LocalTime.parse(nextArg, TIME_FORMATTER).truncatedTo(ChronoUnit.MINUTES); lb.initMittagspause(manualMittagspause); return lb; } private static LoadingBar getLoadingBarZapfenstreich(String[] args, LocalTime startTime) { var lb = new LoadingBar(startTime); if (args.length == 2) { lb.initZapfenstreich(); return lb; } String nextArg = args[2]; LocalTime maxZapfenstreich = null; int endTimeOffset = 0; Integer lunchDuration = null; if (TIME_PATTERN.matcher(nextArg).matches()) { maxZapfenstreich = LocalTime.parse(nextArg, TIME_FORMATTER).truncatedTo(ChronoUnit.MINUTES); } else if (OFFSET_PATTERN.matcher(nextArg).matches()) { endTimeOffset = Integer.parseInt(nextArg); } else { verifyDurationFormat(nextArg, "Argument nach " + DaySection.ZAPFENSTREICH.getParam()); lunchDuration = Integer.parseInt(nextArg); } if (args.length == 3) { if (maxZapfenstreich == null && endTimeOffset == 0) { lb.initZapfenstreich(lunchDuration); } else if (maxZapfenstreich == null) { lb.initZapfenstreich(lunchDuration, endTimeOffset); } else { lb.initZapfenstreich(lunchDuration, maxZapfenstreich); } return lb; } nextArg = args[3]; if (lunchDuration == null) { 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).truncatedTo(ChronoUnit.MINUTES); lb.initZapfenstreich(lunchDuration, maxZapfenstreich); return lb; } verifyOffsetFormat(nextArg, "Letztes Argument nach " + DaySection.ZAPFENSTREICH.getParam() + " und Enduhrzeit"); endTimeOffset = Integer.parseInt(nextArg); lb.initZapfenstreich(lunchDuration, endTimeOffset); return lb; } private static void verifyMinimumNumberOfArgs(String[] args) { if (args.length >= 2) { return; } 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, "Uhrzeitformat (" + TIME_FORMAT + ")", false); } private static void verifyDurationFormat(String param, String errMsgPrefix) { verifyInputFormat(LUNCH_DURATION_PATTERN, param, errMsgPrefix, "Minutenanzahl (ganze Zahl)", true); } private static void verifyOffsetFormat(String param, String errMsgPrefix) { verifyInputFormat(OFFSET_PATTERN, param, errMsgPrefix, "Minutendifferenz (ganze Zahl mit Vorzeichen)", false); } private static void verifyInputFormat(Pattern pattern, String param, String errMsgPrefix, String firstInputPart, boolean timeInputPossible) { if (pattern.matcher(param).matches()) { return; } var possibleTimeInputPart = timeInputPossible ? " oder Uhrzeitformat (" + TIME_FORMAT + ")" : ""; 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); println("Argument nach Startzeit \"" + param + "\" muss Angabe für " + sectionDescsJoined + " sein."); System.exit(1); } private static void printHelp() { 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 boolean hasMittagspauseArrived() { return startTime.until(LocalTime.now(), ChronoUnit.MINUTES) < DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH; } private void initMittagspause() { LocalTime defaultEndTime = startTime.plusMinutes(DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH); realInitMittagspause(defaultEndTime); } private void initMittagspause(int endTimeOffset) { LocalTime offsetEndTime = startTime.plusMinutes(DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH + endTimeOffset); realInitMittagspause(offsetEndTime); } private void initMittagspause(LocalTime manualEndTime) { LocalTime effectiveEndTime = manualEndTime != null ? manualEndTime : startTime.plusMinutes(DEFAULT_NUMBER_WORK_MINS_BEFORE_LUNCH); realInitMittagspause(effectiveEndTime); } private void realInitMittagspause(LocalTime theoreticalEndTime) { setEndTime(theoreticalEndTime.isAfter(LATEST_LUNCH_TIME) ? LATEST_LUNCH_TIME : theoreticalEndTime); } private void initZapfenstreich() { LocalTime trueEndTime = startTime.plusMinutes(MAX_NUMBER_WORK_MINS + MIN_LUNCH_DURATION); realInitZapfenstreich(MIN_LUNCH_DURATION, trueEndTime); } private void initZapfenstreich(Integer manualLunchDuration) { initZapfenstreich(manualLunchDuration, 0); } private void initZapfenstreich(Integer manualLunchDuration, int endTimeOffset) { long minLunchDuration = getMinLunchDuration(endTimeOffset); long realLunchDuration = getRealLunchDuration(manualLunchDuration, minLunchDuration); LocalTime trueEndTime = startTime.plusMinutes(MAX_NUMBER_WORK_MINS + realLunchDuration + endTimeOffset); realInitZapfenstreich(realLunchDuration, trueEndTime); } private void initZapfenstreich(Integer manualLunchDuration, LocalTime manualEndTime) { LocalTime trueEndTime = manualEndTime; long minLunchDuration = getMinLunchDuration(trueEndTime); long realLunchDuration = getRealLunchDuration(manualLunchDuration, minLunchDuration); if (trueEndTime == null) { trueEndTime = startTime.plusMinutes(MAX_NUMBER_WORK_MINS + realLunchDuration); } realInitZapfenstreich(realLunchDuration, trueEndTime); } private 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 long getMinLunchDuration(LocalTime manualEndTime) { if (manualEndTime == null) { return MIN_LUNCH_DURATION; } long totalDuration = startTime.until(manualEndTime, ChronoUnit.MINUTES); long effectiveLunchDuration = totalDuration - MAX_NUMBER_WORK_MINS_WITHOUT_LUNCH; return getMinLunchDuration(effectiveLunchDuration); } private long getMinLunchDuration(long effectiveLunchDuration) { if (effectiveLunchDuration < 0) { effectiveLunchDuration = 0; } return Math.min(effectiveLunchDuration, MIN_LUNCH_DURATION); } private long getRealLunchDuration(Integer manualLunchDuration, long minLunchDuration) { return manualLunchDuration != null && manualLunchDuration >= minLunchDuration ? manualLunchDuration : minLunchDuration; } private void realInitZapfenstreich(long effectiveLunchDuration, LocalTime effectiveEndTime) { if (effectiveLunchDuration > 0) { var totalWorkTime = LocalTime.MIDNIGHT.plusMinutes(startTime.until(effectiveEndTime, ChronoUnit.MINUTES) - effectiveLunchDuration); print("Arbeitszeit: " + TIME_FORMATTER.format(totalWorkTime) + "; "); } setEndTime(effectiveEndTime); } private void showLoadingBar() { long passedMinutes = startTime.until(LocalTime.now().truncatedTo(ChronoUnit.MINUTES), ChronoUnit.MINUTES); // long passedMinutes = 0; // DEBUG if (passedMinutes > totalMinutes) { passedMinutes = totalMinutes; } else if (passedMinutes < 0) { var now = LocalTime.now().truncatedTo(ChronoUnit.SECONDS); println("!ACHTUNG! Startzeit \"" + startTime + ":00\" liegt in der Zukunft von jetzt an (" + now + ") gesehen."); } println(minutesToTimeString(totalMinutesBD) + " gesamt; Endzeit: " + TIME_FORMATTER.format(endTime)); while (passedMinutes < totalMinutes) { print(fillLoadingBar(passedMinutes, true)); waitUntilNextMinute(); passedMinutes++; } println(fillLoadingBar(passedMinutes, false)); } private void waitUntilNextMinute() { try { var now = LocalTime.now(); var oneMinuteLater = now.plusMinutes(1).truncatedTo(ChronoUnit.MINUTES); /* We wait whole seconds to not make it overly complicated. That results in cut milliseconds: if we would have to wait 1 second and 526 milliseconds, we wait only 1 second. So, adjust for ignored milliseconds, add +1 second 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.MILLISECONDS.sleep(100L); // DEBUG } catch (InterruptedException ie) { Thread.currentThread().interrupt(); throw new RuntimeException(ie); } } private String fillLoadingBar(long passedMinutes, boolean progressive) { var nonNegativePassedMinutes = BigDecimal.valueOf(passedMinutes < 0 ? 0 : passedMinutes); BigDecimal wholePercentage = ONE_HUNDRED_PERCENT // kind of reverse dreisatz to avoid having e.g. 99.9999 instead of 100 % .multiply(nonNegativePassedMinutes) .divide(totalMinutesBD, MathContext.DECIMAL64); int numberOfEquals = wholePercentage.intValue(); var sb = new StringBuilder("["); for (int i = 0; i < LINE_LENGTH; i++) { if (i < numberOfEquals) { sb.append("="); } else { sb.append("-"); } } BigDecimal remainingMinutes = totalMinutesBD.subtract(nonNegativePassedMinutes); sb.append("] ").append(PERCENTAGE_FORMAT.format(wholePercentage)).append("% ") .append(minutesToTimeString(nonNegativePassedMinutes)).append(" - ").append(minutesToTimeString(remainingMinutes)); if (progressive) { sb.append("\r"); } return sb.toString(); } private String minutesToTimeString(BigDecimal minutes) { BigDecimal[] hoursAndMinutes = minutes.divideAndRemainder(MINS_PER_HOUR_BD, MC_INTEGER); return LocalTime.of(hoursAndMinutes[0].intValue(), hoursAndMinutes[1].intValue()).format(TIME_FORMATTER); } }