GoogleTypesUtil.java
package net.morimekta.proto.utils;
import com.google.protobuf.Timestamp;
import net.morimekta.strings.Displayable;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static java.lang.Integer.parseInt;
import static java.lang.Long.parseLong;
import static java.time.Duration.ofDays;
import static java.time.Duration.ofHours;
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static java.time.Duration.ofNanos;
import static java.time.Duration.ofSeconds;
import static java.util.Objects.requireNonNull;
import static java.util.concurrent.TimeUnit.SECONDS;
import static net.morimekta.proto.utils.ValueUtil.asString;
import static net.morimekta.proto.utils.ValueUtil.isNullOrDefault;
/**
* Utility for converting between core google types and the
* most used java equivalents.
* <p>
* {@link Timestamp} are converted with {@link Instant}, which is functionally
* equivalent. It is always assumed the Timestamp is in UTC, so no zone conversion
* will be done.
* <p>
* {@link com.google.protobuf.Duration} are converted with {@link Duration}, which
* is functionally equivalent.
*/
public final class GoogleTypesUtil {
// ---- Timestamp ----
/**
* @param ts A google type Timestamp or null.
* @return The java timestamp of milliseconds since epoch.
*/
public static long toEpochMillis(Timestamp ts) {
return toInstant(ts).toEpochMilli();
}
/**
* @param ts A google type Timestamp or null.
* @return The unix timestamp of seconds since epoch.
*/
public static long toEpochSeconds(Timestamp ts) {
if (ts == null) {
return 0;
}
return ts.getSeconds();
}
/**
* @param ts A google type timestamp or null.
* @return The instant of the timestamp, or null if null or default input.
*/
public static Instant toInstantOrNull(Timestamp ts) {
if (isNullOrDefault(ts)) {
return null;
}
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos());
}
/**
* @param ts A google type timestamp or null.
* @return The instant of the timestamp, or epoch if null or default input.
*/
public static Instant toInstant(Timestamp ts) {
if (ts == null) {
return Instant.EPOCH;
}
return Instant.ofEpochSecond(ts.getSeconds(), ts.getNanos());
}
/**
* @param instant A java instant.
* @return The google type Timestamp, or default value if null input.
*/
public static Timestamp toProtoTimestamp(Instant instant) {
if (instant == null) {
return Timestamp.getDefaultInstance();
}
return Timestamp.newBuilder()
.setSeconds(instant.getEpochSecond())
.setNanos(instant.getNano())
.build();
}
/**
* Make a proto timestamp from a time value.
*
* @param ts The timestamp.
* @param unit The unit of the timestamp.
* @return The google type Timestamp.
*/
public static Timestamp makeProtoTimestamp(long ts, TimeUnit unit) {
requireNonNull(unit, "unit == null");
if (ts == 0) {
return Timestamp.getDefaultInstance();
}
var seconds = unit.toSeconds(ts);
var remainder = ts - unit.convert(seconds, SECONDS);
var nanos = (int) unit.toNanos(remainder);
return Timestamp.newBuilder()
.setSeconds(seconds)
.setNanos(nanos)
.build();
}
/**
* @param ts A google type Timestamp.
* @param resolution Time resolution to truncate timestamp to.
* @return The timestamp truncated to given unit.
*/
public static Timestamp truncateTimestamp(Timestamp ts, ChronoUnit resolution) {
return toProtoTimestamp(toInstant(ts).truncatedTo(resolution));
}
/**
* @param ts A google type Timestamp.
* @return The ISO date time string of the instant for the timestamp.
*/
public static String displayableTimestamp(Timestamp ts) {
return Displayable.displayableInstant(toInstantOrNull(ts));
}
/**
* @param value ISO formatted timestamp string.
* @return Parsed timestamp as google timestamp.
*/
public static Timestamp parseTimestampString(String value) {
return toProtoTimestamp(parseJavaTimestampString(value));
}
/**
* @param value ISO formatted timestamp string.
* @return Parsed timestamp as java instant.
*/
public static Instant parseJavaTimestampString(String value) {
Matcher isoDateTime = ISO_DATE_TIME.matcher(value);
if (!isoDateTime.matches()) {
throw new IllegalArgumentException("Expected ISO date format, but found: " + asString(value));
}
var year = parseInt(isoDateTime.group("year"));
var month = parseInt(isoDateTime.group("month"));
var day = parseInt(isoDateTime.group("day"));
if (isoDateTime.group("hour") == null) {
return LocalDate.of(year, month, day).atStartOfDay().toInstant(ZoneOffset.UTC);
}
var hour = parseInt(isoDateTime.group("hour"));
var minute = parseInt(isoDateTime.group("minute"));
var second = parseInt(isoDateTime.group("second"));
var dateTime = LocalDateTime.of(year, month, day, hour, minute, second);
var fractionsStr = isoDateTime.group("fractionSeconds");
if (fractionsStr != null) {
var fractionsInt = parseInt(fractionsStr);
if (fractionsInt > 0) {
for (int i = fractionsStr.length(); i < 9; ++i) {
fractionsInt *= 10;
}
dateTime = dateTime.plus(fractionsInt, ChronoUnit.NANOS);
}
}
return dateTime.toInstant(ZoneOffset.UTC);
}
// ---- Duration ----
/**
* @param protoDuration A proto duration instance or null.
* @return A java duration instance.
*/
public static Duration toJavaDuration(com.google.protobuf.Duration protoDuration) {
if (protoDuration == null) {
return Duration.ZERO;
}
return ofSeconds(protoDuration.getSeconds(), protoDuration.getNanos());
}
/**
* @param protoDuration A proto duration instance or null.
* @return A java duration instant, or null if input was null or default instance.
*/
public static Duration toJavaDurationOrNull(com.google.protobuf.Duration protoDuration) {
if (protoDuration == null || protoDuration.equals(com.google.protobuf.Duration.getDefaultInstance())) {
return null;
}
return ofSeconds(protoDuration.getSeconds(), protoDuration.getNanos());
}
/**
* @param duration A java duration or null.
* @return Proto duration matching input or default if null input.
*/
public static com.google.protobuf.Duration toProtoDuration(Duration duration) {
if (duration == null || duration.isZero()) {
return com.google.protobuf.Duration.getDefaultInstance();
}
return com.google.protobuf.Duration.newBuilder()
.setSeconds(duration.getSeconds())
.setNanos(duration.getNano())
.build();
}
/**
* @param duration Duration units.
* @param unit Duration unit.
* @return The combined duration for the input.
*/
public static com.google.protobuf.Duration makeProtoDuration(long duration, TimeUnit unit) {
var sign = duration < 0 ? -1 : 1;
var seconds = unit.toSeconds(Math.abs(duration));
var remainder = Math.abs(duration) - unit.convert(seconds, SECONDS);
var nanos = (int) unit.toNanos(remainder);
if (sign < 0 && nanos > 0) {
++seconds;
nanos = NANOS_IN_SECOND - nanos;
}
return com.google.protobuf.Duration.newBuilder()
.setSeconds(sign * seconds)
.setNanos(nanos)
.build();
}
/**
* @param value A simple duration string.
* @return Parsed proto duration from the string value.
*/
public static com.google.protobuf.Duration parseDurationString(String value) {
return toProtoDuration(parseJavaDurationString(value));
}
/**
* @param value A simple duration string.
* @return Parsed java duration from the string value.
*/
public static Duration parseJavaDurationString(String value) {
Matcher duration = DURATION_COMPLEX.matcher(value);
if (duration.matches()) {
var out = Duration.ZERO;
out = updatedDuration(out, duration.group("weeks"), s -> ofDays(7L * parseInt(s)));
out = updatedDuration(out, duration.group("days"), s -> ofDays(parseInt(s)));
out = updatedDuration(out, duration.group("hours"), s -> ofHours(parseInt(s)));
out = updatedDuration(out, duration.group("minutes"), s -> ofMinutes(parseInt(s)));
out = updatedDuration(out, duration.group("seconds"), s -> {
var parts = s.split("\\.");
if (parts.length == 2) {
var nanos = parts[1] + "0".repeat(9 - parts[1].length());
return ofSeconds(parseLong(parts[0]), parseInt(nanos));
}
return ofSeconds(parseLong(s));
});
out = updatedDuration(out, duration.group("millis"), s -> ofMillis(parseInt(s)));
out = updatedDuration(out, duration.group("nanos"), s -> ofNanos(parseInt(s)));
if (duration.group("minus") != null) {
return out.negated();
}
return out;
}
throw new IllegalArgumentException("Expected duration string, but found: " + asString(value));
}
/**
* Make a simple duration string from a proto duration instance.
*
* @param duration The duration instance.
* @return The simple duration string.
*/
public static String simpleDurationString(com.google.protobuf.Duration duration) {
if (duration == null) {
return null;
}
return simpleDurationString(toJavaDuration(duration));
}
/**
* Make a simple duration string from a proto duration instance.
*
* @param duration The duration instance.
* @return The simple duration string.
*/
public static String simpleDurationString(Duration duration) {
if (duration == null) {
return null;
}
if (duration.isNegative()) {
return "-" + simpleDurationString(duration.abs());
} else {
if (duration.getNano() != 0) {
var nanos = String.format("%09d", duration.getNano());
var i = nanos.length();
while (i > 0 && nanos.charAt(i - 1) == '0') {
--i;
}
return duration.getSeconds() + "." + nanos.substring(0, i) + "s";
}
return duration.getSeconds() + "s";
}
}
/**
* @param duration A proto duration.
* @return A full duration string from the proto duration.
*/
public static String displayableDuration(com.google.protobuf.Duration duration) {
return Displayable.displayableDuration(toJavaDurationOrNull(duration));
}
// --- Private ---
private static Duration updatedDuration(Duration orig, String amount, Function<String, Duration> parser) {
if (amount == null) {
return orig;
} else {
return orig.plus(parser.apply(amount));
}
}
private static final Pattern DURATION_COMPLEX = Pattern.compile(
"(?<minus>-)?" +
"(?:(?<weeks>\\d+) ?(?:w|weeks?) ?)?" +
"(?:(?<days>\\d+) ?(?:d|days?) ?)?" +
"(?:(?<hours>\\d+) ?(?:h|hours?) ?)?" +
"(?:(?<minutes>\\d+) ?(?:m|min|minutes?) ?)?" +
"(?:(?<seconds>\\d+(?:\\.\\d{1,9})?) ?(?:s|sec|seconds?) ?)?" +
"(?:(?<millis>\\d+) ?(?:ms|millis?) ?)?" +
"(?:(?<nanos>\\d+) ?(?:ns|nanos?))?");
private static final Pattern ISO_DATE_TIME = Pattern.compile(
"^(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})" +
"(?:[ tT](?<hour>\\d{2}):(?<minute>\\d{2}):(?<second>\\d{2})" +
"(?:\\.(?<fractionSeconds>\\d{1,9}))?" +
"(?: ?(Z|UTC|[-+]00:?00))?)?$");
private static final int NANOS_IN_SECOND = (int) SECONDS.toNanos(1);
private GoogleTypesUtil() {
}
}