Jelajahi Sumber

Rework date.go to be legible (as far as possible) and add tests.

Geoffrey J. Teale 11 tahun lalu
induk
melakukan
8739828adb
6 mengubah file dengan 175 tambahan dan 91 penghapusan
  1. 79 88
      date.go
  2. 74 0
      date_test.go
  3. 5 2
      lib.go
  4. 12 0
      style.go
  5. 3 0
      workbook.go
  6. 2 1
      workbook_test.go

+ 79 - 88
date.go

@@ -1,108 +1,99 @@
 package xlsx
 
 import (
-	"fmt"
 	"math"
-	"strconv"
+	"time"
 )
 
-//# Pre-calculate the datetime epochs for efficiency.
-var (
-	_JDN_delta = []int{2415080 - 61, 2416482 - 1}
-	//epoch_1904         = time.Date(1904, 1, 1, 0, 0, 0, 0, time.Local)
-	//epoch_1900         = time.Date(1899, 12, 31, 0, 0, 0, 0, time.Local)
-	//epoch_1900_minus_1 = time.Date(1899, 12, 30, 0, 0, 0, 0, time.Local)
-	_XLDAYS_TOO_LARGE = []int{2958466, 2958466 - 1462} //# This is equivalent to 10000-01-01
-)
-
-var (
-//ErrXLDateBadTuple = errors.New("XLDate is bad tuple")
-//ErrXLDateError    = errors.New("XLDateError")
-)
+const MJD_0 float64 = 2400000.5
+const MJD_JD2000 float64 = 51544.5
 
-func XLDateTooLarge(d float64) error {
-	return fmt.Errorf("XLDate %v is too large", d)
-}
-
-func XLDateAmbiguous(d float64) error {
-	return fmt.Errorf("XLDate %v is ambiguous", d)
-}
-
-func XLDateNegative(d float64) error {
-	return fmt.Errorf("XLDate %v is Negative", d)
+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
 }
 
-func XLDateBadDatemode(datemode int) error {
-	return fmt.Errorf("XLDate is bad datemode %d", datemode)
+// Return the integer values for hour, minutes, seconds and
+// nanoseconds that comprised a given fraction of a day.
+func fractionOfADay(fraction float64) (hours, minutes, seconds, nanoseconds int) {
+	f := 5184000000000000 * fraction
+	nanoseconds = int(math.Mod(f, 1000000000))
+	f = f / 1000000000
+	seconds = int(math.Mod(f, 3600))
+	f = f / 3600
+	minutes = int(math.Mod(f, 60))
+	f = f / 60
+	hours = int(f)
+	return hours, minutes, seconds, nanoseconds
 }
 
-func divmod(a, b int) (int, int) {
-	c := a % b
-	return (a - c) / b, c
+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.Local)
 }
 
-func div(a, b int) int {
-	return (a - a%b) / b
+// 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
 }
 
-func max(a, b int) int {
-	if a > b {
-		return a
-	}
-	return b
-}
 
