argv= new HashMap();
if ( args!=null ) {
String[] ss2= args.split(";",-2);
for (String ss21 : ss2) {
int i3 = ss21.indexOf("=");
if (i3==-1) {
argv.put(ss21.trim(), "");
} else {
argv.put(ss21.substring(0, i3).trim(), ss21.substring(i3+1).trim());
}
}
}
String errm= fh.configure(argv);
if ( errm!=null ) {
throw new IllegalArgumentException(errm);
}
String id= argv.get("id");
if ( id!=null ) {
fieldHandlersById.put( id,fh );
}
}
} else {
handlers[i] = handler;
if (lengths[i] == 0) {
lengths[i] = formatCode_lengths[handler];
}
offsets[i] = pos;
if (lengths[i] < 1 || pos == -1) {
pos = -1;
//lengths[i] = -1; // bugfix: I wonder where this was used. removed to support "$-1Y $-1m $-1d $H$M"
} else {
pos += lengths[i];
}
}
int span=1;
String fieldRegex ;
if (lengths[i] == -1) {
fieldRegex ="(.*)";
} else {
String dots = ".........";
fieldRegex ="(" + dots.substring(0, lengths[i]) + ")";
}
if ( qualifiers[i]!=null ) {
String[] ss2= qualifiers[i].split(";");
for ( String ss21 : ss2 ) {
boolean okay=false;
String qual = ss21.trim();
if ( qual.equals("startTimeOnly") ) {
startTimeOnly= fc[i].charAt(0);
okay= true;
}
int idx= qual.indexOf("=");
if ( !okay && idx>-1 ) {
String name= qual.substring(0,idx).trim();
String val= qual.substring(idx+1).trim();
//FieldHandler fh= (FieldHandler) fieldHandlers.get(name);
//fh.parse( val, context, timeWidth );
switch (name) {
case "Y":
context.year= Integer.parseInt(val);
break;
case "m":
context.month= Integer.parseInt(val);
break;
case "d":
context.day= Integer.parseInt(val);
break;
case "j":
context.doy= Integer.parseInt(val);
break;
case "H":
context.hour= Integer.parseInt(val);
break;
case "M":
context.minute= Integer.parseInt(val);
break;
case "S":
context.seconds= Integer.parseInt(val);
break;
case "N":
context.nanos= Integer.parseInt(val);
break;
case "cadence":
span= Integer.parseInt(val);
break;
case "span":
span= Integer.parseInt(val);
break;
case "delta":
span= Integer.parseInt(val); // see https://github.com/hapi-server/uri-templates/wiki/Specification
break;
case "resolution":
span= Integer.parseInt(val);
break;
case "period":
if ( val.startsWith("P") ) {
try {
int[] r= DatumRangeUtil.parseISO8601Duration(val);
for ( int j=0; j<6; j++ ) {
if (r[j]>0 ) {
lsd= j;
lsdMult= r[j];
logger.log(Level.FINER, "lsd is now {0}, width={1}", new Object[]{lsd, lsdMult});
break;
}
}
} catch (ParseException ex) {
Logger.getLogger(TimeParser.class.getName()).log(Level.SEVERE, null, ex);
}
} else {
char code= val.charAt(val.length()-1);
switch (code) {
case 'Y':
lsd=0;
break;
case 'm':
lsd=1;
break;
case 'd':
lsd=2;
break;
case 'j':
lsd=2;
break;
case 'H':
lsd=3;
break;
case 'M':
lsd=4;
break;
case 'S':
lsd=5;
break;
default:
break;
}
lsdMult= Integer.parseInt(val.substring(0,val.length()-1) );
logger.log(Level.FINER, "lsd is now {0}, width={1}", new Object[]{lsd, lsdMult});
} break;
case "id":
; //TODO: orbit plug in handler...
break;
case "places":
; //TODO: this all needs to be redone...
break;
case "phasestart":
try {
phasestart= TimeUtil.create(val);
} catch (ParseException ex) {
logger.log(Level.SEVERE, null, ex);
}
break;
case "shift":
shift[i]= Integer.parseInt(val);
break;
case "":
;
break;
case "end":
if ( stopTimeDigit==AFTERSTOP_INIT ) {
startLsd= lsd;
stopTimeDigit= i;
} break;
case "regex":
//TODO: Evil regex, where you can craft regexs which will cause Denial of Service (ReDoS)
fieldRegex = val;
break;
default:
if ( !fieldHandlers.containsKey(fc[i]) ) {
throw new IllegalArgumentException("unrecognized/unsupported field: "+name + " in "+qual );
} break;
}
okay= true;
} else if ( !okay ) {
String name= qual.trim();
if ( name.equals("end") ) {
if ( stopTimeDigit==AFTERSTOP_INIT ) {
startLsd= lsd;
stopTimeDigit= i;
}
okay= true;
}
}
if ( !okay && ( qual.equals("Y") || qual.equals("m") || qual.equals("d") || qual.equals("j") ||
qual.equals("H") || qual.equals("M") || qual.equals("S")) ) {
throw new IllegalArgumentException( String.format( "%s must be assigned an integer value (e.g. %s=1) in %s", qual, qual, ss[i] ) );
}
if ( !okay ) {
if ( !fieldHandlers.containsKey(fc[i]) ) {
logger.log(Level.WARNING, "unrecognized/unsupported field:{0} in {1}", new Object[]{qual, ss[i]});
//TODO: check plug-in handlers like orbit...
//throw new IllegalArgumentException("unrecognized/unsupported field:"+qual+ " in " +ss[i] );
}
}
}
} else {
// http://sourceforge.net/p/autoplot/bugs/1506/
if ( fc[i].length()==1 ) {
char code= fc[i].charAt(0);
int thisLsd= -1;
switch (code) {
case 'Y':
thisLsd=0;
break;
case 'm':
thisLsd=1;
break;
case 'd':
thisLsd=2;
break;
case 'j':
thisLsd=2;
break;
case 'H':
thisLsd=3;
break;
case 'M':
thisLsd=4;
break;
case 'S':
thisLsd=5;
break;
default:
break;
}
if ( thisLsd==lsd ) { // allow subsequent repeat fields to reset (T$y$(m,delta=4)/$x_T$y$m$d.DAT)
lsdMult= 1;
}
}
}
if (handler < 100) {
if ( precision[handler] > lsd && lsdMult==1 ) { // omni2_h0_mrg1hr_$Y$(m,span=6)$d_v01.cdf. Essentially we ignore the $d.
lsd = precision[handler];
lsdMult= span;
logger.log(Level.FINER, "lsd is now {0}, width={1}", new Object[]{lsd, lsdMult});
}
}
regex1.append( fieldRegex );
regex1.append( quotePattern(delim[i]) );
}
timeWidth = new TimeStruct();
switch (lsd) { // see https://sourceforge.net/p/autoplot/bugs/1506/
case 0:
timeWidth.year = lsdMult;
break;
case 1:
timeWidth.month = lsdMult;
break;
case 2:
timeWidth.day = lsdMult;
break;
case 3:
timeWidth.hour = lsdMult;
break;
case 4:
timeWidth.minute = lsdMult;
break;
case 5:
timeWidth.seconds = lsdMult;
break;
case 6:
timeWidth.millis = lsdMult;
break;
case 7:
timeWidth.micros = lsdMult;
break;
case -1:
timeWidth.year= 8000;
break;
case 100: /* do nothing */ break; //TODO: handler needs to report it's lsd, if it affects.
}
if ( logger.isLoggable(Level.FINE) ) {
StringBuilder canonical= new StringBuilder( delim[0] );
for (int i = 1; i < ndigits; i++) {
canonical.append("$");
if ( qualifiers[i]==null ) {
canonical.append(fc[i]);
} else {
canonical.append("(").append(fc[i]).append(";").append(qualifiers[i]).append(")");
}
canonical.append(delim[i]);
}
logger.log( Level.FINE, "Canonical: {0}", canonical.toString());
}
if ( this.timeWidth.year!=0 || this.timeWidth.month!=0 ) {
this.timeWidthDatum= null;
} else {
this.timeWidthDatum= TimeUtil.toDatum(this.timeWidth);
}
this.delims = delim;
this.regex = regex1.toString();
}
/**
* the last date represented is 9000/01/01
*/
private static final int MAX_VALID_YEAR = 9000;
/**
* the earliest date represented is 1000/01/01
*/
private static final int MIN_VALID_YEAR = 1000;
/**
* Provide standard means of indicating this appears to be a spec by
* looking for something that would assert the year.
* @param spec
* @return true if the string appears to be a spec.
*/
public static boolean isSpec(String spec) {
spec= makeCanonical( spec );
spec= spec.replaceAll(",",";");
if ( spec.contains("$Y")||spec.contains("$y")||spec.contains("$(Y;")||spec.contains("$(y;") ) return true;
if ( spec.contains(";Y=") ) return true;
if ( spec.contains("$o;")|| spec.contains("$(o;") ) return true;
if ( spec.contains("$(periodic;")) return true;
// See https://sourceforge.net/p/autoplot/bugs/2718/
//if ( spec.contains("$x") ) return true; // Note, $x alone will mean all time, something like year 1000-9000.
//if ( spec.contains("$(x;") ) return true; // Note, $x alone will mean all time, something like year 1000-9000.
return false;
}
/**
* Create a TimeParser object, which is the fast time parser for use when a known format specification is used to
* parse many instances of a formatted string. For example, this would be used to interpret the times in an text file,
* but not times entered in a time range GUI to control an axis. This can also be and is used for filenames,
* for example omni2_h0_mrg1hr_$Y$(m,span=6)01_v01.cdf.
*
* Note field lengths are used when formatting the data, but when parsing often fractional components are accepted. For
* example, the format might be "%Y %j %H", and "2012 365 12.003" is accepted.
*
* Note also that often $(Y) is used where %{Y} is used. These are equivalent, and useful when $() interferes with parsing
* elsewhere.
*
* URI_Templates is a public project with translations to Java, JavaScript, and Python, and provides an specification for
* this. See https://github.com/hapi-server/uri-templates/wiki/Specification .
* {@code
* $[fieldLength]<1-char code> or
* $[fieldLength]()
* $[fieldLength](;qualifiers)
*
* fieldLength=0 --> makes field length indeterminate, deliminator must follow.
*
* $Y 4-digit year
* $y 2-digit year
* $j 3-digit day of year
* $m 2-digit month
* $b 3-char month name (jan,feb,mar,apr,may,jun,jul,aug,sep,oct,nov,dec. Sorry, rest of world...)
* $d 2-digit day
* $H 2-digit hour
* $M 2-digit minute
* $S 2-digit second
* $(milli) 3-digit milliseconds
* $(ignore) skip this field
* $x skip this field
* $(enum) skip this field. If id is specified, then id can be retrieved.
* $v skip this field
* $p am/pm field
* $z RFC-822 numeric time zone
* $(hrinterval;values=0,1,2,3) enumeration of part of day
* $(subsec;places=6) fractional seconds (6->microseconds)
* $(periodic;offset=0;start=2000-001;period=P1D)
*
* Qualifiers:
* span=
* delta=
* Y=2004 Also for Y,m,d,H,M,S
*
* For example:
* $(j;Y=2004) means the day-of-year, within the year 2004.
* $(H;Y=2004;j=117) means the hour of day 2004-117
* $(m;span=6) means the 6-month interval starting at the given month.
*
* }
*
* @param formatString the format string.
* @return the time parser.
*/
public static TimeParser create(String formatString) {
if ( formatString.length()==0 ) {
throw new IllegalArgumentException("formatString length must be at least one character");
}
HashMap map= new HashMap();
map.put("o",new OrbitFieldHandler());
map.put("v",new IgnoreFieldHandler()); // note this is often replaced.
return new TimeParser(formatString,map);
}
/**
* create the time parser, and add specialized handlers. Note the
* typical route create(formatString) adds handlers for orbits ($o) and version
* numbers ($v).
*
* @param formatString like $Y$m$dT$H
* @param fieldName name for the special field, like "o"
* @param handler handler for the special field, like OrbitFieldHandler
* @param moreHandler additional name/handler pairs.
* @return the configured TimeParser, ready to use.
*/
public static TimeParser create(String formatString, String fieldName, FieldHandler handler, Object ... moreHandler ) {
if ( formatString.length()==0 ) {
throw new IllegalArgumentException("formatString length must be at least one character");
}
HashMap map = new HashMap();
map.put(fieldName, handler);
if ( moreHandler!=null ) {
for ( int i=0; i
* tp.parse("2014-01-06T02").getTime( Units.us2000 )
*
* Since this the TimeParser has a state, it is not safe to use simultaneously
* by multiple threads. Each thread should create its own parser.
*
* @param timeString string containing a time
* @return a reference to this TimeParser object, which now contains the time.
* @throws ParseException if the string cannot be parsed.
*/
public TimeParser parse(String timeString) throws ParseException {
return parse( timeString, null );
}
private void copyTime( TimeStruct src, TimeStruct dst ) {
dst.year = src.year;
dst.month = src.month;
dst.day = src.day;
dst.hour = src.hour;
dst.minute = src.minute;
dst.seconds = src.seconds;
dst.micros = src.micros;
dst.nanos = src.nanos;
dst.isLocation= src.isLocation;
}
/**
* attempt to parse the string. The parser itself is returned so that
* so expressions can be chained like so:
* parser.parse("2009-jan").getTimeRange()
* @param timeString
* @param extra map that is passed into field handlers
* @return the TimeParser, call getTimeRange or getTime to get result.
* @throws ParseException
*/
public synchronized TimeParser parse(String timeString, Map extra ) throws ParseException {
logger.log(Level.FINER, "parse {0}", timeString);
lock= Thread.currentThread().getName();
int offs = 0;
int len = 0;
if ( extra==null ) extra= new HashMap();
orbitDatumRange=null;
TimeStruct time;
time= startTime;
copyTime( context, startTime );
for (int idigit = 1; idigit < ndigits; idigit++) {
if ( idigit==stopTimeDigit ) {
copyTime( startTime, stopTime );
time= stopTime;
}
if (offsets[idigit] != -1) { // note offsets[0] is always known
offs = offsets[idigit];
} else {
offs += len + this.delims[idigit - 1].length();
}
if (lengths[idigit] != -1) {
len = lengths[idigit];
} else {
if (this.delims[idigit].equals("")) {
if (idigit == ndigits - 1) {
len = timeString.length() - offs;
} else {
throw new IllegalArgumentException("No delimer specified after unknown length field, \"" + formatName[handlers[idigit]] + "\", field number=" + (1 + idigit) + "");
}
} else {
while ( offs=timeString.length() ) {
throw new ParseException( "expected delimiter \"" + this.delims[idigit] + "\" but reached end of string", offs);
}
int i = timeString.indexOf(this.delims[idigit], offs);
if (i == -1) {
throw new ParseException("expected delimiter \"" + this.delims[idigit] + "\"", offs);
}
len = i - offs;
}
}
if ( timeString.length()=0 ) {
extra.put( "ignore", timeString.substring(offs, offs + len) );
}
} else if (handlers[idigit] == 13) { // month name
time.month = TimeUtil.monthNumber(timeString.substring(offs, offs + len));
} else if (handlers[idigit] == 14) { // "X"
if ( len>=0 ) {
extra.put( "X", timeString.substring(offs, offs + len) );
}
} else if (handlers[idigit] == 15) { // "x"
if ( len>=0 ) {
extra.put( "x", timeString.substring(offs, offs + len) );
}
} else if ( handlers[idigit] == 17 ) { // "Z"
TimeZone tz = TimeZone.getTimeZone( timeString.substring(offs, offs + len) );
int minutes;
minutes= tz.getRawOffset() / (1000 * 60);
time.hour -= minutes / 60;
time.minute -= minutes % 60;
}
} catch ( NumberFormatException ex ) {
throw new ParseException( String.format( "fail to parse digit number %d: %s", idigit, field ), offs );
}
}
if ( this.phasestart!=null ) {
if ( timeWidthDatum==null ) {
logger.warning("phasestart cannot be used for month or year resolution");
} else {
Datum start;
if (startTime.year < 1990) {
start= Units.us1980.createDatum(toUs1980(startTime));
} else {
start= Units.us2000.createDatum(toUs2000(startTime));
}
Datum s1= this.phasestart.add( timeWidthDatum.multiply( DatumUtil.divp( start.subtract(this.phasestart), timeWidthDatum ) ) );
if ( !s1.equals(start) ) {
throw new ParseException("does not obey phasestart: "+timeString,0 );
}
this.startTime= TimeUtil.toTimeStruct(start);
this.stopTime= TimeUtil.add( this.startTime, this.timeWidth );
}
}
this.lock= "";
return this;
}
/**
* return the pad for the spec, like "underscore" "space" "zero" or "none"
* For "none", space is returned, and clients allowing special behavior should check for this.
* @param args
* @return the char, or (char)0.
*/
public static char getPad(Map args) {
String spad= args.get("pad");
if ( spad==null || spad.equals("underscore") ) return '_';
if ( spad.equals("space") ) {
return ' ';
} else if ( spad.equals("zero")) {
return '0';
} else if ( spad.equals("none")) {
return ' ';
} else if ( spad.length()>1 ) {
throw new IllegalArgumentException("unrecognized pad: "+spad );
} else {
return spad.charAt(0);
}
}
private static class FieldSpec {
String spec=null; // unparsed spec
String fieldType= null;
int length= -1;
String params= null;
@Override
public String toString() {
return String.valueOf(spec)+String.valueOf(params);
}
}
/**
* parse field specifications like:
* %{milli;cadence=100}
* %3{skip}
* @param spec
* @return
*/
private FieldSpec parseSpec(String spec) {
FieldSpec result= new FieldSpec();
int i0= spec.charAt(0)=='%' ? 1 : 0;
result.spec= spec.substring(i0);
int i1= i0;
while ( Character.isDigit(spec.charAt(i1)) ) i1++;
if ( i1>i0 ) {
result.length= Integer.parseInt(spec.substring(i0,i1));
i0= i1;
}
int isemi = spec.indexOf(';',i0);
int ibrace = spec.indexOf('}',i0);
i1 = ibrace;
if (isemi > -1 && isemi < ibrace) {
i1 = isemi;
result.params= spec.substring(isemi,ibrace);
} else {
result.params= "";
}
String fieldType = spec.substring(1, i1);
result.fieldType= fieldType;
return result;
}
/**
* set the digit with the integer part, and move the fractional part to the
* less significant digits. Format should contain just one field,
* see setDigit( String format, int value ) to break up fields.
* @param format like "Y"
* @param value like 2014
*/
public synchronized void setDigit(String format, double value) {
TimeStruct time;
time= startTime;
format= makeCanonical(format);
if (format.equals("$(ignore)") || format.equals("$X") || format.equals("$x")) return;
if (value < 0) {
throw new IllegalArgumentException("value must not be negative on field:"+format+" value:"+value );
}
String[] ss = format.split("\\$", -2);
if (ss.length > 2) {
throw new IllegalArgumentException("multiple fields not supported");
}
for (int i = ss.length - 1; i > 0; i--) {
int digit = (int) value;
double fp = value - digit;
switch (ss[i].charAt(0)) {
case 'Y':
time.year = digit;
if (TimeUtil.isLeapYear(time.year)) {
time.seconds += 366 * 24 * 3600 * fp;
} else {
time.seconds += 365 * 24 * 3600 * fp;
}
break;
case 'y':
time.year = digit < 58 ? 2000 + digit : 1900 + digit;
if (TimeUtil.isLeapYear(time.year)) {
time.seconds += 366 * 24 * 3600 * fp;
} else {
time.seconds += 365 * 24 * 3600 * fp;
}
break;
case 'j':
time.month = 1;
time.day = digit;
time.seconds += 24 * 3600 * fp;
break;
case 'm':
time.month = digit;
time.seconds += TimeUtil.daysInMonth(time.month, time.year) * 24 * 3600 * fp;
break;
case 'b': // someone else must parse the month name into 1..12.
time.month = digit;
break;
case 'd':
time.day = digit;
time.seconds += 24 * 3600 * fp;
break;
case 'H':
time.hour = digit;
time.seconds += 3600 * fp;
break;
case 'M':
time.minute = digit;
time.seconds += 60 * fp;
break;
case 'S':
time.seconds = digit + fp;
break;
case '{':
FieldSpec fs= parseSpec(ss[i]);
if (fs.fieldType.equals("milli")) {
time.millis = digit;
time.micros += 1000 * fp;
time.seconds += ((1000 * fp) - time.micros) * 1e-6;
} else if (fs.fieldType.equals("micro")) {
time.micros = digit;
time.seconds += fp * 1e-6;
} else if (fs.fieldType.equals("ignore")) {
// do nothing
}
break;
case '(':
fs= parseSpec(ss[i]);
if (fs.fieldType.equals("milli")) {
time.millis = digit;
time.micros += 1000 * fp;
time.seconds += ((1000 * fp) - time.micros) * 1e-6;
} else if (fs.fieldType.equals("micro")) {
time.micros = digit;
time.seconds += fp * 1e-6;
} else if (fs.fieldType.equals("ignore")) {
// do nothing
}
break;
default:
throw new IllegalArgumentException("format code not supported");
}
}
}
/**
* Set the digit using the format code. If multiple digits are found, then
* the integer provided should be the misinterpreted integer. For example,
* if the format is "%Y%m%d", the integer 20080830 is split apart into
* 2008,08,30.
* @param format spec like "%Y%m%d"
* @param value integer like 20080830.
* @return
*/
public synchronized TimeParser setDigit(String format, int value) {
TimeStruct time= startTime;
String[] ss = format.split("%", -2);
for (int i = ss.length - 1; i > 0; i--) {
int mod = 0;
int digit;
switch (ss[i].charAt(0)) {
case 'Y':
mod = 10000;
digit = value % mod;
time.year = digit;
break;
case 'y':
mod = 100;
digit = value % mod;
time.year = digit < 58 ? 2000 + digit : 1900 + digit;
break;
case 'j':
mod = 1000;
digit = value % mod;
time.month = 1;
time.day = digit;
break;
case 'm':
mod = 100;
digit = value % mod;
time.month = digit;
break;
case 'b': // someone else must parse the month name into two-digit month.
mod = 100;
digit = value % mod;
time.month= digit;
break;
case 'd':
mod = 100;
digit = value % mod;
time.day = digit;
break;
case 'H':
mod = 100;
digit = value % mod;
time.hour = digit;
break;
case 'M':
mod = 100;
digit = value % mod;
time.minute = digit;
break;
case 'S':
mod = 100;
digit = value % mod;
time.seconds = digit;
break;
case 'X':
break;
case '{':
FieldSpec fs= parseSpec(ss[i]);
if (fs.fieldType.equals("milli")) {
mod = 1000;
} else if ( fs.fieldType.equals("micros") ) {
mod = 1000;
} else {
mod= (int)Math.pow( 10, fs.length );
}
digit = value % mod;
if ( fs.fieldType.equals("milli")) {
time.millis = digit;
} else if ( fs.fieldType.equals("micros")) {
time.micros = digit;
} else if ( fs.fieldType.equals("ignore")) {
// do nothing
}
break;
case '(':
fs= parseSpec(ss[i]);
if (fs.fieldType.equals("milli")) {
mod = 1000;
} else if ( fs.fieldType.equals("micros") ) {
mod = 1000;
} else {
mod= (int)Math.pow( 10, fs.length );
}
digit = value % mod;
if ( fs.fieldType.equals("milli")) {
time.millis = digit;
} else if ( fs.fieldType.equals("micros")) {
time.micros = digit;
} else if ( fs.fieldType.equals("ignore")) {
// do nothing
}
break;
default:
throw new IllegalArgumentException("format code not supported");
}
value = value / mod;
}
return this;
}
/**
* This allows for string split into elements to be interpreted here. This
* is to add flexibility to external parsers that have partially parsed the
* number already.
* examples:
* TimeParser p= TimeParser.create("%Y %m %d");
* p.setDigit(0,2007).setDigit(1,12).setDigit(2,5).getTime( Units.us2000 );
* p.format(); // maybe in the future
*
* @param digitNumber, the digit to set (starting with 0).
* @param digit, value to set the digit.
* @return the time parser with the digit set.
* @throws IllegalArgumentException if the digit has a custom field handler
* @throws IllegalArgumentException if the digit does not exist.
*/
public synchronized TimeParser setDigit(int digitNumber, int digit) {
TimeStruct time;
time= startTime;
switch (handlers[digitNumber + 1]) {
case 0:
time.year = digit;
break;
case 1:
time.year = digit < 58 ? 2000 + digit : 1900 + digit;
break;
case 2:
time.month = 1;
time.day = digit;
break;
case 3:
time.month = digit;
break;
case 4:
time.day = digit;
break;
case 5:
time.hour = digit;
break;
case 6:
time.minute = digit;
break;
case 7:
time.seconds = digit;
break;
case 8:
time.millis = digit;
break;
case 9:
time.micros = digit;
break;
case 12:
break; // ignore
case 13:
time.month = digit;
break;
case 14:
break; // ignore
}
return this;
}
/**
* explicitly set the context for time parsing. For example,
* filenames are just $H$M$S.dat, and the context is "Jan 17th, 2015"
* Note that the context is stored internally as just a start time, so
* spans (e.g. 3-day) are not supported.
* @param tr the range
*/
public synchronized void setContext( DatumRange tr ) {
this.context= TimeUtil.toTimeStruct(tr.min());
}
/**
* return the parsed time in the given units. Here Autoplot
* Jython code shows how this is used:
*
* from org.virbo.dataset import SemanticOps
* tp= TimeParser.create("$Y-$m-$dT$H")
* u= SemanticOps.lookupTimeUnits("seconds since 2014-01-01T00:00")
* print tp.parse("2014-01-06T02").getTime( u )
*
* @param units as in Units.us2000
* @return the value in the given units.
*/
public synchronized double getTime(Units units) {
return Units.us2000.convertDoubleTo(units, toUs2000(startTime));
}
/**
* return the parsed time as a Datum. For years less than 1990,
* Units.us1980 is used, otherwise Units.us2000 is used.
* @return a datum representing the parsed time.
*/
public synchronized Datum getTimeDatum() {
if (startTime.year < 1990) {
return Units.us1980.createDatum(toUs1980(startTime));
} else {
return Units.us2000.createDatum(toUs2000(startTime));
}
}
/**
* return the limits of the range we can parse. These limits come from
* orbit files like "$(o,sc=rbspa-pp)"
* or from explicit fields like "$(M,Y=1999)"
* @return
*/
public DatumRange getValidRange() {
if ( fieldHandlers.size()==1 && fieldHandlers.get("o") instanceof OrbitFieldHandler ) {
OrbitFieldHandler ofh= (OrbitFieldHandler)fieldHandlers.get("o");
try {
DatumRange d1 = new OrbitDatumRange( ofh.o.getSpacecraft(), ofh.o.first() );
DatumRange d2= new OrbitDatumRange( ofh.o.getSpacecraft(), ofh.o.last() );
return DatumRangeUtil.union( d1,d2 );
} catch (ParseException ex) {
logger.log(Level.SEVERE, ex.getMessage(), ex);
}
return DatumRangeUtil.parseTimeRangeValid( "1000-9000" );
} else {
return DatumRangeUtil.parseTimeRangeValid( "1000-9000" );
}
}
/**
* Returns the implicit interval as a DatumRange.
* For example, "Jan 1, 2003" would have a getTimeDatum of "Jan 1, 2003 00:00:00",
* and getDatumRange() would go from midnight to midnight.
* This will try to create MonthDatumRanges when possible, to keep it abstract,
* so for example,
* {@code
*tr= tp.getTimeRange() // "Jan 2015"
*tr= tr.next() // "Feb 2015", not 31 days starting Feb 1
*}
*
* This accesses time, timeWidth, orbitDatumRange, startTime.
* @return the DatumRange
*/
public synchronized DatumRange getTimeRange() {
if ( !lock.equals("") ) throw new IllegalArgumentException("someone is messing with the parser on a different thread "+lock+ " this thread is "+Thread.currentThread().getName() );
if ( stopTimeDigit==AFTERSTOP_INIT && startTime.day==1 && startTime.hour==0 && startTime.minute==0 && startTime.seconds==0 && startTime.millis==0 && startTime.micros==0 &&
timeWidth.day==0 && timeWidth.hour==0 && timeWidth.minute==0 && timeWidth.seconds==0 && timeWidth.millis==0 && timeWidth.micros==0 ) { // special code for years.
TimeStruct lstopTime = startTime.add(timeWidth);
lstopTime= TimeUtil.carry(lstopTime);
int[] t1= new int[] { startTime.year, startTime.month, startTime.day, startTime.hour, startTime.minute, (int)startTime.seconds, startTime.millis*1000000 + startTime.micros*1000 + startTime.nanos };
int[] t2= new int[] { lstopTime.year, lstopTime.month, lstopTime.day, lstopTime.hour, lstopTime.minute, (int)lstopTime.seconds, lstopTime.millis*1000000 + lstopTime.micros*1000 + lstopTime.nanos };
return new MonthDatumRange( t1, t2 );
} else if ( orbitDatumRange!=null ) {
return orbitDatumRange;
} else {
if ( stopTimeDigit extra ) {
StringBuilder result = new StringBuilder(100);
int offs = 0;
int len;
if ( this.phasestart!=null ) {
if ( timeWidthDatum==null ) {
logger.warning("phaseStart cannot be used for month or year resolution");
} else {
start= this.phasestart.add( timeWidthDatum.multiply( DatumUtil.divp( start.subtract(this.phasestart), timeWidthDatum ) ) );
}
}
if ( start==null ) {
start= TimeUtil.toDatum( new int[] { 1000, 1, 1, 0, 0, 0, 0 } );
}
TimeUtil.TimeStruct timel = TimeUtil.toTimeStruct(start);
TimeUtil.TimeStruct timeWidthl= new TimeUtil.TimeStruct();
copyTime( timeWidth, timeWidthl ); // make a local copy in case future versions allow variable time widths.
extra= new HashMap(extra);
TimeUtil.TimeStruct stopTimel;
if ( stop==null ) {
if ( timeWidth.year==MAX_VALID_YEAR-MIN_VALID_YEAR ) { // orbits and other strange times
stopTimel= timel;
} else {
stopTimel= TimeUtil.add( timel, timeWidth );
}
} else {
stopTimel= TimeUtil.toTimeStruct(stop);
}
normalizeSeconds(stopTimel);
normalizeSeconds(timel);
NumberFormat[] nf = new NumberFormat[5];
nf[2] = new DecimalFormat("00");
nf[3] = new DecimalFormat("000");
nf[4] = new DecimalFormat("0000");
for (int idigit = 1; idigit < ndigits; idigit++) {
if ( idigit==stopTimeDigit ) {
timel= stopTimel;
}
result.insert(offs, this.delims[idigit - 1]);
if (offsets[idigit] != -1) { // note offsets[0] is always known
offs = offsets[idigit];
} else {
offs += this.delims[idigit - 1].length();
}
if (lengths[idigit] != -1) {
len = lengths[idigit];
} else {
len = -9999; // the field handler will tell us.
}
if (handlers[idigit] < 10) {
String qual= qualifiers[idigit];
int digit;
int span=1;
if ( qual!=null ) {
Pattern p= Pattern.compile("span=(\\d+)"); // TODO: multiple qualifiers
Matcher m= p.matcher(qual);
if ( m.matches() ) {
span= Integer.parseInt(m.group(1));
}
p= Pattern.compile("delta=(\\d+)"); // TODO: multiple qualifiers
m= p.matcher(qual);
if ( m.matches() ) {
span= Integer.parseInt(m.group(1));
}
}
switch (handlers[idigit]) {
case 0:
digit = timel.year;
break;
case 1:
digit = (timel.year < 2000) ? timel.year - 1900 : timel.year - 2000;
break;
case 2:
digit = TimeUtil.dayOfYear(timel.month, timel.day, timel.year);
break;
case 3:
digit = timel.month;
break;
case 4:
digit = timel.day;
break;
case 5:
digit = timel.hour;
break;
case 6:
digit = timel.minute;
break;
case 7:
digit = (int) timel.seconds;
break;
case 8:
digit = timel.millis;
break;
case 9:
digit = timel.micros;
break;
default:
throw new RuntimeException("shouldn't get here");
}
if ( span>1 ) {
if ( handlers[idigit]>0 && handlers[idigit]<5 ) {
logger.fine("uh-oh, span used on ordinal like month, day. Just leave it alone.");
} else {
digit= ( digit / span ) * span;
}
}
if ( len<0 ) {
String ss= String.valueOf(digit);
result.insert(offs, ss);
offs+= ss.length();
} else {
result.insert(offs, nf[len].format(digit));
offs += len;
}
} else if (handlers[idigit] == 16) { // $N nanoseconds
int nanos= timel.millis * 1000000 + timel.micros * 1000 + timel.nanos;
result.insert(offs, String.format("%09d",nanos ));
offs += 9;
} else if (handlers[idigit] == 13) { // month names
result.insert(offs, TimeUtil.monthNameAbbrev(timel.month));
offs += len;
} else if (handlers[idigit] == 12 || handlers[idigit]==14 ) { // ignore
throw new RuntimeException("cannot format spec containing ignore");
} else if (handlers[idigit] == 100) {
if ( fc[idigit].equals("v") ) { // kludge for version. TODO: This can probably use the code below now.
String ins= "00";
if ( len>-1 ) {
if ( len>20 ) throw new IllegalArgumentException("version lengths>20 not supported");
ins= "00000000000000000000".substring(0,len);
}
result.insert( offs, ins );
offs+= ins.length();
} else {
FieldHandler fh1= fieldHandlers.get(fc[idigit]);
TimeUtil.TimeStruct timeEnd = stopTimel;
String ins= fh1.format( timel, TimeUtil.subtract(timeEnd, timel), len, extra );
TimeUtil.TimeStruct startTimeTest= new TimeUtil.TimeStruct();
copyTime( timel, startTimeTest );
TimeUtil.TimeStruct timeWidthTest= new TimeUtil.TimeStruct();
copyTime( timeWidthl, timeWidthTest );
try {
fh1.parse( ins, startTimeTest, timeWidthTest, extra );
copyTime( startTimeTest, timel );
copyTime( timeWidthTest, timeWidthl );
copyTime( TimeUtil.add( timel, timeWidthl ), stopTimel );
} catch (ParseException ex) {
Logger.getLogger(TimeParser.class.getName()).log(Level.SEVERE, null, ex);
}
if ( len>-1 && ins.length()!=len ) {
throw new IllegalArgumentException("length of fh is incorrect, should be "+len+", got \""+ins+"\"");
}
result.insert( offs, ins );
offs+= ins.length();
}
} else if (handlers[idigit] == 10) {
throw new RuntimeException("AM/PM not supported");
} else if (handlers[idigit] == 11) {
throw new RuntimeException("Time Zones not supported");
} else if (handlers[idigit] == 17) { // PST time zone code.
throw new RuntimeException("Time Zones not supported");
} //TODO: $x?
}
result.insert(offs, this.delims[ndigits - 1]);
return result.toString().trim();
}
/**
* return the field handler for the id. For example, enum
* returns the field handler handling enumerations. Note there
* is currently only one field handler for each type, so for example
* two enumerations are not allowed.
*
* @param code
* @return the field handler.
*/
public FieldHandler getFieldHandlerByCode( String code ) {
return fieldHandlers.get(code);
}
/**
* return the field handler for the id. For example, enum
* returns the field handler handling enumerations. Note there
* is currently only one field handler for each type, so for example
* two enumerations are not allowed.
*
* @param id the field handler id
* @return the field handler.
*/
public FieldHandler getFieldHandlerById( String id ) {
return fieldHandlersById.get(id);
}
@Override
public String toString() {
StringBuilder result= new StringBuilder();
for ( int i=0;i " + dr + ", should be "+norm );
}
return true;
}
public static void main( String[] aa ) throws Exception {
TimeParser tp;
//tp= TimeParser.create( "$Y-$m-$dT$H:$M:$S.$(subsec,places=9)" );
//System.err.println( "tpf: " + tp.format( Units.cdfTT2000.parse( "2016-05-05T12:54:54.002232668") ) );
tp= TimeParser.create( "$Y-$m-$(d,phasestart=2019-05-12,delta=7)" );
System.err.println( "tpf: " + tp.format( Units.cdfTT2000.parse( "2019-05-17T00:00Z") ) );
System.err.println( "tpf: " + tp.format( Units.cdfTT2000.parse( "2019-05-04T00:00Z") ) );
testTimeParser();
}
/**
* test time parsing when the format is known. This time parser is much faster than the time parser of Test009, which must
* infer the format as it parses.
* @throws Exception
*/
public static void testTimeParser() throws Exception {
LoggerManager.getLogger("datum.timeparser").setLevel(Level.ALL);
logger.addHandler( new ConsoleHandler() );
logger.getHandlers()[0].setLevel(Level.ALL);
org.das2.datum.DatumRangeUtil.parseTimeRangeValid("2000-022/P1D");
System.err.println( makeCanonical("$Y-$3{J}") );
testTimeParser1( "$Y-$m-$d $H:$M $p", "2019-05-03 12:00 PM", "2019-05-03T12:00/PT1M");
testTimeParser1( "$Y-$m-$d $H:$M $p", "2019-05-03 12:00 AM", "2019-05-03T00:00/PT1M");
//testTimeParser1( "$Y-$m-$d $p $H", "2019-05-03 AM 12", "2019-05-03T00:00/PT1H");
testTimeParser1( "$Y$m$d-$(Y,end)$m$d", "20130202-20140303", "2013-02-02/2014-03-03" );
testTimeParser1( "$Y$m$d-$(d,end)", "20130202-13", "2013-02-02/2013-02-13" );
testTimeParser1( "$(periodic;offset=0;start=2000-001;period=P1D)", "0", "2000-001");
testTimeParser1( "$(periodic;offset=0;start=2000-001;period=P1D)", "20", "2000-021");
testTimeParser1( "$(periodic,offset=2285,start=2000-346,period=P27D)", "1", "1832-02-08/P27D");
testTimeParser1( "$(periodic;offset=2285;start=2000-346;period=P27D)", "2286", "2001-007/P27D");
testTimeParser1( "$Y-$m-$dT$H:$M:$S.$(subsec,places=6)", "2000-01-01T00:00:00.000001", "2000-001T00:00:00.000001/PT.000001S");
testTimeParser1( "$Y-$m-$dT$H:$M:$S.$(subsec,places=6)", "2000-01-01T00:00:05.000001", "2000-001T00:00:05.000001/PT.000001S");
testTimeParser1( "$Y-$m-$dT$H:$M:$S.$(subsec,places=9)", "2000-01-01T00:00:05.000001001", "2000-001T00:00:05.000001001/PT.000000001S");
testTimeParser1( "$Y-$m-$(d,phasestart=2019-05-12,delta=7)", "2019-05-03", "2019-04-28T00:00Z/P7D");
TimeParser tp= TimeParser.create("$Y$m$d_v$v.dat");
System.err.println( tp.parse("20130618_v4.05.dat").getTimeRange() );
System.err.println( makeCanonical( "%Y-%m-%dT%H:%M:%S.%{milli}Z" ) );
}
}