waterml/src/main/java/ucar/nc2/ogc/erddap/util/ErddapCalendar2.java
public static GregorianCalendar parseISODateTime(GregorianCalendar gc, 
        String s) {

        if (s == null)
            s = "";
        boolean negative = s.startsWith("-");
        if (negative) 
            s = s.substring(1);
        if (s.length() < 1 || !ErddapString2.isDigit(s.charAt(0)))
            throw new RuntimeException(
                    ErddapString2.ERROR + " in parseISODateTime: for first character of dateTime='" + s + "' isn't a digit!");
        if (gc == null)
            throw new RuntimeException(ErddapString2.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 = ErddapString2.replaceAll(s, ' ', '+');

        //separators (\u0000=any non-digit)
        char separator[] = {'-','-','\u0000',':',':','.','±', ':', '\u0000'};
        parseN(s, separator, ymdhmsmom);
        if (ymdhmsmom[0] == Integer.MAX_VALUE)
            throw new RuntimeException(
                    ErddapString2.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

        return gc;
    }
public static GregorianCalendar parseISODateTime(GregorianCalendar gc, 
        String s) {

        if (s == null)
            s = "";
        boolean negative = s.startsWith("-");
        if (negative) 
            s = s.substring(1);
        if (s.length() < 1 || !ErddapString2.isDigit(s.charAt(0)))
            throw new RuntimeException(
                    ErddapString2.ERROR + " in parseISODateTime: for first character of dateTime='" + s + "' isn't a digit!");
        if (gc == null)
            throw new RuntimeException(ErddapString2.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 = ErddapString2.replaceAll(s, ' ', '+');

        //separators (\u0000=any non-digit)
        char separator[] = {'-','-','\u0000',':',':','.','±', ':', '\u0000'};
        parseN(s, separator, ymdhmsmom);
        if (ymdhmsmom[0] == Integer.MAX_VALUE)
            throw new RuntimeException(
                    ErddapString2.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

        return gc;
    }
This converts an ISO date time string ([-]YYYY-MM-DDTHH:MM:SS.SSS±ZZ: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.SSS±ZZ:ZZ
or -YYYY-MM-DDTHH:MM:SS.SSS±ZZ: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 #)