148 lines
4.9 KiB
Go
148 lines
4.9 KiB
Go
package xlsx
|
|
|
|
import (
|
|
"math"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
MJD_0 float64 = 2400000.5
|
|
MJD_JD2000 float64 = 51544.5
|
|
|
|
secondsInADay = float64((24*time.Hour)/time.Second)
|
|
nanosInADay = float64((24*time.Hour)/time.Nanosecond)
|
|
)
|
|
|
|
var (
|
|
timeLocationUTC, _ = time.LoadLocation("UTC")
|
|
|
|
unixEpoc = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
// In 1900 mode, Excel takes dates in floating point numbers of days starting with Jan 1 1900.
|
|
// The days are not zero indexed, so Jan 1 1900 would be 1.
|
|
// Except that Excel pretends that Feb 29, 1900 occurred to be compatible with a bug in Lotus 123.
|
|
// So, this constant uses Dec 30, 1899 instead of Jan 1, 1900, so the diff will be correct.
|
|
// http://www.cpearson.com/excel/datetime.htm
|
|
excel1900Epoc = time.Date(1899, time.December, 30, 0, 0, 0, 0, time.UTC)
|
|
excel1904Epoc = time.Date(1904, time.January, 1, 0, 0, 0, 0, time.UTC)
|
|
// Days between epocs, including both off by one errors for 1900.
|
|
daysBetween1970And1900 = float64(unixEpoc.Sub(excel1900Epoc)/(24 * time.Hour))
|
|
daysBetween1970And1904 = float64(unixEpoc.Sub(excel1904Epoc)/(24 * time.Hour))
|
|
)
|
|
|
|
func TimeToUTCTime(t time.Time) time.Time {
|
|
return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), timeLocationUTC)
|
|
}
|
|
|
|
func shiftJulianToNoon(julianDays, julianFraction float64) (float64, float64) {
|
|
switch {
|
|
case -0.5 < julianFraction && julianFraction < 0.5:
|
|
julianFraction += 0.5
|
|
case julianFraction >= 0.5:
|
|
julianDays += 1
|
|
julianFraction -= 0.5
|
|
case julianFraction <= -0.5:
|
|
julianDays -= 1
|
|
julianFraction += 1.5
|
|
}
|
|
return julianDays, julianFraction
|
|
}
|
|
|
|
// Return the integer values for hour, minutes, seconds and
|
|
// nanoseconds that comprised a given fraction of a day.
|
|
// values would round to 1 us.
|
|
func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) {
|
|
|
|
const (
|
|
c1us = 1e3
|
|
c1s = 1e9
|
|
c1day = 24 * 60 * 60 * c1s
|
|
)
|
|
|
|
frac := int64(c1day*fraction + c1us/2)
|
|
nanoseconds = int((frac%c1s)/c1us) * c1us
|
|
frac /= c1s
|
|
seconds = int(frac % 60)
|
|
frac /= 60
|
|
minutes = int(frac % 60)
|
|
hours = int(frac / 60)
|
|
return
|
|
}
|
|
|
|
func julianDateToGregorianTime(part1, part2 float64) time.Time {
|
|
part1I, part1F := math.Modf(part1)
|
|
part2I, part2F := math.Modf(part2)
|
|
julianDays := part1I + part2I
|
|
julianFraction := part1F + part2F
|
|
julianDays, julianFraction = shiftJulianToNoon(julianDays, julianFraction)
|
|
day, month, year := doTheFliegelAndVanFlandernAlgorithm(int(julianDays))
|
|
hours, minutes, seconds, nanoseconds := fractionOfADay(julianFraction)
|
|
return time.Date(year, time.Month(month), day, hours, minutes, seconds, nanoseconds, time.UTC)
|
|
}
|
|
|
|
// By this point generations of programmers have repeated the
|
|
// algorithm sent to the editor of "Communications of the ACM" in 1968
|
|
// (published in CACM, volume 11, number 10, October 1968, p.657).
|
|
// None of those programmers seems to have found it necessary to
|
|
// explain the constants or variable names set out by Henry F. Fliegel
|
|
// and Thomas C. Van Flandern. Maybe one day I'll buy that jounal and
|
|
// expand an explanation here - that day is not today.
|
|
func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
|
|
l := jd + 68569
|
|
n := (4 * l) / 146097
|
|
l = l - (146097*n+3)/4
|
|
i := (4000 * (l + 1)) / 1461001
|
|
l = l - (1461*i)/4 + 31
|
|
j := (80 * l) / 2447
|
|
d := l - (2447*j)/80
|
|
l = j / 11
|
|
m := j + 2 - (12 * l)
|
|
y := 100*(n-49) + i + l
|
|
return d, m, y
|
|
}
|
|
|
|
// Convert an excelTime representation (stored as a floating point number) to a time.Time.
|
|
func TimeFromExcelTime(excelTime float64, date1904 bool) time.Time {
|
|
var date time.Time
|
|
var wholeDaysPart = int(excelTime)
|
|
// Excel uses Julian dates prior to March 1st 1900, and
|
|
// Gregorian thereafter.
|
|
if wholeDaysPart <= 61 {
|
|
const OFFSET1900 = 15018.0
|
|
const OFFSET1904 = 16480.0
|
|
var date time.Time
|
|
if date1904 {
|
|
date = julianDateToGregorianTime(MJD_0, excelTime+OFFSET1904)
|
|
} else {
|
|
date = julianDateToGregorianTime(MJD_0, excelTime+OFFSET1900)
|
|
}
|
|
return date
|
|
}
|
|
var floatPart = excelTime - float64(wholeDaysPart)
|
|
if date1904 {
|
|
date = excel1904Epoc
|
|
} else {
|
|
date = excel1900Epoc
|
|
}
|
|
durationPart := time.Duration(nanosInADay * floatPart)
|
|
return date.AddDate(0,0, wholeDaysPart).Add(durationPart)
|
|
}
|
|
|
|
// TimeToExcelTime will convert a time.Time into Excel's float representation, in either 1900 or 1904
|
|
// mode. If you don't know which to use, set date1904 to false.
|
|
// TODO should this should handle Julian dates?
|
|
func TimeToExcelTime(t time.Time, date1904 bool) float64 {
|
|
// Get the number of days since the unix epoc
|
|
daysSinceUnixEpoc := float64(t.Unix())/secondsInADay
|
|
// Get the number of nanoseconds in days since Unix() is in seconds.
|
|
nanosPart := float64(t.Nanosecond())/nanosInADay
|
|
// Add both together plus the number of days difference between unix and Excel epocs.
|
|
var offsetDays float64
|
|
if date1904 {
|
|
offsetDays = daysBetween1970And1904
|
|
} else {
|
|
offsetDays = daysBetween1970And1900
|
|
}
|
|
daysSinceExcelEpoc := daysSinceUnixEpoc + offsetDays + nanosPart
|
|
return daysSinceExcelEpoc
|
|
}
|