1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
|
package uk.ac.bristol.star.cdf;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Does string formatting of epoch values in various representations.
* The methods of this object are thread-safe.
*
* @author Mark Taylor
* @since 21 Jun 2013
*/
public class EpochFormatter {
private final DateFormat epochMilliFormat_ =
createDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSS" );
private final DateFormat epochSecFormat_ =
createDateFormat( "yyyy-MM-dd'T'HH:mm:ss" );
private final int iMaxValidTtScaler_;
private int iLastTtScaler_ = -1;
private static final TimeZone UTC = TimeZone.getTimeZone( "UTC" );
private static final long HALF_DAY = 1000 * 60 * 60 * 12;
private static final TtScaler[] TT_SCALERS = TtScaler.getTtScalers();
private static final long LAST_KNOWN_LEAP_UNIX_MILLIS =
getLastKnownLeapUnixMillis( TT_SCALERS );
private static final Logger logger_ =
Logger.getLogger( EpochFormatter.class.getName() );
/**
* Configures behaviour when a date is encountered which is known to
* have incorrectly applied leap seconds.
* If true, a RuntimeException is thrown, if false a log message is written.
*/
public static boolean FAIL_ON_LEAP_ERROR = true;
/** 0 A.D. in Unix milliseconds as used by EPOCH/EPOCH16 data types. */
public static final long AD0_UNIX_MILLIS = getAd0UnixMillis();
/**
* Constructs a formatter without leap second awareness.
*/
public EpochFormatter() {
this( 0 );
}
/**
* Constructs a formatter aware of the latest known leap second.
*
* @param leapSecondLastUpdated value of GDR LeapSecondLastUpdated
* field (YYYYMMDD, or -1 for unused, or 0 for no leap seconds)
*/
public EpochFormatter( int leapSecondLastUpdated ) {
/* Add a half day on here to avoid leap-second-sensitive errors in
* working out what epoch the supplied date actually corresponds to.
* Leap second table epochs are not in any case sensitive to
* differences of less than a day (and are most unlikely to be
* issued with a frequency close to daily). If the accumulated
* number of leap seconds approaches half a day, this offset should
* be increased; also, Hi from the distant past! */
long lastDataLeapUnixMillis =
getLastDataLeapUnixMillis( leapSecondLastUpdated ) + HALF_DAY;
/* If we know about leap seconds later than the last known one
* supplied (presumably acquired from a data file),
* issue a warning that an update might be a good idea. */
if ( lastDataLeapUnixMillis > LAST_KNOWN_LEAP_UNIX_MILLIS &&
lastDataLeapUnixMillis - LAST_KNOWN_LEAP_UNIX_MILLIS > HALF_DAY ) {
DateFormat fmt = createDateFormat( "yyyy-MM-dd" );
String msg = new StringBuffer()
.append( "Data knows more leap seconds than library" )
.append( " (" )
.append( fmt.format( new Date( lastDataLeapUnixMillis
+ HALF_DAY ) ) )
.append( " > " )
.append( fmt.format( new Date( LAST_KNOWN_LEAP_UNIX_MILLIS
+ HALF_DAY ) ) )
.append( ")" )
.toString();
logger_.warning( msg );
}
/* If the supplied last known leap second is known to be out of date
* (because we know of a later one), then prepare to complain if
* this formatter is called upon to perform a conversion of
* a date that would be affected by leap seconds we know about,
* but the data file didn't. */
if ( lastDataLeapUnixMillis > 0 ) {
long lastDataLeapTt2kMillis =
lastDataLeapUnixMillis - (long) TtScaler.J2000_UNIXMILLIS;
iMaxValidTtScaler_ = getScalerIndex( lastDataLeapTt2kMillis );
}
else {
iMaxValidTtScaler_ = TT_SCALERS.length - 1;
}
}
/**
* Formats a CDF EPOCH value as an ISO-8601 date.
*
* @param epoch EPOCH value
* @return date string
*/
public String formatEpoch( double epoch ) {
long unixMillis = (long) ( epoch + AD0_UNIX_MILLIS );
Date date = new Date( unixMillis );
return formatDate( epochMilliFormat_, date );
}
/**
* Formats a CDF EPOCH16 value as an ISO-8601 date.
*
* @param epoch1 first element of EPOCH16 pair (seconds since 0AD)
* @param epoch2 second element of EPOCH16 pair (additional picoseconds)
* @return date string
*/
public String formatEpoch16( double epoch1, double epoch2 ) {
long unixMillis = (long) ( epoch1 * 1000 ) + AD0_UNIX_MILLIS;
Date date = new Date( unixMillis );
long plusPicos = (long) epoch2;
if ( plusPicos < 0 || plusPicos >= 1e12 ) {
return "??";
}
String result = new StringBuffer( 32 )
.append( formatDate( epochSecFormat_, date ) )
.append( '.' )
.append( prePadWithZeros( plusPicos, 12 ) )
.toString();
assert result.length() == 32;
return result;
}
/**
* Formats a CDF TIME_TT2000 value as an ISO-8601 date.
*
* @param timeTt2k TIME_TT2000 value
* @return date string
*/
public String formatTimeTt2000( long timeTt2k ) {
// Special case - see "Variable Pad Values" section
// (sec 2.3.20 at v3.4, and footnote) of CDF Users Guide.
if ( timeTt2k == Long.MIN_VALUE ) {
return "9999-12-31T23:59:59.999999999";
}
// Second special case - not sure if this is documented, but
// advised by Michael Liu in email to MBT 12 Aug 2013.
else if ( timeTt2k == Long.MIN_VALUE + 1 ) {
return "0000-01-01T00:00:00.000000000";
}
// Split the raw long value into a millisecond base and
// nanosecond adjustment.
long tt2kMillis = timeTt2k / 1000000;
int plusNanos = (int) ( timeTt2k % 1000000 );
if ( plusNanos < 0 ) {
tt2kMillis--;
plusNanos += 1000000;
}
// Get the appropriate TT scaler object for this epoch.
int scalerIndex = getScalerIndex( tt2kMillis );
if ( scalerIndex > iMaxValidTtScaler_ ) {
String msg = new StringBuffer()
.append( "CDF TIME_TT2000 date formatting failed" )
.append( " - library leap second table known to be out of date" )
.append( " with respect to data." )
.append( " Update " )
.append( TtScaler.LEAP_FILE_ENV )
.append( " environment variable to point at file" )
.append( " http://cdf.gsfc.nasa.gov/html/CDFLeapSeconds.txt" )
.toString();
if ( FAIL_ON_LEAP_ERROR ) {
throw new RuntimeException( msg );
}
else {
logger_.log( Level.SEVERE, msg );
}
}
TtScaler scaler = TT_SCALERS[ scalerIndex ];
// Use it to convert to Unix time, which is UTC.
long unixMillis = (long) scaler.tt2kToUnixMillis( tt2kMillis );
int leapMillis = scaler.millisIntoLeapSecond( tt2kMillis );
// Format the unix time as an ISO-8601 date.
// In most (99.999998%) cases this is straightforward.
final String txt;
if ( leapMillis < 0 ) {
Date date = new Date( unixMillis );
txt = formatDate( epochMilliFormat_, date );
}
// However if we happen to fall during a leap second, we have to
// do some special (and not particularly elegant) handling to
// produce the right string, since the java DateFormat
// implementation can't(?) be persuaded to cope with 61 seconds
// in a minute.
else {
Date date = new Date( unixMillis - 1000 );
txt = formatDate( epochMilliFormat_, date )
.replaceFirst( ":59\\.", ":60." );
}
// Append the nanoseconds part and return.
return txt + prePadWithZeros( plusNanos, 6 );
}
/**
* Returns the index into the TT_SCALERS array of the TtScaler
* instance that is valid for a given time.
*
* @param tt2kMillis TT time since J2000 in milliseconds
* @return index into TT_SCALERS
*/
private int getScalerIndex( long tt2kMillis ) {
// Use the most recently used value as the best guess.
// There's a good chance it's the right one.
// iLastTtScaler_ is not guarded by locks, so it could get a stale
// value, but it's only an optimisation guess, so that wouldn't matter.
int index = TtScaler
.getScalerIndex( tt2kMillis, TT_SCALERS, iLastTtScaler_ );
iLastTtScaler_ = index;
return index;
}
/**
* Constructs a DateFormat object for a given pattern for UTC.
*
* @param pattern formatting pattern
* @return format
* @see java.text.SimpleDateFormat
*/
private static DateFormat createDateFormat( String pattern ) {
DateFormat fmt = new SimpleDateFormat( pattern );
fmt.setTimeZone( UTC );
fmt.setCalendar( new GregorianCalendar( UTC, Locale.UK ) );
return fmt;
}
/**
* Formats a date with a given formatter in a thread-safe way.
*
* @param format format
* @param date date
* @return formatted date
*/
private static String formatDate( DateFormat format, Date date ) {
return ((DateFormat) format.clone()).format( date );
}
/**
* Returns the CDF epoch (0000-01-01T00:00:00)
* in milliseconds since the Unix epoch (1970-01-01T00:00:00).
*
* @return -62,167,219,200,000
*/
private static long getAd0UnixMillis() {
GregorianCalendar cal = new GregorianCalendar( UTC, Locale.UK );
cal.setLenient( true );
cal.clear();
cal.set( 0, 0, 1, 0, 0, 0 );
long ad0 = cal.getTimeInMillis();
// Fudge factor to make this calculation match the apparent result
// from the CDF library. Not quite sure why it's required, but
// I think something to do with the fact that the first day is day 1
// and signs around AD0/BC0.
long fudge = 1000 * 60 * 60 * 24 * 2; // 2 days
return ad0 + fudge;
}
/**
* Pads a numeric value with zeros to return a fixed length string
* representing a given numeric value.
*
* @param value number
* @param leng number of characters in result
* @return leng-character string containing value
* padded at start with zeros
*/
private static String prePadWithZeros( long value, int leng ) {
String txt = Long.toString( value );
int nz = leng - txt.length();
if ( nz == 0 ) {
return txt;
}
else if ( nz < 0 ) {
throw new IllegalArgumentException();
}
else {
StringBuffer sbuf = new StringBuffer( leng );
for ( int i = 0; i < nz; i++ ) {
sbuf.append( '0' );
}
sbuf.append( txt );
return sbuf.toString();
}
}
/**
* Returns the date, in milliseconds since the Unix epoch,
* of the last leap second known by the library.
*
* @param scalers ordered array of all scalers
* @return last leap second epoch in unix milliseconds
*/
private static long getLastKnownLeapUnixMillis( TtScaler[] scalers ) {
TtScaler lastScaler = scalers[ scalers.length - 1 ];
return (long)
lastScaler.tt2kToUnixMillis( lastScaler.getFromTt2kMillis() );
}
/**
* Returns the date, in milliseconds since the Unix epoch,
* of the last leap second indicated by an integer in the form
* used by the GDR LeapSecondLastUpdated field.
* If no definite value is indicated, Long.MIN_VALUE is returned.
*
* @param leapSecondLastUpdated value of GDR LeapSecondLastUpdated
* field (YYYYMMDD, or -1 for unused, or 0 for no leap seconds)
* @return last leap second epoch in unix milliseconds,
* or very negative value
*/
private static long getLastDataLeapUnixMillis( int leapSecondLastUpdated ) {
if ( leapSecondLastUpdated == 0 ) {
return Long.MIN_VALUE;
}
else if ( leapSecondLastUpdated == -1 ) {
return Long.MIN_VALUE;
}
else {
DateFormat fmt = createDateFormat( "yyyyMMdd" );
try {
return fmt.parse( Integer.toString( leapSecondLastUpdated ) )
.getTime();
}
catch ( ParseException e ) {
logger_.warning( "leapSecondLastUpdated="
+ leapSecondLastUpdated
+ "; not YYYYMMDD" );
return Long.MIN_VALUE;
}
}
}
}
|