Convert a Julian Date to an Instant
Asked Answered
Z

4

7

I'm running into a situation where I would like to convert from a Julian date to an java.time.Instant (if that makes sense), or some Java time that can be more easily understood. My understanding of what a Julian date is comes from reading the Wikipedia page. There are bunch of different variants, and the date I am trying to read uses a different epoch than any of these.

For example, let's say the epoch is the beginning of the Calendar (New Style) Act 1750, and the Julian date is 95906.27600694445 which in this case I believe is CE 2015 April 15 06:37:26.9 UT, how do I get an instant from this? I will need to adjust for the timezone later.

I noticed there is a class called JulianFields, but I don't know where/how to use it. Also, most of the methods I see in the package make use of int or long, not really anything for double.

So, is there a simple way to convert from a Julian date using a different epoch to a Java 8 Instant (or some other time if my thinking is wrong).

Zsa answered 14/8, 2015 at 18:16 Comment(4)
#16587306Ottie
@khudren I'm not sure what "Mort" means. In my case I am converting a Julian Date which contains a fraction of another day, and it also starts from a different epoch. Also, based on my understanding of Julian date, the string that the user is formatting in that question (at least in the way the user is formatting it) is not a Julian date.Zsa
There is confusion in this terminology, generally. There is a tendency to use "Julian Date" and "Julian Day Number" interchangeably. However a "Julian Date" is either reckoned according to the Julian Calendar (which preceded the Gregorian reformation in the 16th century) or according to the Julian Epoch from a Julian Day Number. A Julian Day Number is simply a continuous count of elapsed days since Janary 1, 4713 BC. Your question makes it seem as though you are using the latter.Unrivalled
Using a suitable web page to find the MJD of the known date/time, and subtracting the OP's double date from the calculated MJD, one can find the epoch of the OP's double is 14 Sept. 1752 Gregorian calendar, which is indeed the first day the Gregorian calendar was observed in Britain.Woeful
E
7

Here is a solution using the new Java 8 classes:

public class JulianDay {
    private static final double NANOS_PER_DAY = 24.0 * 60.0 * 60.0 * 1000000000.0;

    // Calculate Instants for some epochs as defined in Wikipedia.
    public static final Instant REDUCED_JD =
            ZonedDateTime.of(1858, 11, 16, 12, 0, 0, 0, ZoneOffset.UTC).toInstant();
    public static final Instant MODIFIED_JD =
            ZonedDateTime.of(1858, 11, 17, 0, 0, 0, 0, ZoneOffset.UTC).toInstant();
    public static final Instant JULIAN_DATE =
            REDUCED_JD.minus(2400000, ChronoUnit.DAYS);

    private final Instant epoch;

    public JulianDay(Instant epoch) {
        super();
        this.epoch = epoch;
    }

    public Instant toInstant(double day) {
        long l = (long) day;
        return epoch
                .plus(l, ChronoUnit.DAYS)
                .plusNanos(Math.round((day - l) * NANOS_PER_DAY));
    }

    public static void main(String[] args) {
        // Use the example values from Wikipedia for 2015-09-07 13:21 UTC.
        System.out.println(new JulianDay(REDUCED_JD).toInstant(57273.05625));
        // Output: 2015-09-07T13:21:00.000000126Z
        System.out.println(new JulianDay(MODIFIED_JD).toInstant(57272.55625));
        // Output: 2015-09-07T13:21:00.000000126Z
        System.out.println(new JulianDay(JULIAN_DATE).toInstant(2457273.05625));
        // Output: 2015-09-07T13:20:59.999991953Z
    }
}

Regarding the JulianFields you asked about, you can define a custom formatter like this:

DateTimeFormatter formatter = new DateTimeFormatterBuilder()
    .appendValue(JulianFields.MODIFIED_JULIAN_DAY)
    .toFormatter().withZone(ZoneOffset.UTC);

Unfortunately it doesn't support fractions of days:

