Browse Source

Merge pull request #398 from liepumartins/patch-1

Handle dates in further than time.Duration
Ryan Hollis 7 years ago
parent
commit
8eb140702b
4 changed files with 72 additions and 24 deletions
  1. 1 11
      cell.go
  2. 16 2
      cell_test.go
  3. 53 11
      date.go
  4. 2 0
      date_test.go

+ 1 - 11
cell.go

@@ -129,16 +129,6 @@ func (c *Cell) SetFloatWithFormat(n float64, format string) {
 	c.formula = ""
 }
 
-var timeLocationUTC, _ = time.LoadLocation("UTC")
-
-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 TimeToExcelTime(t time.Time) float64 {
-	return float64(t.UnixNano())/8.64e13 + 25569.0
-}
-
 // DateTimeOptions are additional options for exporting times
 type DateTimeOptions struct {
 	// Location allows calculating times in other timezones/locations
@@ -175,7 +165,7 @@ func (c *Cell) SetDateTime(t time.Time) {
 func (c *Cell) SetDateWithOptions(t time.Time, options DateTimeOptions) {
 	_, offset := t.In(options.Location).Zone()
 	t = time.Unix(t.Unix()+int64(offset), 0)
-	c.SetDateTimeWithFormat(TimeToExcelTime(t.In(timeLocationUTC)), options.ExcelTimeFormat)
+	c.SetDateTimeWithFormat(TimeToExcelTime(t.In(timeLocationUTC), c.date1904), options.ExcelTimeFormat)
 }
 
 func (c *Cell) SetDateTimeWithFormat(n float64, format string) {

+ 16 - 2
cell_test.go

@@ -597,6 +597,20 @@ func (l *CellSuite) TestFormattedValue(c *C) {
 	fvc.Equals(smallCell, "Saturday, December 30, 1899")
 }
 
+func (s *CellSuite) TestTimeToExcelTime(c *C) {
+	c.Assert(0.0, Equals, TimeToExcelTime(time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC), false))
+	c.Assert(-1462.0, Equals, TimeToExcelTime(time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC), true))
+	c.Assert(25569.0, Equals, TimeToExcelTime(time.Unix(0, 0), false))
+	c.Assert(43269.0, Equals, TimeToExcelTime(time.Date(2018, 6, 18, 0, 0, 0, 0, time.UTC), false))
+	c.Assert(401769.0, Equals, TimeToExcelTime(time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC), false))
+	smallDate := time.Date(1899, 12, 30, 0, 0, 0, 1000, time.UTC)
+	smallExcelTime := TimeToExcelTime(smallDate, false)
+
+	c.Assert(true, Equals, 0.0 != smallExcelTime)
+	roundTrippedDate := TimeFromExcelTime(smallExcelTime, false)
+	c.Assert(roundTrippedDate, Equals, smallDate)
+}
+
 // test setters and getters
 func (s *CellSuite) TestSetterGetters(c *C) {
 	cell := Cell{}
@@ -744,7 +758,7 @@ func (s *CellSuite) TestSetDateWithOptions(c *C) {
 	})
 	val, err = cell.Float()
 	c.Assert(err, IsNil)
-	c.Assert(val, Equals, TimeToExcelTime(time.Date(2016, 1, 1, 7, 0, 0, 0, time.UTC)))
+	c.Assert(val, Equals, TimeToExcelTime(time.Date(2016, 1, 1, 7, 0, 0, 0, time.UTC), false))
 
 	// test jp timezone
 	jpTZ, err := time.LoadLocation("Asia/Tokyo")
@@ -755,7 +769,7 @@ func (s *CellSuite) TestSetDateWithOptions(c *C) {
 	})
 	val, err = cell.Float()
 	c.Assert(err, IsNil)
-	c.Assert(val, Equals, TimeToExcelTime(time.Date(2016, 1, 1, 21, 0, 0, 0, time.UTC)))
+	c.Assert(val, Equals, TimeToExcelTime(time.Date(2016, 1, 1, 21, 0, 0, 0, time.UTC), false))
 }
 
 func (s *CellSuite) TestIsTimeFormat(c *C) {

+ 53 - 11
date.go

@@ -5,8 +5,33 @@ import (
 	"time"
 )
 
-const MJD_0 float64 = 2400000.5
-const MJD_JD2000 float64 = 51544.5
+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 {
@@ -78,10 +103,10 @@ func doTheFliegelAndVanFlandernAlgorithm(jd int) (day, month, year int) {
 // 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 intPart int64 = int64(excelTime)
+	var wholeDaysPart = int(excelTime)
 	// Excel uses Julian dates prior to March 1st 1900, and
 	// Gregorian thereafter.
-	if intPart <= 61 {
+	if wholeDaysPart <= 61 {
 		const OFFSET1900 = 15018.0
 		const OFFSET1904 = 16480.0
 		var date time.Time
@@ -92,14 +117,31 @@ func TimeFromExcelTime(excelTime float64, date1904 bool) time.Time {
 		}
 		return date
 	}
-	var floatPart float64 = excelTime - float64(intPart)
-	var dayNanoSeconds float64 = 24 * 60 * 60 * 1000 * 1000 * 1000
+	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 {
-		date = time.Date(1904, 1, 1, 0, 0, 0, 0, time.UTC)
+		offsetDays = daysBetween1970And1904
 	} else {
-		date = time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC)
+		offsetDays = daysBetween1970And1900
 	}
-	durationDays := time.Duration(intPart) * time.Hour * 24
-	durationPart := time.Duration(dayNanoSeconds * floatPart)
-	return date.Add(durationDays).Add(durationPart)
+	daysSinceExcelEpoc := daysSinceUnixEpoc + offsetDays + nanosPart
+	return daysSinceExcelEpoc
 }

+ 2 - 0
date_test.go

@@ -50,6 +50,8 @@ func (d *DateSuite) TestTimeFromExcelTime(c *C) {
 	c.Assert(date, Equals, time.Date(1900, 3, 1, 0, 0, 0, 0, time.UTC))
 	date = TimeFromExcelTime(41275.0, false)
 	c.Assert(date, Equals, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC))
+	date = TimeFromExcelTime(401769, false)
+	c.Assert(date, Equals, time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC))
 }
 
 func (d *DateSuite) TestTimeFromExcelTimeWithFractionalPart(c *C) {