From d9149663e728e090a38b933b6e91b3ef0c642ba0 Mon Sep 17 00:00:00 2001 From: Bert Massop Date: Sat, 30 Nov 2024 13:18:00 +0100 Subject: [PATCH] Refactor TimeUtil: use Java 8 Date-Time API and improve time intervals The time intervals were subtly broken: * Parsing 0.1s would yield 1 ms instead of the correct 100 ms. Fix this by parsing the seconds.millis as fractional seconds. * Formatting would format fractional seconds locale-sensitively where parsing would assume point as a decimal separator, so toMillis(fromMillis(...)) would fail depending on locale. Fix this by always formatting in the root locale (decimal point). As a tiny bonus, time interval parsing is now approximately 3x faster. --- src/freenet/support/TimeUtil.java | 105 ++++++++++--------------- test/freenet/support/TimeUtilTest.java | 14 ++-- 2 files changed, 49 insertions(+), 70 deletions(-) diff --git a/src/freenet/support/TimeUtil.java b/src/freenet/support/TimeUtil.java index 80da0aeb8a..d1a4abbad3 100644 --- a/src/freenet/support/TimeUtil.java +++ b/src/freenet/support/TimeUtil.java @@ -24,9 +24,15 @@ the License, or (at your option) any later version. import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; -import java.text.DecimalFormat; -import java.text.SimpleDateFormat; -import java.util.*; +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** * Time formatting utility. @@ -35,6 +41,8 @@ the License, or (at your option) any later version. public class TimeUtil { public static final TimeZone TZ_UTC = TimeZone.getTimeZone("UTC"); + private static final Pattern TIME_INTERVAL_PATTERN = + Pattern.compile("-?(?:(\\d+)w)?(?:(\\d+)d)?(?:(\\d+)h)?(?:(\\d+)m)?(?:(\\d+)([.]\\d+)?s)?"); /** * It converts a given time interval into a @@ -108,18 +116,13 @@ public static String formatTime(long timeInterval, int maxTerms, boolean withSec } if(withSecondFractions && ((maxTerms - termCount) >= 2)) { if (l > 0) { - double fractionalSeconds = l / (1000.0D); - DecimalFormat fix3 = new DecimalFormat("0.000"); - sb.append(fix3.format(fractionalSeconds)).append('s'); - termCount++; - //l = l - ((long)fractionalSeconds * (long)1000); + double fractionalSeconds = l / (1000.0); + sb.append(String.format(Locale.ROOT, "%.3f", fractionalSeconds)).append('s'); } } else { long seconds = SECONDS.convert(l, MILLISECONDS); if (seconds > 0) { sb.append(seconds).append('s'); - termCount++; - //l = l - ((long)seconds * (long)1000); } } // @@ -135,77 +138,49 @@ public static String formatTime(long timeInterval, int maxTerms) { } public static long toMillis(String timeInterval) { - byte sign = 1; - if (timeInterval.contains("-")) { - sign = -1; - timeInterval = timeInterval.substring(1); + Matcher matcher = TIME_INTERVAL_PATTERN.matcher(timeInterval); + if (!matcher.matches()) { + throw new NumberFormatException("Unknown format: " + timeInterval); } - String[] terms = timeInterval.split("(?<=[a-z])"); - + String group; long millis = 0; - for (String term : terms) { - if (term.isEmpty()) continue; - - char measure = term.charAt(term.length() - 1); - switch(measure){ - case 'w': - millis += 7 * MILLISECONDS.convert(Long.parseLong(term.substring(0, term.length() - 1)), DAYS); - break; - case 'd': - millis += MILLISECONDS.convert(Short.parseShort(term.substring(0, term.length() - 1)), DAYS); - break; - case 'h': - millis += MILLISECONDS.convert(Short.parseShort(term.substring(0, term.length() - 1)), HOURS); - break; - case 'm': - millis += MILLISECONDS.convert(Short.parseShort(term.substring(0, term.length() - 1)), MINUTES); - break; - case 's': - if (term.contains(".")) { - millis += Integer.parseInt(term.replaceAll("[a-z.]", "")); - } else { - millis += MILLISECONDS.convert(Short.parseShort(term.substring(0, term.length() - 1)), SECONDS); - } - break; - default: - throw new NumberFormatException("Unknown format: " + (sign > 0 ? "" : "-") + timeInterval); - } + if ((group = matcher.group(1)) != null) { // weeks + millis += DAYS.toMillis(7 * Long.parseLong(group)); + } + if ((group = matcher.group(2)) != null) { // days + millis += DAYS.toMillis(Long.parseLong(group)); + } + if ((group = matcher.group(3)) != null) { // hours + millis += HOURS.toMillis(Long.parseLong(group)); + } + if ((group = matcher.group(4)) != null) { // minutes + millis += MINUTES.toMillis(Long.parseLong(group)); + } + if ((group = matcher.group(5)) != null) { // seconds + millis += SECONDS.toMillis(Long.parseLong(group)); + } + if ((group = matcher.group(6)) != null) { // fractional seconds + millis += (long) (Double.parseDouble(group) * 1000); } - return millis * sign; + return timeInterval.startsWith("-") ? -millis : millis; } /** * Helper to format time HTTP conform - * @param time - * @return + * + * @param time time in milliseconds since epoch + * @return RFC 1123 formatted date */ public static String makeHTTPDate(long time) { - // For HTTP, GMT == UTC - SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'",Locale.US); - sdf.setTimeZone(TZ_UTC); - return sdf.format(new Date(time)); + return DateTimeFormatter.RFC_1123_DATE_TIME.format(Instant.ofEpochMilli(time).atOffset(ZoneOffset.UTC)); } -// FIXME: For me it returns a parsed time with 2 hours difference, so it seems to parse localtime. WHY? - -// public static Date parseHTTPDate(String date) throws ParseException { -// SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'",Locale.US); -// sdf.setTimeZone(TZ_UTC); -// return sdf.parse(date); -// } - - /** * @return Returns the passed date with the same year/month/day but with the time set to 00:00:00.000 */ public static Date setTimeToZero(final Date date) { - // We need to cut off the hour/minutes/seconds - final GregorianCalendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); - calendar.setTimeInMillis(date.getTime()); // We must not use setTime(date) in case the date is not UTC. - calendar.set(calendar.get(Calendar.YEAR), calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH), 0, 0, 0); - calendar.set(Calendar.MILLISECOND, 0); - return calendar.getTime(); + return Date.from(date.toInstant().truncatedTo(ChronoUnit.DAYS)); } } diff --git a/test/freenet/support/TimeUtilTest.java b/test/freenet/support/TimeUtilTest.java index 733e03fba4..4c6e470605 100644 --- a/test/freenet/support/TimeUtilTest.java +++ b/test/freenet/support/TimeUtilTest.java @@ -218,10 +218,14 @@ public void testToMillis_empty() { @Test public void testToMillis_unknownFormat() { - try { - TimeUtil.toMillis("15250284452w3q7h12m55.807s"); - } catch (NumberFormatException e) { - assertNotNull(e); - } + assertThrows(NumberFormatException.class, () -> TimeUtil.toMillis("15250284452w3q7h12m55.807s")); + } + + @Test + public void testToMillis_fractionalMillis() { + assertEquals(100, TimeUtil.toMillis("0.1s")); + assertEquals(10, TimeUtil.toMillis("0.01s")); + assertEquals(1, TimeUtil.toMillis("0.001s")); + assertEquals(0, TimeUtil.toMillis("0.0001s")); } }