-// this func provide a method to convert date cell string to
-// a slice []int. the []int means []int{year, month, day, hour, minute, second}
-func StrToDate(data string, datemode int) ([]int, error) {
-	xldate, err := strconv.ParseFloat(data, 64)
-	if err != nil {
-		return nil, err
+// 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)
+	// Excel uses Julian dates prior to March 1st 1900, and
+	// Gregorian thereafter.
+	if intPart <= 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
 	}
-
-	if datemode != 0 && datemode != 1 {
-		return nil, XLDateBadDatemode(datemode)
-	}
-	if xldate == 0.00 {
-		return []int{0, 0, 0, 0, 0, 0}, nil
-	}
-	if xldate < 0.00 {
-		return nil, XLDateNegative(xldate)
-	}
-	xldays := int(xldate)
-	frac := xldate - float64(xldays)
-	seconds := int(math.Floor(frac * 86400.0))
-	hour, minute, second := 0, 0, 0
-	//assert 0 <= seconds <= 86400
-	if seconds == 86400 {
-		xldays += 1
+	var floatPart float64 = excelTime - float64(intPart)
+	var dayNanoSeconds float64 = 24 * 60 * 60 * 1000 * 1000 * 1000
+	if date1904 {
+		date = time.Date(1904, 1, 1, 1, 0, 0, 0, time.Local)
 	} else {
-		//# second = seconds % 60; minutes = seconds // 60
-		var minutes int
-		minutes, second = divmod(seconds, 60)
-		//# minute = minutes % 60; hour    = minutes // 60
-		hour, minute = divmod(minutes, 60)
-	}
-	if xldays >= _XLDAYS_TOO_LARGE[datemode] {
-		return nil, XLDateTooLarge(xldate)
-	}
-
-	if xldays == 0 {
-		return []int{0, 0, 0, hour, minute, second}, nil
-	}
-
-	if xldays < 61 && datemode == 0 {
-		return nil, XLDateAmbiguous(xldate)
-	}
-
-	jdn := xldays + _JDN_delta[datemode]
-	yreg := ((((jdn*4+274277)/146097)*3/4)+jdn+1363)*4 + 3
-	mp := ((yreg%1461)/4)*535 + 333
-	d := ((mp % 16384) / 535) + 1
-	//# mp /= 16384
-	mp >>= 14
-	if mp >= 10 {
-		return []int{(yreg / 1461) - 4715, mp - 9, d, hour, minute, second}, nil
+		date = time.Date(1899, 12, 30, 1, 0, 0, 0, time.Local)
 	}
-	return []int{(yreg / 1461) - 4716, mp + 3, d, hour, minute, second}, nil
+	durationDays := time.Duration(intPart) * time.Hour * 24
+	durationPart := time.Duration(dayNanoSeconds * floatPart)
+	return date.Add(durationDays).Add(durationPart)
 }

+ 74 - 0
date_test.go

@@ -0,0 +1,74 @@
+package xlsx
+
+import (
+	. "gopkg.in/check.v1"
+	"time"
+)
+
+type DateSuite struct{}
+
+var _ = Suite(&DateSuite{})
+
+func (d *DateSuite) TestFractionOfADay(c *C) {
+	var h, m, s, n int
+	h, m, s, n = fractionOfADay(0)
+	c.Assert(h, Equals, 0)
+	c.Assert(m, Equals, 0)
+	c.Assert(s, Equals, 0)
+	c.Assert(n, Equals, 0)
+	h, m, s, n = fractionOfADay(1.0 / 24.0)
+	c.Assert(h, Equals, 1)
+	c.Assert(m, Equals, 0)
+	c.Assert(s, Equals, 0)
+	c.Assert(n, Equals, 0)
+}
+
+
+func (d *DateSuite) TestJulianDateToGregorianTime(c *C) {
+	c.Assert(julianDateToGregorianTime(2400000.5, 51544.0),
+		Equals, time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local))
+	c.Assert(julianDateToGregorianTime(2400000.5, 51544.5),
+		Equals, time.Date(2000, 1, 1, 12, 0, 0, 0, time.Local))
+	c.Assert(julianDateToGregorianTime(2400000.5, 51544.245),
+		Equals, time.Date(2000, 1, 1, 6, 40, 0, 13578, time.Local))
+	c.Assert(julianDateToGregorianTime(2400000.5, 51544.1),
+		Equals, time.Date(2000, 1, 1, 3, 22, 59, 999992456, time.Local))
+	c.Assert(julianDateToGregorianTime(2400000.5, 51544.75),
+		Equals, time.Date(2000, 1, 1, 18, 0, 0, 0, time.Local))
+}
+
+
+func (d *DateSuite) TestTimeFromExcelTime(c *C) {
+	date := TimeFromExcelTime(0, false)
+	c.Assert(date, Equals, time.Date(1899, 12, 30, 0, 0, 0, 0, time.Local))
+	date = TimeFromExcelTime(60, false)
+	c.Assert(date, Equals, time.Date(1900, 2, 28, 0, 0, 0, 0, time.Local))
+	date = TimeFromExcelTime(61, false)
+	c.Assert(date, Equals, time.Date(1900, 3, 1, 0, 0, 0, 0, time.Local))
+	date = TimeFromExcelTime(41275.0, false)
+	c.Assert(date, Equals, time.Date(2013, 1, 1, 0, 0, 0, 0, time.Local))
+}
+
+
+func (d *DateSuite) TestTimeFromExcelTimeWithFractionalPart(c *C) {
+	date := TimeFromExcelTime(0.114583333333333, false)
+	c.Assert(date.Round(time.Second), Equals, time.Date(1899, 12, 30, 2, 45, 0, 0, time.Local))
+
+	date = TimeFromExcelTime(60.1145833333333, false)
+	c.Assert(date.Round(time.Second), Equals, time.Date(1900, 2, 28, 2, 45, 0, 0, time.Local))
+
+	date = TimeFromExcelTime(61.3986111111111, false)
+	c.Assert(date.Round(time.Second), Equals, time.Date(1900, 3, 1, 9, 34, 0, 0, time.Local))
+
+	date = TimeFromExcelTime(37947.75, false)
+	c.Assert(date.Round(time.Second), Equals, time.Date(2003, 11, 22, 18, 0, 0, 0, time.Local))
+
+	date = TimeFromExcelTime(41275.1145833333, false)
+	c.Assert(date.Round(time.Second), Equals, time.Date(2013, 1, 1, 2, 45, 0, 0, time.Local))
+}
+
+func (d *DateSuite) TestTimeFromExcelTimeWith1904Offest(c *C){
+	date1904Offset := TimeFromExcelTime(39813.0, true)
+	c.Assert(date1904Offset, Equals, time.Date(2013, 1, 1, 0, 0, 0, 0, time.Local))
+
+}