System.out.println(formatter.format(Instant.now())); // Output: 57249
System.out.println(LocalDate.from(formatter.parse("57249"))); // Output: 2015-08-15
Eagle answered 14/8, 2015 at 20:23 Comment(12)
If his day numbers are reckoned from the New Style Calendar epoch, then your Instants will be off, right? The epoch for Modified Julian Day Numbers is about 75 years later.Unrivalled
@Unrivalled You are referring to the missing days in years 1751 and 1752 right? If dates before 14 September 1752 from that calendar were used, than you are right. An offset would be needed depending on the exact date.Eagle
Well ... I'm not sure what date you're using as your epoch, tbh. Nov 16, 1858 is close to the epoch date for a Modified Julian Day Number ... but the OP isn't working with MJDs. Your solution also works on a specific day number, not an arbitrary one supplied as, say, a parameter to a function.Unrivalled
@Unrivalled It's Reduced JD as defined in the Wikipedia article. It's starting 0.5 days earlier than Modified JD. I have just chosen the first variant in the table, because the OP said he is using one which is not in the list.Eagle
@Unrivalled I have changed my answer to a complete solution, as you wished. :)Eagle
Your method, toInstant(), is correct only if the day number is already reckoned in GMT/UTC. If the day number has been reckoned in a different zone, the Instant will not be correct.Unrivalled
@Unrivalled Quote from the Wikipedia article: The Julian date (JD) of any instant is the Julian day number for the preceding noon in Greenwich Mean Time plus the fraction of the day since that instant.Eagle
Quite right. Julian day numbers are officially reckoned in UTC in order to meet the definition of a JDN. However, just as LocalTime and LocalDateTime can be reckoned without regard to any time zone, so too is it possible to have a continuous count of days in any time zone that reflects the local time. These local or "regional day numbers" cannot be converted to a JDN (or variant of) or an Instant without first being adjusted by offsets for GMT/UTC and daylight savings.Unrivalled
This is the answer I decided to go with, and it seems to be working well since the dates I am dealing with are already in UTC. A few notes about it though. 1) When using a "far in the past" epoch, your nano calculation will become Long.MAX_VALUE and will be incorrect. This happens if you use the 12h Jan 1, 4713 BC epoch. 2) Not so much about your answer, but you have to be careful about converting Julian calendar dates to Gregorian calendar dates. I had some missing knowledge before going into this question that I have learned.Zsa
@mkobit: FWIW, both methods shown in my answer are not susceptible to nanoseconds overflow and are correct for Julian Day Numbers back to 4713 BC (in fact, both methods use arithmetic expressions and are largely independent of the Java Calendar and the Java 8 DateTime APIs). They only have millisecond precision, however. As to your second point, converting day counts to correct local date/time stamps is always where the biggest problems lie (offsets, DST, Julian ->Gregorian calendar). Happily, the Java APIs handle the most common of these problems for us.Unrivalled
@Zsa I have adapted the code from my first answer to prevent overflow problems. Dates outside of the gregorian calender will still need additional manual handling.Eagle
seems to working fine as far as i can tell. i read a few of these comments but they go over my head. at first glance for the first few usages this is fine for me.Repeal
U
3

I am going to assume that you have a numeric timestamp that is a kind of modified Julian Day Number, i.e. a continuous count of days since a defined epoch.

For example, the definition of a "Modified Julian Day Number" is a continuous count of days since midnight on Nov 17, 1858. I believe what you are asking is:

How do I convert a continuous count of days in England since the Gregorian Calendar was officially adopted to an Instant?

I'm not certain where the Gregorian Epoch officially began after the New Style Calendar act. I will assume that it is January 1, 1752, i.e. the number 95906.276 is a continuous count of days since then.

