tgrn2 - 2012-05-14

Hi There,

I have a question about leap second handling in JSR 310. I created my own leap
second table by copying SystemUTCRules as follows:

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;

import javax.time.CalendricalException;
import javax.time.Instant;
import javax.time.MathUtils;
import javax.time.TAIInstant;
import javax.time.UTCInstant;
import javax.time.UTCRules;
import javax.time.calendar.LocalDate;

final class MyUTCRules extends UTCRules implements Serializable {

/
Singleton.
/
static final MyUTCRules INSTANCE = new MyUTCRules();
/

Serialization version.
/
private static final long serialVersionUID = 1L;

/
The table of leap second dates.
/
private AtomicReference<Data> dataRef = new
AtomicReference<Data>(loadLeapSeconds());

/ Data holder. */
private static final class Data implements Serializable {
/
Serialization version. /
private static final long serialVersionUID = 1L;
/ Constructor. */
private Data(long dates, int offsets, long taiSeconds) {
super();
this.dates = dates;
this.offsets = offsets;
this.taiSeconds = taiSeconds;
}
/
The table of leap second date when the leap second occurs.
/
final long dates;
/ The table of TAI offset after the leap second. */
final int offsets;
/
The table of TAI second when the new offset starts. */
final long taiSeconds;
}

//-----------------------------------------------------------------------
/
Restricted constructor.
/
private MyUTCRules() {
}

/
Resolves singleton.

@return the resolved instance, not null
/
private Object readResolve() {
return INSTANCE;
}

//-----------------------------------------------------------------------
/
Adds a new leap second to these rules.

@param mjDay the modified julian date that the leap second occurs at the end of
@param leapAdjustment the leap seconds to add/remove at the end of the day, either -1 or 1
@throws IllegalArgumentException if the leap adjustment is invalid
@throws IllegalArgumentException if the day is before or equal the last known leap second day
and the definition does not match a previously registered leap
@throws ConcurrentModificationException if another thread updates the rules at the same time
*/
void registerLeapSecond(long mjDay, int leapAdjustment) {
if (leapAdjustment != -1 && leapAdjustment != 1) {
throw new IllegalArgumentException("Leap adjustment must be -1 or 1");
}
Data data = dataRef.get();
int pos = Arrays.binarySearch(data.dates, mjDay);
int currentAdj = pos > 0 ? data.offsets - data.offsets : 0;
if (currentAdj == leapAdjustment) {
return; // matches previous definition
}
if (mjDay <= data.dates) {
throw new IllegalArgumentException("Date must be after the last configured
leap second date");
}
long dates = Arrays.copyOf(data.dates, data.dates.length + 1);
int offsets = Arrays.copyOf(data.offsets, data.offsets.length + 1);
long taiSeconds = Arrays.copyOf(data.taiSeconds, data.taiSeconds.length + 1);
int offset = offsets + leapAdjustment;
dates = mjDay;
offsets = offset;
taiSeconds = tai(mjDay, offset);
Data newData = new Data(dates, offsets, taiSeconds);
if (dataRef.compareAndSet(data, newData) == false) {
throw new ConcurrentModificationException("Unable to update leap second rules
as they have already been updated");
}
}

//-----------------------------------------------------------------------
@Override
public String getName() {
return "My";
}

@Override
public int getLeapSecondAdjustment(long mjDay) {
Data data = dataRef.get();
int pos = Arrays.binarySearch(data.dates, mjDay);
return pos > 0 ? data.offsets - data.offsets : 0;
}

@Override
public int getTAIOffset(long mjDay) {
Data data = dataRef.get();
int pos = Arrays.binarySearch(data.dates, mjDay);
pos = (pos < 0 ? ~pos : pos);
return pos > 0 ? data.offsets : 10;
}

@Override
public long getLeapSecondDates() {
Data data = dataRef.get();
return data.dates.clone();
}

//-------------------------------------------------------------------------
@Override
protected UTCInstant convertToUTC(TAIInstant taiInstant) {
Data data = dataRef.get();
long mjds = data.dates;
long tais = data.taiSeconds;
int pos = Arrays.binarySearch(tais, taiInstant.getTAISeconds());
pos = (pos >= 0 ? pos : ~pos - 1);
int taiOffset = (pos >= 0 ? data.offsets : 10);
long adjustedTaiSecs = taiInstant.getTAISeconds() - taiOffset;
long mjd = MathUtils.floorDiv(adjustedTaiSecs, SECS_PER_DAY) + OFFSET_MJD_TAI;
long nod = MathUtils.floorMod(adjustedTaiSecs, SECS_PER_DAY)
NANOS_PER_SECOND + taiInstant.getNanoOfSecond();
long mjdNextRegionStart = (pos + 1 < mjds.length ? mjds + 1 : Long.MAX_VALUE);
if (mjd == mjdNextRegionStart) { // in leap second
mjd--;
nod = SECS_PER_DAY * NANOS_PER_SECOND + (nod / NANOS_PER_SECOND)

NANOS_PER_SECOND + nod % NANOS_PER_SECOND;
}
return UTCInstant.ofModifiedJulianDay(mjd, nod, this);
}

//-------------------------------------------------------------------------
/
Loads the leap seconds from file.

@return an array of two arrays - leap seconds dates and amounts
/
private static Data loadLeapSeconds() {
InputStream in = MyUTCRules.class.getResourceAsStream("MyLeapSeconds.txt");
if (in == null) {
throw new CalendricalException("LeapSeconds.txt resource missing");
}
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(in,
"UTF-8"));
Map<Long, Integer=""> leaps = new TreeMap<Long, Integer="">();
while (true) {
String line = reader.readLine();
if (line == null) {
break;
}
line = line.trim();
if (line.length() > 0 && line.charAt(0) != '#') {
String split = line.split(" ");
if (split.length != 2) {
throw new CalendricalException("LeapSeconds.txt has invalid line format");
}
LocalDate date = LocalDate.parse(split);
int offset = Integer.parseInt(split);
leaps.put(date.toModifiedJulianDay(), offset);
}
}
long dates = new long;
int offsets = new int;
long taiSeconds = new long;
int i = 0;
for (Map.Entry<Long, Integer=""> entry : leaps.entrySet()) {
long changeMjd = entry.getKey() - 1; // subtract one to get date leap second
is added
int offset = entry.getValue();
if (i > 0) {
int adjust = offset - offsets_;
if (adjust < -1 || adjust > 1) {
throw new CalendricalException("Leap adjustment must be -1 or 1");
}
}
dates_ = changeMjd;
offsets_ = offset;
taiSeconds = tai(changeMjd, offset);
}
return new Data(dates, offsets, taiSeconds);
} catch (IOException ex) {
try {
in.close();
} catch (IOException ignored) {
// ignore
}
throw new CalendricalException("Exception reading LeapSeconds.txt", ex);
}
}

/
Gets the TAI seconds for the start of the day following the day passed in.

@param changeMjd the MJD that the leap second is added to
@param offset the new offset after the leap
@return the TAI seconds
/
private static long tai(long changeMjd, int offset) {
return (changeMjd + 1 - OFFSET_MJD_TAI) * SECS_PER_DAY + offset;
}

}

