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}