METHOD1: Here is an algorithm for processing a day number to an integer array representation in year, month(1-12), day(1-31), hours(0-23), min(0-59), sec(0-59), millis:

    private static final int                   YEAR = 0;
    private static final int                  MONTH = 1;
    private static final int                    DAY = 2;
    private static final int                  HOURS = 3;
    private static final int                MINUTES = 4;
    private static final int                SECONDS = 5;
    private static final int                 MILLIS = 6;

    public static int[] toTimeStampArray(double yourEpochDayNumber) {

        int ymd_hms[] = { -1, -1, -1, -1, -1, -1, -1 };
        int a, b, c, d, e, z;

        // convert from your epoch (1/1/1752) to Julian Day Number
        double jd = yourEpochDayNumber + 2360965.5 + 0.5;
        double f, x;

        z = (int) Math.floor(jd);
        f = jd - z;

        if (z >= 2299161) {
            int alpha = (int) Math.floor((z - 1867216.25) / 36524.25);
            a = z + 1 + alpha - (int) Math.floor(alpha / 4);
        } else {
            a = z;
        }

        b = a + 1524;
        c = (int) Math.floor((b - 122.1) / 365.25);
        d = (int) Math.floor(365.25 * c);
        e = (int) Math.floor((b - d) / 30.6001);

        ymd_hms[DAY] = b - d - (int) Math.floor(30.6001 * e);
        ymd_hms[MONTH] = (e < 14)
                ? (e - 1)
                : (e - 13);
        ymd_hms[YEAR] = (ymd_hms[MONTH] > 2)
                ? (c - 4716)
                : (c - 4715);

        for (int i = HOURS; i <= MILLIS; i++) {
            switch(i) {
                case HOURS:
                    f = f * 24.0;
                    break;
                case MINUTES: case SECONDS:
                    f = f * 60.0;
                    break;
                case MILLIS:
                    f = f * 1000.0;
                    break;  
            }
            x = Math.floor(f);
            ymd_hms[i] = (int) x;
            f = f - x;
        }   
        return ymd_hms;
    }

Algorithm is adapted from Meeus J., Astronomical Algorithms, 2nd Ed.

From these data, you can create a LocalDateTime instance. You can combine that with a ZoneId instance to create a ZonedDateTime and get an Instant.

METHOD 2. If your day number is already reckoned in GMT/UTC and does not require any offsets for time zone or daylight savings, then you can convert directly from a day number (in your epoch) to an Instant as follows:

public Instant dayNumberToInstant(double dayNumber) {
    long millisFromPosixEpoch;

    final double POSIX_EPOCH_AS_DAYNUM = 79622.0

    millisFromPosixEpoch = (long) ((dayNumber - POSIX_EPOCH_AS_DAYNUM) *
        (86400.0 * 1000.0));
    return Instant.ofEpochMillis(millisFromPosixEpoch);
}
Unrivalled answered 14/8, 2015 at 19:57 Comment(0)
G
2

Epoch of 14 September 1752

My reading of the Wikipedia page on Calendar (New Style) Act 1750 indicates an epoch reference date of 1752-09-14.

If we add the integer portion of your input number of 95906.27600694445, 95_906L, we do indeed get your target date of April 15, 2015 (in modern calendar system).

long input = 95_906L;
LocalDate epochCalendarNewStyleActOf1750 = LocalDate.of ( 1752 , Month.SEPTEMBER , 14 );
LocalDate localDate = epochCalendarNewStyleActOf1750.plusDays ( input );
System.out.println ( input + " days from epoch of: " + epochCalendarNewStyleActOf1750 + " is " + localDate );

95906 days from epoch of: 1752-09-14 is 2015-04-15

Regarding the fractional number, which I presume is the fraction of the number of seconds in a generic 24-hour day.

While LocalDate is for date-only without time-of-day, we now need time-of-day which is represented by our fractional number. So in place of LocalDate, we switch to OffsetDateTime.

OffsetDateTime epochCalendarNewStyleActOf1750 = LocalDate.of ( 1752 , Month.SEPTEMBER , 14 ).atStartOfDay ().atOffset ( ZoneOffset.UTC );

We use BigDecimal as double and Double are floating-point technology that trades away accuracy for speed of execution.

String input = "95906.27600694445";
BigDecimal bd = new BigDecimal ( input );

Pull from that the number of whole days.

long days = bd.toBigInteger ().longValue ();

Work on the fraction of a day. Extract the fractional number by subtracting the integer portion.

BigDecimal fractionOfADay = bd.subtract ( new BigDecimal ( days ) ); // Extract the fractional number, separate from the integer number.

We assume this decimal fraction is a fraction of the number of seconds in a day. So we can multiply by the number of seconds is a day.

BigDecimal secondsFractional = new BigDecimal ( TimeUnit.DAYS.toSeconds ( 1 ) ).multiply ( fractionOfADay );

