diff --git a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java index 39e1468..4ec4241 100644 --- a/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java +++ b/src/main/java/me/tongfei/progressbar/DefaultProgressBarRenderer.java @@ -2,29 +2,39 @@ import java.text.DecimalFormat; import java.time.Duration; -import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Optional; import java.util.function.Function; -import static me.tongfei.progressbar.StringDisplayUtils.*; +import static me.tongfei.progressbar.StringDisplayUtils.getStringDisplayLength; +import static me.tongfei.progressbar.StringDisplayUtils.trimDisplayLength; /** * Default progress bar renderer (see {@link ProgressBarRenderer}). + * * @author Tongfei Chen * @author Muhammet Sakarya * @since 0.8.0 */ public class DefaultProgressBarRenderer implements ProgressBarRenderer { - private ProgressBarStyle style; - private String unitName; - private long unitSize; - private boolean isSpeedShown; - private DecimalFormat speedFormat; - private ChronoUnit speedUnit; - private boolean isEtaShown; - private Function> eta; + private final ProgressBarStyle style; + + private final String unitName; + + private final long unitSize; + + private final boolean isSpeedShown; + + private final TimeFormat timeFormat; + + private final DecimalFormat speedFormat; + + private final ChronoUnit speedUnit; + + private final boolean isEtaShown; + + private final Function> eta; protected DefaultProgressBarRenderer( ProgressBarStyle style, @@ -34,12 +44,14 @@ protected DefaultProgressBarRenderer( DecimalFormat speedFormat, ChronoUnit speedUnit, boolean isEtaShown, - Function> eta + Function> eta, + TimeFormat timeFormat ) { this.style = style; this.unitName = unitName; this.unitSize = unitSize; this.isSpeedShown = isSpeedShown; + this.timeFormat = timeFormat; this.speedFormat = isSpeedShown && speedFormat == null ? new DecimalFormat() : speedFormat; this.speedUnit = speedUnit; this.isEtaShown = isEtaShown; @@ -48,7 +60,7 @@ protected DefaultProgressBarRenderer( // Number of full blocks protected int progressIntegralPart(ProgressState progress, int length) { - return (int)(progress.getNormalizedProgress() * length); + return (int) (progress.getNormalizedProgress() * length); } protected int progressFractionalPart(ProgressState progress, int length) { @@ -58,19 +70,21 @@ protected int progressFractionalPart(ProgressState progress, int length) { } protected String etaString(ProgressState progress) { - Optional eta = this.eta.apply(progress); - if (eta.isPresent()) { - return Util.formatDuration(eta.get()); - } - else { + final Optional optEta = this.eta.apply(progress); + if (optEta.isPresent()) { + return DurationFormatter.formatDuration(optEta.get(), timeFormat); + } else { return "?"; } } protected String percentage(ProgressState progress) { String res; - if (progress.max <= 0 || progress.indefinite) res = "? %"; - else res = String.valueOf((int) Math.floor(100.0 * progress.current / progress.max)) + "%"; + if (progress.max <= 0 || progress.indefinite) { + res = "? %"; + } else { + res = String.valueOf((int) Math.floor(100.0 * progress.current / progress.max)) + "%"; + } return Util.repeat(' ', 4 - res.length()) + res; } @@ -84,7 +98,7 @@ protected String speed(ProgressState progress) { String suffix = "/s"; double elapsedSeconds = progress.getElapsedAfterStart().getSeconds(); double elapsedInUnit = elapsedSeconds; - if (null != speedUnit) + if (null != speedUnit) { switch (speedUnit) { case MINUTES: suffix = "/min"; @@ -99,9 +113,11 @@ protected String speed(ProgressState progress) { elapsedInUnit /= (60 * 60 * 24); break; } + } - if (elapsedSeconds == 0) + if (elapsedSeconds == 0) { return "?" + unitName + suffix; + } double speed = (double) (progress.current - progress.start) / elapsedInUnit; double speedWithUnit = speed / unitSize; return speedFormat.format(speedWithUnit) + unitName + suffix; @@ -125,7 +141,7 @@ public String render(ProgressState progress, int maxLength) { String speedString = isSpeedShown ? speed(progress) : ""; String suffix = style.rightBracket + " " + ratio(progress) + " (" - + Util.formatDuration(progress.getTotalElapsed()) + + DurationFormatter.formatDuration(progress.getTotalElapsed(), timeFormat) + (isEtaShown ? " / " + etaString(progress) : "") + ") " + speedString + progress.extraMessage; @@ -143,7 +159,7 @@ public String render(ProgressState progress, int maxLength) { // case of indefinite progress bars if (progress.indefinite) { - int pos = (int)(progress.current % length); + int pos = (int) (progress.current % length); sb.append(Util.repeat(style.space, pos)); sb.append(style.block); sb.append(Util.repeat(style.space, length - pos - 1)); @@ -156,8 +172,7 @@ public String render(ProgressState progress, int maxLength) { if (fraction != 0) { sb.append(style.fractionSymbols.charAt(fraction)); sb.append(style.delimitingSequence); - } - else { + } else { sb.append(style.delimitingSequence); sb.append(style.rightSideFractionSymbol); } diff --git a/src/main/java/me/tongfei/progressbar/DurationFormatter.java b/src/main/java/me/tongfei/progressbar/DurationFormatter.java new file mode 100644 index 0000000..6aad1ee --- /dev/null +++ b/src/main/java/me/tongfei/progressbar/DurationFormatter.java @@ -0,0 +1,48 @@ +package me.tongfei.progressbar; + +import java.time.Duration; + +public class DurationFormatter { + + private DurationFormatter() { + } + + public static String formatDuration(Duration d) { + return formatDuration(d, TimeFormat.DEFAULT); + } + + public static String formatDuration(Duration d, TimeFormat format) { + final long s = d.getSeconds(); + switch (format) { + case SECONDS: + return formatAsSeconds(s); + case MINUTES_SECONDS: + return formatAsMinutesAndSeconds(s); + case ADAPTED: { + if (s < 60) { + return formatAsSeconds(s); + } else if (s < 3600) { + return formatAsMinutesAndSeconds(s); + } else { + return formatAsHoursMinutedAndSeconds(s); + } + } + case DEFAULT: + default: + return formatAsHoursMinutedAndSeconds(s); + } + } + + private static String formatAsSeconds(long s) { + return String.format("%d", s); + } + + private static String formatAsMinutesAndSeconds(long s) { + return String.format("%d:%02d", s / 60, s % 60); + } + + private static String formatAsHoursMinutedAndSeconds(long s) { + return String.format("%d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60); + } + +} diff --git a/src/main/java/me/tongfei/progressbar/ProgressBar.java b/src/main/java/me/tongfei/progressbar/ProgressBar.java index 03170bf..0d5da0d 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBar.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBar.java @@ -71,11 +71,11 @@ public ProgressBar( long processed, Duration elapsed ) { - this(task, initialMax, updateIntervalMillis, continuousUpdate, clearDisplayOnFinish, processed, elapsed, + this(task, null, initialMax, updateIntervalMillis, continuousUpdate, clearDisplayOnFinish, processed, elapsed, new DefaultProgressBarRenderer( style, unitName, unitSize, showSpeed, speedFormat, speedUnit, - true, Util::linearEta + true, Util::linearEta, TimeFormat.DEFAULT ), createConsoleConsumer(os) ); @@ -85,6 +85,7 @@ public ProgressBar( * Creates a progress bar with the specific name, initial maximum value, customized update interval (default 1s), * and the provided progress bar renderer ({@link ProgressBarRenderer}) and consumer ({@link ProgressBarConsumer}). * @param task Task name + * @param taskFixedLength Fixed length of task name (null equals no fixed length) * @param initialMax Initial maximum value * @param updateIntervalMillis Update time interval (default value 1000ms) * @param continuousUpdate Rerender every time the update interval happens regardless of progress count. @@ -96,6 +97,7 @@ public ProgressBar( */ public ProgressBar( String task, + Integer taskFixedLength, long initialMax, int updateIntervalMillis, boolean continuousUpdate, @@ -105,7 +107,7 @@ public ProgressBar( ProgressBarRenderer renderer, ProgressBarConsumer consumer ) { - this.progress = new ProgressState(task, initialMax, processed, elapsed); + this.progress = new ProgressState(task, taskFixedLength, initialMax, processed, elapsed); this.action = new ProgressUpdateAction(progress, renderer, consumer, continuousUpdate, clearDisplayOnFinish); scheduledTask = Util.executor.scheduleAtFixedRate( action, 0, updateIntervalMillis, TimeUnit.MILLISECONDS diff --git a/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java b/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java index 7c6d035..bb0941b 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java +++ b/src/main/java/me/tongfei/progressbar/ProgressBarBuilder.java @@ -14,6 +14,7 @@ public class ProgressBarBuilder { private String task = ""; + private Integer taskFixedLength = null; private long initialMax = -1; private int updateIntervalMillis = 1000; private boolean continuousUpdate = false; @@ -22,6 +23,7 @@ public class ProgressBarBuilder { private boolean clearDisplayOnFinish = false; private String unitName = ""; private long unitSize = 1; + private TimeFormat timeFormat = TimeFormat.DEFAULT; private boolean showSpeed = false; private boolean hideEta = false; private Function> eta = Util::linearEta; @@ -40,6 +42,11 @@ public ProgressBarBuilder setTaskName(String task) { return this; } + public ProgressBarBuilder setTaskFixedLength(Integer fixedLength) { + this.taskFixedLength = fixedLength; + return this; + } + boolean initialMaxIsSet() { return this.initialMax != -1; } @@ -90,6 +97,11 @@ public ProgressBarBuilder setRenderer(ProgressBarRenderer renderer) { return this; } + public ProgressBarBuilder setTimeFormat(TimeFormat timeFormat) { + this.timeFormat = timeFormat; + return this; + } + public ProgressBarBuilder showSpeed() { return showSpeed(new DecimalFormat("#.0")); } @@ -130,6 +142,7 @@ public ProgressBarBuilder startsFrom(long processed, Duration elapsed) { public ProgressBar build() { return new ProgressBar( task, + taskFixedLength, initialMax, updateIntervalMillis, continuousUpdate, @@ -138,9 +151,9 @@ public ProgressBar build() { elapsed, (renderer == null ? new DefaultProgressBarRenderer( - style, unitName, unitSize, + style, unitName, unitSize, showSpeed, speedFormat, speedUnit, - !hideEta, eta) + !hideEta, eta, timeFormat) : renderer ), (consumer == null diff --git a/src/main/java/me/tongfei/progressbar/ProgressState.java b/src/main/java/me/tongfei/progressbar/ProgressState.java index b15f722..6959de1 100644 --- a/src/main/java/me/tongfei/progressbar/ProgressState.java +++ b/src/main/java/me/tongfei/progressbar/ProgressState.java @@ -11,12 +11,13 @@ public class ProgressState { String taskName; + + private final Integer taskFixedLength; + String extraMessage = ""; boolean indefinite = false; - // 0 start current max - // [===============|=========> ] long start; long current; long max; @@ -27,8 +28,9 @@ public class ProgressState { volatile boolean alive = true; volatile boolean paused = false; - ProgressState(String taskName, long initialMax, long startFrom, Duration elapsedBeforeStart) { + ProgressState(String taskName, Integer taskFixedLength, long initialMax, long startFrom, Duration elapsedBeforeStart) { this.taskName = taskName; + this.taskFixedLength = taskFixedLength; if (initialMax < 0) indefinite = true; else this.max = initialMax; @@ -40,9 +42,18 @@ public class ProgressState { } public String getTaskName() { + if (taskFixedLength != null) { + return getPaddedTaskName(taskName, taskFixedLength); + } return taskName; } + private String getPaddedTaskName(String task, int length) { + final String str = task.length() < length ? task : task.substring(0, length); + final String format = "%-" + length + "s"; + return String.format(format, str); + } + public synchronized String getExtraMessage() { return extraMessage; } diff --git a/src/main/java/me/tongfei/progressbar/TimeFormat.java b/src/main/java/me/tongfei/progressbar/TimeFormat.java new file mode 100644 index 0000000..420d8a4 --- /dev/null +++ b/src/main/java/me/tongfei/progressbar/TimeFormat.java @@ -0,0 +1,8 @@ +package me.tongfei.progressbar; + +public enum TimeFormat { + DEFAULT, + ADAPTED, + SECONDS, + MINUTES_SECONDS +} diff --git a/src/main/java/me/tongfei/progressbar/Util.java b/src/main/java/me/tongfei/progressbar/Util.java index d07e991..565c1ac 100644 --- a/src/main/java/me/tongfei/progressbar/Util.java +++ b/src/main/java/me/tongfei/progressbar/Util.java @@ -39,11 +39,6 @@ static String repeat(char c, int n) { return new String(s); } - static String formatDuration(Duration d) { - long s = d.getSeconds(); - return String.format("%d:%02d:%02d", s / 3600, (s % 3600) / 60, s % 60); - } - static Optional linearEta(ProgressState progress) { if (progress.getMax() <= 0 || progress.isIndefinite()) return Optional.empty(); else if (progress.getCurrent() - progress.getStart() == 0) return Optional.empty(); diff --git a/src/test/java/me/tongfei/progressbar/DurationFormatterTest.java b/src/test/java/me/tongfei/progressbar/DurationFormatterTest.java new file mode 100644 index 0000000..3826c53 --- /dev/null +++ b/src/test/java/me/tongfei/progressbar/DurationFormatterTest.java @@ -0,0 +1,42 @@ +package me.tongfei.progressbar; + +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.*; + +class DurationFormatterTest { + + @Test + void testFormatWhenSeconds() { + assertEquals("5", DurationFormatter.formatDuration(Duration.ofSeconds(5), TimeFormat.SECONDS)); + assertEquals("22", DurationFormatter.formatDuration(Duration.ofSeconds(22), TimeFormat.SECONDS)); + assertEquals("63", DurationFormatter.formatDuration(Duration.ofSeconds(63), TimeFormat.SECONDS)); + assertEquals("3682", DurationFormatter.formatDuration(Duration.ofSeconds(3682), TimeFormat.SECONDS)); + } + + @Test + void testFormatWhenMinutesAndSeconds() { + assertEquals("0:05", DurationFormatter.formatDuration(Duration.ofSeconds(5), TimeFormat.MINUTES_SECONDS)); + assertEquals("0:22", DurationFormatter.formatDuration(Duration.ofSeconds(22), TimeFormat.MINUTES_SECONDS)); + assertEquals("1:03", DurationFormatter.formatDuration(Duration.ofSeconds(63), TimeFormat.MINUTES_SECONDS)); + assertEquals("61:22", DurationFormatter.formatDuration(Duration.ofSeconds(3682), TimeFormat.MINUTES_SECONDS)); + } + + @Test + void testFormatWhenAdapted() { + assertEquals("5", DurationFormatter.formatDuration(Duration.ofSeconds(5), TimeFormat.ADAPTED)); + assertEquals("22", DurationFormatter.formatDuration(Duration.ofSeconds(22), TimeFormat.ADAPTED)); + assertEquals("1:03", DurationFormatter.formatDuration(Duration.ofSeconds(63), TimeFormat.ADAPTED)); + assertEquals("1:01:22", DurationFormatter.formatDuration(Duration.ofSeconds(3682), TimeFormat.ADAPTED)); + } + + @Test + void testFormatWhenDefault() { + assertEquals("0:00:05", DurationFormatter.formatDuration(Duration.ofSeconds(5), TimeFormat.DEFAULT)); + assertEquals("0:00:22", DurationFormatter.formatDuration(Duration.ofSeconds(22), TimeFormat.DEFAULT)); + assertEquals("0:01:03", DurationFormatter.formatDuration(Duration.ofSeconds(63), TimeFormat.DEFAULT)); + assertEquals("1:01:22", DurationFormatter.formatDuration(Duration.ofSeconds(3682), TimeFormat.DEFAULT)); + } +} \ No newline at end of file