Then I supplied a simple table as follows:

From Leap Table

1970-01-01 22
1972-01-01 23
1972-07-01 24
1973-01-01 25
1974-01-01 26

And then ran the leap second rules with the following test code:

import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import javax.time.Instant;
import javax.time.TAIInstant;
import javax.time.UTCInstant;
import javax.time.UTCRules;

public class TestClass {

static final long MILLIS_PER_SECOND = 1000;

static final long NANOSECONDS_PER_MILLISECOND = 1000000L;

public static void main( String in )
{
Calendar cal = GregorianCalendar.getInstance( );
cal.setTimeZone( TimeZone.getTimeZone( "GMT" ) );
cal.set( Calendar.YEAR , 2010 );
cal.set( Calendar.MONTH, Calendar.MARCH );
cal.set( Calendar.HOUR_OF_DAY , 6 );
cal.set( Calendar.MINUTE , 6 );
cal.set( Calendar.SECOND , 27 );
cal.set( Calendar.MILLISECOND , 548 );
System.out.println( cal );
final long timeMillis = cal.getTimeInMillis();

Instant inst = Instant.ofEpochSecond( timeMillis / MILLIS_PER_SECOND ,
( ( timeMillis % MILLIS_PER_SECOND ) * NANOSECONDS_PER_MILLISECOND ) ); //
seconds and nanoseconds since 1970

TAIInstant ta = TAIInstant.of( inst );
System.out.println( inst );

UTCInstant utc_my = UTCInstant.of( inst , MyUTCRules.INSTANCE );

UTCInstant utc_my2 = UTCInstant.of( ta , MyUTCRules.INSTANCE );

// These two answers should be the same, but they aren't:
System.out.println( utc_my );
System.out.println( utc_my2 );

}

}

And what I am getting as output is the following:

java.util.GregorianCalendar,firstDayOfWeek=1,minimalDaysInFirstWeek=1,ERA=1,YE
AR=2010,MONTH=2,WEEK_OF_YEAR=20,WEEK_OF_MONTH=3,DAY_OF_MONTH=14,DAY_OF_YEAR=13
5,DAY_OF_WEEK=2,DAY_OF_WEEK_IN_MONTH=2,AM_PM=1,HOUR=5,HOUR_OF_DAY=6,MINUTE=6,S
ECOND=27,MILLISECOND=548,ZONE_OFFSET=-25200000,DST_OFFSET=3600000]
2010-03-14T06:06:27.548Z
2010-03-14T06:06:27.548000000(UTC)
2010-03-14T06:06:35.548000000(UTC)

It seems that the bottom two lines of the output SHOULD match, but they don't.
On one path I convert to TAI before performing the leap second conversion, but
this shouldn't matter because the conversion from Instant to TAIInstant
shouldn't require a leap second transform. I don't override the convertToUTC()
method for Instant, but neither does the SystemUTCRules class that I patterned
the code on.

So my question is, why do the last two lines of the output (above) have
different times? It seems that there's no way to make the coordinate
transforms consistent with each other.

/** Thorn