From that, extract the number of whole seconds. From the remainder, produce a whole number nanoseconds, the resolution of the java.time classes including OffsetDateTime and Duration.

long secondsWhole = secondsFractional.longValue ();
long nanos = secondsFractional.subtract ( new BigDecimal ( secondsWhole ) ).multiply ( new BigDecimal ( 1_000_000_000L ) ).longValue ();

Create a Duration to represent the amount of time we want to add to our epoch.

Duration duration = Duration.ofDays ( days ).plusSeconds ( secondsWhole ).plusNanos ( nanos );

Add the duration to the epoch to get our final result.

OffsetDateTime odt = epochCalendarNewStyleActOf1750.plus ( duration );

You can extract a Instant object from the OffsetDateTime.

Instant instant = odt.toInstant();

Dump to console.

System.out.println ( "bd: " + bd );
System.out.println ( "days: " + days );
System.out.println ( "fractionOfADay.toString(): " + fractionOfADay );
System.out.println ( "secondsFractional: " + secondsFractional );
System.out.println ( "secondsWhole: " + secondsWhole );
System.out.println ( "nanos: " + nanos );
System.out.println ( "duration.toString(): " + duration );
System.out.println ( "duration.toDays(): " + duration.toDays () );
System.out.println ( "odt.toString(): " + odt );

This code seems to be working properly. The result here matches the expectation stated in the Question to the second, though we disagree on the fraction of a second.

See this code run live at IdeOne.com.

bd: 95906.27600694445

days: 95906

fractionOfADay.toString(): 0.27600694445

secondsFractional: 23847.00000048000

secondsWhole: 23847

nanos: 480

duration.toString(): PT2301750H37M27.00000048S

duration.toDays(): 95906

odt.toString(): 2015-04-15T06:37:27.000000480Z

Of course this math could be simpler. But I thought it might be interesting to show the pieces. One simpler ways is to multiply the 95906.27600694445 BigDecimal by the number of seconds in a generic 24 hour day. Then separate the resulting integer from its decimal fraction, and feed each to Duration.ofSeconds and Duration::plusNanos as that fits the internal data model of Duration, a total number of seconds and a total number of nanos in a fraction of a second. We would be skipping the part where we called Duration.ofDays.


About java.time

The java.time framework is built into Java 8 and later. These classes supplant the troublesome old legacy date-time classes such as java.util.Date, Calendar, & SimpleDateFormat.

The Joda-Time project, now in maintenance mode, advises migration to the java.time classes.

To learn more, see the Oracle Tutorial. And search Stack Overflow for many examples and explanations. Specification is JSR 310.

Where to obtain the java.time classes?

The ThreeTen-Extra project extends java.time with additional classes. This project is a proving ground for possible future additions to java.time. You may find some useful classes here such as Interval, YearWeek, YearQuarter, and more.

Glyptodont answered 1/3, 2017 at 8:31 Comment(0)
C
0

The most complete and also shortest approach is obtained by my library Time4J, see this snippet using the class JulianDay:

double customJD = 95906.27600694445; 

// my comment: I have never seen Julian days with that epoch until now    
HistoricCalendar hcal = // date when the new style calendar act took effect
    HistoricCalendar.of(ChronoHistory.of(Locale.UK), HistoricEra.AD, 1752, 9, 14);

// last term 0.5 necessary because julian days start at noon
double value = customJD + hcal.get(EpochDays.JULIAN_DAY_NUMBER) - 0.5;
JulianDay jd = JulianDay.ofSimplifiedTime(value);
Instant instant = jd.toMoment().toTemporalAccessor();

System.out.println(instant); // 2015-04-15T06:37:27Z

However, it should be noted that the most dominating field of application of Julian days is the astronomy, see also the official recommendation of the IAU. And on that field, it is much more common to include a delta-T-correction i.e. to define the Julian days on the time scale TT (Terrestrial Time). Time4J offers the methods JulianDay.ofEphemerisTime(...) for this purpose. If you seriously consider to handle time scales such as TT then you should rather work with the class Moment instead of Instant because last one cannot understand TT, UTC, UT including leap second handling etc.

Carrycarryall answered 24/10, 2017 at 11:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.