package org.yamcs.utils;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class to convert between TAI and UTC.
* It reads the UTC-TAI.history (available at http://hpiers.obspm.fr/eoppc/bul/bulc/UTC-TAI.history)
*
* It only supports added leap seconds not removed ones (negative leap seconds have never happened and probably never will).
*
* It only works correctly with the times after 1972 when the difference between TAI and UTC is an integer number of seconds.
*
* Most of the code is copied or inspired from the TAI C library http://cr.yp.to/libtai.html
*
*
*/
public class TaiUtcConverter {
long[] timesecs; //TAI time in seconds when leap seconds are added
int diffTaiUtc; //the difference between the TAI and UTC at the last interval
static String UTC_TAI_HISTORY_FN="UTC-TAI.history";
static final int[] times365 = new int[]{ 0, 365, 730, 1095 } ;
static final int[] times36524 = new int[]{ 0, 36524, 73048, 109572 } ;
static final int[] montab = { 0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337 } ;
/* month length after february is (306 * m + 5) / 10 */
static final int[] PREVIOUS_MONTH_END_DAY = {0, 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334};
static final int[] PREVIOUS_MONTH_END_DAY_LS = {0, 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335};
public TaiUtcConverter() throws IOException, ParseException {
InputStream is = TaiUtcConverter.class.getResourceAsStream("/"+UTC_TAI_HISTORY_FN);
if(is==null) throw new RuntimeException("Cannot find "+UTC_TAI_HISTORY_FN+" in the classpath");
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line =null;
// 1974 Jan. 1 - 1975 Jan. 1 13s
String dp = "\\s?(\\d+)?\\s+(\\w{3})\\.?\\s+(\\d+)";
Pattern p = Pattern.compile(dp+"\\s*\\.?\\-\\s*("+dp+")?\\s*(\\d+)s\\s*");
ArrayList<Long> tmp1 = new ArrayList<Long>();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy MMM dd", Locale.ENGLISH);
sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
diffTaiUtc = -1;
int lineNum=0;
String prevYear = null;
while((line=reader.readLine()) != null) {
lineNum++;
Matcher m = p.matcher(line);
if(m.matches()) {
String year = m.group(1);
if(year==null) {
year = prevYear;
}
Date d = sdf.parse(year+" "+m.group(2)+ " "+m.group(3));
tmp1.add(d.getTime());
int ls = Integer.valueOf(m.group(8));
if((diffTaiUtc!=-1) && (ls!=diffTaiUtc+1)) {
throw new RuntimeException("Error reading line "+lineNum+" of UTC-TAI.history: only positive leap seconds are supported");
}
diffTaiUtc = ls;
prevYear = year;
}
}
timesecs = new long[tmp1.size()];
for(int i = 0; i<timesecs.length; i++) {
timesecs[i]=tmp1.get(i)/1000 + diffTaiUtc - timesecs.length + i;
}
}
//converts Modified Julian Day to calendar day
private void caldateFromMjd(DateTimeComponents cd, long day) {
int year;
int month;
int yday;
year = (int)(day / 146097);
day %= 146097L;
day += 678881L;
while (day >= 146097L) { day -= 146097L; ++year; }
/* year * 146097 + day - 678881 is MJD; 0 <= day < 146097 */
/* 2000-03-01, MJD 51604, is year 5, day 0 */
year *= 4;
if (day == 146096L) { year += 3; day = 36524; }
else { year += day / 36524L; day %= 36524L; }
year *= 25;
year += day / 1461;
day %= 1461;
year *= 4;
yday = (day < 306)?1:0;
if (day == 1460) { year += 3; day = 365; }
else { year += day / 365; day %= 365; }
yday += day;
day *= 10;
month = (int)((day + 5) / 306);
day = (day + 5) % 306;
day /= 10;
if (month >= 10) { yday -= 306; ++year; month -= 10; }
else { yday += 59; month += 2; }
cd.year = year;
cd.month = month + 1;
cd.day = (int)(day + 1);
cd.doy=yday+1;
}
//converts calendar date to Modified Julian Day
//doy is ignored
private long caldateToMjd(DateTimeComponents dtc) {
int y;
int m;
int d;
d = dtc.day - 678882;
m = dtc.month - 1;
y = dtc.year;
d += 146097L * (y / 400);
y %= 400;
if (m >= 2) m -= 2; else { m += 10; --y; }
y += (m / 12);
m %= 12;
if (m < 0) { m += 12; --y; }
d += montab[m];
d += 146097L * (y / 400);
y %= 400;
if (y < 0) { y += 400; d -= 146097L; }
d += times365[y & 3];
y >>= 2;
d += 1461L * (y % 25);
y /= 25;
d += times36524[y & 3];
return d;
}
DateTimeComponents instantToUtc(long t) {
DateTimeComponents dtc = new DateTimeComponents();
long u;
int leap;
long s;
u = t/1000;
dtc.millisec = (int)( t % 1000);
//leap = leapsecs_sub(&t2);
leap = 0;
int ls = diffTaiUtc;
for(int i = timesecs.length -1 ; i>=0; i--){
if (u > timesecs[i]) break;
if (u == timesecs[i]) { leap = 1; break;}
ls--;
}
u-=ls;
u+=86400L; //to avoid u being negative
s = u % 86400L;
dtc.second = (int)((s % 60) + leap); s /= 60;
dtc.minute = (int)(s % 60); s /= 60;
dtc.hour = (int)s;
u /= 86400L;
long mjd = 40586+u;
caldateFromMjd(dtc, mjd);
return dtc;
}
/**
* transforms Instant to Unix time expressed in milliseconds since 1970
* @param t
* @return
*/
long instantToUnix(long t) {
long u = t/1000;
int ls = diffTaiUtc;
for(int i = timesecs.length -1 ; i>=0; i--){
if (u >= timesecs[i]) break;
ls--;
}
return t-ls*1000;
}
/**
* Converts UTC to instant.
* WARNING: DOY is ignored.
* @param dtc
* @return
*/
long utcToInstant(DateTimeComponents dtc) {
long day = caldateToMjd(dtc);
long s = dtc.hour * 60 + dtc.minute;
s = s * 60 + dtc.second + (day - 40587) * 86400L;
int ls = diffTaiUtc;
for(int i = timesecs.length -1 ; i>=0; i--) {
long u = timesecs[i]-ls+1;
if (s > u) break;
if((s < u) || (dtc.second==60)) ls--;
}
s+=ls;
return dtc.millisec + 1000 * s;
}
/**
* transforms unix time expressed in milliseconds since 1970 to instant
* @param t
* @return
*/
long unixToInstant(long t) {
long u = t/1000;
int ls = diffTaiUtc;
for(int i = timesecs.length -1 ; i>=0; i--){
if (u >= timesecs[i]-ls+1) break;
ls--;
}
return t+ls*1000;
}
public static boolean isLeap(final int year) {
return ((year % 4) == 0) && (((year % 400) == 0) || ((year % 100) != 0));
}
public static class DateTimeComponents {
public int year;
public int month; //month starting with 1
public int day;
public int hour;
public int minute;
public int second;
public int millisec;
public int doy;
public DateTimeComponents(int year, int month, int day, int hour,
int minute, int second, int millisec) {
this.year=year;
this.month=month;
this.day=day;
this.hour=hour;
this.minute=minute;
this.second=second;
this.millisec=millisec;
}
private DateTimeComponents() { }
public DateTimeComponents(int year, int doy, int hour, int minute,
int second, int millisec) {
this.year=year;
this.doy = doy;
this.hour=hour;
this.minute=minute;
this.second=second;
this.millisec=millisec;
if(isLeap(year)) {
this.month = (doy < 32) ? 1 : (10 * doy + 313) / 306;
this.day = doy - PREVIOUS_MONTH_END_DAY_LS[this.month];
} else {
this.month = (doy < 32) ? 1 : (10 * doy + 323) / 306;
this.day = doy - PREVIOUS_MONTH_END_DAY[this.month];
}
}
@Override
public String toString() {
return "DateTimeComponents [year=" + year + ", month=" + month
+ ", day=" + day + ", hour=" + hour + ", minute=" + minute
+ ", second=" + second + ", millisec=" + millisec
+ ", doy=" + doy + "]";
}
public String toIso8860String() {
return String.format("%04d-%02d-%02dT%02d:%02d:%02d.%03d", year, month, day, hour, minute, second, millisec);
}
}
}