blob: 7b996bdac19f170f6ddaab90d133900ff5af3ee8 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.brooklyn.util.time;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import org.apache.brooklyn.util.collections.MutableList;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.guava.Maybe;
import org.apache.brooklyn.util.text.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Stopwatch;
public class Time {
private static final Logger log = LoggerFactory.getLogger(Time.class);
public static final String DATE_FORMAT_PREFERRED_W_TZ = "yyyy-MM-dd HH:mm:ss.SSS Z";
public static final String DATE_FORMAT_PREFERRED = "yyyy-MM-dd HH:mm:ss.SSS";
public static final String DATE_FORMAT_STAMP = "yyyyMMdd-HHmmssSSS";
public static final String DATE_FORMAT_SIMPLE_STAMP = "yyyy-MM-dd-HHmm";
public static final String DATE_FORMAT_OF_DATE_TOSTRING = "EEE MMM dd HH:mm:ss zzz yyyy";
public static final String DATE_FORMAT_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
public static final String DATE_FORMAT_ISO8601_NO_MILLIS = "yyyy-MM-dd'T'HH:mm:ssZ";
public static final long MILLIS_IN_SECOND = 1000;
public static final long MILLIS_IN_MINUTE = 60*MILLIS_IN_SECOND;
public static final long MILLIS_IN_HOUR = 60*MILLIS_IN_MINUTE;
public static final long MILLIS_IN_DAY = 24*MILLIS_IN_HOUR;
public static final long MILLIS_IN_YEAR = 365*MILLIS_IN_DAY;
/** GMT/UTC/Z time zone constant */
public static final TimeZone TIME_ZONE_UTC = TimeZone.getTimeZone("");
/** as {@link #makeDateString(Date)} for current date/time */
public static String makeDateString() {
return makeDateString(System.currentTimeMillis());
}
/** as {@link #makeDateString(Date)} for long millis since UTC epoch */
public static String makeDateString(long date) {
return makeDateString(new Date(date), DATE_FORMAT_PREFERRED);
}
/** returns the time in {@value #DATE_FORMAT_PREFERRED} format for the given date;
* this format is numeric big-endian but otherwise optimized for people to read, with spaces and colons and dots;
* time is local to the server and time zone is <i>not</i> included */
public static String makeDateString(Date date) {
return makeDateString(date, DATE_FORMAT_PREFERRED);
}
/** as {@link #makeDateString(Date, String, TimeZone)} for the local time zone */
public static String makeDateString(Date date, @Nullable String format) {
return makeDateString(date, format, null);
}
public static String makeDateStringUtc(Date date, String format) {
return makeDateString(date, format, TimeZone.getTimeZone("UTC"));
}
public static String makeDateStringUtc(Date date) {
return makeDateString(date, TimeZone.getTimeZone("UTC"));
}
public static String makeDateString(Date date, @Nullable TimeZone tz) {
return makeDateString(date, null, null);
}
/** as {@link #makeDateString(Date, String, TimeZone)} for the given time zone; consider {@link TimeZone#getTimeZone(String)} with "GMT" */
public static String makeDateString(Date date, @Nullable String format, @Nullable TimeZone tz) {
SimpleDateFormat fmt = new SimpleDateFormat(format!=null ? format :
tz==null ? DATE_FORMAT_PREFERRED : DATE_FORMAT_PREFERRED_W_TZ);
if (tz!=null) fmt.setTimeZone(tz);
return fmt.format(date);
}
/** as {@link #makeDateString(Date, String)} using {@link #DATE_FORMAT_PREFERRED_W_TZ} */
public static String makeDateString(Calendar date) {
return makeDateString(date.getTime(), DATE_FORMAT_PREFERRED_W_TZ);
}
/** as {@link #makeDateString(Date, String, TimeZone)} for the time zone of the given calendar object */
public static String makeDateString(Calendar date, String format) {
return makeDateString(date.getTime(), format, date.getTimeZone());
}
/** as {@link #makeDateString(Date)} with the given format*/
public static String makeDateString(Instant date, String format, Locale locale, ZoneId zone) {
DateTimeFormatter fmt = DateTimeFormatter.ofPattern(format).withLocale(locale==null ? Locale.ROOT : locale).withZone(zone==null ? ZoneId.of("UTC") : zone);
return fmt.format(date);
}
/** as {@link #makeDateString(Date)} with the simple preferred format (no TZ), dropping trailing zero millis and seconds */
public static String makeDateString(Instant date) {
String result = makeDateString(date, DATE_FORMAT_PREFERRED, null, null);
result = Strings.removeFromEnd(result, ".000");
result = Strings.removeFromEnd(result, ":00");
return result;
}
public static Function<Long, String> toDateString() {
return dateString;
}
private static Function<Long, String> dateString = new Function<Long, String>() {
@Override
@Nullable
public String apply(@Nullable Long input) {
if (input == null) return null;
return Time.makeDateString(input);
}
};
/** returns the current time in {@value #DATE_FORMAT_STAMP} format,
* suitable for machines to read with only numbers and dashes and quite precise (ms) */
public static String makeDateStampString() {
return makeDateStampString(System.currentTimeMillis());
}
/** as {@link #makeDateStampString()} with 'Z' at the end to indicate UTC */
public static String makeDateStampStringZ() {
return makeDateStampString(System.currentTimeMillis())+"Z";
}
/** as {@link #makeDateStampString()}, with millis and possibly seconds removed if 0, with 'Z' at the end to indicate UTC */
public static String makeDateStampStringZ(Instant instant) {
String s = makeDateStampStringZ(instant.toEpochMilli());
if (s.endsWith("000")) {
s = Strings.removeFromEnd(s, "000");
s = Strings.removeFromEnd(s, "00");
}
return s+"Z";
}
/** as {@link #makeDateStampString()}, with millis and possibly seconds removed if 0, with 'Z' at the end to indicate UTC */
public static String makeDateStampStringZ(Date date) {
String s = makeDateStampStringZ(date.getTime());
if (s.endsWith("000")) {
s = Strings.removeFromEnd(s, "000");
s = Strings.removeFromEnd(s, "00");
}
return s+"Z";
}
public static String makeIso8601DateString() {
return replaceZeroZoneWithZ(makeIso8601DateStringLocal(Instant.now()));
}
/** ISO 8601 format for UTC */
public static String makeIso8601DateStringZ(Instant instant) {
return replaceZeroZoneWithZ(makeDateString(instant, DATE_FORMAT_ISO8601, null, null));
}
private static String replaceZeroZoneWithZ(String s) {
if (s==null) return s;
String sz = null;
if (s.endsWith("+0000")) sz = Strings.removeFromEnd(s, "+0000");
else if (s.endsWith("+00:00")) sz = Strings.removeFromEnd(s, "+00:00");
if (sz==null) return s;
return sz+"Z";
}
/** ISO 8601 format for local */
public static String makeIso8601DateStringLocal(Instant instant) {
return makeDateString(instant, DATE_FORMAT_ISO8601, Locale.getDefault(), ZoneId.systemDefault());
}
/** ISO 8601 format for UTC */
public static String makeIso8601DateString(Date date) {
return replaceZeroZoneWithZ(makeDateStringUtc(date, DATE_FORMAT_ISO8601));
}
/** returns the time in {@value #DATE_FORMAT_STAMP} format, given a long (e.g. returned by System.currentTimeMillis);
* cf {@link #makeDateStampString()} */
public static String makeDateStampString(long date) {
return new SimpleDateFormat(DATE_FORMAT_STAMP).format(new Date(date));
}
/** returns the time in {@value #DATE_FORMAT_STAMP} format, given a long (e.g. returned by System.currentTimeMillis), in UTC timezone;
* cf {@link #makeDateStampStringZ()} */
public static String makeDateStampStringZ(long date) {
SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT_STAMP);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat.format(new Date(date));
}
/** returns the current time in {@value #DATE_FORMAT_SIMPLE_STAMP} format,
* suitable for machines to read but easier for humans too,
* like {@link #makeDateStampString()} but not as precise */
public static String makeDateSimpleStampString() {
return makeDateSimpleStampString(System.currentTimeMillis());
}
/** returns the time in {@value #DATE_FORMAT_SIMPLE_STAMP} format, given a long (e.g. returned by System.currentTimeMillis);
* cf {@link #makeDateSimpleStampString()} */
public static String makeDateSimpleStampString(long date) {
return new SimpleDateFormat(DATE_FORMAT_SIMPLE_STAMP).format(new Date(date));
}
public static Function<Long, String> toDateStampString() {
return dateStampString;
}
private static Function<Long, String> dateStampString = new Function<Long, String>() {
@Override
@Nullable
public String apply(@Nullable Long input) {
if (input == null) return null;
return Time.makeDateStampString(input);
}
};
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringExact(long t, TimeUnit unit) {
long nanos = unit.toNanos(t);
return makeTimeStringNanoExact(nanos);
}
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringRounded(long t, TimeUnit unit) {
long nanos = unit.toNanos(t);
return makeTimeStringNanoRounded(nanos);
}
/**
* A nice string representation of the stopwatch's elapsed time; or null if null is passed in.
*/
public static String makeTimeStringRounded(@Nullable Stopwatch timer) {
return (timer == null) ? null : makeTimeStringRounded(timer.elapsed(TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS);
}
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringExact(long t) {
return makeTimeString(t, false);
}
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringRounded(long t) {
return makeTimeString(t, true);
}
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringRoundedSince(long utc) {
return makeTimeString(System.currentTimeMillis() - utc, true);
}
/**
* A nice string representation of the duration; or null if null is passed in.
* @see #makeTimeString(long, boolean)
*/
public static String makeTimeStringExact(@Nullable Duration d) {
return (d == null) ? null : makeTimeStringNanoExact(d.toNanoseconds());
}
/**
* A nice string representation of the duration; or null if null is passed in.
* @see #makeTimeString(long, boolean)
*/
public static String makeTimeStringRounded(@Nullable Duration d) {
return (d == null) ? null : makeTimeStringNanoRounded(d.toNanoseconds());
}
/** given an elapsed time, makes it readable, eg 44d 6h, or 8s 923ms, optionally rounding */
public static String makeTimeString(long t, boolean round) {
return makeTimeStringNano(t*1000000L, round);
}
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringNanoExact(long tn) {
return makeTimeStringNano(tn, false);
}
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringNanoRounded(long tn) {
return makeTimeStringNano(tn, true);
}
/** @see #makeTimeString(long, boolean) */
public static String makeTimeStringNano(long tn, boolean round) {
if (tn<0) return "-"+makeTimeStringNano(-tn, round);
// units don't matter, but since ms is the usual finest granularity let's use it
// (previously was just "0" but that was too ambiguous in contexts like "took 0")
if (tn==0) return "0ms";
long tnm = tn % 1000000;
long t = tn/1000000;
String result = "";
long d = t/MILLIS_IN_DAY; t %= MILLIS_IN_DAY;
long h = t/MILLIS_IN_HOUR; t %= MILLIS_IN_HOUR;
long m = t/MILLIS_IN_MINUTE; t %= MILLIS_IN_MINUTE;
long s = t/MILLIS_IN_SECOND; t %= MILLIS_IN_SECOND;
long ms = t;
int segments = 0;
if (d>0) { result += d+"d "; segments++; }
if (h>0) { result += h+"h "; segments++; }
if (round && segments>=2) return Strings.removeAllFromEnd(result, " ");
if (m>0) { result += m+"m "; segments++; }
if (round && (segments>=2 || d>0)) return Strings.removeAllFromEnd(result, " ");
if (s>0) {
if (ms==0 && tnm==0) {
result += s+"s"; segments++;
return result;
}
if (round && segments>0) {
result += s+"s"; segments++;
return result;
}
if (round && s>10) {
result += toDecimal(s, ms/1000.0, 1)+"s"; segments++;
return result;
}
if (round) {
result += toDecimal(s, ms/1000.0, 2)+"s"; segments++;
return result;
}
result += s+"s ";
}
if (round && segments>0)
return Strings.removeAllFromEnd(result, " ");
if (ms>0) {
if (tnm==0) {
result += ms+"ms"; segments++;
return result;
}
if (round && ms>=100) {
result += toDecimal(ms, tnm/1000000.0, 1)+"ms"; segments++;
return result;
}
if (round && ms>=10) {
result += toDecimal(ms, tnm/1000000.0, 2)+"ms"; segments++;
return result;
}
if (round) {
result += toDecimal(ms, tnm/1000000.0, 3)+"ms"; segments++;
return result;
}
result += ms+"ms ";
}
long us = tnm/1000;
long ns = tnm % 1000;
if (us>0) {
if (ns==0) {
result += us+"us"; segments++;
return result;
}
if (round && us>=100) {
result += toDecimal(us, ns/1000.0, 1)+"us"; segments++;
return result;
}
if (round && us>=10) {
result += toDecimal(us, ns/1000.0, 2)+"us"; segments++;
return result;
}
if (round) {
result += toDecimal(us, ns/1000.0, 3)+"us"; segments++;
return result;
}
result += us+"us ";
}
if (ns>0) result += ns+"ns";
return Strings.removeAllFromEnd(result, " ");
}
public static Function<Long, String> fromLongToTimeStringExact() {
return LONG_TO_TIME_STRING_EXACT;
}
private static final Function<Long, String> LONG_TO_TIME_STRING_EXACT = new FunctionLongToTimeStringExact();
private static final class FunctionLongToTimeStringExact implements Function<Long, String> {
@Override @Nullable
public String apply(@Nullable Long input) {
if (input == null) return null;
return Time.makeTimeStringExact(input);
}
}
/** @deprecated since 0.7.0; kept for persisted state backwards compatibility */
@Deprecated
private static Function<Long, String> timeString = new Function<Long, String>() {
@Override
@Nullable
public String apply(@Nullable Long input) {
if (input == null) return null;
return Time.makeTimeStringExact(input);
}
};
public static Function<Long, String> fromLongToTimeStringRounded() {
return LONG_TO_TIME_STRING_ROUNDED;
}
private static final Function<Long, String> LONG_TO_TIME_STRING_ROUNDED = new FunctionLongToTimeStringRounded();
private static final class FunctionLongToTimeStringRounded implements Function<Long, String> {
@Override @Nullable
public String apply(@Nullable Long input) {
if (input == null) return null;
return Time.makeTimeStringRounded(input);
}
}
/** @deprecated since 0.7.0; kept for persisted state backwards compatibility */
@Deprecated
private static Function<Long, String> timeStringRounded = new Function<Long, String>() {
@Override
@Nullable
public String apply(@Nullable Long input) {
if (input == null) return null;
return Time.makeTimeStringRounded(input);
}
};
public static Function<Duration, String> fromDurationToTimeStringRounded() {
return DURATION_TO_TIME_STRING_ROUNDED;
}
private static final Function<Duration, String> DURATION_TO_TIME_STRING_ROUNDED = new FunctionDurationToTimeStringRounded();
private static final class FunctionDurationToTimeStringRounded implements Function<Duration, String> {
@Override @Nullable
public String apply(@Nullable Duration input) {
if (input == null) return null;
return Time.makeTimeStringRounded(input);
}
}
private static String toDecimal(long intPart, double fracPart, int decimalPrecision) {
long powTen = 1;
for (int i=0; i<decimalPrecision; i++) powTen *= 10;
long fpr = Math.round(fracPart * powTen);
if (fpr==powTen) {
intPart++;
fpr = 0;
}
return intPart + "." + Strings.makePaddedString(""+fpr, decimalPrecision, "0", "");
}
/** sleep which propagates Interrupted as unchecked */
public static void sleep(long millis) {
try {
if (millis > 0) Thread.sleep(millis);
} catch (InterruptedException e) {
throw Exceptions.propagate(e);
}
}
/** as {@link #sleep(long)} */
public static void sleep(Duration duration) {
Time.sleep(duration.toMillisecondsRoundingUp());
}
/**
* Calculates the number of milliseconds past midnight for a given UTC time.
*/
public static long getTimeOfDayFromUtc(long timeUtc) {
GregorianCalendar gregorianCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
gregorianCalendar.setTimeInMillis(timeUtc);
int hour = gregorianCalendar.get(Calendar.HOUR_OF_DAY);
int min = gregorianCalendar.get(Calendar.MINUTE);
int sec = gregorianCalendar.get(Calendar.SECOND);
int millis = gregorianCalendar.get(Calendar.MILLISECOND);
return (((((hour * 60) + min) * 60) + sec) * 1000) + millis;
}
/**
* Calculates the number of milliseconds past epoch for a given UTC time.
*/
public static long getTimeUtc(TimeZone zone, int year, int month, int date, int hourOfDay, int minute, int second, int millis) {
GregorianCalendar time = new GregorianCalendar(zone);
time.set(year, month, date, hourOfDay, minute, second);
time.set(Calendar.MILLISECOND, millis);
return time.getTimeInMillis();
}
public static long roundFromMillis(long millis, TimeUnit units) {
if (units.compareTo(TimeUnit.MILLISECONDS) > 0) {
double result = ((double)millis) / units.toMillis(1);
return Math.round(result);
} else {
return units.convert(millis, TimeUnit.MILLISECONDS);
}
}
public static long roundFromMillis(long millis, long millisPerUnit) {
double result = ((double)millis) / millisPerUnit;
return Math.round(result);
}
/**
* Calculates how long until maxTime has passed since the given startTime.
* However, maxTime==0 is a special case (e.g. could mean wait forever), so the result is guaranteed
* to be only 0 if maxTime was 0; otherwise -1 will be returned.
*/
public static long timeRemaining(long startTime, long maxTime) {
if (maxTime == 0) {
return 0;
}
long result = (startTime+maxTime) - System.currentTimeMillis();
return (result == 0) ? -1 : result;
}
/** Convenience for {@link Duration#parse(String)}. */
public static Duration parseDuration(@Nullable String timeString) {
return Duration.parse(timeString);
}
/**
* As {@link #parseElapsedTimeAsDouble(String)}. Consider using {@link #parseDuration(String)} for a more usable return type.
*
* @throws NumberFormatException if cannot be parsed (or if null)
*/
public static long parseElapsedTime(String timeString) {
return (long) parseElapsedTimeAsDouble(timeString);
}
/**
* Parses a string eg '5s' or '20m 22.123ms', returning the number of milliseconds it represents;
* -1 on blank or never or off or false.
* Assumes unit is millisections if no unit is specified.
* <p>
* Negation is permitted if there is just one unit or if it is at the start with a space.
* Negative values are not permitted elsewhere. So the following are allowed:
*
* * -1m is clear (-60 seconds) -- initial negation with just one unit
* * - 1m 1s = -61 seconds -- ie initial negation _with space_ applies to entire line
*
* But these are not:
*
* * -1m 1s
* * -1m -1s
* * - 1m -1s
* * 1m -1s
*
* @throws NullPointerException if arg is null
* @throws NumberFormatException if cannot be parsed
*/
public static double parseElapsedTimeAsDouble(final String timeString) {
return parseElapsedTimeAsDouble(timeString, timeString, true, true);
}
private static double parseElapsedTimeAsDouble(final String timeStringOrig, String timeString, boolean allowNonUnit, boolean allowNegative) {
if (timeString==null)
throw new NullPointerException("GeneralHelper.parseTimeString cannot parse a null string");
try {
if (allowNonUnit) {
if (timeString.trim().matches(".*[A-Za-z]$")) {
// disable parsing eg 1d as a double
} else {
double d = Double.parseDouble(timeString);
return d;
}
}
} catch (NumberFormatException e) {
log.trace("Unable to parse '%s' as pure number. Trying smart parse.", timeStringOrig, e);
}
boolean isNegative = false;
String multipleUnitsDisallowedBecause = null;
try {
//look for a type marker
timeString = timeString.trim();
int i=0;
if (timeString.startsWith("-")) {
if (allowNegative) {
isNegative = true;
} else {
throw new NumberFormatException("Negation is not permitted on an individual time unit in a duration");
}
timeString = timeString.substring(1);
if (!timeString.startsWith(" ")) {
multipleUnitsDisallowedBecause = "Negation must have a space after it to apply to mulitple units";
} else while (timeString.startsWith(" ")) {
timeString = timeString.substring(1);
}
}
while (i < timeString.length()) {
char c = timeString.charAt(i);
if (c=='.' || Character.isDigit(c)) i++;
else break;
}
String num = timeString.substring(0, i);
timeString = timeString.substring(i).trim();
long multiplier = 0;
if (num.isEmpty()) {
//must be never or something
// TODO does 'never' work?
if (allowNegative && !isNegative && (timeString.equalsIgnoreCase("never") || timeString.equalsIgnoreCase("off") || timeString.equalsIgnoreCase("false"))) {
return -1;
}
throw new NumberFormatException("Invalid duration string '"+timeStringOrig+"'");
}
String s = Strings.getFirstWord(timeString);
timeString = timeString.substring(s.length()).trim();
if (s.equalsIgnoreCase("ms") || s.equalsIgnoreCase("milli") || s.equalsIgnoreCase("millis")
|| s.equalsIgnoreCase("millisec") || s.equalsIgnoreCase("millisecs")
|| s.equalsIgnoreCase("millisecond") || s.equalsIgnoreCase("milliseconds"))
multiplier = 1;
else if (s.equalsIgnoreCase("s") || s.equalsIgnoreCase("sec") || s.equalsIgnoreCase("secs")
|| s.equalsIgnoreCase("second") || s.equalsIgnoreCase("seconds"))
multiplier = 1000;
else if (s.equalsIgnoreCase("m") || s.equalsIgnoreCase("min") || s.equalsIgnoreCase("mins")
|| s.equalsIgnoreCase("minute") || s.equalsIgnoreCase("minutes"))
multiplier = 60*1000;
else if (s.equalsIgnoreCase("h") || s.equalsIgnoreCase("hr") || s.equalsIgnoreCase("hrs")
|| s.equalsIgnoreCase("hour") || s.equalsIgnoreCase("hours"))
multiplier = 60*60*1000;
else if (s.equalsIgnoreCase("d") || s.equalsIgnoreCase("day") || s.equalsIgnoreCase("days"))
multiplier = 24*60*60*1000;
else
throw new NumberFormatException("Unknown unit '"+s+"' in time string '"+timeStringOrig+"'");
double d = Double.parseDouble(num);
double dd = 0;
if (timeString.length()>0) {
if (multipleUnitsDisallowedBecause!=null) {
throw new NumberFormatException(multipleUnitsDisallowedBecause);
}
dd = parseElapsedTimeAsDouble(timeStringOrig, timeString, false, false);
if (dd==-1) {
throw new NumberFormatException("Cannot combine '"+timeString+"' with '"+num+" "+s+"'");
}
}
return (d*multiplier + dd) * (isNegative ? -1 : 1);
} catch (Exception ex) {
if (ex instanceof NumberFormatException) throw (NumberFormatException)ex;
log.trace("Details of parse failure:", ex);
throw new NumberFormatException("Cannot parse time string '"+timeStringOrig+"'");
}
}
public static Calendar newCalendarFromMillisSinceEpochUtc(long timestamp) {
GregorianCalendar cal = new GregorianCalendar();
cal.setTimeInMillis(timestamp);
return cal;
}
public static Calendar newCalendarFromDate(Date date) {
return newCalendarFromMillisSinceEpochUtc(date.getTime());
}
/** As {@link #parseCalendar(String)} but returning a {@link Date},
* (i.e. a record where the time zone has been applied and forgotten). */
public static Date parseDate(@Nullable String input) {
if (input==null) return null;
return parseCalendarMaybe(input).get().getTime();
}
public static Instant parseInstant(@Nullable String input) {
return parseCalendarMaybe(input).get().toInstant();
}
/** Parses dates from string, accepting many formats including ISO-8601 and http://yaml.org/type/timestamp.html,
* e.g. 2015-06-15 16:00:00 +0000.
* <p>
* Millis since epoch (1970) is also supported to represent the epoch (0) or dates in this millenium,
* but to prevent ambiguity of e.g. "20150615", any other dates prior to the year 2001 are not accepted.
* (However if a type Long is supplied, e.g. from a YAML parse, it will always be treated as millis since epoch.)
* <p>
* Other formats including locale-specific variants, e.g. recognising month names,
* are supported but this may vary from platform to platform and may change between versions. */
public static Calendar parseCalendar(@Nullable String input) {
if (input==null) return null;
return parseCalendarMaybe(input).get();
}
/** as {@link #parseCalendar(String)} but returning a {@link Maybe} rather than throwing or returning null */
public static Maybe<Calendar> parseCalendarMaybe(@Nullable String input) {
if (input==null) return Maybe.absent("value is null");
input = input.trim();
Maybe<Calendar> result;
if (input.equalsIgnoreCase("now")) return Maybe.of(Calendar.getInstance());
result = parseCalendarUtc(input);
if (result.isPresent()) return result;
result = parseCalendarSimpleFlexibleFormatParser(input);
if (result.isPresent()) return result;
// return the error from this method
Maybe<Calendar> returnResult = result;
result = parseCalendarFormat(input, new SimpleDateFormat(DATE_FORMAT_OF_DATE_TOSTRING, Locale.ROOT));
if (result.isPresent()) return result;
result = parseCalendarDefaultParse(input);
if (result.isPresent()) return result;
return returnResult;
}
@SuppressWarnings("deprecation")
private static Maybe<Calendar> parseCalendarDefaultParse(String input) {
try {
long ms = Date.parse(input);
if (ms>=new Date(1999, 12, 25).getTime() && ms <= new Date(2200, 1, 2).getTime()) {
// accept default date parse for this century and next
GregorianCalendar c = new GregorianCalendar();
c.setTimeInMillis(ms);
return Maybe.of((Calendar)c);
}
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
}
return Maybe.absent();
}
private static Maybe<Calendar> parseCalendarUtc(String input) {
input = input.trim();
if (input.matches("\\d+")) {
if ("0".equals(input)) {
// accept 0 as epoch UTC
return Maybe.of(newCalendarFromMillisSinceEpochUtc(0));
}
Maybe<Calendar> result = Maybe.of(newCalendarFromMillisSinceEpochUtc(Long.parseLong(input)));
if (result.isPresent()) {
int year = result.get().get(Calendar.YEAR);
if (year >= 2000 && year < 2200) {
// only applicable for dates in this century
return result;
} else {
return Maybe.absent("long is probably not millis since epoch UTC; millis as string is not in acceptable range");
}
}
}
return Maybe.absent("not long millis since epoch UTC");
}
private final static String DIGIT = "\\d";
private final static String LETTER = "\\p{L}";
private final static String COMMON_SEPARATORS = "-\\.";
private final static String TIME_SEPARATOR = COMMON_SEPARATORS+":";
private final static String DATE_SEPARATOR = COMMON_SEPARATORS+"/ ";
private final static String DATE_TIME_ANY_ORDER_GROUP_SEPARATOR = COMMON_SEPARATORS+":/ ";
private final static String DATE_ONLY_WITH_INNER_SEPARATORS =
namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT) +
anyChar(DATE_SEPARATOR) +
namedGroup("month", options(optionally(DIGIT)+DIGIT, anyChar(LETTER)+"+")) +
anyChar(DATE_SEPARATOR) +
namedGroup("day", optionally(DIGIT)+DIGIT);
private final static String DATE_WORDS_2 =
namedGroup("month", anyChar(LETTER)+"+") +
anyChar(DATE_SEPARATOR) +
namedGroup("day", optionally(DIGIT)+DIGIT) +
",?"+anyChar(DATE_SEPARATOR)+"+" +
namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
// we could parse NN-NN-NNNN as DD-MM-YYYY always, but could be confusing for MM-DD-YYYY oriented people, so require month named
private final static String DATE_WORDS_3 =
namedGroup("day", optionally(DIGIT)+DIGIT) +
anyChar(DATE_SEPARATOR) +
namedGroup("month", anyChar(LETTER)+"+") +
",?"+anyChar(DATE_SEPARATOR)+"+" +
namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT);
private final static String DATE_ONLY_NO_SEPARATORS =
namedGroup("year", DIGIT+DIGIT+DIGIT+DIGIT) +
namedGroup("month", DIGIT+DIGIT) +
namedGroup("day", DIGIT+DIGIT);
private final static String MERIDIAN = anyChar("aApP")+optionally(anyChar("mM"));
private final static String TIME_ONLY_WITH_INNER_SEPARATORS =
namedGroup("hours", optionally(DIGIT)+DIGIT)+
optionally(
anyChar(TIME_SEPARATOR)+
namedGroup("mins", DIGIT+DIGIT)+
optionally(
anyChar(TIME_SEPARATOR)+
namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+"))))+
optionally(" *" + namedGroup("meridian", notMatching(LETTER+LETTER+LETTER)+MERIDIAN));
private final static String TIME_ONLY_NO_SEPARATORS =
namedGroup("hours", DIGIT+DIGIT)+
namedGroup("mins", DIGIT+DIGIT)+
optionally(
namedGroup("secs", DIGIT+DIGIT+optionally( optionally("\\.")+DIGIT+"+")))+
namedGroup("meridian", "");
private final static String TZ_CODE =
namedGroup("tzCode",
notMatching(MERIDIAN+options("$", anyChar("^"+LETTER))) + // not AM or PM
anyChar(LETTER)+"+"+anyChar(LETTER+DIGIT+"\\/\\-\\' _")+"*");
private final static String TIME_ZONE_SIGNED_OFFSET =
namedGroup("tz",
options(
namedGroup("tzOffset", options("\\+", "-")+
DIGIT+optionally(DIGIT)+optionally(optionally(":")+DIGIT+DIGIT)),
optionally("\\+")+TZ_CODE));
private final static String TIME_ZONE_OPTIONALLY_SIGNED_OFFSET =
namedGroup("tz",
options(
namedGroup("tzOffset", options("\\+", "-", " ")+
options("0"+DIGIT, "10", "11", "12")+optionally(optionally(":")+DIGIT+DIGIT)),
TZ_CODE));
private static String getDateTimeSeparatorPattern(String extraChars) {
return
options(
" +"+optionally(anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")),
anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars+",")) +
anyChar(DATE_TIME_ANY_ORDER_GROUP_SEPARATOR+extraChars)+"*";
}
@SuppressWarnings("deprecation")
// we have written our own parsing because the alternatives were either too specific or too general
// java and apache and even joda-time are too specific, and would require explosion of patterns to be flexible;
// Natty - https://github.com/joestelmach/natty - is very cool, but it drags in ANTLR,
// it doesn't support dashes between date and time, and
// it encourages relative time which would be awesome but only if we resolved it on read
// (however there is natty code to parseDateNatty in the git history if we did want to use it)
private static Maybe<Calendar> parseCalendarSimpleFlexibleFormatParser(String input) {
input = input.trim();
String[] DATE_PATTERNS = new String[] {
DATE_ONLY_WITH_INNER_SEPARATORS,
DATE_ONLY_NO_SEPARATORS,
DATE_WORDS_2,
DATE_WORDS_3,
};
String[] TIME_PATTERNS = new String[] {
TIME_ONLY_WITH_INNER_SEPARATORS,
TIME_ONLY_NO_SEPARATORS
};
String[] TZ_PATTERNS = new String[] {
// space then time zone with sign (+-) or code is preferred
optionally(getDateTimeSeparatorPattern("")) + " " + TIME_ZONE_SIGNED_OFFSET,
// then no TZ - but declare the named groups
namedGroup("tz", namedGroup("tzOffset", "")+namedGroup("tzCode", "")),
// then any separator then offset with sign
getDateTimeSeparatorPattern("") + TIME_ZONE_SIGNED_OFFSET,
// try parsing with enforced separators before TZ first
// (so e.g. in the case of DATE-0100, the -0100 is the time, not the timezone)
// then relax below (e.g. in the case of DATE-TIME+0100)
// finally match DATE-TIME-1000 as time zone -1000
// or DATE-TIME 1000 as TZ +1000 in case a + was supplied but converted to ' ' by web
// (but be stricter about the format, two or four digits required, and hours <= 12 so as not to confuse with a year)
optionally(getDateTimeSeparatorPattern("")) + TIME_ZONE_OPTIONALLY_SIGNED_OFFSET
};
List<String> basePatterns = MutableList.of();
// patterns with date first
String[] DATE_PATTERNS_UNCLOSED = new String[] {
// separator before time *required* if date had separators
DATE_ONLY_WITH_INNER_SEPARATORS + "("+getDateTimeSeparatorPattern("Tt"),
// separator before time optional if date did not have separators
DATE_ONLY_NO_SEPARATORS + "("+optionally(getDateTimeSeparatorPattern("Tt")),
// separator before time required if date has words
DATE_WORDS_2 + "("+getDateTimeSeparatorPattern("Tt"),
DATE_WORDS_3 + "("+getDateTimeSeparatorPattern("Tt"),
};
for (String tzP: TZ_PATTERNS)
for (String dateP: DATE_PATTERNS_UNCLOSED)
for (String timeP: TIME_PATTERNS)
basePatterns.add(dateP + timeP+")?" + tzP);
// also allow time first, with TZ after, then before
for (String tzP: TZ_PATTERNS)
for (String dateP: DATE_PATTERNS)
for (String timeP: TIME_PATTERNS)
basePatterns.add(timeP + getDateTimeSeparatorPattern("") + dateP + tzP);
// also allow time first, with TZ after, then before
for (String tzP: TZ_PATTERNS)
for (String dateP: DATE_PATTERNS)
for (String timeP: TIME_PATTERNS)
basePatterns.add(timeP + tzP + getDateTimeSeparatorPattern("") + dateP);
Maybe<Matcher> mm = Maybe.absent();
for (String p: basePatterns) {
mm = match(p, input);
if (mm.isPresent()) break;
}
if (mm.isPresent()) {
Matcher m = mm.get();
Calendar result;
String tz = m.group("tz");
int year = Integer.parseInt(m.group("year"));
int day = Integer.parseInt(m.group("day"));
String monthS = m.group("month");
int month;
if (monthS.matches(DIGIT+"+")) {
month = Integer.parseInt(monthS)-1;
} else {
try {
month = new SimpleDateFormat("yyyy-MMM-dd", Locale.ROOT).parse("2015-"+monthS+"-15").getMonth();
} catch (ParseException e) {
return Maybe.absent("Unknown date format '"+input+"': invalid month '"+monthS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
}
}
if (Strings.isNonBlank(tz)) {
TimeZone tzz = null;
String tzCode = m.group("tzCode");
if (Strings.isNonBlank(tzCode)) {
tz = tzCode;
}
if (tz.matches(DIGIT+"+")) {
// stick a plus in front in case it was submitted by a web form and turned into a space
tz = "+"+tz;
} else {
tzz = getTimeZone(tz);
}
if (tzz==null) {
Maybe<Matcher> tmm = match(" ?(?<tzH>(\\+|\\-||)"+DIGIT+optionally(DIGIT)+")"+optionally(optionally(":")+namedGroup("tzM", DIGIT+DIGIT)), tz);
if (tmm.isAbsent()) {
return Maybe.absent("Unknown date format '"+input+"': invalid timezone '"+tz+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
}
Matcher tm = tmm.get();
String tzM = tm.group("tzM");
int offset = (60*Integer.parseInt(tm.group("tzH")) + Integer.parseInt("0"+(tzM!=null ? tzM : "")))*60;
tzz = new SimpleTimeZone(offset*1000, tz);
}
tz = getTimeZoneOffsetString(tzz, year, month, day);
result = new GregorianCalendar(tzz);
} else {
result = new GregorianCalendar();
}
result.clear();
result.set(Calendar.YEAR, year);
result.set(Calendar.MONTH, month);
result.set(Calendar.DAY_OF_MONTH, day);
if (m.group("hours")!=null) {
int hours = Integer.parseInt(m.group("hours"));
String meridian = m.group("meridian");
if (Strings.isNonBlank(meridian) && meridian.toLowerCase().startsWith("p")) {
if (hours>12) {
return Maybe.absent("Unknown date format '"+input+"': can't be "+hours+" PM; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
}
hours += 12;
}
result.set(Calendar.HOUR_OF_DAY, hours);
String minsS = m.group("mins");
if (Strings.isNonBlank(minsS)) {
result.set(Calendar.MINUTE, Integer.parseInt(minsS));
}
String secsS = m.group("secs");
if (Strings.isBlank(secsS)) {
// leave at zero
} else if (secsS.matches(DIGIT+DIGIT+"?")) {
result.set(Calendar.SECOND, Integer.parseInt(secsS));
} else {
double s = Double.parseDouble(secsS);
if (secsS.indexOf('.')>=0) {
// accept
} else if (secsS.length()==5) {
// allow ssSSS with no punctuation
s = s/=1000;
} else {
return Maybe.absent("Unknown date format '"+input+"': invalid seconds '"+secsS+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
}
result.set(Calendar.SECOND, (int)s);
result.set(Calendar.MILLISECOND, (int)Math.round(s*1000) % 1000);
}
}
return Maybe.of(result);
}
return Maybe.absent("Unknown date format '"+input+"'; try http://yaml.org/type/timestamp.html format e.g. 2015-06-15 16:00:00 +0000");
}
public static TimeZone getTimeZone(String code) {
if (code.indexOf('/')==-1) {
if ("Z".equals(code)) return TIME_ZONE_UTC;
if ("UTC".equals(code)) return TIME_ZONE_UTC;
if ("GMT".equals(code)) return TIME_ZONE_UTC;
// get the time zone -- most short codes aren't accepted, so accept (and prefer) certain common codes
if ("EST".equals(code)) return getTimeZone("America/New_York");
if ("EDT".equals(code)) return getTimeZone("America/New_York");
if ("PST".equals(code)) return getTimeZone("America/Los_Angeles");
if ("PDT".equals(code)) return getTimeZone("America/Los_Angeles");
if ("CST".equals(code)) return getTimeZone("America/Chicago");
if ("CDT".equals(code)) return getTimeZone("America/Chicago");
if ("MST".equals(code)) return getTimeZone("America/Denver");
if ("MDT".equals(code)) return getTimeZone("America/Denver");
if ("BST".equals(code)) return getTimeZone("Europe/London"); // otherwise BST is Bangladesh!
if ("CEST".equals(code)) return getTimeZone("Europe/Paris");
// IST falls through to below, where it is treated as India (not Irish); IDT not recognised
}
TimeZone tz = TimeZone.getTimeZone(code);
if (tz!=null && !tz.equals(TimeZone.getTimeZone("GMT"))) {
// recognized
return tz;
}
// possibly unrecognized -- GMT returned if not known, bad TimeZone API!
String timeZones[] = TimeZone.getAvailableIDs();
for (String tzs: timeZones) {
if (tzs.equals(code)) return tz;
}
// definitely unrecognized
return null;
}
/** convert a TimeZone e.g. Europe/London to an offset string as at the given day, e.g. +0100 or +0000 depending daylight savings,
* absent with nice error if zone unknown */
public static Maybe<String> getTimeZoneOffsetString(String tz, int year, int month, int day) {
TimeZone tzz = getTimeZone(tz);
if (tzz==null) return Maybe.absent("Unknown time zone code: "+tz);
return Maybe.of(getTimeZoneOffsetString(tzz, year, month, day));
}
/** as {@link #getTimeZoneOffsetString(String, int, int, int)} where the {@link TimeZone} is already instantiated */
@SuppressWarnings("deprecation")
public static String getTimeZoneOffsetString(TimeZone tz, int year, int month, int day) {
int tzMins = tz.getOffset(new Date(year, month, day).getTime())/60/1000;
String tzStr = (tzMins<0 ? "-" : "+") + Strings.makePaddedString(""+(Math.abs(tzMins)/60), 2, "0", "")+Strings.makePaddedString(""+(Math.abs(tzMins)%60), 2, "0", "");
return tzStr;
}
private static String namedGroup(String name, String pattern) {
return "(?<"+name+">"+pattern+")";
}
private static String anyChar(String charSet) {
return "["+charSet+"]";
}
private static String optionally(String pattern) {
return "("+pattern+")?";
}
private static String options(String ...patterns) {
return "("+Strings.join(patterns,"|")+")";
}
private static String notMatching(String pattern) {
return "(?!"+pattern+")";
}
private static Maybe<Matcher> match(String pattern, String input) {
Matcher m = Pattern.compile("^"+pattern+"$").matcher(input);
if (m.find()) return Maybe.of(m);
return Maybe.absent();
}
public static Maybe<Calendar> parseCalendarFormat(String dateString, String format) {
return parseCalendarFormat(dateString, new SimpleDateFormat(format, Locale.ROOT));
}
public static Maybe<Calendar> parseCalendarFormat(String dateString, DateFormat format) {
if (dateString == null) {
return Maybe.absent(() -> new NumberFormatException("GeneralHelper.parseDateString cannot parse a null string"));
}
try {
Preconditions.checkNotNull(format, "date format");
dateString = dateString.trim();
ParsePosition p = new ParsePosition(0);
Date result = format.parse(dateString, p);
if (result != null) {
// accept results even if the entire thing wasn't parsed, as enough was to match the requested format
return Maybe.of(newCalendarFromDate(result));
}
if (log.isTraceEnabled())
log.trace("Could not parse date " + dateString + " using format " + format + ": " + p);
return Maybe.absent();
} catch (Exception e) {
if (log.isTraceEnabled()) e = new IllegalArgumentException("Could not parse date " + dateString + " using format " + format, e);
return Maybe.absent(e);
}
}
/** removes milliseconds from the date object; needed if serializing to ISO-8601 format
* and want to serialize back and get the same data */
public static Date dropMilliseconds(Date date) {
return date==null ? null : date.getTime()%1000!=0 ? new Date(date.getTime() - (date.getTime()%1000)) : date;
}
/** returns the duration elapsed since the given timestamp (UTC) */
public static Duration elapsedSince(long timestamp) {
return Duration.millis(System.currentTimeMillis() - timestamp);
}
/** true iff it has been longer than the given duration since the given timestamp */
public static boolean hasElapsedSince(long timestamp, Duration duration) {
return elapsedSince(timestamp).compareTo(duration) > 0;
}
/** more readable and shorter convenience for System.currentTimeMillis() */
public static long now() {
return System.currentTimeMillis();
}
}