// import sprintf.js function arraycopy( srcPts, srcOff, dstPts, dstOff, size) { // private if (srcPts !== dstPts || dstOff >= srcOff + size) { while (--size >= 0) dstPts[dstOff++] = srcPts[srcOff++]; } else { var tmp = srcPts.slice(srcOff, srcOff + size); for (var i = 0; i < size; i++) dstPts[dstOff++] = tmp[i]; } } /** * Utilities for times in IsoTime strings (limited set of ISO8601 times) * Examples of isoTime strings include: * * @author jbf */ class TimeUtil { /** * Number of time components: year, month, day, hour, minute, second, nanosecond */ static TIME_DIGITS = 7; /** * Number of components in time representation: year, month, day */ static DATE_DIGITS = 3; /** * Number of components in a time range, which is two times. */ static TIME_RANGE_DIGITS = 14; /** * When array of components represents a time, the zeroth component is the year. */ static COMPONENT_YEAR = 0; /** * When array of components represents a time, the first component is the month. */ static COMPONENT_MONTH = 1; /** * When array of components represents a time, the second component is the day of month. */ static COMPONENT_DAY = 2; /** * When array of components represents a time, the third component is the hour of day. */ static COMPONENT_HOUR = 3; /** * When array of components represents a time, the fourth component is the minute of hour. */ static COMPONENT_MINUTE = 4; /** * When array of components represents a time, the fifth component is the second of minute (0 to 61). */ static COMPONENT_SECOND = 5; /** * When array of components represents a time, the sixth component is the nanosecond of the second (0 to 99999999). */ static COMPONENT_NANOSECOND = 6; /** * the number of days in each month. DAYS_IN_MONTH[0][12] is number of days in December of a non-leap year */ static DAYS_IN_MONTH = [[0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 0], [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 0]]; /** * the number of days to the first of each month. DAY_OFFSET[0][12] is offset to December 1st of a non-leap year */ static DAY_OFFSET = [[0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365], [0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366]]; /** * short English abbreviations for month names. Note monthNames[0] is "Jan", not monthNames[1]. */ static MONTH_NAMES = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; /** * fast parser requires that each character of string is a digit. Note this * does not check the the numbers are digits! * * @param s string containing an integer * @return the integer */ static parseInteger(s) { return parseInt(s,10); } /** * fast parser requires that each character of string is a digit. * * @param s the number, containing 1 or more digits. * @param deft the number to return when s is missing. * @return the int value */ static parseIntegerDeft(s, deft) { if (s === undefined || s === null) { return deft; } return parseInt(s, 10); } static parseDouble(val, deft) { if (val === undefined || val === null) { if (deft !== -99) { return deft; } else { throw "bad digit"; } } var n = val.length - 1; if (/[a-z]/i.test(val.charAt(n))) { return parseFloat(val.substring(0, n)); } else { return parseFloat(val); } } /** * return the seven element start time from the time range. Note * it is fine to use a time range as the start time, because codes * will only read the first seven components, and this is only added * to make code more readable. * @param timerange a fourteen-element time range. * @return the start time. */ static getStartTime(timerange) { var result = []; arraycopy( timerange, 0, result, 0, TimeUtil.TIME_DIGITS ); return result; } /** * return the seven element stop time from the time range. Note * it is fine to use a time range as the start time, because codes * will only read the first seven components. * @param timerange a fourteen-element time range. * @return the stop time. */ static getStopTime(timerange) { var result = []; arraycopy( timerange, TimeUtil.TIME_DIGITS, result, 0, TimeUtil.TIME_DIGITS ); return result; } /** * copy the components of time into the start position (indeces 7-14) of the time range. * This one-line method was introduced to clarify code and make conversion to * other languages (in particular Python) easier. * @param time the seven-element start time * @param timerange the fourteen-element time range. */ static setStartTime(time, timerange) { arraycopy( time, 0, timerange, 0, TimeUtil.TIME_DIGITS ); } /** * copy the components of time into the stop position (indeces 7-14) of the time range. * @param time the seven-element stop time * @param timerange the fourteen-element time range. */ static setStopTime(time, timerange) { arraycopy( time, 0, timerange, TimeUtil.TIME_DIGITS, TimeUtil.TIME_DIGITS ); } /** * format the time as (non-leap) milliseconds since 1970-01-01T00:00.000Z into a string. The * number of milliseconds should not include leap seconds. The milliseconds are always present. * * @param time the number of milliseconds since 1970-01-01T00:00.000Z * @return the formatted time. * @see #toMillisecondsSince1970(java.lang.String) */ static fromMillisecondsSince1970(time) { return new Date(time).toISOString(); } /** * given the two times, return a 14 element time range. * @param t1 a seven digit time * @param t2 a seven digit time after the first time. * @return a fourteen digit time range. * @throws IllegalArgumentException when the first time is greater than or equal to the second time. */ static createTimeRange(t1, t2) { if (!TimeUtil.gt(t2, t1)) { throw "t1 is not smaller than t2"; } var result = []; TimeUtil.setStartTime(result, t1); TimeUtil.setStopTime(result, t2); return result; } /** * true if the year between 1582 and 2400 is a leap year. * @param year the year * @return true if the year between 1582 and 2400 is a leap year. */ static isLeapYear(year) { if (year < 1582 || year > 2400) { throw "year must be between 1582 and 2400"; } return (year % 4) === 0 && (year % 400 === 0 || year % 100 !== 0); } /** * return the English month name, abbreviated to three letters, for the * month number. * * @param i month number, from 1 to 12. * @return the month name, like "Jan" or "Dec" */ static monthNameAbbrev(i) { return TimeUtil.MONTH_NAMES[i]; } /** * return the month number for the English month name, such as "Jan" (1) or * "December" (12). The first three letters are used to look up the number, * and must be one of: "Jan", "Feb", "Mar", "Apr", "May", "Jun", * "Jul", "Aug", "Sep", "Oct", "Nov", or "Dec" (case insensitive). * @param s the name (case-insensitive, only the first three letters are used.) * @return the number, for example 1 for "January" * @throws ParseException when month name is not recognized. */ static monthNumber(s) { if (s.length < 3) { throw "need at least three letters"; } s = s.substring(0, 3); for ( var i = 1; i < 13; i++) { if (s.toUpperCase()===TimeUtil.MONTH_NAMES[i].toUpperCase()) { return i; } } throw "Unable to parse month"; } /** * return the day of year for the given year, month, and day. For example, in * Jython: *
     * {@code
     * from org.hapiserver.TimeUtil import *
     * print dayOfYear( 2020, 4, 21 ) # 112
     * }
     * 
