org.conqat.lib.commons.date.DateTimeUtils Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
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());
}
}
}