All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.lib.commons.date.DateTimeUtils Maven / Gradle / Ivy

There is a newer version: 2024.7.2
Show newest version
package org.conqat.lib.commons.date;

import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoField;
import java.time.temporal.Temporal;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;
import java.util.function.Function;
import java.util.function.UnaryOperator;

import org.conqat.lib.commons.assertion.CCSMAssert;

import com.google.common.annotations.VisibleForTesting;

/**
 * Utility class for working on objects from the Date/Time API (package: {@code java.time})
 * 

* The most prominent feature of this class, is the provision of the {@link Clock}, that should be * used for creating {@link Temporal} instances.
* For this, the utility methods {@link #now()}, {@link #zonedNow()}, {@link #getClock()} and * {@link #withClock(Function)} can be used.
* Tests then may overwrite the clock using {@link #setClock(Clock)} or the various * {@link #setFixedClock(Instant)} method, or by providing a fixed date in format * {@value #NOW_SYSTEM_PROPERTY_PATTERN} as the system property {@value #NOW_SYSTEM_PROPERTY_NAME}. *
* Otherwise the {@link Clock#systemDefaultZone() system default} is used. */ public class DateTimeUtils { /** * Name of the system property, which contains a fixed value ({@link #NOW_SYSTEM_PROPERTY_PATTERN}), * that will be used as time component of the {@link #getClock() configured clock}. * * @apiNote Is package private, to allow access in {@link DateUtils}. */ /* package */ static final String NOW_SYSTEM_PROPERTY_NAME = "org.conqat.lib.commons.date.now"; /** * Pattern, that the system property {@value #NOW_SYSTEM_PROPERTY_NAME} expects. * * @apiNote Is package private, to allow access in {@link DateUtils}. */ /* package */ static final String NOW_SYSTEM_PROPERTY_PATTERN = "yyyyMMddHHmmss"; /** * Internal holder of the {@link Clock} instance. */ private static final ClockProvider CLOCK_PROVIDER = new ClockProvider(); /** * {@link DateTimeFormatter}, which matches the UI format, e.g. "Jan 07 2022 12:27". *

* Required {@link java.time.temporal.TemporalField fields} are: *

    *
  • {@link ChronoField#MONTH_OF_YEAR}
  • *
  • {@link ChronoField#DAY_OF_MONTH}
  • *
  • {@link ChronoField#YEAR_OF_ERA}
  • *
  • {@link ChronoField#HOUR_OF_DAY}
  • *
  • {@link ChronoField#MINUTE_OF_HOUR}
  • *
*/ public static final DateTimeFormatter UI_FORMATTER = createDateTimeFormatter("MMM dd yyyy HH:mm"); /** * Provides the currently configured {@link Clock} instance. *

* Usually, this is the {@link Clock#systemDefaultZone() system default}, but may be configured * otherwise in tests either by calling the {@link #setClock(Clock)}/{@link #setFixedClock(Instant)} * method, or by providing a fixed date in format {@value #NOW_SYSTEM_PROPERTY_PATTERN} as the * system property {@value #NOW_SYSTEM_PROPERTY_NAME}. */ public static Clock getClock() { return CLOCK_PROVIDER.getClock(); } /** * Provides the currently configured {@link ZoneId} instance, taken from the {@link #getClock() * configured clock}. */ public static ZoneId getZone() { return getClock().getZone(); } /** * Obtains the current {@link Instant} from the {@link #getClock() configured clock}. *

* Using this method allows the use of an alternate clock for testing. * * @see #getClock() * @see Instant#now(Clock) */ public static Instant now() { return withClock(Clock::instant); } /** * Gets the current millisecond instant of the {@link #getClock() configured clock}. *

* This returns the millisecond-based instant, measured from 1970-01-01T00:00Z (UTC). This is * equivalent to the definition of {@link System#currentTimeMillis()}. *

* Using this method allows the use of an alternate clock for testing. *

* Most applications should avoid this method and use {@link Instant} to represent an instant on the * time-line rather than a raw millisecond value. This method is provided to allow the use of the * clock in high performance use cases where the creation of an object would be unacceptable. * * @see #now() * @see Clock#millis() */ public static long millisNow() { return getClock().millis(); } /** * Obtains the current {@link ZonedDateTime} from the {@link #getClock() configured clock}. *

* Using this method allows the use of an alternate clock for testing. * * @see #getClock() * @see ZonedDateTime#now(Clock) */ public static ZonedDateTime zonedNow() { return withClock(ZonedDateTime::now); } /** * Obtains the current Temporal {@code T} using the {@link #getClock() configured clock}. *

* An example usage may be: {@code DateTimeUtils.withClock(OffsetDateTime::now)} * * @see #getClock() */ public static T withClock(Function creator) { return creator.apply(getClock()); } /** * Converts the provided {@link Instant} into the {@link #getZone() configured zone}. */ public static ZonedDateTime atZone(Instant instant) { return instant.atZone(getZone()); } /** * Converts the provided {@code timestamp} into the {@link #getZone() configured zone}. */ public static ZonedDateTime atZone(long timestamp) { return atZone(Instant.ofEpochMilli(timestamp)); } /** * Computes the difference of the provided {@link Instant}s in the provided {@link TimeUnit}. *

* Warning: For {@link TimeUnit#DAYS}, the result will be complete 24-hour days. This may result in * "wrong" values, if the provided instants stem from {@link ZonedDateTime}s with a daylights saving * time gap (i.e. "2022-03-26T15:00:00+01:00[Europe/Berlin]" and * "2022-03-27T15:00:00+02:00[Europe/Berlin]"). */ public static long diff(Instant start, Instant end, TimeUnit timeUnit) { CCSMAssert.isNotNull(start, () -> String.format("Expected \"%s\" to be not null", "start")); CCSMAssert.isNotNull(end, () -> String.format("Expected \"%s\" to be not null", "end")); CCSMAssert.isNotNull(timeUnit, () -> String.format("Expected \"%s\" to be not null", "timeUnit")); long millis = Duration.between(start, end).toMillis(); return timeUnit.convert(millis, TimeUnit.MILLISECONDS); } /** * Creates a {@link DateTimeFormatter} for the provided {@code pattern} and the * {@link Locale#ENGLISH english} locale. * * @see DateTimeFormatter#ofPattern(String) */ public static DateTimeFormatter createDateTimeFormatter(String pattern) { return DateTimeFormatter.ofPattern(pattern, Locale.ENGLISH); } /** * Formats the provided {@code timestamp} in the {@link #getZone() configured zone} in the * {@link #UI_FORMATTER ui format}. */ public static String getUiFormattedDateString(long timestamp) { return formatTimestamp(timestamp, UI_FORMATTER); } /** * Formats the provided {@code timestamp} in the {@link #getZone() configured zone} in the * {@link #UI_FORMATTER ui format} with seconds. */ public static String getUiFormattedDateStringWithSeconds(long timestamp) { return formatTimestamp(timestamp, createDateTimeFormatter("MMM dd yyyy HH:mm:ss")); } /** * Formats the provided {@code timestamp} in the {@link #getZone() configured zone}. */ public static String formatTimestamp(long timestamp, DateTimeFormatter formatter) { CCSMAssert.isNotNull(formatter, () -> String.format("Expected \"%s\" to be not null", "formatter")); return atZone(timestamp).format(formatter); } /** * Allows a manual overwrite of the {@link #getClock() configured clock}. *

* Should only be used within tests, and always {@link #resetClock() reset} afterwards. */ @VisibleForTesting public static void setClock(Clock clock) { CCSMAssert.isNotNull(clock, () -> String.format("Expected \"%s\" to be not null", "clock")); CLOCK_PROVIDER.setClock(clock); } /** * Allows a manual overwrite of the {@link #getZone() configured zone}. *

* Should only be used within tests, and always {@link #resetClock() reset} afterwards. */ @VisibleForTesting public static void setZone(ZoneId zone) { CCSMAssert.isNotNull(zone, () -> String.format("Expected \"%s\" to be not null", "zone")); CLOCK_PROVIDER.updateClock(clock -> clock.withZone(zone)); } /** * Allows a manual overwrite of the {@link #getClock() configured clock}, to a * {@link Clock#fixed(Instant, ZoneId) fixed clock}, keeping the current {@link #getZone() zone}. *

* Should only be used within tests, and always {@link #resetClock() reset} afterwards. */ @VisibleForTesting public static void setFixedClock(Instant fixed) { CCSMAssert.isNotNull(fixed, () -> String.format("Expected \"%s\" to be not null", "fixed")); CLOCK_PROVIDER.updateClock(clock -> Clock.fixed(fixed, clock.getZone())); } /** * Allows a manual overwrite of the {@link #getClock() configured clock}, to a * {@link Clock#fixed(Instant, ZoneId) fixed clock} at start of the local date, keeping the current * {@link #getZone() zone}. *

* Should only be used within tests, and always {@link #resetClock() reset} afterwards. */ @VisibleForTesting public static void setFixedClock(LocalDate date) { setFixedClock(date.atStartOfDay()); } /** * Allows a manual overwrite of the {@link #getClock() configured clock}, to a * {@link Clock#fixed(Instant, ZoneId) fixed clock} at the local date time, keeping the current * {@link #getZone() zone}. *

* Should only be used within tests, and always {@link #resetClock() reset} afterwards. */ @VisibleForTesting public static void setFixedClock(LocalDateTime dateTime) { setFixedClock(dateTime.atZone(getZone()).toInstant()); } /** * Allows a manual overwrite of the {@link #getClock() configured clock}, to a * {@link Clock#fixed(Instant, ZoneId) fixed clock} at the provided date time, also overwriting the * {@link #getZone() zone} to the provided {@link OffsetDateTime#getOffset() offset}. *

* Should only be used within tests, and always {@link #resetClock() reset} afterwards. */ @VisibleForTesting public static void setFixedClock(OffsetDateTime dateTime) { setClock(Clock.fixed(dateTime.toInstant(), dateTime.getOffset())); } /** * Allows a manual overwrite of the {@link #getClock() configured clock}, to a * {@link Clock#fixed(Instant, ZoneId) fixed clock} at the provided date time, also overwriting the * {@link #getZone() zone} to the provided {@link ZonedDateTime#getZone() zone}. *

* Should only be used within tests, and always {@link #resetClock() reset} afterwards. */ @VisibleForTesting public static void setFixedClock(ZonedDateTime dateTime) { setClock(Clock.fixed(dateTime.toInstant(), dateTime.getZone())); } /** * Resets the {@link #getClock() configured clock} back to the initial state. *

* Should only be used within tests. */ @VisibleForTesting public static void resetClock() { CLOCK_PROVIDER.resetClock(); } /** * Holder of the current {@link Clock} instance. * * @see DateTimeUtils#setClock(Clock) * @see DateTimeUtils#resetClock() */ private static class ClockProvider { /** Formatter for the {@link #NOW_SYSTEM_PROPERTY_PATTERN}. */ private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern(NOW_SYSTEM_PROPERTY_PATTERN); /** * Lock for fine granulated read/write of {@link #clock}. */ private final StampedLock lock = new StampedLock(); /** The current {@link Clock} instance. */ private Clock clock; /** * Retrieves the currently {@link #setClock(Clock) configured} {@link Clock}. *

* If nothing is {@link #setClock(Clock) configured}, the clock will be {@link #resolveClock() * resolved}, and the result used as configured instance. * * @see #setClock(Clock) * @see #resetClock() */ public Clock getClock() { long stamp = lock.readLock(); try { while (clock == null) { long tmpStamp = lock.tryConvertToWriteLock(stamp); if (tmpStamp != 0L) { // We can safely write stamp = tmpStamp; clock = resolveClock(); break; } else { // Someone already has the write-lock // Unlock the read-lock, so he can proceed lock.unlockRead(stamp); // Now we want to write ourselves stamp = lock.writeLock(); } } return clock; } finally { lock.unlock(stamp); } } /** * Sets the {@link #getClock() configured clock} to the provided value. */ public void setClock(Clock clock) { long stamp = lock.writeLock(); try { this.clock = clock; } finally { lock.unlockWrite(stamp); } } /** * Resets the {@link #getClock() configured clock}, so that it will be {@link #resolveClock() * resolved} again. */ public void resetClock() { setClock(null); } /** * Updates the {@link #getClock() configured clock} with the provided {@code operator}. */ public void updateClock(UnaryOperator operator) { CCSMAssert.isNotNull(operator, () -> String.format("Expected \"%s\" to be not null", "operator")); long stamp = lock.writeLock(); try { // do not use getClock(), as StampedLock is not reentrant Clock tmp = clock; if (tmp == null) { tmp = resolveClock(); } tmp = operator.apply(tmp); clock = tmp; } finally { lock.unlockWrite(stamp); } } /** * Resolves the {@link Clock} to use. *

* Will try to look up a fixed date-time value using the system property * {@value #NOW_SYSTEM_PROPERTY_NAME} in the format {@value #NOW_SYSTEM_PROPERTY_PATTERN}. If no * property is set, the {@link Clock#systemDefaultZone() system default} will be used. */ private static Clock resolveClock() { String nowProperty = System.getProperty(NOW_SYSTEM_PROPERTY_NAME); if (nowProperty == null) { return Clock.systemDefaultZone(); } LocalDateTime dateTime = LocalDateTime.parse(nowProperty, FORMATTER); return Clock.fixed(dateTime.atZone(ZoneId.systemDefault()).toInstant(), ZoneId.systemDefault()); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy