Java tutorial
/* This file is Copyright (c) 2005 Robert Simons (CoHortSoftware@gmail.com). * See the MIT/X-like license in LICENSE.txt. * For more information visit www.cohort.com or contact CoHortSoftware@gmail.com. */ package com.cohort.util; import com.cohort.array.DoubleArray; import com.cohort.array.StringArray; import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.TimeZone; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; /** * This class has static methods for dealing with dates and times. * * <p><b>newGCalendar only accounts for daylight savings if * your computer is correctly * set up.</b> E.g., in Windows, make sure "Start : Control Panel : * Date and Time : Time Zone : Automatically adjust clock for daylight * savings changes" is checked. Otherwise, the TimeZone used by GregorianCalendar * will be for standard time (not including daylight savings time, if any). * * <p>Comments about working with Java's GregorianCalendar class: * <ul> * <li>GregorianCalendar holds millis since Jan 1, 1970 and a timeZone * which influences the values that get/set deal with. * <li>Using a simpleDateFormat to parse a string to a Gregorian Calendar: * the simpleDateFormat has a timeZone which specified where the * the strings value is from (e.g., 2005-10-31T15:12:10 in PST). * When parsed, it is then interpreted by the GregorianCalendar's timeZone * (e.g., it was 3pm PST but now I'll treat it as 6pm EST). * <li>Similarly, using a simpleDateFormat to format a Gregorian Calendar * to a String: * the simpleDateFormat has a timeZone which specified where the * the strings value will be for (e.g., 6pm EST will be formatted as * 5pm Central). * </ul> * * <p>But this class seeks to simplify things to the more common cases * of parsing and formatting using the same time zone as the GregorianCalendar * class, and offering GregorianCalendar constructors for Local (with * daylight savings if that is what your area does) and Zulu (aka GMT and UTC, * which doesn't ever use daylight savings). * * <p>A summary of ISO 8601 Date Time formats is at * http://www.cl.cam.ac.uk/~mgk25/iso-time.html * https://en.wikipedia.org/wiki/ISO_8601 * and http://dotat.at/tmp/ISO_8601-2004_E.pdf * (was http://www.iso.org/iso/date_and_time_format) * and years B.C at http://www.tondering.dk/claus/cal/node4.html#SECTION00450000000000000000 * * <p>Calendar2 does not use ERA designations. It uses negative year values for B.C years * (calendar2Year = 1 - BCYear). Note that BCYears are 1..., so 1 BC is calendar2Year 0 (or 0000), * and 2 BC is calendar2Year -1 (or -0001). * */ public class Calendar2 { //useful static variables public final static int ERA = Calendar.ERA; public final static int BC = GregorianCalendar.BC; public final static int YEAR = Calendar.YEAR; public final static int MONTH = Calendar.MONTH; //java counts 0.. public final static int DATE = Calendar.DATE; //1.. of month public final static int DAY_OF_YEAR = Calendar.DAY_OF_YEAR; //1.. public final static int HOUR = Calendar.HOUR; //0..11 //rarely used public final static int HOUR_OF_DAY = Calendar.HOUR_OF_DAY; //0..23 public final static int MINUTE = Calendar.MINUTE; public final static int SECOND = Calendar.SECOND; public final static int MILLISECOND = Calendar.MILLISECOND; public final static int AM_PM = Calendar.AM_PM; public final static int ZONE_OFFSET = Calendar.ZONE_OFFSET; //millis public final static int DST_OFFSET = Calendar.DST_OFFSET; //millis public final static int MINUTES_PER_DAY = 1440; public final static int MINUTES_PER_7DAYS = 7 * MINUTES_PER_DAY; //10080 public final static int MINUTES_PER_30DAYS = 30 * MINUTES_PER_DAY; //43200 public final static int SECONDS_PER_MINUTE = 60; public final static int SECONDS_PER_HOUR = 60 * 60; //3600 public final static int SECONDS_PER_DAY = 24 * 60 * 60; //86400 31Days=2678400 365days=31536000 public final static long MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * 1000L; public final static long MILLIS_PER_HOUR = SECONDS_PER_HOUR * 1000L; public final static long MILLIS_PER_DAY = SECONDS_PER_DAY * 1000L; public final static String SECONDS_SINCE_1970 = "seconds since 1970-01-01T00:00:00Z"; public final static TimeZone zuluTimeZone = TimeZone.getTimeZone("Zulu"); private final static String[] MONTH_3 = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; private final static String[] MONTH_FULL = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" }; private final static String[] DAY_OF_WEEK_3 = { //corresponding to DAY_OF_WEEK values "", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }; private final static String[] DAY_OF_WEEK_FULL = { "", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" }; /** special Formats for ISO date time without a suffix (assumed to be UTC) */ public final static String ISO8601T_FORMAT = "yyyy-MM-dd'T'HH:mm:ss"; public final static String ISO8601T3_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS"; /** special case format supports suffix 'Z' or +/-HH:MM */ public final static String ISO8601TZ_FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; public final static String ISO8601T3Z_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; /** * This has alternating regex/timeFormat for formats where the first char is a digit. * This is used by suggestDateTimeFormat. */ public final static String digitRegexTimeFormat[] = { //* Compact (number-only) formats only support years 0000 - 4999. // That makes it likely that numbers won't be interpreted as compact date times. //check for julian date before ISO 8601 format "[0-9]{4}-[0-3][0-9]{2}", "yyyy-DDD", "[0-4][0-9]{3}[0-3][0-9]{2}", "yyyyDDD", //compact //variants of space-separated 1970-01-01 00:00:00.000 "[0-9]{4}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9].[0-9]{1,3}[+-][0-9].*", "yyyy-MM-dd HH:mm:ss.sssZ", "[0-9]{4}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9].[0-9]{1,3}", "yyyy-MM-dd HH:mm:ss.sss", "[0-9]{4}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9][+-][0-9].*", "yyyy-MM-dd HH:mm:ssZ", "[0-9]{4}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", "yyyy-MM-dd HH:mm:ss", "[0-9]{4}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]", "yyyy-MM-dd HH:mm", "[0-9]{4}-[0-1][0-9]-[0-3][0-9] [0-2][0-9]", "yyyy-MM-dd HH", //all other variants go to T-separated 1970-01-01T00:00:00.000 (for formatting date times) "[0-9]{4}-[0-1][0-9]-[0-3][0-9].[0-2][0-9]:[0-5][0-9]:[0-5][0-9].[0-9]{1,3}[+-][0-9].*", "yyyy-MM-dd'T'HH:mm:ss.sssZ", "[0-9]{4}-[0-1][0-9]-[0-3][0-9].[0-2][0-9]:[0-5][0-9]:[0-5][0-9].[0-9]{1,3}", "yyyy-MM-dd'T'HH:mm:ss.sss", "[0-9]{4}-[0-1][0-9]-[0-3][0-9].[0-2][0-9]:[0-5][0-9]:[0-5][0-9][+-][0-9].*", "yyyy-MM-dd'T'HH:mm:ssZ", "[0-9]{4}-[0-1][0-9]-[0-3][0-9].[0-2][0-9]:[0-5][0-9]:[0-5][0-9]", "yyyy-MM-dd'T'HH:mm:ss", "[0-9]{4}-[0-1][0-9]-[0-3][0-9].[0-2][0-9]:[0-5][0-9]", "yyyy-MM-dd'T'HH:mm", "[0-9]{4}-[0-1][0-9]-[0-3][0-9].[0-2][0-9]", "yyyy-MM-dd'T'HH", //remaining ISO dates "[0-9]{4}-[0-1][0-9]-[0-3][0-9]", "yyyy-MM-dd", "[0-9]{4}-[0-1][0-9].*", "yyyy-MM", //compact ISO "[0-4][0-9]{3}[0-1][0-9][0-3][0-9][0-2][0-9][0-5][0-9][0-5][0-9]", "yyyyMMddHHmmss", "[0-4][0-9]{3}[0-1][0-9][0-3][0-9][0-2][0-9][0-5][0-9]", "yyyyMMddHHmm", "[0-4][0-9]{3}[0-1][0-9][0-3][0-9][0-2][0-9]", "yyyyMMddHH", "[0-4][0-9]{3}[0-1][0-9][0-3][0-9]", "yyyyMMdd", "[0-4][0-9]{3}[0-1][0-9]", "yyyyMM", //note that yy handles conversion of 2 digit year to 4 digits (e.g., 85 -> 1985) "[0-9]{1,2}/[0-9]{1,2}/[0-9]{2,4}", "M/d/yy", //assume US ordering "[0-9]{1,2} [a-zA-Z]{3} [0-9]{2,4}", "d MMM yy", //2 Jan 85 "[0-9]{1,2}-[a-zA-Z]{3}-[0-9]{2,4}", "d-MMM-yy" //02-JAN-1985 }; /** * This has alternating regex/timeFormat for formats where the first char is a digit. * This is used by suggestDateTimeFormat. */ public final static String letterRegexTimeFormat[] = { //test formats that start with a letter "[a-zA-Z]{3} [0-9]{1,2}, [0-9]{2,4}", "MMM d, yy", //Jan 2, 1985 // "Sun, 06 Nov 1994 08:49:37 GMT" //GMT is literal. Joda doesn't parse z "[a-zA-Z]{3}, [0-9]{2} [a-zA-Z]{3} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} GMT", "EEE, dd MMM yyyy HH:mm:ss 'GMT'", //RFC 822 format date time // "Sun, 06 Nov 1994 08:49:37 -0800" or -08:00 "[a-zA-Z]{3}, [0-9]{2} [a-zA-Z]{3} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} -[0-9]{2}:?[0-9]{2}", "EEE, dd MMM yyyy HH:mm:ss Z" //RFC 822 format date time }; public static HashMap<String, Pattern> dateTimeFormatPatternHM = new HashMap(); static { for (int i = 0; i < digitRegexTimeFormat.length; i += 2) dateTimeFormatPatternHM.put(digitRegexTimeFormat[i + 1], Pattern.compile(digitRegexTimeFormat[i])); for (int i = 0; i < letterRegexTimeFormat.length; i += 2) dateTimeFormatPatternHM.put(letterRegexTimeFormat[i + 1], Pattern.compile(letterRegexTimeFormat[i])); } /** The IDEAL values are used for makeIdealGC. */ public static String IDEAL_N_OPTIONS[] = new String[100]; static { for (int i = 0; i < 100; i++) IDEAL_N_OPTIONS[i] = "" + (i + 1); } public static String IDEAL_UNITS_OPTIONS[] = new String[] { "second(s)", "minute(s)", "hour(s)", "day(s)", "month(s)", "year(s)" }; public static double IDEAL_UNITS_SECONDS[] = new double[] { //where imprecise, these are on the low end 1, 60, SECONDS_PER_HOUR, SECONDS_PER_DAY, 30.0 * SECONDS_PER_DAY, 365.0 * SECONDS_PER_DAY }; public static int IDEAL_UNITS_FIELD[] = new int[] { SECOND, MINUTE, HOUR_OF_DAY, DATE, MONTH, YEAR }; //month is 0.. /** * Set this to true (by calling verbose=true in your program, * not by changing the code here) * if you want lots of diagnostic messages sent to String2.log. */ public static boolean verbose = false; /** * Set this to true (by calling reallyVerbose=true in your program, * not by changing the code here) * if you want lots of diagnostic messages sent to String2.log. */ public static boolean reallyVerbose = false; /** * For diagnostic purposes, this returns the name of one of the fields defined above (or "unknown_field"). * * @param field * @return the name of the field */ public static String fieldName(int field) { if (field == YEAR) return "year"; if (field == MONTH) return "month"; if (field == DATE) return "date"; if (field == DAY_OF_YEAR) return "day_of_year"; if (field == HOUR) return "hour"; if (field == HOUR_OF_DAY) return "hour_of_day"; if (field == MINUTE) return "minute"; if (field == SECOND) return "second"; if (field == MILLISECOND) return "millisecond"; if (field == AM_PM) return "am_pm"; if (field == ZONE_OFFSET) return "zone_offset"; if (field == DST_OFFSET) return "dst_offset"; return "unknown_field"; } /** * This tests if the units are numeric time units (regex="[a-zA-Z]+ +since +[0-9].*"). * This is a good, quick hueristic. For a definitive test, use getTimeBaseAndFactor(String tsUnits). */ public static boolean isNumericTimeUnits(String tUnits) { if (tUnits == null) return false; tUnits = tUnits.toLowerCase(); return tUnits.matches(" *[a-z]+ +since +[0-9].+"); } /** * This tests if the units are * numeric time units (regex="[a-zA-Z]+ +since +[0-9].*") or * or String time units ("yy" or "YY": a formatting string which has the year designator). * * <p>The test for numeric time units is a good, quick hueristic. * For a definitive test, use getTimeBaseAndFactor(String tsUnits). */ public static boolean isTimeUnits(String tUnits) { if (tUnits == null) return false; tUnits = tUnits.toLowerCase(); return tUnits.indexOf("yy") >= 0 || tUnits.matches(" *[a-z]+ +since +[0-9].+"); } /** * This converts a string "[units] since [isoDate]" * (e.g., "minutes since 1985-01-01") into * a baseSeconds (seconds since 1970-01-01) and a factor ("minutes" returns 60). * <br>So simplistically, epochSeconds = storedTime * factor + baseSeconds. * <br>Or simplistically, storedTime = (epochSeconds - baseSeconds) / factor. * * <p>WARNING: don't use the equations above. Use unitsSinceToEpochSeconds or * epochSecondsToUnitsSince which correctly handle special cases. * * @param tsUnits e.g., "minutes since 1985-01-01". * This may include hours, minutes, seconds, decimal, and Z or timezone offset (default=Zulu). * @return double[]{baseSeconds, factorToGetSeconds} * @throws Exception if trouble (tsUnits is null or invalid) */ public static double[] getTimeBaseAndFactor(String tsUnits) throws Exception { String errorInMethod = String2.ERROR + " in Calendar2.getTimeBaseAndFactor(" + tsUnits + "):\n"; Test.ensureNotNull(tsUnits, errorInMethod + "units string is null."); int sincePo = tsUnits.toLowerCase().indexOf(" since "); if (sincePo <= 0) throw new SimpleException(errorInMethod + "units string doesn't contain \" since \"."); double factorToGetSeconds = factorToGetSeconds(tsUnits.substring(0, sincePo)); //it is trimmed GregorianCalendar baseGC = parseISODateTimeZulu(tsUnits.substring(sincePo + 7)); //it is trimmed double baseSeconds = baseGC.getTimeInMillis() / 1000.0; //String2.log(" time unitsString (" + tsUnits + // ") converted to factorToGetSeconds=" + factorToGetSeconds + // " baseSeconds=" + baseSeconds); return new double[] { baseSeconds, factorToGetSeconds }; } /** * This converts a unitsSince value into epochSeconds. * This properly handles 'special' factorToGetSeconds values (for month and year). * * @param baseSeconds * @param factorToGetSeconds * @param unitsSince * @return seconds since 1970-01-01 (or NaN if unitsSince is NaN) */ public static double unitsSinceToEpochSeconds(double baseSeconds, double factorToGetSeconds, double unitsSince) { if (factorToGetSeconds >= 30 * SECONDS_PER_DAY) { //i.e. >= a month //floor yields consistent results below for decimal months int intUnitsSince = Math2.roundToInt(Math.floor(unitsSince)); if (intUnitsSince == Integer.MAX_VALUE) return Double.NaN; int field; if (factorToGetSeconds == 30 * SECONDS_PER_DAY) field = MONTH; else if (factorToGetSeconds == 360 * SECONDS_PER_DAY) field = YEAR; else throw new RuntimeException( String2.ERROR + " in Calendar2.unitsSinceToEpochSeconds: factorToGetSeconds=\"" + factorToGetSeconds + "\" not expected."); GregorianCalendar gc = epochSecondsToGc(baseSeconds); gc.add(field, intUnitsSince); if (unitsSince != intUnitsSince) { double frac = unitsSince - intUnitsSince; //will be positive because floor was used if (field == MONTH) { //Round fractional part to nearest day. Better if based on nDays in current month? //(Note this differs from UDUNITS month = 3.15569259747e7 / 12 seconds.) gc.add(DATE, Math2.roundToInt(frac * 30)); } else if (field == YEAR) { //Round fractional part to nearest month. //(Note this differs from UDUNITS year = 3.15569259747e7 seconds.) gc.add(MONTH, Math2.roundToInt(frac * 12)); } } return gcToEpochSeconds(gc); } return baseSeconds + unitsSince * factorToGetSeconds; } /** * This converts an epochSeconds value into a unitsSince value. * This properly handles 'special' factorToGetSeconds values (for month and year). * * @param baseSeconds * @param factorToGetSeconds * @param epochSeconds * @return seconds since 1970-01-01 (or NaN if epochSeconds is NaN) */ public static double epochSecondsToUnitsSince(double baseSeconds, double factorToGetSeconds, double epochSeconds) { if (factorToGetSeconds >= 30 * SECONDS_PER_DAY) { if (!Math2.isFinite(epochSeconds)) return Double.NaN; GregorianCalendar es = epochSecondsToGc(epochSeconds); GregorianCalendar bs = epochSecondsToGc(baseSeconds); if (factorToGetSeconds == 30 * SECONDS_PER_DAY) { //months (and days) //expand this to support fractional months??? int esm = getYear(es) * 12 + es.get(MONTH); int bsm = getYear(bs) * 12 + bs.get(MONTH); return esm - bsm; } else if (factorToGetSeconds == 360 * SECONDS_PER_DAY) { //years (and months) //expand this to support fractional years??? return getYear(es) - getYear(bs); } else throw new RuntimeException( String2.ERROR + " in Calendar2.epochSecondsToUnitsSince: factorToGetSeconds=\"" + factorToGetSeconds + "\" not expected."); } return (epochSeconds - baseSeconds) / factorToGetSeconds; } /** * This returns the factor to multiply by 'units' data to get seconds * data (e.g., "minutes" returns 60). * This is used for part of dealing with udunits-style "minutes since 1970-01-01"-style * strings. * * @param units * @return the factor to multiply by 'units' data to get seconds data. * Since there is no exact value for months or years, this returns * special values of 30*SECONDS_PER_DAY and 360*SECONDS_PER_DAY, respectively. * @throws Exception if trouble (e.g., units is null or not an expected value) */ public static double factorToGetSeconds(String units) throws Exception { units = units.trim().toLowerCase(); if (units.equals("ms") || units.equals("msec") || units.equals("msecs") || units.equals("millis") || units.equals("millisec") || units.equals("millisecs") || units.equals("millisecond") || units.equals("milliseconds")) return 0.001; if (units.equals("s") || units.equals("sec") || units.equals("secs") || units.equals("second") || units.equals("seconds")) return 1; if (units.equals("m") || units.equals("min") || units.equals("mins") || units.equals("minute") || units.equals("minutes")) return SECONDS_PER_MINUTE; if (units.equals("h") || units.equals("hr") || units.equals("hrs") || units.equals("hour") || units.equals("hours")) return SECONDS_PER_HOUR; if (units.equals("d") || units.equals("day") || units.equals("days")) return SECONDS_PER_DAY; if (units.equals("week") || units.equals("weeks")) return 7 * SECONDS_PER_DAY; if (units.equals("mon") || units.equals("mons") || units.equals("month") || units.equals("months")) return 30 * SECONDS_PER_DAY; if (units.equals("yr") || units.equals("yrs") || units.equals("year") || units.equals("years")) return 360 * SECONDS_PER_DAY; Test.error(String2.ERROR + " in Calendar2.factorToGetSeconds: units=\"" + units + "\" is invalid."); return Double.NaN; //won't happen, but method needs return statement } /** * This converts an ISO Zulu dateTime String to seconds since 1970-01-01T00:00:00Z, * rounded to the nearest milli. * [Before 2012-05-22, millis were removed. Now they are kept.] * In many ways trunc would be better, but doubles are often bruised. * round works symmetrically with + and - numbers. * If any of the end of the dateTime is missing, a trailing portion of * "1970-01-01T00:00:00" is added. * The 'T' connector can be any non-digit. * This may include hours, minutes, seconds, decimal, and Z or timezone offset (default=Zulu). * * @param isoZuluString (to millis precision) * @return seconds * @throws RuntimeException if trouble (e.g., input is null or invalid format) */ public static double isoStringToEpochSeconds(String isoZuluString) { return isoZuluStringToMillis(isoZuluString) / 1000.0; } /** * This is like isoStringToEpochSeconds, but returns NaN if trouble. */ public static double safeIsoStringToEpochSeconds(String isoZuluString) { if (isoZuluString == null || isoZuluString.length() < 4) return Double.NaN; try { return isoZuluStringToMillis(isoZuluString) / 1000.0; } catch (Exception e) { return Double.NaN; } } /** * This converts an EDDTable "now-nUnits" string to epochSeconds. * - can also be + or space. * units can be singular or plural. * * @param nowString * @return epochSeconds (rounded up to the next second) (or Double.NaN if trouble) * @throws SimpleException if trouble */ public static double nowStringToEpochSeconds(String nowString) { //now is next second (ms=0) GregorianCalendar gc = newGCalendarZulu(); gc.add(SECOND, 1); gc.set(MILLISECOND, 0); String tError = "Query error: Invalid \"now\" constraint: \"" + nowString + "\". " + "Timestamp constraints with \"now\" must be in the form " + "\"now[+|-positiveInteger[millis|seconds|minutes|hours|days|months|years]]\" (or singular units)."; if (nowString == null || !nowString.startsWith("now") || nowString.length() == 4) throw new SimpleException(tError); if (nowString.length() == 3) return gcToEpochSeconds(gc); // e.g., now-5hours char ch = nowString.charAt(3); int start = -1; //trouble //non-%encoded '+' will be decoded as ' ', so treat ' ' as equal to '+' if (ch == '+' || ch == ' ') start = 4; else if (ch == '-') start = 3; else throw new SimpleException(tError); //find the end of the number int n = 1; int end = 4; while (nowString.length() > end && String2.isDigit(nowString.charAt(end))) end++; //parse the number n = String2.parseInt(nowString.substring(start, end) + (end == 4 ? "1" : "")); //if no digits if (n == Integer.MAX_VALUE) throw new SimpleException(tError); start = end; //find the units, adjust gc //test sUnits.equals to ensure no junk at end of constraint String sUnits = nowString.substring(start); if (sUnits.equals("milli") || sUnits.equals("millis") || sUnits.equals("millisecond") || sUnits.equals("milliseconds")) gc.add(MILLISECOND, n); else if (sUnits.length() == 0 || //default sUnits.equals("second") || sUnits.equals("seconds")) gc.add(SECOND, n); else if (sUnits.equals("minute") || sUnits.equals("minutes")) gc.add(MINUTE, n); else if (sUnits.equals("hour") || sUnits.equals("hours")) gc.add(HOUR, n); else if (sUnits.equals("day") || sUnits.equals("days")) gc.add(DATE, n); else if (sUnits.equals("month") || sUnits.equals("months")) gc.add(MONTH, n); else if (sUnits.equals("year") || sUnits.equals("years")) gc.add(YEAR, n); else throw new SimpleException(tError); return gcToEpochSeconds(gc); } /** * This is like nowStringToEpochSeconds, but returns troubleValue if trouble. * * @param nowString * @param troubleValue * @return epochSeconds (or troubleValue if trouble) */ public static double safeNowStringToEpochSeconds(String nowString, double troubleValue) { try { return nowStringToEpochSeconds(nowString); } catch (Throwable t) { String2.log(t.toString()); return troubleValue; } } /** * This converts an EDDTable "min(varName)-nUnits" or "max(varName)-nUnits" * string the resulting value. * - can also be + or space. * n is a positive floating point number * If allowTimeUnits, units is optional (default=seconds) and can be singular or plural * and n must be a positive integer. * * @param mmString presumably, the min(varName) or max(varName) part has already * been parsed and dealt with (see the mmValue param) * @param mmValue the variable's min or max value (known because mmString was * already partly parsed). * @param allowTimeUnits specify true if var is a timestamp variable * @return epochSeconds (rounded up to the next second) (or Double.NaN if trouble) * @throws SimpleException if trouble */ public static double parseMinMaxString(String mmString, double mmValue, boolean allowTimeUnits) { if (!mmString.startsWith("min(") && !mmString.startsWith("max(")) throw new SimpleException("Query error: \"min(\" or \"max(\" expected."); String mm = mmString.substring(0, 4); String tError = "Query error: Invalid \"" + mm + ")\" constraint: \"" + mmString + "\". " + (allowTimeUnits ? "T" : "Non-t") + "imestamp constraints with \"" + mm + ")\" must be in the form " + "\"" + mmString.substring(0, 3) + "(varName)[+|-" + (allowTimeUnits ? "positiveInteger[millis|seconds|minutes|hours|days|months|years]]\" (or singular units)." : "positiveNumber]\"."); int start = mmString.indexOf(')'); if (start < 0) throw new SimpleException(tError); if (start == mmString.length() - 1) return mmValue; start++; if (start == mmString.length() - 1) throw new SimpleException(tError); //can't be just min(varName)+ char ch = mmString.charAt(start); //non-%encoded '+' will be decoded as ' ', so treat ' ' as equal to '+' if (ch == '+' || ch == ' ') start++; else if (ch == '-') { } else throw new SimpleException(tError); //parse the number double d = 1; int end = start; while (end < mmString.length() && "0123456789eE+-.".indexOf(mmString.charAt(end)) >= 0) end++; if (end > start) d = String2.parseDouble(mmString.substring(start, end)); if (Double.isNaN(d)) throw new SimpleException(tError); start = end; if (start >= mmString.length()) return mmValue += d; //test sUnits.equals to ensure no junk at end of constraint String sUnits = mmString.substring(start); //it will be something if (allowTimeUnits) { int n = Math2.roundToInt(d); if (n != d) throw new SimpleException(tError); GregorianCalendar gc = epochSecondsToGc(mmValue); if (sUnits.equals("milli") || sUnits.equals("millis") || sUnits.equals("millisecond") || sUnits.equals("milliseconds")) gc.add(MILLISECOND, n); else if (sUnits.equals("second") || sUnits.equals("seconds")) gc.add(SECOND, n); else if (sUnits.equals("minute") || sUnits.equals("minutes")) gc.add(MINUTE, n); else if (sUnits.equals("hour") || sUnits.equals("hours")) gc.add(HOUR, n); else if (sUnits.equals("day") || sUnits.equals("days")) gc.add(DATE, n); else if (sUnits.equals("month") || sUnits.equals("months")) gc.add(MONTH, n); else if (sUnits.equals("year") || sUnits.equals("years")) gc.add(YEAR, n); else throw new SimpleException(tError); mmValue = gcToEpochSeconds(gc); } else { //!allowTimeUnits throw new SimpleException(tError); } return mmValue; } /** * This returns true if the string appears to be an ISO date/time * (matching YYYY-MM...). * * @param s * @return true if the string appears to be an ISO date/time * (matching YYYY-MM...). */ public static boolean isIsoDate(String s) { if (s == null) return false; return s.matches("-?\\d{4}-\\d{2}.*"); } /** * This converts a GregorianCalendar to seconds since 1970-01-01T00:00:00Z. * Note that System.currentTimeMillis/1000 = epochSeconds(zulu). * * @param gc * @return seconds, including fractional seconds (Double.NaN if trouble) * @throws RuntimeException if trouble (e.g., gc is null) */ public static double gcToEpochSeconds(GregorianCalendar gc) { return gc.getTimeInMillis() / 1000.0; } /** * This converts seconds since 1970-01-01T00:00:00Z to a GregorianCalendar * (Zulu timezone). * * @param seconds (including fractional seconds) * @return an iso zulu time-zone GregorianCalendar (rounded to nearest ms) * @throws RuntimeException if trouble (e.g., seconds is NaN) */ public static GregorianCalendar epochSecondsToGc(double seconds) { if (!Math2.isFinite(seconds)) Test.error(String2.ERROR + " in epochSecondsToGc: seconds value is NaN!"); return newGCalendarZulu(Math2.roundToLong(seconds * 1000.0)); } /** * This converts an ISO Zulu dateTime String to hours since 1970-01-01T00:00:00Z, * rounded to the nearest hour. * In many ways trunc would be better, but doubles are often bruised. * round works symmetrically with + and - numbers. * If any of the end of the dateTime is missing, a trailing portion of * "1970-01-01T00:00:00Z" or "1970-01-01T00:00:00-00:00" is added. * The 'T' connector can be any non-digit. * This may include hours, minutes, seconds, decimal, and timezone offset (default=Zulu). * * @param isoZuluString * @return seconds * @throws RuntimeException if trouble (e.g., input is null or invalid format) */ public static int isoStringToEpochHours(String isoZuluString) { long tl = isoZuluStringToMillis(isoZuluString); return Math2.roundToInt(tl / (double) MILLIS_PER_HOUR); } /** * This converts seconds since 1970-01-01T00:00:00Z * to an ISO Zulu dateTime String with 'T'. * The doubles are rounded to the nearest millisecond. * In many ways trunc would be better, but doubles are often bruised. * round works symmetrically with + and - numbers. * * @param seconds with optional fractional part * @return isoZuluString with 'T' (without the trailing Z) * @throws RuntimeException if trouble (e.g., seconds is NaN) */ public static String epochSecondsToIsoStringT(double seconds) { if (!Math2.isFinite(seconds)) Test.error(String2.ERROR + " in epochSecondsToIsoStringT: seconds is NaN!"); return millisToIsoZuluString(Math2.roundToLong(seconds * 1000)); } /** * This is like epochSecondsToIsoStringT, but includes millis. */ public static String epochSecondsToIsoStringT3(double seconds) { if (!Math2.isFinite(seconds)) Test.error(String2.ERROR + " in epochSecondsToIsoStringT3: seconds is NaN!"); return millisToIso3ZuluString(Math2.roundToLong(seconds * 1000)); } /** * This is like epochSecondsToIsoStringT, but returns NaNString if seconds is NaN. */ public static String safeEpochSecondsToIsoStringT(double seconds, String NaNString) { return Math2.isFinite(seconds) ? millisToIsoZuluString(Math2.roundToLong(seconds * 1000)) : NaNString; } /** * This is like epochSecondsToIsoStringT, but add "Z" at end of time, * and returns NaNString if seconds is NaN.. */ public static String safeEpochSecondsToIsoStringTZ(double seconds, String NaNString) { return Math2.isFinite(seconds) ? millisToIsoZuluString(Math2.roundToLong(seconds * 1000)) + "Z" : NaNString; } /** * This is like epochSecondsToIsoStringT3, but returns NaNString if seconds is NaN. */ public static String safeEpochSecondsToIsoStringT3(double seconds, String NaNString) { return Math2.isFinite(seconds) ? millisToIso3ZuluString(Math2.roundToLong(seconds * 1000)) : NaNString; } /** * This is like epochSecondsToIsoStringT3, but add "Z" at end of time, * and returns NaNString if seconds is NaN.. */ public static String safeEpochSecondsToIsoStringT3Z(double seconds, String NaNString) { return Math2.isFinite(seconds) ? millisToIso3ZuluString(Math2.roundToLong(seconds * 1000)) + "Z" : NaNString; } /** * This is like safeEpochSecondsToIsoStringT3Z, but returns a * limited precision string. * * @param time_precision can be "1970", "1970-01", "1970-01-01", "1970-01-01T00Z", * "1970-01-01T00:00Z", "1970-01-01T00:00:00Z" (used if time_precision not matched), * "1970-01-01T00:00:00.0Z", "1970-01-01T00:00:00.00Z", "1970-01-01T00:00:00.000Z". * Or any of those without "Z". But ERDDAP requires any format with hours(min(sec)) to have Z. */ public static String epochSecondsToLimitedIsoStringT(String time_precision, double seconds, String NaNString) { if (!Math2.isFinite(seconds)) return NaNString; //should be floor(?), but round avoids issues with computer precision return limitedFormatAsISODateTimeT(time_precision, newGCalendarZulu(Math2.roundToLong(seconds * 1000))); } /** * This converts seconds since 1970-01-01T00:00:00Z * to an ISO Zulu dateTime String with space. * The doubles are rounded to the nearest milli. * [Before 2012-05-22, millis were removed. Now they are kept.] * In many ways trunc would be better, but doubles are often bruised. * round works symmetrically with + and - numbers. * * @param seconds with optional fractional part * @return isoZuluString with space (without the trailing Z) * @throws RuntimeException if trouble (e.g., seconds is NaN) */ public static String epochSecondsToIsoStringSpace(double seconds) { if (!Math2.isFinite(seconds)) Test.error(String2.ERROR + " in epochSecondsToIsoStringSpace: seconds value is NaN!"); String s = millisToIsoZuluString(Math2.roundToLong(seconds * 1000)); return String2.replaceAll(s, 'T', ' '); } /** * This converts hours since 1970-01-01T00:00:00Z * to an ISO Zulu dateTime String 'T'. * If your hours are doubles, use Math2.roundToInt first. * In many ways trunc would be better, but doubles are often bruised. * round works symmetrically with + and - numbers. * * @param hours * @return isoZuluString 'T' (without the trailing Z). * If hours==Integer.MAX_VALUE, this returns null. * @throws RuntimeException if trouble (e.g., hours is Integer.MAX_VALUE) */ public static String epochHoursToIsoString(int hours) { if (hours == Integer.MAX_VALUE) Test.error(String2.ERROR + " in epochHoursToIsoString: hours value is Integer.MAX_VALUE!"); return millisToIsoZuluString(hours * MILLIS_PER_HOUR); } /** * This returns a 3 character month name (eg. "Jan"). * * @param month 1..12 * @throws RuntimeException if month is out of range */ public static String getMonthName3(int month) { return MONTH_3[month - 1]; } /** * This returns the full month name (e.g., "January"). * * @param month 1..12 * @throws RuntimeException if month is out of range */ public static String getMonthName(int month) { return MONTH_FULL[month - 1]; } /** * This returns a gregorianCalendar object which has the correct * current time * (e.g., wall clock time, for the local time zone, * which includes daylight savings, if applicable) and the local time zone. * * @return a new GregorianCalendar object (local time zone) */ public static GregorianCalendar newGCalendarLocal() { GregorianCalendar gc = new GregorianCalendar(); //TimeZone tz = gc.getTimeZone(); //String2.log("getGCalendar inDaylightTime="+ tz.inDaylightTime(gc.getTime()) + // " useDaylightTime=" + tz.useDaylightTime() + // " timeZone=" + tz); return gc; } /** * Get a GregorianCalendar object with the specified millis time (UTC), * but with the local time zone (when displayed). * * @return the GregorianCalendar object. * @throws RuntimeException if trouble (e.g., millis == Long.MAX_VALUE) */ public static GregorianCalendar newGCalendarLocal(long millis) { if (millis == Long.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarLocal: millis value is Long.MAX_VALUE!"); GregorianCalendar gcL = newGCalendarLocal(); gcL.setTimeInMillis(millis); return gcL; } /** * Get a GregorianCalendar object with the current UTC * (A.K.A., GMT or Zulu) time and a UTC time zone. * You can find the current Zulu/GMT time at: http://www.xav.com/time.cgi * Info about UTC vs GMT vs TAI... see http://www.leapsecond.com/java/gpsclock.htm. * And there was another good site... can't find it. * * @return the GregorianCalendar object for right now (Zulu time zone) */ public static GregorianCalendar newGCalendarZulu() { //GregorianCalendar gc = new GregorianCalendar(); //gc.add(MILLISECOND, -TimeZone.getDefault().getOffset()); //return gc; //* Note that the time zone is still local, but the day and hour are correct for gmt. //* To try to do this correctly leads to Java's timeZone hell hole. //return localToUtc(new GregorianCalendar()); return new GregorianCalendar(zuluTimeZone); } /** * Get a GregorianCalendar object with the specified millis time (UTC) * and a UTC time zone. * * @return the GregorianCalendar object. * @throws RuntimeException if trouble (e.g., millis == Long.MAX_VALUE) */ public static GregorianCalendar newGCalendarZulu(long millis) { if (millis == Long.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarZulu: millis value is Long.MAX_VALUE!"); GregorianCalendar gcZ = newGCalendarZulu(); gcZ.setTimeInMillis(millis); return gcZ; } /** * Given a time in the local time zone, this determines the equivalent * time in Greenwich England. * Note that the time zone is still local, but the day and hour are correct * for gmt. * To try to do this correctly leads to Java's timeZone hell hole. * * @return the same GregorianCalendar object (for convenience) */ // public static GregorianCalendar localToUtc(GregorianCalendar gc) { // gc.add(MILLISECOND, -gc.get(ZONE_OFFSET) - gc.get(DST_OFFSET)); // return gc; // } /** * Given a time in Greenwich, this determines the equivalent local time. * The time zone of the gc should be local (even though it holds the day/hour * of a Greenwich time). * To try to do this correctly leads to Java's timeZone hell hole. * * @return the same GregorianCalendar object (for convenience) */ // public static GregorianCalendar utcToLocal(GregorianCalendar gc) { // gc.add(MILLISECOND, gc.get(ZONE_OFFSET) + gc.get(DST_OFFSET)); // return gc; // } /** * This converts a GregorianCalendar * (where year/month/day/hour/min/sec indicate UTC time, * but time zone may be incorrect) to millis since Jan 1 1970 UTC. * This is at least correct from 1901 to 2099 (every intervening %4=0 year * was indeed a leap year). */ /* public static long utcToMillis(GregorianCalendar gc) { int years = getYear(gc) - 1970; //-.1 ensures that x.5 rounds down int leapYears = Math2.roundToInt((years / 4.0) - .1); int nDays = leapYears * 366 + (years - leapYears) * 365 + gc.get(DAY_OF_YEAR) - 1; //DAY_OF_YEAR is 1.. return ((((nDays * 24 + gc.get(HOUR_OF_DAY)) * 60L + gc.get(MINUTE)) * 60L + gc.get(SECOND)) * 1000L) + gc.get(MILLISECOND); } */ /** * This sets endGC so that it will appear to have the UTC time created by * adding 'millis' to 1970-01-01T00:00:00Z (with no daylight savings influence). * * @param millis some number of millis since 1970-01-01T00:00:00Z * @param endGC captures the result. * It will appear to have the UTC date/time (ignore the time zone). */ /* public static void millisToUTC(long millis, GregorianCalendar endGC) { //This is a clumsy approach. Isn't there a better way? // endGC.setTimeInMillis(millis); // if (endGC.getTimeZone().inDaylightTime(endGC.getTime())) //adjust for daylight savings // endGC.add(MILLISECOND, -endGC.getTimeZone().getDSTSavings()); } */ /** * This converts a GregorianCalendar * (where year/month/day/hour/min/sec indicate UTC time, * but time zone may be incorrect) to seconds (rounded) since Jan 1 1970 UTC. * This is at least correct from 1901 to 2099 (every intervening %4=0 year * was indeed a leap year). */ /* public static int utcToSeconds(GregorianCalendar gc) { return Math2.roundToInt(utcToMillis(gc) / 1000.0); } */ /** * Get a GregorianCalendar object (local time zone) for the specified. * [Currently, it is lenient -- e.g., Dec 32 -> Jan 1 of the next year.] * Information can be retrieved via calendar.get(Calendar.XXXX), * where XXXX is one of the Calendar constants, like DAY_OF_YEAR. * * @param year (e.g., 2005) * @param month (1..12) (this is consciously different than Java's standard) * @param dayOfMonth (1..31) * @return the corresponding GregorianCalendar object (local time zone) * @throws RuntimeException if trouble (e.g., year is Integer.MAX_VALUE) */ public static GregorianCalendar newGCalendarLocal(int year, int month, int dayOfMonth) { if (year == Integer.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarLocal: year value is Integer.MAX_VALUE!"); return new GregorianCalendar(year, month - 1, dayOfMonth); } /** * Get a GregorianCalendar object (Zulu time zone) for the specified time. * [Currently, it is lenient -- e.g., Dec 32 -> Jan 1 of the next year.] * Information can be retrieved via calendar.get(Calendar.XXXX), * where XXXX is one of the Calendar constants, like DAY_OF_YEAR. * * @param year (e.g., 2005) * @param month (1..12) (this is consciously different than Java's standard) * @param dayOfMonth (1..31) * @return the corresponding GregorianCalendar object (Zulu time zone) * @throws RuntimeException if trouble (e.g., year is Integer.MAX_VALUE) */ public static GregorianCalendar newGCalendarZulu(int year, int month, int dayOfMonth) { if (year == Integer.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarZulu: year is Integer.MAX_VALUE!"); return newGCalendarZulu(year, month, dayOfMonth, 0, 0, 0, 0); } /** * Get a GregorianCalendar object (local time zone) for the specified time. * [Currently, it is lenient -- e.g., Dec 32 -> Jan 1 of the next year.] * Information can be retrieved via calendar.get(Calendar.XXXX), * where XXXX is one of the Calendar constants, like DAY_OF_YEAR. * * @param year (e.g., 2005) * @param month (1..12) (this is consciously different than Java's standard) * @param dayOfMonth (1..31) * @param hour (0..23) * @param minute (0..59) * @param second (0..59) * @param millis (0..999) * @return the corresponding GregorianCalendar object (local time zone) * @throws RuntimeException if trouble (e.g., year is Integer.MAX_VALUE) */ public static GregorianCalendar newGCalendarLocal(int year, int month, int dayOfMonth, int hour, int minute, int second, int millis) { if (year == Integer.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarLocal: year value is Integer.MAX_VALUE!"); GregorianCalendar gc = new GregorianCalendar(year, month - 1, dayOfMonth, hour, minute, second); gc.add(MILLISECOND, millis); return gc; } /** * Get a GregorianCalendar object (Zulu time zone) for the specified time. * [Currently, it is lenient -- e.g., Dec 32 -> Jan 1 of the next year.] * Information can be retrieved via calendar.get(Calendar.XXXX), * where XXXX is one of the Calendar constants, like DAY_OF_YEAR. * * @param year (e.g., 2005) * @param month (1..12) (this is consciously different than Java's standard) * @param dayOfMonth (1..31) * @param hour (0..23) * @param minute (0..59) * @param second (0..59) * @param millis (0..999) * @return the corresponding GregorianCalendar object (Zulu time zone) * @throws RuntimeException if trouble (e.g., year is Integer.MAX_VALUE) */ public static GregorianCalendar newGCalendarZulu(int year, int month, int dayOfMonth, int hour, int minute, int second, int millis) { if (year == Integer.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarZulu: year value is Integer.MAX_VALUE!"); GregorianCalendar gc = new GregorianCalendar(zuluTimeZone); gc.clear(); gc.set(year, month - 1, dayOfMonth, hour, minute, second); gc.set(MILLISECOND, millis); gc.get(YEAR); //force recalculations return gc; } /** * Get a GregorianCalendar object (local time zone) for the specified time. * [Currently, it is lenient -- e.g., day 366 -> Jan 1 of the next year.] * Information can be retrieved via calendar.get(Calendar.XXXX), * where XXXX is one of the Calendar constants, like DAY_OF_YEAR. * * @param year (e.g., 2005) * @param dayOfYear (usually 1..365, but 1..366 in leap years) * @return the corresponding GregorianCalendar object (local time zone) * @throws RuntimeException if trouble (e.g., year is Integer.MAX_VALUE) */ public static GregorianCalendar newGCalendarLocal(int year, int dayOfYear) { if (year == Integer.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarLocal: year value is Integer.MAX_VALUE!"); GregorianCalendar gc = new GregorianCalendar(year, 0, 1); gc.set(Calendar.DAY_OF_YEAR, dayOfYear); gc.get(YEAR); //force recalculations return gc; } /** * Get a GregorianCalendar object (Zulu time zone) for the specified time. * [Currently, it is lenient -- e.g., day 366 -> Jan 1 of the next year.] * Information can be retrieved via calendar.get(Calendar.XXXX), * where XXXX is one of the Calendar constants, like DAY_OF_YEAR. * * @param year (e.g., 2005) * @param dayOfYear (usually 1..365, but 1..366 in leap years) * @return the corresponding GregorianCalendar object (Zulu time zone) * @throws RuntimeException if trouble (e.g., year is Integer.MAX_VALUE) */ public static GregorianCalendar newGCalendarZulu(int year, int dayOfYear) { if (year == Integer.MAX_VALUE) Test.error(String2.ERROR + " in newGCalendarLocal: year value is Integer.MAX_VALUE!"); GregorianCalendar gc = newGCalendarZulu(year, 1, 1); gc.set(Calendar.DAY_OF_YEAR, dayOfYear); gc.get(YEAR); //force recalculations return gc; } /** * This returns the year. * For years B.C., this returns Calendar2Year = 1 - BCYear. * Note that BCYears are 1..., so 1 BC is calendar2Year 0, * and 2 BC is calendar2Year -1. * @param gc * @return the year (negative for BC). */ public static int getYear(GregorianCalendar gc) { return gc.get(ERA) == BC ? 1 - gc.get(YEAR) : gc.get(YEAR); } /** * This returns the year as YYYY. * For years B.C., this returns Calendar2Year = 1 - BCYear. * Note that BCYears are 1..., so 1 BC is calendar2Year 0000, * and 2 BC is calendar2Year -0001. * @param gc * @return the year as YYYY (or -YYYY for BC). */ public static String formatAsISOYear(GregorianCalendar gc) { int year = getYear(gc); return (year < 0 ? "-" : "") + String2.zeroPad("" + Math.abs(year), 4); } /** * This returns a ISO-style formatted date string e.g., "2004-01-02" * using its current get() values (not influenced by the format's timeZone). * * @param gc a GregorianCalendar object * @return the date in gc, formatted as (for example) "2004-01-02" * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsISODate(GregorianCalendar gc) { return formatAsISOYear(gc) + "-" + String2.zeroPad("" + (gc.get(MONTH) + 1), 2) + "-" + String2.zeroPad("" + gc.get(DATE), 2); //this method is influenced by the format's timeZone //synchronized (isoDateFormat) { // return isoDateFormat.format(gc.getTime()); //} } /** * This converts a GregorianCalendar object into an * ISO-format dateTime string (with 'T' separator: [-]YYYY-MM-DDTHH:MM:SS) * using its current get() values (not influenced by the format's timeZone). * [was calendarToString] * * @param gc * @return the corresponding dateTime String (without the trailing Z). * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsISODateTimeT(GregorianCalendar gc) { return formatAsISODate(gc) + "T" + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2); //this method is influenced by the format's timeZone //synchronized (isoDateTimeFormat) { // return isoDateTimeFormat.format(gc.getTime()); //} } /** * Like formatAsISODateTimeT, but seconds will have 3 decimal digits. * * @param gc * @return the corresponding dateTime String (without the trailing Z). * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsISODateTimeT3(GregorianCalendar gc) { return formatAsISODate(gc) + "T" + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2) + "." + String2.zeroPad("" + gc.get(MILLISECOND), 3); } /** * This is like formatAsISODateTime, but returns a * limited precision string. * * @param time_precision can be "1970", "1970-01", "1970-01-01", "1970-01-01T00Z", * "1970-01-01T00:00Z", "1970-01-01T00:00:00Z" (used if time_precision is null or not matched), * "1970-01-01T00:00:00.0Z", "1970-01-01T00:00:00.00Z", "1970-01-01T00:00:00.000Z". * Versions without 'Z' are allowed here, but ERDDAP requires hours or finer to have 'Z'. */ public static String limitedFormatAsISODateTimeT(String time_precision, GregorianCalendar gc) { String zString = ""; if (time_precision == null || time_precision.length() == 0) time_precision = "1970-01-01T00:00:00Z"; if (time_precision.charAt(time_precision.length() - 1) == 'Z') { time_precision = time_precision.substring(0, time_precision.length() - 1); zString = "Z"; } //build it //Warning: year may be 5 chars, e.g., -0003 StringBuilder sb = new StringBuilder(formatAsISOYear(gc)); if (time_precision.equals("1970")) { sb.append(zString); return sb.toString(); } sb.append("-" + String2.zeroPad("" + (gc.get(MONTH) + 1), 2)); if (time_precision.equals("1970-01")) { sb.append(zString); return sb.toString(); } sb.append("-" + String2.zeroPad("" + gc.get(DATE), 2)); if (time_precision.equals("1970-01-01")) { sb.append(zString); return sb.toString(); } sb.append("T" + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2)); if (time_precision.equals("1970-01-01T00")) { sb.append(zString); return sb.toString(); } sb.append(":" + String2.zeroPad("" + gc.get(MINUTE), 2)); if (time_precision.equals("1970-01-01T00:00")) { sb.append(zString); return sb.toString(); } sb.append(":" + String2.zeroPad("" + gc.get(SECOND), 2)); if (time_precision.length() == 0 || //-> default time_precision.equals("1970-01-01T00:00:00")) { sb.append(zString); return sb.toString(); } sb.append("." + String2.zeroPad("" + gc.get(MILLISECOND), 3)); if (time_precision.equals("1970-01-01T00:00:00.0")) { sb.setLength(sb.length() - 2); sb.append(zString); return sb.toString(); } if (time_precision.equals("1970-01-01T00:00:00.00")) { sb.setLength(sb.length() - 1); sb.append(zString); return sb.toString(); } if (time_precision.equals("1970-01-01T00:00:00.000")) { sb.append(zString); return sb.toString(); } //default sb.setLength(sb.length() - 4); sb.append('Z'); //default has Z return sb.toString(); } /** * This converts a GregorianCalendar object into an ISO-format * dateTime string (with space separator: [-]YYYY-MM-DD HH:MM:SS) * using its current get() values (not influenced by the format's timeZone). * [was calendarToString] * * @param gc * @return the corresponding dateTime String (without the trailing Z). * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsISODateTimeSpace(GregorianCalendar gc) { return formatAsISODate(gc) + " " + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2); //this method is influenced by the format's timeZone //synchronized (isoDateTimeFormat) { // return isoDateTimeFormat.format(gc.getTime()); //} } /** * This converts a GregorianCalendar object into an ESRI * dateTime string (YYYY/MM/DD HH:MM:SS UTC) * using its current get() values (not influenced by the format's timeZone). * * @param gc * @return the corresponding ESRI dateTime String. * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsEsri(GregorianCalendar gc) { return formatAsISOYear(gc) + "/" + String2.zeroPad("" + (gc.get(MONTH) + 1), 2) + "/" + String2.zeroPad("" + gc.get(DATE), 2) + " " + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2) + " UTC"; } /** * This returns a compact formatted [-]YYYYMMDDHHMMSS string e.g., "20040102030405" * using its current get() values (not influenced by the format's timeZone). * * @param gc a GregorianCalendar object * @return the date in gc, formatted as (for example) "20040102030405". * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsCompactDateTime(GregorianCalendar gc) { return formatAsISOYear(gc) + String2.zeroPad("" + (gc.get(MONTH) + 1), 2) + String2.zeroPad("" + gc.get(DATE), 2) + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + String2.zeroPad("" + gc.get(MINUTE), 2) + String2.zeroPad("" + gc.get(SECOND), 2); //this method is influenced by the format's timeZone //synchronized (CompactDateTimeFormat) { // return CompactDateTimeFormat.format(gc.getTime()); //} } /** * This returns a [-]YYYYDDD string e.g., "2004001" * using its current get() values (not influenced by the format's timeZone). * * @param gc a GregorianCalendar object * @return the date in gc, formatted as (for example) "2004001". * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsYYYYDDD(GregorianCalendar gc) { return formatAsISOYear(gc) + String2.zeroPad("" + gc.get(DAY_OF_YEAR), 3); //this method is influenced by the format's timeZone //synchronized (YYYYDDDFormat) { // return YYYYDDDFormat.format(gc.getTime()); //} } /** * This returns a [-]YYYYMM string e.g., "200401" * using its current get() values (not influenced by the format's timeZone). * * @param gc a GregorianCalendar object * @return the date in gc, formatted as (for example) "200401". * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsYYYYMM(GregorianCalendar gc) { return formatAsISOYear(gc) + String2.zeroPad("" + (gc.get(MONTH) + 1), 2); //this method is influenced by the format's timeZone //synchronized (YYYYMMFormat) { // return YYYYMMFormat.format(gc.getTime()); //} } /** * This returns a DD-Mon-[-]YYYY string e.g., "31-Jul-2004 00:00:00" * using its current get() values (not influenced by the format's timeZone). * Ferret often uses this format. * * @param gc a GregorianCalendar object * @return the date in gc, formatted as (for example) "31-Jul-2004 00:00:00". * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsDDMonYYYY(GregorianCalendar gc) { return String2.zeroPad("" + gc.get(DATE), 2) + "-" + MONTH_3[gc.get(MONTH)] + "-" + //0 based formatAsISOYear(gc) + " " + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2); //this method is influenced by the format's timeZone //synchronized (YYYYMMFormat) { // return YYYYMMFormat.format(gc.getTime()); //} } /** * This returns a US-style slash format date time string * ("1/20/2006 9:00:00 pm"). * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in gc's time zone. * @return gc in the US slash format ("1/20/2006 9:00:00 pm"). * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsUSSlashAmPm(GregorianCalendar gc) { int hour = gc.get(HOUR); //0..11 return (gc.get(MONTH) + 1) + "/" + gc.get(DATE) + "/" + formatAsISOYear(gc) + " " + (hour == 0 ? 12 : hour) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2) + " " + (gc.get(AM_PM) == Calendar.AM ? "am" : "pm"); } /** * This returns an RFC 822 format date time string * ("Sun, 06 Nov 1994 08:49:37 GMT"). * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in the gc's time zone (which should always be GMT because "GMT" is put at the end). * @return gc in the RFC 822 format ("Sun, 06 Nov 1994 08:49:37 GMT"). * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsRFC822GMT(GregorianCalendar gc) { return DAY_OF_WEEK_3[gc.get(Calendar.DAY_OF_WEEK)] + ", " + String2.zeroPad("" + gc.get(DATE), 2) + " " + MONTH_3[gc.get(MONTH)] + " " + //0 based formatAsISOYear(gc) + " " + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2) + " GMT"; //not UTC or Z } /** * This returns a US-style slash format date 24-hour time string * ("1/20/2006 21:00:00") (commonly used by Microsoft Access). * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in gc's time zone. * @return gc in the US slash date 24 hour format ("1/20/2006 21:00:00"). * @throws RuntimeException if trouble (e.g., gc is null) */ public static String formatAsUSSlash24(GregorianCalendar gc) { return (gc.get(MONTH) + 1) + "/" + gc.get(DATE) + "/" + formatAsISOYear(gc) + " " + String2.zeroPad("" + gc.get(HOUR_OF_DAY), 2) + ":" + String2.zeroPad("" + gc.get(MINUTE), 2) + ":" + String2.zeroPad("" + gc.get(SECOND), 2); } /** This parses n int values from s and stores results in resultsN (or leaves * items in resultsN untouched if no value available). * * @param s the date time string * @param separatorN is the separators (use "\u0000" to match any non-digit). * ( matches + or - and that becomes part of the number) * (. matches . or , (the European decimal point)) * @param resultsN should initially have the defaults and * will receive the results. If trouble, resultsN[0] will be Integer.MAX_VALUE, * so caller can throw exception with good error message. */ private static void parseN(String s, char separatorN[], int resultsN[]) { //ensure s starts with a digit if (s == null) s = ""; s = s.trim(); int sLength = s.length(); if (sLength < 1 || !(s.charAt(0) == '-' || String2.isDigit(s.charAt(0)))) { resultsN[0] = Integer.MAX_VALUE; return; } int po1, po2 = -1; //String2.log("parseN " + s); //search for digits, non-digit. "1970-01-01T00:00:00.000-01:00" boolean mMode = s.charAt(0) == '-'; //initial '-' is required and included when evaluating number int nParts = separatorN.length; for (int part = 0; part < nParts; part++) { if (po2 + 1 < sLength) { //accumulate digits po1 = po2 + 1; po2 = po1; if (mMode) { if (po2 < sLength && s.charAt(po2) == '-') po2++; else { resultsN[0] = Integer.MAX_VALUE; return; } } while (po2 < sLength && String2.isDigit(s.charAt(po2))) po2++; //digit //if no number, return; we're done if (po2 == po1) return; if (part > 0 && separatorN[part - 1] == '.') { resultsN[part] = Math2.roundToInt(1000 * String2.parseDouble("0." + s.substring(po1, po2))); //String2.log(" millis=" + resultsN[part]); } else { resultsN[part] = String2.parseInt(s.substring(po1, po2)); } //if invalid number, return trouble if (resultsN[part] == Integer.MAX_VALUE) { resultsN[0] = Integer.MAX_VALUE; return; } //if no more source characters, we're done if (po2 >= sLength) { //String2.log(" " + String2.toCSSVString(resultsN)); return; } //if invalid separator, stop trying to read more; return trouble mMode = false; char ch = s.charAt(po2); if (ch == ',') ch = '.'; if (separatorN[part] == '\u0000') { } else if (separatorN[part] == '') { if (ch == '+') { //do nothing } else if (ch == '-') { po2--; //number starts with - mMode = true; } else { resultsN[0] = Integer.MAX_VALUE; return; } } else if (ch != separatorN[part]) { //if not exact match ... //if current part is ':' or '.' and not matched, try to skip forward to '' if ((separatorN[part] == ':' || separatorN[part] == '.') && part < nParts - 1) { int pmPart = String2.indexOf(separatorN, '', part + 1); if (pmPart >= 0) { //String2.log(" jump to +/-"); part = pmPart; if (ch == '+') { //do nothing } else if (ch == '-') { po2--; //number starts with - mMode = true; } else { resultsN[0] = Integer.MAX_VALUE; return; } continue; } //if < 0, fall through to failure } resultsN[0] = Integer.MAX_VALUE; //String2.log(" " + String2.toCSSVString(resultsN)); return; } } } //String2.log(" " + String2.toCSSVString(resultsN)); } /** * This tests if s is probably an ISO 8601 Date Time (at least [-]YYYY-M). * null and "" return false; * This isn't strict since it doesn't test the remainder of the string. */ public static boolean probablyISODateTime(String s) { char ch; if (s == null) return false; int sLength = s.length(); if (sLength < 6) return false; int po = 0; if (s.charAt(po) == '-') { po++; if (sLength < 7) return false; } if (!String2.isDigit(s.charAt(po++))) return false; if (!String2.isDigit(s.charAt(po++))) return false; if (!String2.isDigit(s.charAt(po++))) return false; if (!String2.isDigit(s.charAt(po++))) return false; if (s.charAt(po++) != '-') return false; if (!String2.isDigit(s.charAt(po++))) return false; //perhaps not 0-padded return true; } /** * This converts an ISO date time string ([-]YYYY-MM-DDTHH:MM:SS.SSSZZ:ZZ) into * a GregorianCalendar object. * <br>It is lenient; so Jan 32 is converted to Feb 1; * <br>The 'T' may be any non-digit. * <br>The time zone can be omitted. * <br>The parts at the end of the time can be omitted. * <br>If there is no time, the end parts of the date can be omitted. Year is required. * <br>This tries hard to be tolerant of non-valid formats (e.g., "1971-1-2", "1971-01") * <br>As of 11/9/2006, NO LONGER TRUE: If year is 0..49, it is assumed to be 2000..2049. * <br>As of 11/9/2006, NO LONGER TRUE: If year is 50..99, it is assumed to be 1950..1999. * <br>If the string is too short, the end of "1970-01-01T00:00:00.000Z" will be added (effectively). * <br>If the string is too long, the excess will be ignored. * <br>If a required separator is incorrect, it is an error. * <br>If the date is improperly formatted, it returns null. * <br>Timezone "Z" or "" is treated as "-00:00" (UTC/Zulu time) * <br>Timezones: e.g., 2007-01-02T03:04:05-01:00 is same as 2007-01-02T04:04:05 * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in gc's time zone. * Timezone info is relative to the gc's time zone. * @param s the dateTimeString in the ISO format (YYYY-MM-DDTHH:MM:SS.SSSZZ:ZZ * or -YYYY-MM-DDTHH:MM:SS.SSSZZ:ZZ for years B.C.) * For years B.C., use calendar2Year = 1 - BCYear. * Note that BCYears are 1..., so 1 BC is calendar2Year 0 (or 0000), * and 2 BC is calendar2Year -1 (or -0001). * This supports SS.SSS and SS,SSS (which ISO 8601 prefers!). * @return the same GregorianCalendar object, but with the date info * @throws RuntimeException if trouble (e.g., gc is null or s is null or * not at least #) */ public static GregorianCalendar parseISODateTime(GregorianCalendar gc, String s) { if (s == null) s = ""; s = s.trim(); boolean negative = s.startsWith("-"); if (negative) s = s.substring(1); if (s.length() < 1 || !String2.isDigit(s.charAt(0))) Test.error(String2.ERROR + " in parseISODateTime: for first character of dateTime='" + s + "' isn't a digit!"); if (gc == null) Test.error(String2.ERROR + " in parseISODateTime: gc is null!"); //default ymdhmsmom year is the only required value int ymdhmsmom[] = { Integer.MAX_VALUE, 1, 1, 0, 0, 0, 0, 0, 0 }; //remove trailing Z or "UTC" s = s.trim(); if (Character.toLowerCase(s.charAt(s.length() - 1)) == 'z') s = s.substring(0, s.length() - 1).trim(); if (s.length() >= 3) { String last3 = s.substring(s.length() - 3).toLowerCase(); if (last3.equals("utc") || last3.equals("gmt")) s = s.substring(0, s.length() - 3).trim(); } //if e.g., 1970-01-01 00:00:00 0:00, change ' ' to '+' (first ' '->'+' is irrelevant) s = String2.replaceAll(s, ' ', '+'); //separators (\u0000=any non-digit) char separator[] = { '-', '-', '\u0000', ':', ':', '.', '', ':', '\u0000' }; parseN(s, separator, ymdhmsmom); if (ymdhmsmom[0] == Integer.MAX_VALUE) Test.error(String2.ERROR + " in parseISODateTime: dateTime='" + s + "' has an invalid format!"); //do time zone adjustment //String2.log("#7=" + ymdhmsmom[7] + " #8=" + ymdhmsmom[8]); if (ymdhmsmom[7] != 0) ymdhmsmom[3] -= ymdhmsmom[7]; if (ymdhmsmom[8] != 0) ymdhmsmom[4] -= ymdhmsmom[7] < 0 ? -ymdhmsmom[8] : ymdhmsmom[8]; //set gc month -1 since gc month is 0.. gc.set((negative ? -1 : 1) * ymdhmsmom[0], ymdhmsmom[1] - 1, ymdhmsmom[2], ymdhmsmom[3], ymdhmsmom[4], ymdhmsmom[5]); gc.set(MILLISECOND, ymdhmsmom[6]); gc.get(YEAR); //force recalculations //synchronized (isoDateTimeFormat) { // gc.setTime(isoDateTimeFormat.parse(isoDateTimeString)); //} //String2.log(" " + gc.getTimeInMillis() + " = " + formatAsISODateTimeT3(gc)); return gc; } /** * This converts an ISO (default *ZULU* time zone) date time string ([-]YYYY-MM-DDTHH:MM:SSZZ:ZZ) into * a GregorianCalendar object with the Zulu time zone. * See parseISODateTime documentation. * * @param s the dateTimeString in the ISO format ([-]YYYY-MM-DDTHH:MM:SS) * This may include hours, minutes, seconds, decimal, and Z or timezone offset (default=Zulu). * @return a GregorianCalendar object * @throws RuntimeException if trouble (e.g., s is null or not at least #) */ public static GregorianCalendar parseISODateTimeZulu(String s) { return parseISODateTime(newGCalendarZulu(), s); } /** * This converts a US slash 24 hour string ("1/20/2006" or "1/20/2006 14:23:59") * (commonly used by Microsoft Access) into a GregorianCalendar object. * <br>It is lenient; so Jan 32 is converted to Feb 1. * <br>If year is 0..49, it is assumed to be 2000..2049. * <br>If year is 50..99, it is assumed to be 1950..1999. * <br>The year may be negative (calendar2Year = 1 - BCYear). (But 0 - 24 assumed to be 2000 - 2049!) * <br>There must be at least #/#/#, or this returns null. * <br>The time is optional; if absent, it is assumed to be 00:00:00 * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in gc's time zone. * @param s the dateString in the US slash format ("1/20/2006" or * "1/20/2006 14:23:59") * @return the same GregorianCalendar object, but with the date info * @throws RuntimeException if trouble (e.g., gc is null or s is null or not at least #/#/#) */ public static GregorianCalendar parseUSSlash24(GregorianCalendar gc, String s) { //default mdyhms month is the only required value int mdyhms[] = { Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE, 0, 0, 0 }; //separators (\u0000=any non-digit) char separator[] = { '/', '/', ' ', ':', ':', '\u0000' }; parseN(s, separator, mdyhms); if (mdyhms[0] == Integer.MAX_VALUE || mdyhms[1] == Integer.MAX_VALUE || mdyhms[2] == Integer.MAX_VALUE) { Test.error(String2.ERROR + " in parseUSSlash24: s=" + s + " has an invalid format!"); } //clean up year if (mdyhms[2] >= 0 && mdyhms[2] <= 49) mdyhms[2] += 2000; if (mdyhms[2] >= 50 && mdyhms[2] <= 99) mdyhms[2] += 1900; //set as ymdhms month -1 since gc month is 0.. gc.set(mdyhms[2], mdyhms[0] - 1, mdyhms[1], mdyhms[3], mdyhms[4], mdyhms[5]); gc.set(MILLISECOND, 0); gc.get(YEAR); //force recalculations //synchronized (isoDateTimeFormat) { // gc.setTime(isoDateTimeFormat.parse(isoDateTimeString)); //} return gc; } /** * This is like parseUSSlash24, but assumes the time zone is Zulu. * * @throws RuntimeException if trouble (e.g., s is null or not at least #/#/#) */ public static GregorianCalendar parseUSSlash24Zulu(String s) { return parseUSSlash24(newGCalendarZulu(), s); } /** * This converts compact string (must be [-]YYYYMMDD, [-]YYYYMMDDhh, * [-]YYYYMMDDhhmm, or [-]YYYYMMDDhhmmss) into * a GregorianCalendar object. * It is lenient; so Jan 32 is converted to Feb 1. * If the date is improperly formatted, it returns null. * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in gc's time zone. * @param s dateTimeString in compact format (must be [-]YYYYMMDD, [-]YYYYMMDDhh, * [-]YYYYMMDDhhmm, or [-]YYYYMMDDhhmmss) * @return the same GregorianCalendar object, but with the date info * @throws RuntimeException if trouble (e.g., gc is null or s is null or not at least * YYYYMMDD) */ public static GregorianCalendar parseCompactDateTime(GregorianCalendar gc, String s) { //ensure it has at least 8 characters, and all characters are digits if (s == null) s = ""; boolean negative = s.startsWith("-"); if (negative) s = s.substring(1); int sLength = s.length(); if (sLength < 8) Test.error(String2.ERROR + " in parseCompactDateTime: s=" + s + " has an invalid format!"); for (int i = 0; i < sLength; i++) if (!String2.isDigit(s.charAt(i))) Test.error(String2.ERROR + " in parseCompactDateTime: s=" + s + " has an invalid format!"); s += String2.makeString('0', 14 - sLength); gc.clear(); gc.set((negative ? -1 : 1) * String2.parseInt(s.substring(0, 4)), String2.parseInt(s.substring(4, 6)) - 1, //-1 = month is 0.. String2.parseInt(s.substring(6, 8)), String2.parseInt(s.substring(8, 10)), String2.parseInt(s.substring(10, 12)), String2.parseInt(s.substring(12, 14))); gc.set(MILLISECOND, 0); gc.get(YEAR); //force recalculations //synchronized (CompactDateTimeFormat) { // gc.setTime(CompactDateTimeFormat.parse(s)); //} //String2.log("parseCompactDateTime " + s + " -> " + formatAsISODateTimeT(gc)); return gc; } /** * This is like parseCompactDateTime, but assumes the time zone is Zulu. * * @throws RuntimeException if trouble (e.g., s is null or invalid) */ public static GregorianCalendar parseCompactDateTimeZulu(String s) { return parseCompactDateTime(newGCalendarZulu(), s); } /** * This converts a DD-Mon-[-]YYYY string e.g., "31-Jul-2004 00:00:00" * into a GregorianCalendar object. * It is lenient; so day 0 is converted to Dec 31 of previous year. * If the date is shortenend, this does the best it can, or returns null. * Ferret often uses this format. * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in gc's time zone. * @param s dateTimeString in DDMonYYY format. * The time part can be shorter or missing. * @return the same GregorianCalendar object, but with the date info * @throws RuntimeException if trouble (e.g., gc is null or s is null or not * DDMonYYYY) */ public static GregorianCalendar parseDDMonYYYY(GregorianCalendar gc, String s) { if (s == null) s = ""; int sLength = s.length(); boolean negative = sLength >= 8 && s.charAt(7) == '-'; if (negative) s = s.substring(0, 7) + s.substring(8); if (sLength < 11 || !String2.isDigit(s.charAt(0)) || !String2.isDigit(s.charAt(1)) || s.charAt(2) != '-' || s.charAt(6) != '-' || !String2.isDigit(s.charAt(7)) || !String2.isDigit(s.charAt(8)) || !String2.isDigit(s.charAt(9)) || !String2.isDigit(s.charAt(10))) Test.error(String2.ERROR + " in parseDDMonYYYY: s=" + s + " has an invalid format!"); gc.clear(); int hour = 0, min = 0, sec = 0; if (sLength >= 13) { if (s.charAt(11) != ' ' || !String2.isDigit(s.charAt(12)) || !String2.isDigit(s.charAt(13))) Test.error(String2.ERROR + " in parseDDMonYYYY: s=" + s + " has an invalid format!"); hour = String2.parseInt(s.substring(12, 14)); } if (sLength >= 16) { if (s.charAt(14) != ':' || !String2.isDigit(s.charAt(15)) || !String2.isDigit(s.charAt(16))) Test.error(String2.ERROR + " in parseDDMonYYYY: s=" + s + " has an invalid format!"); min = String2.parseInt(s.substring(15, 17)); } if (sLength >= 19) { if (s.charAt(17) != ':' || !String2.isDigit(s.charAt(18)) || !String2.isDigit(s.charAt(19))) Test.error(String2.ERROR + " in parseDDMonYYYY: s=" + s + " has an invalid format!"); sec = String2.parseInt(s.substring(18, 20)); } String month = s.substring(3, 6).toLowerCase(); int mon = 0; while (mon < 12) { if (MONTH_3[mon].toLowerCase().equals(month)) break; mon++; } if (mon == 12) Test.error(String2.ERROR + " in parseDDMonYYYY: s=" + s + " has an invalid format!"); gc.set((negative ? -1 : 1) * String2.parseInt(s.substring(7, 11)), mon, //month is already 0.. String2.parseInt(s.substring(0, 2)), hour, min, sec); gc.get(YEAR); //force recalculations return gc; } /** * This is like parseDDMonYYYY, but assumes the time zone is Zulu. * * @throws RuntimeException if trouble (e.g., s is null or invalid) */ public static GregorianCalendar parseDDMonYYYYZulu(String s) { return parseDDMonYYYY(newGCalendarZulu(), s); } /** * This converts a [-]YYYYDDD string into * a GregorianCalendar object. * It is lenient; so day 0 is converted to Dec 31 of previous year. * If the date is improperly formatted, this does the best * it can, or returns null. * * @param gc a GregorianCalendar object. The dateTime will be interpreted * as being in gc's time zone. * @param s dateTimeString in YYYYDDD format * @return the same GregorianCalendar object, but with the date info * @throws RuntimeException if trouble (e.g., gc is null or s is null or not * YYYYDDDD) */ public static GregorianCalendar parseYYYYDDD(GregorianCalendar gc, String s) { //ensure it is a string with 7 digits if (s == null) s = ""; boolean negative = s.startsWith("-"); if (negative) s = s.substring(1); int sLength = s.length(); if (sLength != 7) Test.error(String2.ERROR + " in parseYYYYDDD: s=" + s + " has an invalid format!"); for (int i = 0; i < sLength; i++) if (!String2.isDigit(s.charAt(i))) Test.error(String2.ERROR + " in parseYYYYDDD: s=" + s + " has an invalid format!"); gc.clear(); gc.set((negative ? -1 : 1) * String2.parseInt(s.substring(0, 4)), 1 - 1, //-1 = month is 0.. 1, 0, 0, 0); gc.set(Calendar.DAY_OF_YEAR, String2.parseInt(s.substring(4, 7))); gc.set(MILLISECOND, 0); gc.get(YEAR); //force recalculations //synchronized (YYYYDDDFormat) { // gc.setTime(YYYYDDDFormat.parse(YYYYDDDString)); //} //String2.log("parseYYYYDDD " + s + " -> " + formatAsISODate(gc)); return gc; } /** * This is like parseYYYYDDD, but assumes the time zone is Zulu. * @throws RuntimeException if trouble (e.g., s is null or not YYYYDDD) */ public static GregorianCalendar parseYYYYDDDZulu(String s) { return parseYYYYDDD(newGCalendarZulu(), s); } /** * This returns an error message * indicating that the specified isoDateString couldn't be parsed. * * @param s dateTimeString * @param e a Exception * @return an error string */ public static String getParseErrorString(String s, Exception e) { String error = MustBe.throwable(String2.ERROR + " while parsing \"" + s + "\".", e); //String2.log(error); return error; } /** * Convert a String with [-]yyyyddd to a String with YYYY-mm-dd. * This works the same for Local or Zulu or other time zones. * * @param s a String with a date in the form yyyyddd * @return the date formatted as YYYY-mm-dd * @throws RuntimeException if trouble (e.g., s is null or not YYYYDDD) */ public static String yyyydddToIsoDate(String s) { //ensure it is a string with 7 digits if (s == null) s = ""; boolean negative = s.startsWith("-"); if (negative) s = s.substring(1); int sLength = s.length(); if (sLength != 7) Test.error(String2.ERROR + " in yyyydddToIsoDate: yyyyddd='" + s + "' has an invalid format!"); for (int i = 0; i < sLength; i++) if (!String2.isDigit(s.charAt(i))) Test.error(String2.ERROR + " in yyyydddToIsoDate: yyyyddd='" + s + "' has an invalid format!"); GregorianCalendar gc = newGCalendarZulu((negative ? -1 : 1) * Integer.parseInt(s.substring(0, 4)), Integer.parseInt(s.substring(4))); return formatAsISODate(gc); } /** * This returns the current local dateTime in ISO T format. * * @return the current local dateTime in ISO T format (with no timezone id) */ public static String getCurrentISODateTimeStringLocal() { return formatAsISODateTimeT(newGCalendarLocal()); } /** * This returns the current local dateTime in compact ISO format (yyyyMMddhhmmss). * * @return the current local dateTime in compact ISO format (yyyyMMddhhmmss). */ public static String getCompactCurrentISODateTimeStringLocal() { return formatAsCompactDateTime(newGCalendarLocal()); } /** * This returns the current Zulu dateTime in ISO T format. * * @return the current Zulu dateTime in ISO T format (without the trailing Z) */ public static String getCurrentISODateTimeStringZulu() { return formatAsISODateTimeT(newGCalendarZulu()); } /** * This returns the current Zulu date in RFC 822 format. * * @return the current Zulu date in RFC 822 format */ public static String getCurrentRFC822Zulu() { return formatAsRFC822GMT(newGCalendarZulu()); } /** * This returns the current Zulu date in ISO format. * * @return the current Zulu date in ISO format */ public static String getCurrentISODateStringZulu() { return formatAsISODate(newGCalendarZulu()); } /** * This returns the current local date in ISO format. * * @return the current local date in ISO format */ public static String getCurrentISODateStringLocal() { return formatAsISODate(newGCalendarLocal()); } /** * This converts an ISO Zulu DateTime string to millis since 1970-01-01T00:00:00Z. * * @param s the ISO Zulu DateTime string. * This may include hours, minutes, seconds, millis and Z or timezone offset (default=Zulu). * @return the millis since 1970-01-01T00:00:00Z * @throws RuntimeException if trouble (e.g., s is null or not at least #) */ public static long isoZuluStringToMillis(String s) { GregorianCalendar gc = parseISODateTime(newGCalendarZulu(), s); return gc.getTimeInMillis(); } /** * This converts millis since 1970-01-01T00:00:00Z to an ISO Zulu DateTime string. * * @param millis the millis since 1970-01-01T00:00:00Z * @return the ISO Zulu DateTime string 'T' (without the trailing Z) * @throws RuntimeException if trouble (e.g., millis is Long.MAX_VALUE) */ public static String millisToIsoZuluString(long millis) { GregorianCalendar gc = newGCalendarZulu(millis); return formatAsISODateTimeT(gc); } /** * This converts millis since 1970-01-01T00:00:00Z to an ISO Zulu DateTime string. * * @param millis the millis since 1970-01-01T00:00:00Z * @return the ISO Zulu DateTime string 'T' (with 3 decimal places) (without the trailing Z) * @throws RuntimeException if trouble (e.g., millis is Long.MAX_VALUE) */ public static String millisToIso3ZuluString(long millis) { GregorianCalendar gc = newGCalendarZulu(millis); return formatAsISODateTimeT3(gc); } /** * Remove any spaces, dashes (except optional initial dash), colons, and T's from s. * * @param s a string * @return s with any spaces, dashes, colons removed * (if s == null, this throws RuntimeException) * @throws RuntimeException if trouble (e.g., s is null) */ public static String removeSpacesDashesColons(String s) { boolean negative = s.startsWith("-"); if (negative) s = s.substring(1); s = String2.replaceAll(s, " ", ""); s = String2.replaceAll(s, "-", ""); s = String2.replaceAll(s, "T", ""); return (negative ? "-" : "") + String2.replaceAll(s, ":", ""); } /** * Find the closest match for timeValue in isoDates * which must be sorted in ascending order. * This gives precise answer if there is an exact match * (and gives closest answer timeValue is imprecise, e.g., if "2006-01-07" is used * to represent a precise time of "2006-01-07 12:00:00"). * * <p>This throws RuntimeException if some years are negative (0000 is ok). * * @param isoDates is an ascending sorted list of ISO dates [times]. * It the array has duplicates and timeValue equals one of them, * it isn't specified which duplicate's index will be returned. * @param timeValue the ISO timeValue to be matched * (with connector "T" or " " matching the isoDates). * This may include hours, minutes, seconds, decimal, and timezone offset (default=Zulu). * @return the index (in isoDates) of the best match for timeValue. * If timeValue is null or "", this returns isoDates.length-1. */ public static int binaryFindClosest(String isoDates[], String timeValue) { try { if (isoDates[0].startsWith("-")) throw new RuntimeException( String2.ERROR + ": Calendar2.binaryFindClosest doesn't work with years < 0."); //likely place for exception thrown (that's ok) double timeValueSeconds = isoStringToEpochSeconds(timeValue); //do standard String binary search //(since isoDate strings work with standard String ordering) int i = Arrays.binarySearch(isoDates, timeValue); if (i >= 0) return i; //success //insertionPoint at end point? int insertionPoint = -i - 1; //0.. isoDates.length if (insertionPoint == 0) return 0; if (insertionPoint >= isoDates.length) return insertionPoint - 1; //insertionPoint between 2 points //tie? favor later time so "2006-01-07" finds "2006-01-07 12:00:00", // not "2006-01-06 12:00:00" if (Math.abs(isoStringToEpochSeconds(isoDates[insertionPoint - 1]) - timeValueSeconds) < Math .abs(isoStringToEpochSeconds(isoDates[insertionPoint]) - timeValueSeconds)) return insertionPoint - 1; else return insertionPoint; } catch (Exception e) { return isoDates.length - 1; } } /** * Find the last element which is <= timeValue in isoDates (sorted ascending). * * <p>If firstGE > lastLE, there are no matching elements (because * the requested range is less than or greater than all the values, * or between two adjacent values). * * <p>This throws RuntimeException if some years are negative (0000 is ok). * * @param isoDates is an ascending sorted list of ISO dates [times] * which may have duplicates * @param timeValue an iso formatted date value * (with connector "T" or " " matching the isoDates). * This may include hours, minutes, seconds, decimal, and timezone offset (default=Zulu). * @return the index of the last element which is <= timeValue in an ascending sorted array. * If timeValue is invalid or timeValue < the smallest element, this returns -1 (no element is appropriate). * If timeValue > the largest element, this returns isoDates.length-1. */ public static int binaryFindLastLE(String[] isoDates, String timeValue) { try { if (isoDates[0].startsWith("-")) throw new RuntimeException( String2.ERROR + ": Calendar2.binaryFindLastLE doesn't work with years < 0."); //likely place for exception thrown (that's ok) double timeValueSeconds = isoStringToEpochSeconds(timeValue); int i = Arrays.binarySearch(isoDates, timeValue); //String2.log("binaryLE: i=" + i); //if (i >= 0) an exact match; look for duplicates if (i < 0) { int insertionPoint = -i - 1; //0.. isoDates.length i = insertionPoint - 1; } while (i < isoDates.length - 1 && isoStringToEpochSeconds(isoDates[i + 1]) <= timeValueSeconds) { //String2.log("binaryLE: i++ because " + isoStringToEpochSeconds(isoDates[i + 1]) + " <= " + timeValueSeconds); i++; } return i; } catch (Exception e) { return -1; } } /** * Find the first element which is >= timeValue in isoDates (sorted ascending. * * <p>If firstGE > lastLE, there are no matching elements (because * the requested range is less than or greater than all the values, * or between two adjacent values). * * <p>This throws RuntimeException if some years are negative (0000 is ok). * * @param isoDates is a sorted list of ISO dates [times] * which may have duplicates * @param timeValue an iso formatted date value * (with connector "T" or " " matching the isoDates). * This may include hours, minutes, seconds, decimal, and timezone offset (default=Zulu). * @return the index of the first element which is >= timeValue in an ascending sorted array. * <br>If timeValue < the smallest element, this returns 0. * <br>If timeValue is invalid or timeValue > the largest element, * this returns isoDates.length (no element is appropriate). */ public static int binaryFindFirstGE(String[] isoDates, String timeValue) { try { if (isoDates[0].startsWith("-")) throw new RuntimeException( String2.ERROR + ": Calendar2.binaryFindFirstGE doesn't work with years < 0."); //likely place for exception thrown (that's ok) double timeValueSeconds = isoStringToEpochSeconds(timeValue); int i = Arrays.binarySearch(isoDates, timeValue); //if (i >= 0) an exact match; look for duplicates if (i < 0) i = -i - 1; //the insertion point, 0.. isoDates.length while (i > 0 && isoStringToEpochSeconds(isoDates[i - 1]) >= timeValueSeconds) i--; return i; } catch (Exception e) { return isoDates.length; } } /** * This adds the specified n field's to the isoDate, * and returns the resulting GregorianCalendar object. * * <p>This correctly handles B.C. dates. * * @param isoDate an iso formatted date time string. * This may include hours, minutes, seconds, decimal, and Z or timezone offset (default=Zulu). * @param n the number of 'units' to be added * @param field one of the Calendar or Calendar2 constants for a field * (e.g., Calendar2.YEAR). * @return the GregorianCalendar for isoDate with the specified n field's added * @throws Exception if trouble e.g., n is Integer.MAX_VALUE */ public static GregorianCalendar isoDateTimeAdd(String isoDate, int n, int field) throws Exception { if (n == Integer.MAX_VALUE) Test.error(String2.ERROR + " in Calendar2.isoDateTimeAdd: invalid addN=" + n); GregorianCalendar gc = parseISODateTimeZulu(isoDate); gc.add(field, n); //no need to adjust for B.C. gc handles it. return gc; } /** * This converts a millis elapsed time value (139872234 ms or 783 ms) to a nice * string (e.g., "7h 4m 5s", "5.783 s", or "783 ms"). * <br>was (e.g., "7:04:05.233" or "783 ms"). * * @param millis may be negative * @return a simplified approximate string representation of elapsed time * (or "infinite[!]" if trouble, e.g., millis is Double.NaN). */ public static String elapsedTimeString(double millis) { if (!Math2.isFinite(millis)) return "infinity"; long time = Math2.roundToLong(millis); String negative = ""; if (time < 0) { negative = "-"; time = Math.abs(time); } if (time == Long.MAX_VALUE) return "infinity"; long ms = time % 1000; long sec = time / 1000; long min = sec / 60; sec = sec % 60; long hr = min / 60; min = min % 60; long day = hr / 24; hr = hr % 24; if (day + hr + min + sec == 0) return negative + time + " ms"; if (day + hr + min == 0) return negative + sec + "." + String2.zeroPad("" + ms, 3) + " s"; String ds = day + (day == 1 ? " day" : " days"); if (hr + min + sec == 0) return negative + ds; //was //return (day > 0? negative + ds + " " : negative) + // String2.zeroPad("" + hr, 2) + ":" + // String2.zeroPad("" + min, 2) + ":" + // String2.zeroPad("" + sec, 2) + // (ms > 0? "." + String2.zeroPad("" + ms, 3) : ""); //e.g., 4h 17m 3s apple uses this style; easier to read return (day > 0 ? negative + ds + " " : negative) + ((day > 0 || hr > 0) ? hr + "h " : "") + min + "m " + //hr or min will be >0, so always include it sec + //since >59 seconds, don't include millis //(ms > 0? "." + String2.zeroPad("" + ms, 3) : "") + "s"; } /** * This converts the date, hour, minute, second so gc is at the exact center * of its current month. * * @param gc * @return the same gc, but modified, for convenience * @throws Exception if trouble (e.g., gc is null) */ public static GregorianCalendar centerOfMonth(GregorianCalendar gc) throws Exception { int nDaysInMonth = gc.getActualMaximum(Calendar.DATE); gc.set(DATE, 1 + nDaysInMonth / 2); gc.set(HOUR_OF_DAY, Math2.odd(nDaysInMonth) ? 12 : 0); gc.set(MINUTE, 0); gc.set(SECOND, 0); gc.set(MILLISECOND, 0); return gc; } /** * This clears the fields smaller than 'field' * (e.g., HOUR_OF_DAY clears MINUTE, SECOND, and MILLISECOND, * but not HOUR_OF_DAY, MONTH, or YEAR). * * @param gc * @param field e.g., HOUR_OF_DAY * @return the same gc, but modified, for convenience * @throws Exception if trouble (e.g., gc is null or field is not supported) */ public static GregorianCalendar clearSmallerFields(GregorianCalendar gc, int field) throws Exception { if (field == MILLISECOND || field == SECOND || field == MINUTE || field == HOUR || field == HOUR_OF_DAY || field == DATE || field == DAY_OF_YEAR || field == MONTH || field == YEAR) { } else { Test.error(String2.ERROR + " in Calendar2.clearSmallerFields: unsupported field=" + field); } if (field == MILLISECOND) return gc; gc.set(MILLISECOND, 0); if (field == SECOND) return gc; gc.set(SECOND, 0); if (field == MINUTE) return gc; gc.set(MINUTE, 0); if (field == HOUR || field == HOUR_OF_DAY) return gc; gc.set(HOUR_OF_DAY, 0); if (field == DATE) return gc; gc.set(DATE, 1); if (field == MONTH) return gc; gc.set(MONTH, 0); //DAY_OF_YEAR works like YEAR return gc; } /** * This returns the start of a day, n days back from max (or from now if max=NaN). * * @param nDays * @param max seconds since epoch * @return seconds since epoch for the start of a day, n days back from max (or from now if max=NaN). * @throws Exception if trouble */ public static double backNDays(int nDays, double max) throws Exception { GregorianCalendar gc = Math2.isFinite(max) ? epochSecondsToGc(max) : newGCalendarZulu(); //round to previous midnight, then go back nDays clearSmallerFields(gc, DATE); return gcToEpochSeconds(gc) - SECONDS_PER_DAY * nDays; } /** * This returns a double[] of maxNValues (or fewer) * evenly spaced, between start and stop. * The first and last values will be start and stop. * The intermediate values will be evenly spaced in a human sense (eg monthly) * but the start and stop won't necessarily use the same stride. * * @param start epoch seconds * @param stop epoch seconds * @param maxNValues maximum desired nValues * @return a double[] of nValues (or fewer) epoch seconds values, * evenly spaced, between start and stop. * <br>If start or stop is not finite, this returns null. * <br>If start=stop, this returns just one value. * <br>If start > stop, they are swapped so the results are always ascending. * <br>If trouble, this returns null. */ public static double[] getNEvenlySpaced(double start, double stop, int maxNValues) { try { if (!Math2.isFinite(start) || !Math2.isFinite(stop)) return null; if (start == stop) return new double[] { start }; if (start > stop) { double d = start; start = stop; stop = d; } double spm = SECONDS_PER_MINUTE; //double avoids int MAX_VALUE problem double sph = SECONDS_PER_HOUR; double spd = SECONDS_PER_DAY; double range = stop - start; double mnv2 = maxNValues / 2; //double avoids int MAX_VALUE problem int field, biggerField, nice[]; double divisor; if (range <= mnv2 * spm) { field = SECOND; biggerField = MINUTE; divisor = 1; nice = new int[] { 1, 2, 5, 10, 15, 20, 30, 60 }; } else if (range <= mnv2 * sph) { field = MINUTE; biggerField = HOUR_OF_DAY; divisor = spm; nice = new int[] { 1, 2, 5, 10, 15, 20, 30, 60 }; } else if (range <= mnv2 * spd) { field = HOUR_OF_DAY; biggerField = DATE; divisor = sph; nice = new int[] { 1, 2, 3, 4, 6, 12, 24 }; } else if (range <= mnv2 * 30 * spd) { field = DATE; biggerField = MONTH; divisor = spd; nice = new int[] { 1, 2, 5, 7 }; } else if (range <= mnv2 * 365 * spd) { field = MONTH; biggerField = YEAR; divisor = 30 * spd; nice = new int[] { 1, 2, 3, 6, 12 }; } else { field = YEAR; biggerField = -9999; divisor = 365 * spd; nice = new int[] { 1, 2, 5, 10 }; } //find stride (some number of fields, e.g., 10 seconds) //range testing above ensures range/divisor=n, e.g. seconds will be < 60, // or n minutes will be < 60, nHours < 24, ... //and ensure stride is at least 1. double dnValues = (range / divisor) / maxNValues; int stride = nextNice(dnValues, nice); //minimum stride will be 1 if (field == DATE) stride = Math.min(14, stride); DoubleArray da = new DoubleArray(); da.add(start); GregorianCalendar nextGc = epochSecondsToGc(start); if (field != YEAR) clearSmallerFields(nextGc, biggerField); double next = gcToEpochSeconds(nextGc); while (next < stop) { if (next > start) da.add(next); //it may not be for the first few if (field == DATE) { //repeatedly using DATE=1 is nice, so ... //will subsequent value be in next month? //non-permanent test of this: ndbcSosSalinity has stride = 2 days; results have 2008-09-27 then 2008-10-01 int oMonth = nextGc.get(MONTH); nextGc.add(field, 2 * stride); //2* sets subsequent value if (nextGc.get(MONTH) == oMonth) { nextGc.add(field, -stride); //go back to regular value } else { nextGc.set(DATE, 1); //go for DATE=1 in next month e.g., 1,15,1,15 or 1,8,14,21,1,8,14,21, } } else { nextGc.add(field, stride); } next = gcToEpochSeconds(nextGc); } da.add(stop); if (reallyVerbose) String2.log("Calendar2.getNEvenlySpaced start=" + epochSecondsToIsoStringT(start) + " stop=" + epochSecondsToIsoStringT(stop) + " field=" + fieldName(field) + "\n divisor=" + divisor + " range/divisor/maxNValues=" + dnValues + " stride=" + stride + " nValues=" + da.size()); return da.toArray(); } catch (Exception e) { String2.log(MustBe.throwableToString(e)); return null; } } /** * This returns the value in nice which is >= d, or a multiple of the last value which is * higher than d. * This is used to suggest the division distance along an axis. * * @param d a value e.g., 2.3 seconds * @param nice an ascending list. e.g., for seconds: 1,2,5,10,15,20,30,60 * @return the value in nice which is >= d, or a multiple of the last value which is * higher than d */ public static int nextNice(double d, int nice[]) { int n = nice.length; for (int i = 0; i < n; i++) { if (d <= nice[i]) return nice[i]; } return Math2.roundToInt(Math.ceil(d / nice[n - 1])); } /** * This rounds to the nearest idealN, idealUnits (e.g., 2 months) * (starting at Jan 1, 0000). * * @param epochSeconds * @param idealN e.g., 1 to 100 * @param idealUnits an index of one of the IDEAL_UNITS * @return epochSeconds, converted to Zulu GC and rounded to the nearest idealN, idealUnits * (e.g., 2 months) */ public static GregorianCalendar roundToIdealGC(double epochSeconds, int idealN, int idealUnits) { GregorianCalendar gc = newGCalendarZulu(Math2.roundToLong(epochSeconds * 1000)); if (idealUnits == 5) { //year double td = getYear(gc) + gc.get(MONTH) / 12.0; //month is 0.. int ti = Math2.roundToInt(td / idealN) * idealN; //round to nearest n units gc = newGCalendarZulu(ti, 1, 1); } else if (idealUnits == 4) { //months double td = getYear(gc) * 12 + gc.get(MONTH); //month is 0.. int ti = Math2.roundToInt(td / idealN) * idealN; //round to nearest n units gc = newGCalendarZulu(ti / 12, (ti % 12) + 1, 1); } else { //seconds ... days: all have consistent length double chunk = idealN * IDEAL_UNITS_SECONDS[idealUnits]; //e.g., decimal number of days double td = Math.rint(epochSeconds / chunk) * chunk; //round to nearest n units gc = newGCalendarZulu(Math2.roundToLong(td * 1000)); } return gc; } /** * Given a date time string, this suggests a Java/Joda date/time format suitable * for parsing and output formatting. * * @param sample * @return an appropriate Java/Joda date/time format * or "" if not matched. * If the response starts with "yyyy-MM", parse with Calendar2.parseISODateTimeZulu(); * else parse with Joda. */ public static String suggestDateTimeFormat(String sample) { if (sample == null || sample.length() == 0) return ""; char ch = Character.toLowerCase(sample.charAt(0)); if (ch >= '0' && ch <= '9') { for (int i = 0; i < digitRegexTimeFormat.length; i += 2) { if (sample.matches(digitRegexTimeFormat[i])) return digitRegexTimeFormat[i + 1]; } } else if (ch >= 'a' && ch <= 'z') { for (int i = 0; i < letterRegexTimeFormat.length; i += 2) { if (sample.matches(letterRegexTimeFormat[i])) return letterRegexTimeFormat[i + 1]; } } //fail return ""; } /** * This looks for a date time format which is suitable for all elements of sa * (other than nulls and ""'s). * * @param sa a StringArray, perhaps with consistently formatted date time String values. * @return a date time format which is suitable for all elements of sa * (other than nulls and ""'s), or "" if no suggestion. * The format is suitable for parsing and output formatting * If the response starts with "yyyy-MM", parse with Calendar2.parseISODateTimeZulu(); * else parse with Joda. */ public static String suggestDateTimeFormat(StringArray sa) { boolean debugMode = false; int size = sa.size(); String format = null; Pattern pattern = null; for (int row = 0; row < size; row++) { String s = sa.get(row); if (s == null || s.length() == 0) continue; if (pattern == null) { format = suggestDateTimeFormat(s); if (format.length() == 0) { if (debugMode) String2.log(" suggestDateTimeFormat: no format for \"" + s + "\"."); return ""; } if (debugMode) String2.log(" suggestDateTimeFormat: format for \"" + s + "\" = " + format); pattern = dateTimeFormatToPattern(format); if (pattern == null) return ""; } else { Matcher matcher = pattern.matcher(s); if (!matcher.matches()) { if (debugMode) String2.log(" suggestDateTimeFormat: [" + row + "]=\"" + s + "\" doesn't match format=\"" + format + "\"."); return ""; } } } return format == null ? "" : format; } /** * Given one of the known dateTimeFormats, this returns a Joda Pattern for it. * Patterns are thread safe. * * @return the relevant pattern, or null if not matched. */ public static Pattern dateTimeFormatToPattern(String dateTimeFormat) { return dateTimeFormatPatternHM.get(dateTimeFormat); } /** * This converts s into a double with epochSeconds. * * @param dateTimeFormat one of the ISO8601 formats above, or a Joda format. * If it starts with "yyyy-MM", sourceTime will be parsed with Calendar2.parseISODateTimeZulu(); * else parse with Joda. * @return the epochSeconds value or NaN if trouble */ public static double toEpochSeconds(String sourceTime, String dateTimeFormat) { try { if (dateTimeFormat.startsWith("yyyy-MM")) //parse with Calendar2.parseISODateTime return safeIsoStringToEpochSeconds(sourceTime); //parse with Joda DateTimeFormatter formatter = DateTimeFormat.forPattern(dateTimeFormat).withZone(DateTimeZone.UTC); return formatter.parseMillis(sourceTime) / 1000.0; //thread safe } catch (Throwable t) { if (verbose && sourceTime != null && sourceTime.length() > 0) String2.log(" EDVTimeStamp.sourceTimeToEpochSeconds: Invalid sourceTime=" + sourceTime + " format=" + dateTimeFormat + "\n" + t.toString()); return Double.NaN; } } /** * This converts sa into a DoubleArray with epochSeconds. * * @param dateTimeFormat one of the ISO8601 formats above, or a Joda format. * If it starts with "yyyy-MM", sa strings will be parsed with Calendar2.parseISODateTimeZulu(); * else parse with Joda. * @return a DoubleArray with the epochSeconds values (any/all will be NaN if touble) */ public static DoubleArray toEpochSeconds(StringArray sa, String dateTimeFormat) { int n = sa.size(); DoubleArray da = new DoubleArray(n, false); if (dateTimeFormat == null || dateTimeFormat.length() == 0) { da.addN(n, Double.NaN); return da; } try { if (dateTimeFormat.startsWith("yyyy-MM")) { //use Calendar2 for (int i = 0; i < n; i++) da.add(safeIsoStringToEpochSeconds(sa.get(i))); } else { //use Joda boolean printError = verbose; DateTimeFormatter formatter = DateTimeFormat.forPattern(dateTimeFormat).withZone(DateTimeZone.UTC); da.addN(n, Double.NaN); for (int i = 0; i < n; i++) { String s = sa.get(i); if (s != null && s.length() > 0) { try { da.set(i, formatter.parseMillis(s) / 1000.0); //thread safe } catch (Throwable t2) { if (printError) { String2.log( " EDVTimeStamp.sourceTimeToEpochSeconds: error while parsing sourceTime=" + s + " with format=" + dateTimeFormat + "\n" + t2.toString()); printError = false; } } } } } } catch (Throwable t) { if (verbose) String2.log(" Calendar2.toEpochSeconds: format=" + dateTimeFormat + ", Unexpected error=" + t.toString()); } return da; } }