* * @param year the year * @param month the month, from 1 to 12. * @param day the day in the month. * @return the day of year. */ static dayOfYear(year, month, day) { if (month === 1) { return day; } if (month < 1) { throw "month must be greater than 0."; } if (month > 12) { throw "month must be less than 12."; } if (day > 366) { throw "day (" + day + ") must be less than 366."; } var leap = TimeUtil.isLeapYear(year) ? 1 : 0; return TimeUtil.DAY_OFFSET[leap][month] + day; } /** * return "2" (February) for 45 for example. * @param year the year * @param doy the day of year. * @return the month 1-12 of the day. */ static monthForDayOfYear(year, doy) { var leap = TimeUtil.isLeapYear(year) ? 1 : 0; var dayOffset = TimeUtil.DAY_OFFSET[leap]; if (doy < 1) throw "doy must be 1 or more"; if (doy > dayOffset[13]) { throw "doy must be less than or equal to " + dayOffset[13]; } for ( var i = 12; i > 1; i--) { if (dayOffset[i] < doy) { return i; } } return 1; } /** * This class is not to be instantiated. */ constructor() { } /** * count off the days between startTime and stopTime, but not including * stopTime. For example, countOffDays("1999-12-31Z", "2000-01-03Z") * will return [ "1999-12-31Z", "2000-01-01Z", "2000-01-02Z" ]. * * @param startTime an iso time string * @param stopTime an iso time string * @return array of times, complete days, in the form $Y-$m-$d */ static countOffDays(startTime, stopTime) { if (stopTime.length < 10 || /[0-9]/.test(stopTime.charAt(10))) { throw "arguments must be $Y-$m-$dZ"; } var t1 var t2; try { t1 = TimeUtil.parseISO8601Time(startTime); t2 = TimeUtil.parseISO8601Time(stopTime); } catch (ex) { throw ex; } var j1 = TimeUtil.julianDay(t1[0], t1[1], t1[2]); var j2 = TimeUtil.julianDay(t2[0], t2[1], t2[2]); var result = []; var time = TimeUtil.normalizeTimeString(startTime).substring(0, 10) + 'Z'; stopTime = TimeUtil.floor(stopTime).substring(0, 10) + 'Z'; var i = 0; var nn = TimeUtil.isoTimeToArray(time); while (time < stopTime) { result[i] = time; nn[2] = nn[2] + 1; if (nn[2] > 28) TimeUtil.normalizeTime(nn); time = sprintf("%04d-%02d-%02dZ",nn[0], nn[1], nn[2]); i += 1; } return result; } /** * return the next day boundary. Note hours, minutes, seconds and * nanoseconds are ignored. * * @param day any isoTime format string. * @return the next day in $Y-$m-$dZ * @see #ceil(java.lang.String) * @see #previousDay(java.lang.String) */ static nextDay(day) { var nn = TimeUtil.isoTimeToArray(day); nn[2] = nn[2] + 1; TimeUtil.normalizeTime(nn); return sprintf("%04d-%02d-%02dZ",nn[0], nn[1], nn[2]); } /** * return the previous day boundary. Note hours, minutes, seconds and * nanoseconds are ignored. * * @param day any isoTime format string. * @return the next day in $Y-$m-$dZ * @see #floor(java.lang.String) * @see #nextDay(java.lang.String) */ static previousDay(day) { var nn = TimeUtil.isoTimeToArray(day); nn[2] = nn[2] - 1; TimeUtil.normalizeTime(nn); return sprintf("%04d-%02d-%02dZ",nn[0], nn[1], nn[2]); } /** * return the $Y-$m-$dT00:00:00.000000000Z of the next boundary, or the same * value (normalized) if we are already at a boundary. * * @param time any isoTime format string. * @return the next midnight or the value if already at midnight. */ static ceil(time) { time = TimeUtil.normalizeTimeString(time); if (time.substring(11)=="00:00:00.000000000Z") { return time; } else { return TimeUtil.nextDay(time.substring(0, 11)).substring(0, 10) + "T00:00:00.000000000Z"; } } /** * return the $Y-$m-$dT00:00:00.000000000Z of the next boundary, or the same * value (normalized) if we are already at a boundary. * * @param time any isoTime format string. * @return the previous midnight or the value if already at midnight. */ static floor(time) { time = TimeUtil.normalizeTimeString(time); if (time.substring(11)=="00:00:00.000000000Z") { return time; } else { return time.substring(0, 10) + "T00:00:00.000000000Z"; } } /** * return $Y-$m-$dT$H:$M:$S.$(subsec,places=9)Z * * @param time any isoTime format string. * @return the time in standard form. */ static normalizeTimeString(time) { var nn = TimeUtil.isoTimeToArray(time); TimeUtil.normalizeTime(nn); return sprintf("%d-%02d-%02dT%02d:%02d:%02d.%09dZ",nn[0], nn[1], nn[2], nn[3], nn[4], nn[5], nn[6]); } /** * return the time as milliseconds since 1970-01-01T00:00Z. This does not * include leap seconds. For example, in Jython: *
     * {@code
     * from org.hapiserver.TimeUtil import *
     * x= toMillisecondsSince1970('2000-01-02T00:00:00.0Z')
     * print x / 86400000   # 10958.0 days
     * print x % 86400000   # and no milliseconds
     * }
     * 
* * @param time the isoTime, which is parsed using * DateTimeFormatter.ISO_INSTANT.parse. * @return number of non-leap-second milliseconds since 1970-01-01T00:00Z. * @see #fromMillisecondsSince1970(long) */ static toMillisecondsSince1970(time) { time = TimeUtil.normalizeTimeString(time); return new Date(time).getTime(); } /** * return the array formatted as ISO8601 time, formatted to nanoseconds. * For example, int[] nn = new int[] { 1999, 12, 31, 23, 0, 0, 0 } is * formatted to "1999-12-31T23:00:00.000000000Z"; * @param nn the decomposed time * @return the formatted time. * @see #isoTimeToArray(java.lang.String) */ static isoTimeFromArray(nn) { if (nn[1] === 1 && nn[2] > 31) { var month = TimeUtil.monthForDayOfYear(nn[0], nn[2]); var dom1 = TimeUtil.dayOfYear(nn[0], month, 1); nn[2] = nn[2] - dom1 + 1; nn[1] = month; } return sprintf("%04d-%02d-%02dT%02d:%02d:%02d.%09dZ",nn[0], nn[1], nn[2], nn[3], nn[4], nn[5], nn[6]); } /** * format the time range components into iso8601 time range. * @param timerange 14-element time range * @return efficient representation of the time range */ static formatIso8601TimeRange(timerange) { var ss1 = TimeUtil.formatIso8601TimeInTimeRange(timerange, 0); var ss2 = TimeUtil.formatIso8601TimeInTimeRange(timerange, TimeUtil.TIME_DIGITS); var firstNonZeroDigit = 7; while (firstNonZeroDigit > 3 && timerange[firstNonZeroDigit - 1] === 0 && timerange[firstNonZeroDigit + TimeUtil.TIME_DIGITS - 1] === 0) { firstNonZeroDigit -= 1; } switch (firstNonZeroDigit) { case 2: return ss1.substring(0, 10) + "/" + ss2.substring(0, 10); case 3: return ss1.substring(0, 10) + "/" + ss2.substring(0, 10); case 4: return ss1.substring(0, 16) + "Z/" + ss2.substring(0, 16) + "Z"; case 5: return ss1.substring(0, 16) + "Z/" + ss2.substring(0, 16) + "Z"; case 6: return ss1.substring(0, 19) + "Z/" + ss2.substring(0, 19) + "Z"; default: return ss1 + "/" + ss2; } } /** * return the string as a formatted string, which can be at an offset of seven positions * to format the end date. * @param nn fourteen-element array of [ Y m d H M S nanos Y m d H M S nanos ] * @param offset 0 or 7 * @return formatted time "1999-12-31T23:00:00.000000000Z" * @see #isoTimeFromArray(int[]) */ static formatIso8601TimeInTimeRange(nn, offset) { switch (offset) { case 0: return TimeUtil.isoTimeFromArray(nn); case 7: var copy = TimeUtil.getStopTime(nn); return TimeUtil.isoTimeFromArray(copy); default: throw "offset must be 0 or 7"; } } /** * return the string as a formatted string. * @param nn seven-element array of [ Y m d H M S nanos ] * @return formatted time "1999-12-31T23:00:00.000000000Z" * @see #isoTimeFromArray(int[]) */ static formatIso8601Time(nn) { return TimeUtil.isoTimeFromArray(nn); } /** * format the duration into human-readable time, for example * [ 0, 0, 7, 0, 0, 6 ] is formatted into "P7DT6S" * @param nn seven-element array of [ Y m d H M S nanos ] * @return ISO8601 duration */ static formatIso8601Duration(nn) { var units = ['Y', 'M', 'D', 'H', 'M', 'S']; if (nn.length > 7) throw "decomposed time can have at most 7 digits"; var sb = "P"; var n = (nn.length < 5) ? nn.length : 5; var needT = false; for ( var i = 0; i < n; i++) { if (i === 3) needT = true; if (nn[i] > 0) { if (needT) { sb+= "T"; needT = false; } sb+= nn[i] + units[i]; } } if (nn.length > 5 && nn[5] > 0 || nn.length > 6 && nn[6] > 0 || sb.length === 2) { if (needT) { sb+= "T"; } var seconds = nn[5]; var nanoseconds = nn.length === 7 ? nn[6] : 0; if (nanoseconds === 0) { sb+= seconds; } else { if (nanoseconds % 1000000 === 0) { sb+= sprintf("%.3f",seconds + nanoseconds / 1e9); } else { if (nanoseconds % 1000 === 0) { sb+= sprintf("%.6f",seconds + nanoseconds / 1e9); } else { sb+= sprintf("%.9f",seconds + nanoseconds / 1e9); } } } sb+= "S"; } if (sb.length === 1) { if (nn.length > 3) { sb+= "T0S"; } else { sb+= "0D"; } } return sb; } static iso8601duration = "P((\\d+)Y)?((\\d+)M)?((\\d+)D)?(T((\\d+)H)?((\\d+)M)?((\\d?\\.?\\d+)S)?)?"; /** * Pattern matching valid ISO8601 durations, like "P1D" and "PT3H15M" */ static iso8601DurationPattern = new RegExp("P((\\d+)Y)?((\\d+)M)?((\\d+)D)?(T((\\d+)H)?((\\d+)M)?((\\d?\\.?\\d+)S)?)?"); /** * returns a 7 element array with [year,mon,day,hour,min,sec,nanos]. Note * this does not allow fractional day, hours or minutes! Examples * include: * TODO: there exists more complete code elsewhere. * * @param stringIn theISO8601 duration. * @return 7-element array with [year,mon,day,hour,min,sec,nanos] * @throws ParseException if the string does not appear to be valid. * @see #iso8601duration * @see #TIME_DIGITS * */ static parseISO8601Duration(stringIn) { var m = TimeUtil.iso8601DurationPattern.exec(stringIn); if (m!=null) { var dsec = TimeUtil.parseDouble(m[13], 0); var sec = Math.trunc( dsec ); var nanosec = Math.trunc( ((dsec - sec) * 1e9) ); return [TimeUtil.parseIntegerDeft(m[2], 0), TimeUtil.parseIntegerDeft(m[4], 0), TimeUtil.parseIntegerDeft(m[6], 0), TimeUtil.parseIntegerDeft(m[9], 0), TimeUtil.parseIntegerDeft(m[11], 0), sec, nanosec]; } else { if (stringIn.contains("P") && stringIn.contains("S") && !stringIn.contains("T")) { throw "ISO8601 duration expected but not found. Was the T missing before S?"; } else { throw "ISO8601 duration expected but not found."; } } } /** * return the UTC current time, to the millisecond, in seven components. * @return the current time, to the millisecond */ static now() { var s = new Date().toISOString(); return [ parseInt(s.substring(0,4)), parseInt(s.substring(5,7)), parseInt(s.substring(8,10)), parseInt(s.substring(11,13)), parseInt(s.substring(14,16)), parseInt(s.substring(17,19)), parseInt(s.substring(20,23)) * 1000000 ] } /** * return seven-element array [ year, months, days, hours, minutes, seconds, nanoseconds ] * preserving the day of year notation if this was used. See the class * documentation for allowed time formats, which are a subset of ISO8601 * times. This also supports "now", "now-P1D", and other simple extensions. Note * ISO8601-1:2019 disallows 24:00 to be used for the time, but this is still allowed here. * The following are valid inputs: * * @param time isoTime to decompose * @return the decomposed time * @throws IllegalArgumentException when the time cannot be parsed. * @see #isoTimeFromArray(int[]) * @see #parseISO8601Time(java.lang.String) */ static isoTimeToArray(time) { var result; if (time.length === 4) { result = [parseInt(time), 1, 1, 0, 0, 0, 0]; } else { if (time.startsWith("now") || time.startsWith("last")) { var n; var remainder = null; if (time.startsWith("now")) { n = TimeUtil.now(); remainder = time.substring(3); } else { var p = new RegExp("last([a-z]+)([\\+|\\-]P.*)?"); var m = p.exec(time); if (m!=null) { n = TimeUtil.now(); var unit = m[1]; remainder = m[2]; var idigit; switch (unit) { case "year": idigit = 1; break case "month": idigit = 2; break case "day": idigit = 3; break case "hour": idigit = 4; break case "minute": idigit = 5; break case "second": idigit = 6; break default: throw "unsupported unit: " + unit; } for ( var id = Math.max(1, idigit); id < TimeUtil.DATE_DIGITS; id++) { n[id] = 1; } for ( var id = Math.max(TimeUtil.DATE_DIGITS, idigit); id < TimeUtil.TIME_DIGITS; id++) { n[id] = 0; } } else { throw "expected lastday+P1D, etc"; } } if ( remainder === undefined || remainder === null || remainder.length === 0) { return n; } else { if (remainder.charAt(0) == '-') { try { return TimeUtil.subtract(n, TimeUtil.parseISO8601Duration(remainder.substring(1))); } catch (ex) { throw ex; } } else { if (remainder.charAt(0) == '+') { try { return TimeUtil.add(n, TimeUtil.parseISO8601Duration(remainder.substring(1))); } catch (ex) { throw ex; } } } } return TimeUtil.now(); } else { if (time.length < 7) { throw "time must have 4 or greater than 7 elements"; } if (time.length === 7) { if (time.charAt(4) == 'W') { // 2022W08 var year = TimeUtil.parseInteger(time.substring(0, 4)); var week = TimeUtil.parseInteger(time.substring(5)); result = [year, 0, 0, 0, 0, 0, 0]; TimeUtil.fromWeekOfYear(year, week, result); time = ""; } else { result = [TimeUtil.parseInteger(time.substring(0, 4)), TimeUtil.parseInteger(time.substring(5, 7)), 1, 0, 0, 0, 0]; time = ""; } } else { if (time.length === 8) { if (time.charAt(5) == 'W') { // 2022-W08 var year = TimeUtil.parseInteger(time.substring(0, 4)); var week = TimeUtil.parseInteger(time.substring(6)); result = [year, 0, 0, 0, 0, 0, 0]; TimeUtil.fromWeekOfYear(year, week, result); time = ""; } else { result = [TimeUtil.parseInteger(time.substring(0, 4)), 1, TimeUtil.parseInteger(time.substring(5, 8)), 0, 0, 0, 0]; time = ""; } } else { if (time.charAt(8) == 'T') { result = [TimeUtil.parseInteger(time.substring(0, 4)), 1, TimeUtil.parseInteger(time.substring(5, 8)), 0, 0, 0, 0]; time = time.substring(9); } else { if (time.charAt(8) == 'Z') { result = [TimeUtil.parseInteger(time.substring(0, 4)), 1, TimeUtil.parseInteger(time.substring(5, 8)), 0, 0, 0, 0]; time = time.substring(9); } else { result = [TimeUtil.parseInteger(time.substring(0, 4)), TimeUtil.parseInteger(time.substring(5, 7)), TimeUtil.parseInteger(time.substring(8, 10)), 0, 0, 0, 0]; if (time.length === 10) { time = ""; } else { time = time.substring(11); } } } } } if (time.endsWith("Z")) { time = time.substring(0, time.length - 1); } if (time.length >= 2) { result[3] = TimeUtil.parseInteger(time.substring(0, 2)); } if (time.length >= 5) { result[4] = TimeUtil.parseInteger(time.substring(3, 5)); } if (time.length >= 8) { result[5] = TimeUtil.parseInteger(time.substring(6, 8)); } if (time.length > 9) { result[6] = Math.trunc( (Math.pow(10, 18 - time.length)) ) * TimeUtil.parseInteger(time.substring(9)); } TimeUtil.normalizeTime(result); } } return result; } /** * Rewrite the time using the format of the example time, which must start with * $Y-$jT, $Y-$jZ, or $Y-$m-$d. For example, *
     * {@code
     * from org.hapiserver.TimeUtil import *
     * print rewriteIsoTime( '2020-01-01T00:00Z', '2020-112Z' ) # ->  '2020-04-21T00:00Z'
     * }
     * 
This allows direct comparisons of times for sorting. * This works by looking at the character in the 8th position (starting with zero) of the * exampleForm to see if a T or Z is present (YYYY-jjjTxxx). * * TODO: there's * an optimization here, where if input and output are both $Y-$j or both * $Y-$m-$d, then we need not break apart and recombine the time * (isoTimeToArray call can be avoided). * * @param exampleForm isoTime string. * @param time the time in any allowed isoTime format * @return same time but in the same form as exampleForm. */ static reformatIsoTime(exampleForm, time) { var c = exampleForm.charAt(8); var nn = TimeUtil.isoTimeToArray(TimeUtil.normalizeTimeString(time)); switch (c) { case 'T': // $Y-$jT nn[2] = TimeUtil.dayOfYear(nn[0], nn[1], nn[2]); nn[1] = 1; time = sprintf("%d-%03dT%02d:%02d:%02d.%09dZ",nn[0], nn[2], nn[3], nn[4], nn[5], nn[6]); break case 'Z': nn[2] = TimeUtil.dayOfYear(nn[0], nn[1], nn[2]); nn[1] = 1; time = sprintf("%d-%03dZ",nn[0], nn[2]); break default: if (exampleForm.length === 10) { c = 'Z'; } else { c = exampleForm.charAt(10); } if (c == 'T') { // $Y-$jT time = sprintf("%d-%02d-%02dT%02d:%02d:%02d.%09dZ",nn[0], nn[1], nn[2], nn[3], nn[4], nn[5], nn[6]); } else { if (c == 'Z') { time = sprintf("%d-%02d-%02dZ",nn[0], nn[1], nn[2]); } } break } if (exampleForm.endsWith("Z")) { return time.substring(0, exampleForm.length - 1) + "Z"; } else { return time.substring(0, exampleForm.length); } } static VALID_FIRST_YEAR = 1900; static VALID_LAST_YEAR = 2100; /** * this returns true or throws an IllegalArgumentException indicating the problem. * @param time the seven-component time. * @return true or throws an IllegalArgumentException */ static isValidTime(time) { var year = time[0]; if (year < TimeUtil.VALID_FIRST_YEAR) throw "invalid year at position 0"; if (year > TimeUtil.VALID_LAST_YEAR) throw "invalid year at position 0"; var month = time[1]; if (month < 1) throw "invalid month at position 1"; if (month > 12) throw "invalid month at position 1"; var leap = TimeUtil.isLeapYear(year) ? 1 : 0; var dayOfMonth = time[2]; if (month > 1) { if (dayOfMonth > TimeUtil.DAYS_IN_MONTH[leap][month]) { throw "day of month is too large at position 2"; } } else { if (dayOfMonth > TimeUtil.DAY_OFFSET[leap][13]) { throw "day of year is too large at position 2"; } } if (dayOfMonth < 1) throw "day is less than 1 at position 2"; return true; } /** * return the number of days in the month. * @param year the year * @param month the month * @return the number of days in the month. * @see #isLeapYear(int) */ static daysInMonth(year, month) { var leap = TimeUtil.isLeapYear(year) ? 1 : 0; return TimeUtil.DAYS_IN_MONTH[leap][month]; } /** * normalize the decomposed (seven digit) time by expressing day of year and month and day * of month, and moving hour="24" into the next day. This also handles day * increment or decrements, by: * Note that [Y,1,dayOfYear,...] is accepted, but the result will be Y,m,d. * @param time the seven-component time Y,m,d,H,M,S,nanoseconds */ static normalizeTime(time) { while (time[6] >= 1000000000) { time[5] += 1; time[6] -= 1000000000; } while (time[5] > 59) { // TODO: leap seconds? time[4] += 1; time[5] -= 60; } while (time[4] > 59) { time[3] += 1; time[4] -= 60; } while (time[3] >= 24) { time[2] += 1; time[3] -= 24; } if (time[6] < 0) { time[5] -= 1; time[6] += 1000000000; } if (time[5] < 0) { time[4] -= 1; // take a minute time[5] += 60; } if (time[4] < 0) { time[3] -= 1; // take an hour time[4] += 60; } if (time[3] < 0) { time[2] -= 1; // take a day time[3] += 24; } if (time[2] < 1) { time[1] -= 1; // take a month var daysInMonth; if (time[1] === 0) { daysInMonth = 31; } else { if (TimeUtil.isLeapYear(time[0])) { // This was TimeUtil.DAYS_IN_MONTH[isLeapYear(time[0]) ? 1 : 0][time[1]] . TODO: review! daysInMonth = TimeUtil.DAYS_IN_MONTH[1][time[1]]; } else { daysInMonth = TimeUtil.DAYS_IN_MONTH[0][time[1]]; } } time[2] += daysInMonth; } if (time[1] < 1) { time[0] -= 1; // take a year time[1] += 12; } if (time[3] > 24) { throw "time[3] is greater than 24 (hours)"; } if (time[1] > 12) { time[0] += 1; time[1] -= 12; } if (time[1] === 12 && time[2] > 31 && time[2] < 62) { time[0] += 1; time[1] = 1; time[2] -= 31; return; } var leap = TimeUtil.isLeapYear(time[0]) ? 1 : 0; if (time[2] === 0) { //TODO: tests don't hit this branch, and I'm not sure it can occur. time[1] -= 1; if (time[1] === 0) { time[0] -= 1; time[1] = 12; } time[2] = TimeUtil.DAYS_IN_MONTH[leap][time[1]]; } var d = TimeUtil.DAYS_IN_MONTH[leap][time[1]]; while (time[2] > d) { time[1] += 1; time[2] -= d; d = TimeUtil.DAYS_IN_MONTH[leap][time[1]]; if (time[1] > 12) { throw "time[2] is too big"; } } } /** * return the julianDay for the year month and day. This was verified * against another calculation (julianDayWP, commented out above) from * http://en.wikipedia.org/wiki/Julian_day. Both calculations have 20 * operations. * * @param year calendar year greater than 1582. * @param month the month number 1 through 12. * @param day day of month. For day of year, use month=1 and doy for day. * @return the Julian day * @see #fromJulianDay(int) */ static julianDay(year, month, day) { if (year <= 1582) { throw "year must be more than 1582"; } var jd = 367 * year - Math.floor(7 * (year + Math.floor((month + 9) / 12)) / 4) - Math.floor(3 * (Math.floor((year + Math.floor((month - 9) / 7)) / 100) + 1) / 4) + Math.floor(275 * month / 9) + day + 1721029; return jd; } /** * Break the Julian day apart into month, day year. This is based on * http://en.wikipedia.org/wiki/Julian_day (GNU Public License), and was * introduced when toTimeStruct failed when the year was 1886. * * @see #julianDay( int year, int mon, int day ) * @param julian the (integer) number of days that have elapsed since the * initial epoch at noon Universal Time (UT) Monday, January 1, 4713 BC * @return a TimeStruct with the month, day and year fields set. */ static fromJulianDay(julian) { var j = julian + 32044; var g = Math.floor(j / 146097); var dg = j % 146097; var c = Math.floor((Math.floor(dg / 36524) + 1) * 3 / 4); var dc = dg - c * 36524; var b = Math.floor(dc / 1461); var db = dc % 1461; var a = Math.floor((Math.floor(db / 365) + 1) * 3 / 4); var da = db - a * 365; var y = g * 400 + c * 100 + b * 4 + a; var m = Math.floor((da * 5 + 308) / 153) - 2; var d = da - Math.floor((m + 4) * 153 / 5) + 122; var Y = y - 4800 + Math.floor((m + 2) / 12); var M = (m + 2) % 12 + 1; var D = d + 1; var result = []; result[0] = Y; result[1] = M; result[2] = D; result[3] = 0; result[4] = 0; result[5] = 0; result[6] = 0; return result; } /** * calculate the day of week, where 0 means Monday, 1 means Tuesday, etc. For example, * 2022-03-12 is a Saturday, so 5 is returned. * @param year the year * @param month the month * @param day the day of the month * @return the day of the week. */ static dayOfWeek(year, month, day) { var jd = TimeUtil.julianDay(year, month, day); var daysSince2022 = jd - TimeUtil.julianDay(2022, 1, 1); var mod7 = (daysSince2022 - 2) % 7; if (mod7 < 0) mod7 = mod7 + 7; return mod7; } /** * calculate the week of year, inserting the month into time[1] and day into time[2] * for the Monday which is the first day of that week. Note week 0 is excluded from * ISO8601, but since the Linux date command returns this in some cases, it is allowed to * mean the same as week 52 of the previous year. See * Wikipedia ISO8601#Week_dates. * * @param year the year of the week. * @param weekOfYear the week of the year, where week 01 is starting with the Monday in the period 29 December - 4 January. * @param time the result is placed in here, where time[0] is the year provided, and the month and day are calculated. */ static fromWeekOfYear(year, weekOfYear, time) { time[0] = year; var day = TimeUtil.dayOfWeek(year, 1, 1); var doy; if (day < 4) { doy = (weekOfYear * 7 - 7 - day) + 1; if (doy < 1) { time[0] = time[0] - 1; if (TimeUtil.isLeapYear(time[0])) { // was doy= doy + ( isLeapYear(time[0]) ? 366 : 365 ); TODO: verify doy = doy + 366; } else { doy = doy + 365; } } } else { doy = weekOfYear * 7 - day + 1; } time[1] = 1; time[2] = doy; TimeUtil.normalizeTime(time); } /** * use consistent naming so that the parser is easier to find. * @param string iso8601 time like "2022-03-12T11:17" (Z is assumed). * @return seven-element decomposed time [ Y, m, d, H, M, S, N ] * @throws ParseException when the string cannot be parsed. * @see #isoTimeToArray(java.lang.String) */ static parseISO8601Time(string) { return TimeUtil.isoTimeToArray(string); } /** * return true if the time appears to be properly formatted. Properly formatted strings include: * @param {string} time * @returns {invalid|Boolean} */ static isValidFormattedTime( time ) { time; var b1= time.length>0; var b2= (/[0-9]/.test(time.charAt(0)) || time.charAt(0) === 'P' || time.startsWith("now") || time.startsWith("last") ); return b1 && b2; } /** * parse the ISO8601 time range, like "1998-01-02/1998-01-17", into * start and stop times, returned in a 14 element array of ints. * @param stringIn string to parse, like "1998-01-02/1998-01-17" * @return the time start and stop [ Y,m,d,H,M,S,nano, Y,m,d,H,M,S,nano ] * @throws ParseException when the string cannot be used */ static parseISO8601TimeRange(stringIn) { var ss = stringIn.split("/"); if (ss.length !== 2) { throw "expected one slash (/) splitting start and stop times."; } if ( !TimeUtil.isValidFormattedTime(ss[0]) ) throw "first time/duration is misformatted. Should be ISO8601 time or duration like P1D."; if ( !TimeUtil.isValidFormattedTime(ss[1]) ) throw "second time/duration is misformatted. Should be ISO8601 time or duration like P1D."; var result = [0,0,0,0,0,0,0,0,0,0,0,0,0,0]; if (ss[0].startsWith("P")) { var duration = TimeUtil.parseISO8601Duration(ss[0]); var time = TimeUtil.isoTimeToArray(ss[1]); for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { result[i] = time[i] - duration[i]; } TimeUtil.normalizeTime(result); TimeUtil.setStopTime(time, result); return result; } else { if (ss[1].startsWith("P")) { var time = TimeUtil.isoTimeToArray(ss[0]); var duration = TimeUtil.parseISO8601Duration(ss[1]); TimeUtil.setStartTime(time, result); var stoptime = []; for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { stoptime[i] = time[i] + duration[i]; } TimeUtil.normalizeTime(stoptime); TimeUtil.setStopTime(stoptime, result); return result; } else { var starttime = TimeUtil.isoTimeToArray(ss[0]); var stoptime = TimeUtil.isoTimeToArray(ss[1]); TimeUtil.setStartTime(starttime, result); TimeUtil.setStopTime(stoptime, result); return result; } } } /** * subtract the offset from the base time. * * @param base a time * @param offset offset in each component. * @return a time */ static subtract(base, offset) { var result = []; for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { result[i] = base[i] - offset[i]; } if (result[0] > 400) { TimeUtil.normalizeTime(result); } return result; } /** * add the offset to the base time. This should not be used to combine two * offsets, because the code has not been verified for this use. * * @param base a time * @param offset offset in each component. * @return a time */ static add(base, offset) { var result = []; for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { result[i] = base[i] + offset[i]; } TimeUtil.normalizeTime(result); return result; } /** * true if t1 is after t2. * @param t1 seven-component time * @param t2 seven-component time * @return true if t1 is after t2. */ static gt(t1, t2) { TimeUtil.normalizeTime(t1); TimeUtil.normalizeTime(t2); for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { if (t1[i] > t2[i]) { return true; } else { if (t1[i] < t2[i]) { return false; } } } return false; } /** * true if t1 is equal to t2. * @param t1 seven-component time * @param t2 seven-component time * @return true if t1 is equal to t2. */ static eq(t1, t2) { TimeUtil.normalizeTime(t1); TimeUtil.normalizeTime(t2); for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { if (t1[i] !== t2[i]) { return false; } } return true; } /** * format the time, but omit trailing zeros. $Y-$m-$dT$H:$M is the coursest resolution returned. * @param time seven element time range * @return formatted time, possibly truncated to minutes, seconds, milliseconds, or microseconds * @see #formatIso8601TimeInTimeRangeBrief(int[] time, int offset ) */ static formatIso8601TimeBrief(time) { return TimeUtil.formatIso8601TimeInTimeRangeBrief(time, 0); } /** * format the time, but omit trailing zeros. $Y-$m-$dT$H:$M is the coursest resolution returned. * @param time seven element time range * @param offset the offset into the time array (7 for stop time in 14-element range array). * @return formatted time, possibly truncated to minutes, seconds, milliseconds, or microseconds * @see #formatIso8601TimeBrief(int[]) */ static formatIso8601TimeInTimeRangeBrief(time, offset) { var stime = TimeUtil.formatIso8601TimeInTimeRange(time, offset); var nanos = time[TimeUtil.COMPONENT_NANOSECOND + offset]; var micros = nanos % 1000; var millis = nanos % 10000000; if (nanos === 0) { if (time[5 + offset] === 0) { return stime.substring(0, 16) + "Z"; } else { return stime.substring(0, 19) + "Z"; } } else { if (millis === 0) { return stime.substring(0, 23) + "Z"; } else { if (micros === 0) { return stime.substring(0, 26) + "Z"; } else { return stime; } } } } /** * return the next interval, given the 14-component time interval. This * has the restrictions: * @param timerange 14-component time interval. * @return 14-component time interval. */ static nextRange(timerange) { var result = []; var width = []; for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { width[i] = timerange[i + TimeUtil.TIME_DIGITS] - timerange[i]; } if (width[5] < 0) { width[5] = width[5] + 60; width[4] = width[4] - 1; } if (width[4] < 0) { width[4] = width[4] + 60; width[3] = width[3] - 1; } if (width[3] < 0) { width[3] = width[3] + 24; width[2] = width[2] - 1; } if (width[2] < 0) { var daysInMonth = TimeUtil.daysInMonth(timerange[TimeUtil.COMPONENT_YEAR], timerange[TimeUtil.COMPONENT_MONTH]); width[2] = width[2] + daysInMonth; width[1] = width[1] - 1; } if (width[1] < 0) { width[1] = width[1] + 12; width[0] = width[0] - 1; } // System.arraycopy( range, TimeUtil.TIME_DIGITS, result, 0, TimeUtil.TIME_DIGITS ); TimeUtil.setStartTime(TimeUtil.getStopTime(timerange), result); // This creates an extra array, but let's not worry about that. TimeUtil.setStopTime(TimeUtil.add(TimeUtil.getStopTime(timerange), width), result); return result; } /** * return the previous interval, given the 14-component time interval. This * has the restrictions: * @param timerange 14-component time interval. * @return 14-component time interval. */ static previousRange(timerange) { var result = []; var width = []; for ( var i = 0; i < TimeUtil.TIME_DIGITS; i++) { width[i] = timerange[i + TimeUtil.TIME_DIGITS] - timerange[i]; } if (width[5] < 0) { width[5] = width[5] + 60; width[4] = width[4] - 1; } if (width[4] < 0) { width[4] = width[4] + 60; width[3] = width[3] - 1; } if (width[3] < 0) { width[3] = width[3] + 24; width[2] = width[2] - 1; } if (width[2] < 0) { var daysInMonth = TimeUtil.daysInMonth(timerange[TimeUtil.COMPONENT_YEAR], timerange[TimeUtil.COMPONENT_MONTH]); width[2] = width[2] + daysInMonth; width[1] = width[1] - 1; } if (width[1] < 0) { width[1] = width[1] + 12; width[0] = width[0] - 1; } TimeUtil.setStopTime(TimeUtil.getStartTime(timerange), result); TimeUtil.setStartTime(TimeUtil.subtract(TimeUtil.getStartTime(timerange), width), result); return result; } /** * return true if this is a valid time range having a non-zero width. * @param timerange * @return */ static isValidTimeRange(timerange) { var start = TimeUtil.getStartTime(timerange); var stop = TimeUtil.getStopTime(timerange); return TimeUtil.isValidTime(start) && TimeUtil.isValidTime(stop) && TimeUtil.gt(stop, start); } }