File: EpochFormatter.java

package info (click to toggle)
jcdf 1.2.5%2Bdfsg-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 572 kB
  • sloc: java: 5,315; makefile: 198; sh: 98
file content (356 lines) | stat: -rw-r--r-- 13,452 bytes parent folder | download
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;
            }
        }
    }
}