Skip to content

Commit dded536

Browse files
author
edelgadoh
committed
Adding labels to split StopWatch feature
1 parent f4762c3 commit dded536

File tree

3 files changed

+374
-3
lines changed

3 files changed

+374
-3
lines changed

src/main/java/org/apache/commons/lang3/time/StopWatch.java

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import java.time.Duration;
2121
import java.time.Instant;
22+
import java.util.ArrayList;
23+
import java.util.List;
2224
import java.util.Objects;
2325
import java.util.concurrent.TimeUnit;
2426
import java.util.function.Supplier;
@@ -248,6 +250,11 @@ public static StopWatch createStarted() {
248250
*/
249251
private long stopTimeNanos;
250252

253+
/**
254+
* The list of splits
255+
*/
256+
private List<Split> splits;
257+
251258
/**
252259
* Constructs a new instance.
253260
*/
@@ -626,6 +633,59 @@ public void split() {
626633
splitState = SplitState.SPLIT;
627634
}
628635

636+
/**
637+
* <p>
638+
* Splits the time to track the elapsed time between two consecutive {@code split()} calls.
639+
* The label specified is used to identify each split
640+
* </p>
641+
*
642+
* <p>
643+
* After calling {@link #stop()}, we can call {@link #getReport()} to have a report with all time between each {@code split()} call, example:
644+
* </p>
645+
*
646+
* <pre>
647+
* 1 00:14:00.000
648+
* 2 00:02:00.000
649+
* 3 00:04:00.000
650+
* </pre>
651+
*
652+
* @param label A number to identify this split
653+
*
654+
* @throws IllegalStateException
655+
* if the StopWatch is not running.
656+
*/
657+
public void split(int label) {
658+
split(String.valueOf(label));
659+
}
660+
661+
/**
662+
* <p>
663+
* Splits the time to track the elapsed time between two consecutive {@code split()} calls.
664+
* The label specified is used to identify each split
665+
* </p>
666+
*
667+
* <p>
668+
* After calling {@link #stop()}, we can call {@link #getReport()} to have a report with all time between each {@code split()} call, example:
669+
* </p>
670+
*
671+
* <pre>
672+
* Baking cookies 00:14:00.000
673+
* Serving 00:02:00.000
674+
* Eating 00:04:00.000
675+
* </pre>
676+
*
677+
* @param label A message for string presentation.
678+
*
679+
* @throws IllegalStateException
680+
* if the StopWatch is not running.
681+
*/
682+
public void split(String label) {
683+
if (this.runningState != State.RUNNING) {
684+
throw new IllegalStateException("Stopwatch is not running.");
685+
}
686+
splits.add(new Split(label));
687+
}
688+
629689
/**
630690
* Starts this StopWatch.
631691
*
@@ -645,6 +705,7 @@ public void start() {
645705
startTimeNanos = System.nanoTime();
646706
startInstant = Instant.now();
647707
runningState = State.RUNNING;
708+
splits = new ArrayList<>();
648709
}
649710

650711
/**
@@ -674,10 +735,20 @@ public void stop() {
674735
if (runningState == State.RUNNING) {
675736
stopTimeNanos = System.nanoTime();
676737
stopInstant = Instant.now();
738+
split(StringUtils.EMPTY);
677739
}
678740
runningState = State.STOPPED;
679741
}
680742

743+
/**
744+
* Stops the watch if necessary
745+
*/
746+
private void stopIfNecessary() {
747+
if (this.runningState == State.RUNNING || this.runningState == State.SUSPENDED) {
748+
stop();
749+
}
750+
}
751+
681752
/**
682753
* Suspends this StopWatch for later resumption.
683754
*
@@ -746,4 +817,182 @@ public void unsplit() {
746817
splitState = SplitState.UNSPLIT;
747818
}
748819

820+
/**
821+
* Stops the watch and returns the list of splits with duration on each split (using milliseconds)
822+
* @return list of splits
823+
*/
824+
public List<Split> getProcessedSplits() {
825+
return getProcessedSplits(TimeUnit.MILLISECONDS);
826+
}
827+
828+
/**
829+
* Stops the watch and returns the list of splits with duration on each split (using nanoseconds)
830+
* @return list of splits
831+
*/
832+
public List<Split> getNanoProcessedSplits() {
833+
return getProcessedSplits(TimeUnit.NANOSECONDS);
834+
}
835+
836+
/**
837+
* Stops the watch and returns the list of splits with duration on each split
838+
*
839+
* @param timeUnit the unit of time, not null. Any value will calculate with milliseconds precision unless
840+
* {@code TimeUnit.NANOSECONDS} is specified.
841+
* @return list of splits
842+
*/
843+
public List<Split> getProcessedSplits(TimeUnit timeUnit) {
844+
stopIfNecessary();
845+
processSplits(timeUnit);
846+
final List<Split> result = new ArrayList<>(splits);
847+
848+
// we remove the last split because it's an internal and automatic split
849+
result.remove(result.size() - 1);
850+
851+
return result;
852+
}
853+
854+
/**
855+
* Fill durations (time took) on each split
856+
*
857+
* @param timeUnit the unit of time, not null. Any value will calculate with milliseconds precision unless
858+
* {@code TimeUnit.NANOSECONDS} is specified.
859+
*/
860+
private void processSplits(TimeUnit timeUnit) {
861+
// we need at least 2 splits to calculate the elapsed time
862+
if (splits.size() < 2) {
863+
return;
864+
}
865+
866+
for (int i = 0; i < splits.size() - 1; i++) {
867+
final long durationNanos = splits.get(i + 1).getStartNanoTime() - splits.get(i).getStartNanoTime();
868+
final long duration = (timeUnit == TimeUnit.NANOSECONDS)
869+
? durationNanos
870+
: TimeUnit.MILLISECONDS.convert(durationNanos, TimeUnit.NANOSECONDS);
871+
splits.get(i).setDuration(duration);
872+
}
873+
874+
}
875+
876+
/**
877+
* <p>
878+
* Stops the watch and returns the splits report.
879+
* This report contains the elapsed time (on milliseconds) between each split
880+
* </p>
881+
*
882+
* @return the splits report
883+
*/
884+
public String getReport() {
885+
return getReport(TimeUnit.MILLISECONDS);
886+
}
887+
888+
/**
889+
* <p>
890+
* Stops the watch and returns the splits report.
891+
* This report contains the elapsed time (on nanoseconds) between each split
892+
* </p>
893+
*
894+
* @return the splits report
895+
*/
896+
public String getNanoReport() {
897+
return getReport(TimeUnit.NANOSECONDS);
898+
}
899+
900+
/**
901+
* <p>
902+
* Stops the watch and returns the splits report.
903+
* This report contains the elapsed time between each split
904+
* </p>
905+
*
906+
* @param timeUnit the unit of time, not null. Any value will calculate with milliseconds precision unless
907+
* {@code TimeUnit.NANOSECONDS} is specified.
908+
* @return the splits report
909+
*/
910+
private String getReport(TimeUnit timeUnit) {
911+
final StringBuilder report = new StringBuilder();
912+
913+
String duration;
914+
for (final Split split : getProcessedSplits(timeUnit)) {
915+
report.append(System.lineSeparator());
916+
report.append(split.getLabel()).append(StringUtils.SPACE);
917+
918+
if (timeUnit == TimeUnit.NANOSECONDS) {
919+
duration = String.valueOf(split.getDuration());
920+
} else {
921+
duration = DurationFormatUtils.formatDurationHMS(split.getDuration());
922+
}
923+
924+
report.append(duration);
925+
}
926+
927+
return report.toString();
928+
}
929+
930+
/**
931+
* Class to store details of each split
932+
*/
933+
protected static class Split {
934+
935+
/**
936+
* The start nano time of this split
937+
*/
938+
private long startNanoTime = System.nanoTime();
939+
940+
/**
941+
* The duration (time took) on this split
942+
* This field is filled when user calls getSplits() or tries to print the splits report
943+
*/
944+
private long duration;
945+
946+
/*
947+
* The label for this split
948+
*/
949+
private String label;
950+
951+
/**
952+
* Constructor with label
953+
* @param label Label for this split
954+
*/
955+
public Split(String label) {
956+
this.label = label;
957+
}
958+
959+
/**
960+
* <p>
961+
* Get the timestamp when this split was created
962+
* </p>
963+
*
964+
* @return startNanoTime
965+
*/
966+
public long getStartNanoTime() {
967+
return startNanoTime;
968+
}
969+
970+
/**
971+
* <p>
972+
* Get the label of this split
973+
* </p>
974+
*
975+
* @return label
976+
*/
977+
public String getLabel() {
978+
return label;
979+
}
980+
981+
/**
982+
* Duration of this split
983+
* @return duration (time on ms or nano)
984+
*/
985+
public long getDuration() {
986+
return duration;
987+
}
988+
989+
/**
990+
* Set the duration of this split
991+
* @param duration time (on ms or nano)
992+
*/
993+
private void setDuration(long duration) {
994+
this.duration = duration;
995+
}
996+
}
997+
749998
}