+ 5 - 2
lib.go

@@ -323,13 +323,16 @@ func getValueFromCellData(rawcell xlsxC, reftable []string) string {
 	var data string = rawcell.V
 	if len(data) > 0 {
 		vval := strings.Trim(data, " \t\n\r")
-		if rawcell.T == "s" {
+		switch rawcell.T {
+		case "s":  // Shared String
 			ref, error := strconv.Atoi(vval)
 			if error != nil {
 				panic(error)
 			}
 			value = reftable[ref]
-		} else {
+		case "n": // Number
+			
+		default:
 			value = vval
 		}
 	}

+ 12 - 0
style.go

@@ -17,8 +17,20 @@ type xlsxStyles struct {
 	Borders      []xlsxBorder `xml:"borders>border"`
 	CellStyleXfs []xlsxXf     `xml:"cellStyleXfs>xf"`
 	CellXfs      []xlsxXf     `xml:"cellXfs>xf"`
+	NumFmts      []xlsxNumFmt `xml:numFmts>numFmt"`
 }
 
+// xlsxNumFmt directly maps the numFmt element in the namespace
+// http://schemas.openxmlformats.org/spreadsheetml/2006/main -
+// currently I have not checked it for completeness - it does as much
+// as I need.
+type xlsxNumFmt struct {
+	NumFmtId int `xml:"numFmtId"`
+	FormatCode string `xml:"formatCode"`
+}
+
+
+
 // xlsxFont directly maps the font element in the namespace
 // http://schemas.openxmlformats.org/spreadsheetml/2006/main -
 // currently I have not checked it for completeness - it does as much

+ 3 - 0
workbook.go

@@ -49,6 +49,9 @@ type xlsxFileVersion struct {
 // much as I need.
 type xlsxWorkbookPr struct {
 	DefaultThemeVersion string `xml:"defaultThemeVersion,attr"`
+	BackUpFile bool `xml:"backupFile,attr"`
+	ShowObjects string `xml:"showObjects,attr"`
+	Date1904 bool `xml:"date1904,attr"`
 }
 
 // xlsxBookViews directly maps the bookViews element from the

+ 2 - 1
workbook_test.go

@@ -23,7 +23,7 @@ func (w *WorkbookSuite) TestUnmarshallWorkbookXML(c *C) {
                        lastEdited="4"
                        lowestEdited="4"
                        rupBuild="4506"/>
-          <workbookPr defaultThemeVersion="124226"/>
+          <workbookPr defaultThemeVersion="124226" date1904="true"/>
           <bookViews>
             <workbookView xWindow="120"
                           yWindow="75"
@@ -56,6 +56,7 @@ func (w *WorkbookSuite) TestUnmarshallWorkbookXML(c *C) {
 	c.Assert(workbook.FileVersion.LowestEdited, Equals, "4")
 	c.Assert(workbook.FileVersion.RupBuild, Equals, "4506")
 	c.Assert(workbook.WorkbookPr.DefaultThemeVersion, Equals, "124226")
+	c.Assert(workbook.WorkbookPr.Date1904, Equals, true)
 	c.Assert(workbook.BookViews.WorkBookView, HasLen,  1)
 	workBookView := workbook.BookViews.WorkBookView[0]
 	c.Assert(workBookView.XWindow, Equals, "120")