julianday.go

  1// Package julianday provides Time to Julian day conversions.
  2package julianday
  3
  4import (
  5	"bytes"
  6	"errors"
  7	"math"
  8	"strconv"
  9	"time"
 10)
 11
 12const secs_per_day = 86_400
 13const nsec_per_sec = 1_000_000_000
 14const nsec_per_day = nsec_per_sec * secs_per_day
 15const epoch_days = 2_440_587
 16const epoch_secs = secs_per_day / 2
 17
 18func jd(t time.Time) (day, nsec int64) {
 19	sec := t.Unix()
 20	// guaranteed not to overflow
 21	day, sec = sec/secs_per_day+epoch_days, sec%secs_per_day+epoch_secs
 22	return day, sec*nsec_per_sec + int64(t.Nanosecond())
 23}
 24
 25// Date returns the Julian day number for t,
 26// and the nanosecond offset within that day,
 27// in the range [0, 86399999999999].
 28func Date(t time.Time) (day, nsec int64) {
 29	day, nsec = jd(t)
 30	switch {
 31	case nsec < 0:
 32		day -= 1
 33		nsec += nsec_per_day
 34	case nsec >= nsec_per_day:
 35		day += 1
 36		nsec -= nsec_per_day
 37	}
 38	return day, nsec
 39}
 40
 41// Float returns the Julian date for t as a float64.
 42//
 43// In the XXI century, this has submillisecond precision.
 44func Float(t time.Time) float64 {
 45	day, nsec := jd(t)
 46	// converting day and nsec to float64 is exact
 47	return float64(day) + float64(nsec)/nsec_per_day
 48}
 49
 50// Format returns the Julian date for t as a string.
 51//
 52// This has nanosecond precision.
 53func Format(t time.Time) string {
 54	var buf [32]byte
 55	return string(AppendFormat(buf[:0], t))
 56}
 57
 58// AppendFormat is like Format but appends the textual representation to dst
 59// and returns the extended buffer.
 60func AppendFormat(dst []byte, t time.Time) []byte {
 61	day, nsec := Date(t)
 62	if day < 0 && nsec != 0 {
 63		dst = append(dst, '-')
 64		day = ^day
 65		nsec = nsec_per_day - nsec
 66	}
 67	var buf [20]byte
 68	dst = strconv.AppendInt(dst, day, 10)
 69	frac := strconv.AppendFloat(buf[:0], float64(nsec)/nsec_per_day, 'f', 15, 64)
 70	return append(dst, bytes.TrimRight(frac[1:], ".0")...)
 71}
 72
 73// Time returns the UTC Time corresponding to the Julian day number
 74// and nanosecond offset within that day.
 75// Not all day values have a corresponding time value.
 76func Time(day, nsec int64) time.Time {
 77	return time.Unix((day-epoch_days)*secs_per_day-epoch_secs, nsec).UTC()
 78}
 79
 80// FloatTime returns the UTC Time corresponding to a Julian date.
 81// Not all date values have a corresponding time value.
 82//
 83// In the XXI century, this has submillisecond precision.
 84func FloatTime(date float64) time.Time {
 85	day, frac := math.Modf(date)
 86	nsec := math.Floor(frac * nsec_per_day)
 87	return Time(int64(day), int64(nsec))
 88}
 89
 90// Parse parses a formatted Julian date and returns the UTC Time it represents.
 91//
 92// This has nanosecond precision.
 93func Parse(s string) (time.Time, error) {
 94	digits := 0
 95	dot := len(s)
 96	for i, b := range []byte(s) {
 97		if '0' <= b && b <= '9' {
 98			digits++
 99			continue
100		}
101		if b == '.' && i < dot {
102			dot = i
103			continue
104		}
105		if (b == '+' || b == '-') && i == 0 {
106			continue
107		}
108		return time.Time{}, errors.New("julianday: invalid syntax")
109	}
110	if digits == 0 {
111		return time.Time{}, errors.New("julianday: invalid syntax")
112	}
113
114	day, err := strconv.ParseInt(s[:dot], 10, 64)
115	if err != nil && dot > 0 {
116		return time.Time{}, errors.New("julianday: value out of range")
117	}
118	frac, _ := strconv.ParseFloat(s[dot:], 64)
119	nsec := int64(math.Round(frac * nsec_per_day))
120	if s[0] == '-' {
121		nsec = -nsec
122	}
123	return Time(day, nsec), nil
124}