src/test/java/org/apache/commons/lang3/time/FastDateParser_TimeZoneStrategyTest.java

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,16 @@ private void testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(final Locale l
170170
Objects.requireNonNull(locale, "locale");
171171
assumeFalse(LocaleUtils.isLanguageUndetermined(locale), () -> toFailureMessage(locale, null, null));
172172
assumeTrue(LocaleUtils.isAvailableLocale(locale), () -> toFailureMessage(locale, null, null));
173+
//Only breaks in GitHub in macOS 13 + JDK 25 builds:
174+
//Unparseable date: hora estándar de Sarátov: with id = 'Europe/Saratov', displayName = 'hora estándar de Sarátov', locale = es_US
175+
//Unparseable date: hora estándar de Casablanca: with id = 'Africa/Casablanca', displayName = 'hora estándar de Casablanca', locale = es_UY
176+
//Unparseable date: hora estándar de Casablanca: with id = 'Africa/Casablanca', displayName = 'hora estándar de Casablanca', locale = es_VE
177+
assumeFalse(
178+
System.getProperty("os.name").toLowerCase().contains("mac") &&
179+
System.getProperty("os.version").startsWith("13") &&
180+
System.getProperty("java.version", "").startsWith("25"),
181+
"Skipping timezone parsing test on macOS 13 + JDK 25 due to CLDR mismatch"
182+
);
173183
for (final String id : ArraySorter.sort(TimeZone.getAvailableIDs())) {
174184
final TimeZone timeZone = TimeZone.getTimeZone(id);
175185
final String displayName = timeZone.getDisplayName(locale);
@@ -180,7 +190,7 @@ private void testTimeZoneStrategyPattern_TimeZone_getAvailableIDs(final Locale l
180190
// Missing "Zulu" or something else in broken JDK's GH builds?
181191
// Call LocaleUtils again
182192
fail(String.format("%s: with id = '%s', displayName = '%s', %s, parser = '%s'", e, id, displayName,
183-
toFailureMessage(locale, null, timeZone), parser.toStringAll()), e);
193+
toFailureMessage(locale, locale.toLanguageTag(), timeZone), parser.toStringAll()), e);
184194
}
185195
}
186196
}
@@ -223,7 +233,7 @@ void testTimeZoneStrategyPatternSuriname() throws ParseException {
223233
}
224234

225235
private String toFailureMessage(final Locale locale, final String languageTag, final TimeZone timeZone) {
226-
return String.format("locale = %s, languageTag = '%s', isAvailableLocale = %s, isLanguageUndetermined = %s, timeZone = %s", languageTag, locale,
227-
LocaleUtils.isAvailableLocale(locale), LocaleUtils.isLanguageUndetermined(locale), TimeZones.toTimeZone(timeZone));
236+
return String.format("locale = %s, languageTag = '%s', isAvailableLocale = %s, isLanguageUndetermined = %s, timeZone = %s", locale, languageTag,
237+
LocaleUtils.isAvailableLocale(locale), LocaleUtils.isLanguageUndetermined(locale), timeZone);
228238
}
229239
}

0 commit comments

Comments
 (0)