File: TtScaler.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 (624 lines) | stat: -rw-r--r-- 24,703 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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
package uk.ac.bristol.star.cdf;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Handles conversions between TT_TIME2000 (TT since J2000.0)
 * and Unix (UTC since 1970-01-01) times.
 * An instance of this class is valid for a certain range of TT2000 dates
 * (one that does not straddle a leap second).
 * To convert between TT_TIME2000 and Unix time, first acquire the
 * right instance of this class for the given time, and then use it
 * for the conversion.
 *
 * <p>An external leap seconds table can be referenced with the
 * {@value #LEAP_FILE_ENV} environment variable in exactly the same way
 * as for the NASA library.  Otherwise an internal leap seconds table
 * will be used.
 *
 * @author   Mark Taylor
 * @since    8 Aug 2013
 */
public abstract class TtScaler {

    private final double fixOffset_;
    private final double scaleBase_;
    private final double scaleFactor_;
    private final long fromTt2kMillis_;
    private final long toTt2kMillis_;

    /** Number of milliseconds in a day. */
    private static final double MILLIS_PER_DAY = 1000 * 60 * 60 * 24;

    /** Date of the J2000 epoch as a Modified Julian Date. */
    private static final double J2000_MJD = 51544.5;

    /** Date of the Unix epoch (1970-01-01T00:00:00) as an MJD. */
    private static final double UNIXEPOCH_MJD = 40587.0;

    /** TT is ahead of TAI by approximately 32.184 seconds. */
    private static final double TT_TAI_MILLIS = 32184;

    /** Fixed time zone. */
    private static final TimeZone UTC = TimeZone.getTimeZone( "UTC" );

    /** Date of the J2000 epoch (2000-01-01T12:00:00) as a Unix time. */
    public static final double J2000_UNIXMILLIS = 946728000000.0;

    /**
     * Environment variable to locate external leap seconds file ({@value}).
     * The environment variable name and file format are just the same
     * as for the NASA CDF library.
     */
    public static final String LEAP_FILE_ENV = "CDF_LEAPSECONDSTABLE";

    private static final Logger logger_ =
        Logger.getLogger( TtScaler.class.getName() );

    /**
     * TT2000 coefficients:
     *    year, month (1=Jan), day_of_month (1-based),
     *    fix_offset, scale_base, scale_factor.
     * year month day_of_month:
     * TAI-UTC= fix_offset S + (MJD - scale_base) * scale_factor S
     *
     * <p>Array initialiser lifted from gsfc.nssdc.cdf.util.CDFTT2000
     * source code.  That derives it from
     * http://maia.usno.navy.mil/ser7/tai-utc.dat.
     * See also http://cdf.gsfc.nasa.gov/html/CDFLeapSeconds.txt.
     */
    private static final double[][] LTS = new double[][] {
        { 1960,  1,  1,  1.4178180, 37300.0, 0.0012960 },
        { 1961,  1,  1,  1.4228180, 37300.0, 0.0012960 },
        { 1961,  8,  1,  1.3728180, 37300.0, 0.0012960 },
        { 1962,  1,  1,  1.8458580, 37665.0, 0.0011232 },
        { 1963, 11,  1,  1.9458580, 37665.0, 0.0011232 },
        { 1964,  1,  1,  3.2401300, 38761.0, 0.0012960 },
        { 1964,  4,  1,  3.3401300, 38761.0, 0.0012960 },
        { 1964,  9,  1,  3.4401300, 38761.0, 0.0012960 },
        { 1965,  1,  1,  3.5401300, 38761.0, 0.0012960 },
        { 1965,  3,  1,  3.6401300, 38761.0, 0.0012960 },
        { 1965,  7,  1,  3.7401300, 38761.0, 0.0012960 },
        { 1965,  9,  1,  3.8401300, 38761.0, 0.0012960 },
        { 1966,  1,  1,  4.3131700, 39126.0, 0.0025920 },
        { 1968,  2,  1,  4.2131700, 39126.0, 0.0025920 },
        { 1972,  1,  1, 10.0,           0.0, 0.0       },
        { 1972,  7,  1, 11.0,           0.0, 0.0       },
        { 1973,  1,  1, 12.0,           0.0, 0.0       },
        { 1974,  1,  1, 13.0,           0.0, 0.0       },
        { 1975,  1,  1, 14.0,           0.0, 0.0       },
        { 1976,  1,  1, 15.0,           0.0, 0.0       },
        { 1977,  1,  1, 16.0,           0.0, 0.0       },
        { 1978,  1,  1, 17.0,           0.0, 0.0       },
        { 1979,  1,  1, 18.0,           0.0, 0.0       },
        { 1980,  1,  1, 19.0,           0.0, 0.0       },
        { 1981,  7,  1, 20.0,           0.0, 0.0       },
        { 1982,  7,  1, 21.0,           0.0, 0.0       },
        { 1983,  7,  1, 22.0,           0.0, 0.0       },
        { 1985,  7,  1, 23.0,           0.0, 0.0       },
        { 1988,  1,  1, 24.0,           0.0, 0.0       },
        { 1990,  1,  1, 25.0,           0.0, 0.0       },
        { 1991,  1,  1, 26.0,           0.0, 0.0       },
        { 1992,  7,  1, 27.0,           0.0, 0.0       },
        { 1993,  7,  1, 28.0,           0.0, 0.0       },
        { 1994,  7,  1, 29.0,           0.0, 0.0       },
        { 1996,  1,  1, 30.0,           0.0, 0.0       },
        { 1997,  7,  1, 31.0,           0.0, 0.0       },
        { 1999,  1,  1, 32.0,           0.0, 0.0       },
        { 2006,  1,  1, 33.0,           0.0, 0.0       },
        { 2009,  1,  1, 34.0,           0.0, 0.0       },
        { 2012,  7,  1, 35.0,           0.0, 0.0       },
        { 2015,  7,  1, 36.0,           0.0, 0.0       },
        { 2017,  1,  1, 37.0,           0.0, 0.0       },
    };

    /**
     * Constructor.
     *
     * @param   fixOffset  fixed offset of UTC in seconds from TAI
     * @param   scaleBase  MJD base for scaling
     * @param   scaleFactor   factor for scaling
     * @param   fromTt2kMillis  start of validity range
     *                          in TT milliseconds since J2000
     * @param   toTt2kMillis    end of validity range
     *                          in TT milliseconds since J2000
     */
    public TtScaler( double fixOffset, double scaleBase, double scaleFactor,
                     long fromTt2kMillis, long toTt2kMillis ) {
        fixOffset_ = fixOffset;
        scaleBase_ = scaleBase;
        scaleFactor_ = scaleFactor;
        fromTt2kMillis_ = fromTt2kMillis;
        toTt2kMillis_ = toTt2kMillis;
    }

    /**
     * Converts time in milliseconds from TT since J2000 to UTC since 1970
     * for this scaler.
     *
     * @param  tt2kMillis  TT milliseconds since J2000
     * @return  UTC milliseconds since Unix epoch
     */
    public double tt2kToUnixMillis( long tt2kMillis ) {
        return tt2kToUnixMillis( tt2kMillis,
                                 fixOffset_, scaleBase_, scaleFactor_ );
    }

    /**
     * Returns the start of the validity range of this scaler
     * in TT milliseconds since J2000.
     *
     * @return   validity range start
     */
    public long getFromTt2kMillis() {
        return fromTt2kMillis_;
    }

    /**
     * Returns the end of the validity range of this scaler
     * in TT milliseconds since J2000.
     *
     * @return   validity range end
     */
    public long getToTt2kMillis() {
        return toTt2kMillis_;
    }

    /**
     * Assesses validity of this scaler for a given time.
     * The result will be zero if this scaler is valid,
     * negative if the given time is earlier than this scaler's range, and
     * positive if the given time is later than this scaler's range.
     *
     * @param  tt2kMillis  TT milliseconds since J2000
     * @return  validity signum
     */
    public int compareTt2kMillis( long tt2kMillis ) {
        if ( tt2kMillis < fromTt2kMillis_ ) {
            return -1;
        }
        else if ( tt2kMillis >= toTt2kMillis_ ) {
            return +1;
        }
        else {
            return 0;
        }
    }

    /**
     * Indicates whether and how far a given time is into the duration of
     * a leap second.  If the supplied time falls during a leap second,
     * the number of milliseconds elapsed since the leap second's start
     * is returned.  Otherwise (i.e. nearly always) -1 is returned.
     *
     * @param   tt2kMillis  TT time in milliseconds since J2000
     * @return  a value in the range 0...1000 if in a leap second, otherwise -1
     */
    public abstract int millisIntoLeapSecond( long tt2kMillis );

    /**
     * Searches an ordered array of scaler instances for one that is
     * applicable to a supplied TT time.
     * The supplied array of instances must be ordered and cover the
     * supplied time value; the result of {@link #getTtScalers} is suitable
     * and most likely what you want to use here.
     *
     * @param   tt2kMillis  TT time in milliseconds since J2000
     * @param   orderedScalers  list of TtScaler instances ordered in time
     * @param   i0  initial guess at index of the right answer;
     *              if negative no best guess is assumed
     */
    public static int getScalerIndex( long tt2kMillis,
                                      TtScaler[] orderedScalers, int i0 ) {
        int ns = orderedScalers.length;
        return scalerBinarySearch( tt2kMillis, orderedScalers,
                                   i0 >= 0 ? i0 : ns / 2, 0, ns - 1 );
    }

    /**
     * Recursive binary search of an ordered array of scaler instances
     * for one that covers a given point in time.
     *
     * @param   tt2kMillis  TT time in milliseconds since J2000
     * @param   orderedScalers  list of TtScaler instances ordered in time
     * @param   i0  initial guess at index of the right answer
     * @param   imin  minimum possible value of the right answer
     * @parma   imax  maximum possible value of the right answer
     */
    private static int scalerBinarySearch( long tt2kMillis, TtScaler[] scalers,
                                           int i0, int imin, int imax ) {

        // If the guess is correct, return it directly.
        int icmp = scalers[ i0 ].compareTt2kMillis( tt2kMillis );
        if ( icmp == 0 ) {
            return i0;
        }

        // Sanity check.  This condition shouldn't happen, but could do
        // for one of two reasons: a programming error in this code,
        // or an improperly ordered scalers array.
        if ( i0 < imin || i0 > imax ) {
            return -1;
        }
        assert i0 >= imin && i0 <= imax;

        // Bisect up or down and recurse.
        if ( icmp < 0 ) {
            return scalerBinarySearch( tt2kMillis, scalers,
                                       i0 - ( i0 - imin + 1 ) / 2,
                                       imin, i0 - 1 );
        }
        else {
            assert icmp > 0;
            return scalerBinarySearch( tt2kMillis, scalers,
                                       i0 + ( imax - i0 + 1 ) / 2,
                                       i0 + 1, imax );
        }
    }

    /**
     * Converts time in milliseconds from TT since J2000 to UTC since 1970
     * for given coefficients.
     *
     * @param   tt2kMillis  TT milliseconds since J2000
     * @param   fixOffset  fixed offset of UTC in seconds from TAI
     * @param   scaleBase  MJD base for scaling
     * @param   scaleFactor   factor for scaling
     * @return  UTC milliseconds since Unix epoch
     */
    private static double tt2kToUnixMillis( long tt2kMillis, double fixOffset,
                                            double scaleBase,
                                            double scaleFactor ) {
        double mjd = ((double) tt2kMillis) / MILLIS_PER_DAY + J2000_MJD;
        double utcOffsetSec = fixOffset + ( mjd - scaleBase ) * scaleFactor;
        double utcOffsetMillis = utcOffsetSec * 1000;
        return tt2kMillis - TT_TAI_MILLIS - utcOffsetMillis + J2000_UNIXMILLIS;
    }

    /**
     * Converts time in milliseconds from UTC since 1970 to TT since J2000
     * for given coefficients.
     *
     * @param   unixMillis  UTC milliseconds since the Unix epoch
     * @param   fixOffset  fixed offset of UTC in seconds from TAI
     * @param   scaleBase  MJD base for scaling
     * @param   scaleFactor   factor for scaling
     * @return  TT milliseconds since J2000
     */
    private static double unixToTt2kMillis( long unixMillis, double fixOffset,
                                            double scaleBase,
                                            double scaleFactor ) {
        double mjd = ((double) unixMillis) / MILLIS_PER_DAY + UNIXEPOCH_MJD;
        double utcOffsetSec = fixOffset + ( mjd - scaleBase ) * scaleFactor;
        double utcOffsetMillis = utcOffsetSec * 1000;
        return unixMillis + TT_TAI_MILLIS + utcOffsetMillis - J2000_UNIXMILLIS;
    }

    /**
     * Returns an ordered list of scalers covering the whole range of times.
     * Ordering is by time, as per the {@link #compareTt2kMillis} method;
     * every long <code>tt2kMillis</code> value will be valid for one of
     * the list.
     *
     * @return  ordered list of time scalers
     */
    public static TtScaler[] getTtScalers() {
        return InstancesHolder.ORDERED_INSTANCES.clone();
    }

    /**
     * Creates an ordered list of instances covering the whole range of times.
     *
     * @return  ordered list of time scaler instances
     */
    private static TtScaler[] createTtScalers() {

        // Acquire leap seconds table.
        LtEntry[] ents = readLtEntries();
        int nent = ents.length;
        logger_.config( "CDF Leap second table: " + ents.length + " entries, "
                      + "last is " + ents[ nent - 1 ] );
        List<TtScaler> list = new ArrayList<TtScaler>();

        // Add a scaler valid from the start of time till the first LTS entry.
        // I'm not certain this has the correct formula, but using TT
        // prior to 1960 is a bit questionable in any case.
        LtEntry firstEnt = ents[ 0 ];
        list.add( new NoLeapTtScaler( 0, 0, 0, Long.MIN_VALUE,
                                      firstEnt.getDateTt2kMillis() ) );

        // Add scalers corresponding to each entry in the LTS array except
        // the final one.
        for ( int ie = 0; ie < nent - 1; ie++ ) {
            LtEntry ent0 = ents[ ie ];
            LtEntry ent1 = ents[ ie + 1 ];
            long fromValid = ent0.getDateTt2kMillis();
            long toValid = ent1.getDateTt2kMillis();

            // In case of a leap second, add two: one to cover just the leap
            // second, and another to cover the rest of the range till the
            // next entry starts.
            if ( ent1.hasPrecedingLeapSecond() ) {
                list.add( new NoLeapTtScaler( ent0, fromValid,
                                              toValid - 1000 ) );
                list.add( new LeapDurationTtScaler( ent0, toValid - 1000 ) );
            }

            // In case of no leap second, add a single scaler covering
            // the whole period.
            else {
                list.add( new NoLeapTtScaler( ent0, fromValid, toValid ) );
            }
        }

        // Add a scaler covering the period from the start of the last
        // entry till the end of time.
        LtEntry lastEnt = ents[ nent - 1 ];
        list.add( new NoLeapTtScaler( lastEnt, lastEnt.getDateTt2kMillis(),
                                      Long.MAX_VALUE ) );

        // Return as array.
        return list.toArray( new TtScaler[ 0 ] );
    }

    /**
     * Acquires the table of leap seconds from an internal array or external
     * file as appropriate.
     *
     * @return   leap second entry file
     */
    private static LtEntry[] readLtEntries() {

        // Attempt to read the leap seconds from an external file.
        LtEntry[] fentries = null;
        try {
            fentries = readLtEntriesFile();
        }
        catch ( IOException e ) {
            logger_.log( Level.WARNING,
                         "Failed to read external leap seconds file: " + e, e );
        }
        catch ( RuntimeException e ) {
            logger_.log( Level.WARNING,
                         "Failed to read external leap seconds file: " + e, e );
        }
        if ( fentries != null ) {
            return fentries;
        }

        // If that doesn't work, use the internal hard-coded table.
        else {
            logger_.config( "Using internal leap seconds table" );
            int nent = LTS.length;
            LtEntry[] entries = new LtEntry[ nent ];
            for ( int i = 0; i < nent; i++ ) {
                entries[ i ] = new LtEntry( LTS[ i ] );
            }
            return entries;
        }
    }

    /**
     * Attempts to read the leap seconds table from an external file.
     * As per the NASA library, this is pointed at by an environment variable.
     *
     * @return  leap seconds table, or null if not found
     */
    private static LtEntry[] readLtEntriesFile() throws IOException {
        String ltLoc;
        try {
            ltLoc = System.getenv( LEAP_FILE_ENV );
        }
        catch ( SecurityException e ) {
            logger_.config( "Can't access external leap seconds file: " + e );
            return null;
        }
        if ( ltLoc == null ) {
            return null;
        }
        logger_.config( "Reading leap seconds from file " + ltLoc );
        File file = new File( ltLoc );
        BufferedReader in = new BufferedReader( new FileReader( file ) );
        List<LtEntry> list = new ArrayList<LtEntry>();
        for ( String line; ( line = in.readLine() ) != null; ) {
            if ( ! line.startsWith( ";" ) ) {
                String[] fields = line.trim().split( "\\s+" );
                if ( fields.length != 6 ) {
                    throw new IOException( "Bad leap second file format - got "
                                         + fields.length + " fields not 6"
                                         + " at line \"" + line + "\"" );
                }
                try {
                    int year = Integer.parseInt( fields[ 0 ] );
                    int month = Integer.parseInt( fields[ 1 ] );
                    int dom = Integer.parseInt( fields[ 2 ] );
                    double fixOffset = Double.parseDouble( fields[ 3 ] );
                    double scaleBase = Double.parseDouble( fields[ 4 ] );
                    double scaleFactor = Double.parseDouble( fields[ 5 ] );
                    list.add( new LtEntry( year, month, dom, fixOffset,
                                           scaleBase, scaleFactor ) );
                }
                catch ( NumberFormatException e ) {
                    throw (IOException)
                          new IOException( "Bad entry in leap seconds file" )
                         .initCause( e );
                }
            }
        }
        return list.toArray( new LtEntry[ 0 ] );
    }

    /**
     * Lazy initialiser holder class idiom.
     * Class is initialised, hence initialiser is called, only when
     * the static member is actually used.
     */
    private static class InstancesHolder {
        static final TtScaler[] ORDERED_INSTANCES = createTtScalers();
    }

    /**
     * TtScaler implementation which does not contain any leap seconds.
     */
    private static class NoLeapTtScaler extends TtScaler {

        /**
         * Constructs a NoLeapScaler from coefficients.
         *
         * @param   fixOffset  fixed offset of UTC in seconds from TAI
         * @param   scaleBase  MJD base for scaling
         * @param   scaleFactor   factor for scaling
         * @param   fromTt2kMillis  start of validity range
         *                          in TT milliseconds since J2000
         * @param   toTt2kMillis    end of validity range
         *                          in TT milliseconds since J2000
         */
        NoLeapTtScaler( double fixOffset, double scaleBase, double scaleFactor,
                        long fromTt2kMillis, long toTt2kMillis ) {
            super( fixOffset, scaleBase, scaleFactor,
                   fromTt2kMillis, toTt2kMillis );
        }

        /**
         * Constructs a NoLeapTtScaler from an LtEntry.
         *
         * @param  ltEnt   LTS table entry object
         * @param   fromTt2kMillis  start of validity range
         *                          in TT milliseconds since J2000
         * @param   toTt2kMillis    end of validity range
         *                          in TT milliseconds since J2000
         */
        NoLeapTtScaler( LtEntry ltEnt,
                        long fromTt2kMillis, long toTt2kMillis ) {
            this( ltEnt.fixOffset_, ltEnt.scaleBase_, ltEnt.scaleFactor_,
                  fromTt2kMillis, toTt2kMillis );
        }

        public int millisIntoLeapSecond( long tt2kMillis ) {
            return -1;
        }
    }

    /**
     * TtScaler implementation whose whole duration represents a single
     * positive leap second.
     */
    private static class LeapDurationTtScaler extends TtScaler {
        private final long leapStartTt2kMillis_;

        /**
         * Constructor.
         *
         * @param  ltEnt   LTS table entry object
         * @param  leapStartTt2kMillis  start of leap second (hence validity
         *                              range) in TT milliseconds since J2000
         */
        LeapDurationTtScaler( LtEntry ltEnt, long leapStartTt2kMillis ) {
            super( ltEnt.fixOffset_, ltEnt.scaleBase_, ltEnt.scaleFactor_,
                   leapStartTt2kMillis, leapStartTt2kMillis + 1000 );
            leapStartTt2kMillis_ = leapStartTt2kMillis;
        }

        public int millisIntoLeapSecond( long tt2kMillis ) {
            long posdiff = tt2kMillis - leapStartTt2kMillis_;
            return posdiff >= 0 && posdiff <= 1000 ? (int) posdiff : -1;
        }
    }

    /**
     * Represents one entry in the LTS array corresponding to leap second
     * ranges.
     */
    private static class LtEntry {
        final int year_;
        final int month_;
        final int dom_;
        final double fixOffset_;
        final double scaleBase_;
        final double scaleFactor_;

        /**
         * Constructs entry from enumerated coefficients.
         *
         * @param   year   leap second year AD
         * @param   month  leap second month (1-based)
         * @param   dom    leap second day of month (1-based)
         * @param   fixOffset  fixed offset of UTC in seconds from TAI
         * @param   scaleBase  MJD base for scaling
         * @param   scaleFactor   factor for scaling
         */
        public LtEntry( int year, int month, int dom, double fixOffset,
                        double scaleBase, double scaleFactor ) {
            year_ = year;
            month_ = month;
            dom_ = dom;
            fixOffset_ = fixOffset;
            scaleBase_ = scaleBase;
            scaleFactor_ = scaleFactor;
        }

        /**
         * Constructs entry from array of 6 doubles.
         *
         * @param   ltCoeffs  6-element array of coefficients from LTS array:
         *                    year, month, dom, offset, base, factor
         */
        public LtEntry( double[] ltCoeffs ) {
             this( (int) ltCoeffs[ 0 ],
                   (int) ltCoeffs[ 1 ],
                   (int) ltCoeffs[ 2 ],
                   ltCoeffs[ 3 ],
                   ltCoeffs[ 4 ],
                   ltCoeffs[ 5 ] );
             assert year_ == ltCoeffs[ 0 ];
             assert month_ == ltCoeffs[ 1 ];
             assert dom_ == ltCoeffs[ 2 ];
        }

        /**
         * Returns the number of milliseconds in TT since J2000 corresponding
         * to the date associated with this entry.
         *
         * @return   TT millis since J2000
         */
        public long getDateTt2kMillis() {
             GregorianCalendar gcal = new GregorianCalendar( UTC, Locale.UK );
             gcal.clear();
             gcal.set( year_, month_ - 1, dom_ );
             long unixMillis = gcal.getTimeInMillis();
             return (long) unixToTt2kMillis( unixMillis, fixOffset_,
                                             scaleBase_, scaleFactor_ );
        }

        /**
         * Indicates whether there is a single positive leap second
         * immediately preceding the date associated with this entry.
         *
         * @return  true iff there is an immediately preceding leap second
         */
        public boolean hasPrecedingLeapSecond() {

            // This implementation is not particularly intuitive or robust,
            // but it's correct for the LTS hard-coded at time of writing,
            // and that array is not likely to undergo changes which would
            // invalidate this algorithm.
            return scaleFactor_ == 0;
        }

        @Override
        public String toString() {
            return year_ + "-" + month_ + "-" + dom_ + ": "
                 + fixOffset_ + ", " + scaleBase_ + ", " + scaleFactor_;
        }
    }
}