Explorar o código

Merge writeable-xlsx.

Geoffrey J. Teale %!s(int64=11) %!d(string=hai) anos
pai
achega
7ecfa0d472
Modificáronse 22 ficheiros con 1953 adicións e 802 borrados
  1. 299 0
      cell.go
  2. 358 0
      cell_test.go
  3. 48 0
      contenttypes.go
  4. 50 0
      contenttypes_test.go
  5. 190 0
      file.go
  6. 403 0
      file_test.go
  7. 141 302
      lib.go
  8. 91 447
      lib_test.go
  9. 1 1
      macExcel_test.go
  10. 3 1
      macNumbers_test.go
  11. 11 0
      row.go
  12. 20 0
      row_test.go
  13. 63 17
      sharedstrings.go
  14. 61 7
      sharedstrings_test.go
  15. 57 0
      sheet.go
  16. 79 0
      sheet_test.go
  17. 2 1
      style.go
  18. BIN=BIN
      testfile.xlsx
  19. 28 16
      workbook.go
  20. 35 3
      workbook_test.go
  21. 12 6
      worksheet.go
  22. 1 1
      wpsBlankLine_test.go

+ 299 - 0
cell.go

@@ -0,0 +1,299 @@
+package xlsx
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+	"math"
+)
+
+// Style is a high level structure intended to provide user access to
+// the contents of Style within an XLSX file.
+type Style struct {
+	Border Border
+	Fill   Fill
+	Font   Font
+}
+
+func NewStyle() *Style {
+	return &Style{}
+}
+
+func (s *Style) SetFont(font Font) {
+	s.Font = font
+}
+
+func (s *Style) SetFill(fill Fill) {
+	s.Fill = fill
+}
+
+func (s *Style) SetBorder(border Border) {
+	s.Border = border
+}
+
+// Border is a high level structure intended to provide user access to
+// the contents of Border Style within an Sheet.
+type Border struct {
+	Left   string
+	Right  string
+	Top    string
+	Bottom string
+}
+
+// Fill is a high level structure intended to provide user access to
+// the contents of background and foreground color index within an Sheet.
+type Fill struct {
+	PatternType string
+	BgColor     string
+	FgColor     string
+}
+
+func NewFill(patternType, fgColor, bgColor string) *Fill {
+	return &Fill{PatternType: patternType, FgColor: fgColor, BgColor: bgColor}
+}
+
+type Font struct {
+	Size    int
+	Name    string
+	Family  int
+	Charset int
+}
+
+func NewFont(size int, name string) *Font {
+	return &Font{Size: size, Name: name}
+}
+
+// Cell is a high level structure intended to provide user access to
+// the contents of Cell within an xlsx.Row.
+type Cell struct {
+	Value      string
+	styleIndex int
+	styles     *xlsxStyles
+	numFmtRefTable map[int]xlsxNumFmt
+	date1904       bool
+}
+
+// CellInterface defines the public API of the Cell.
+type CellInterface interface {
+	String() string
+}
+
+// String returns the value of a Cell as a string.
+func (c *Cell) String() string {
+	return c.Value
+}
+
+// GetStyle returns the Style associated with a Cell
+func (c *Cell) GetStyle() Style {
+	var err error
+	style := Style{}
+	if c.styleIndex > -1 && c.styleIndex <= len(c.styles.CellXfs) {
+		xf := c.styles.CellXfs[c.styleIndex]
+		if xf.ApplyBorder {
+			var border Border = Border{}
+			border.Left = c.styles.Borders[xf.BorderId].Left.Style
+			border.Right = c.styles.Borders[xf.BorderId].Right.Style
+			border.Top = c.styles.Borders[xf.BorderId].Top.Style
+			border.Bottom = c.styles.Borders[xf.BorderId].Bottom.Style
+			style.SetBorder(border)
+		}
+		if xf.ApplyFill {
+			var fill Fill = Fill{}
+			fill.PatternType = c.styles.Fills[xf.FillId].PatternFill.PatternType
+			fill.BgColor = c.styles.Fills[xf.FillId].PatternFill.BgColor.RGB
+			fill.FgColor = c.styles.Fills[xf.FillId].PatternFill.FgColor.RGB
+			style.SetFill(fill)
+		}
+		if xf.ApplyFont {
+			var xfont = c.styles.Fonts[xf.FontId]
+			var font = Font{}
+			font.Size, err = strconv.Atoi(xfont.Sz.Val)
+			if err != nil {
+				panic(err.Error())
+			}
+			font.Name = xfont.Name.Val
+			font.Family, _ = strconv.Atoi(xfont.Family.Val)
+			font.Charset, _ = strconv.Atoi(xfont.Charset.Val)
+			style.SetFont(font)
+		}
+	}
+	return style
+}
+
+// The number format string is returnable from a cell.
+func (c *Cell) GetNumberFormat() string {
+	var numberFormat string = ""
+	if c.styleIndex > -1 && c.styleIndex <= len(c.styles.CellXfs) {
+		xf := c.styles.CellXfs[c.styleIndex]
+		numFmt := c.numFmtRefTable[xf.NumFmtId]
+		numberFormat = numFmt.FormatCode
+	}
+	return strings.ToLower(numberFormat)
+}
+
+func (c *Cell) formatToTime(format string) string {
+	f, err := strconv.ParseFloat(c.Value, 64)
+	if err != nil {
+		return err.Error()
+	}
+	return TimeFromExcelTime(f, c.date1904).Format(format)
+}
+
+func (c *Cell) formatToFloat(format string) string {
+	f, err := strconv.ParseFloat(c.Value, 64)
+	if err != nil {
+		return err.Error()
+	}
+	return fmt.Sprintf(format, f)
+}
+
+func (c *Cell) formatToInt(format string) string {
+	f, err := strconv.ParseFloat(c.Value, 64)
+	if err != nil {
+		return err.Error()
+	}
+	return fmt.Sprintf(format, int(f))
+}
+
+// Return the formatted version of the value.
+func (c *Cell) FormattedValue() string {
+	var numberFormat string = c.GetNumberFormat()
+	switch numberFormat {
+	case "general":
+		return c.Value
+	case "0", "#,##0":
+		return c.formatToInt("%d")
+	case "0.00", "#,##0.00", "@":
+		return c.formatToFloat("%.2f")
+	case "#,##0 ;(#,##0)", "#,##0 ;[red](#,##0)":
+		f, err := strconv.ParseFloat(c.Value, 64)
+		if err != nil {
+			return err.Error()
+		}
+		if f < 0 {
+			i := int(math.Abs(f))
+			return fmt.Sprintf("(%d)", i)
+		}
+		i := int(f)
+		return fmt.Sprintf("%d", i)
+	case "#,##0.00;(#,##0.00)", "#,##0.00;[red](#,##0.00)":
+		f, err := strconv.ParseFloat(c.Value, 64)
+		if err != nil {
+			return err.Error()
+		}
+		if f < 0 {
+			return fmt.Sprintf("(%.2f)", f)
+		}
+		return fmt.Sprintf("%.2f", f)
+	case "0%":
+		f, err := strconv.ParseFloat(c.Value, 64)
+		if err != nil {
+			return err.Error()
+		}
+		f = f * 100
+		return fmt.Sprintf("%d%%", int(f))
+	case "0.00%":
+		f, err := strconv.ParseFloat(c.Value, 64)
+		if err != nil {
+			return err.Error()
+		}
+		f = f * 100
+		return fmt.Sprintf("%.2f%%", f)
+	case "0.00e+00", "##0.0e+0":
+		return c.formatToFloat("%e")
+	case "mm-dd-yy":
+		return c.formatToTime("01-02-06")
+	case "d-mmm-yy":
+		return c.formatToTime("2-Jan-06")
+	case "d-mmm":
+		return c.formatToTime("2-Jan")
+	case "mmm-yy":
+		return c.formatToTime("Jan-06")
+	case "h:mm am/pm":
+		return c.formatToTime("3:04 pm")
+	case "h:mm:ss am/pm":
+		return c.formatToTime("3:04:05 pm")
+	case "h:mm":
+		return c.formatToTime("15:04")
+	case "h:mm:ss":
+		return c.formatToTime("15:04:05")
+	case "m/d/yy h:mm":
+		return c.formatToTime("1/2/06 15:04")
+	case "mm:ss":
+		return c.formatToTime("04:05")
+	case "[h]:mm:ss":
+		f, err := strconv.ParseFloat(c.Value, 64)
+		if err != nil {
+			return err.Error()
+		}
+		t := TimeFromExcelTime(f, c.date1904)
+		if t.Hour() > 0 {
+			return t.Format("15:04:05")
+		}
+		return t.Format("04:05")
+	case "mmss.0":
+		f, err := strconv.ParseFloat(c.Value, 64)
+		if err != nil {
+			return err.Error()
+		}
+		t := TimeFromExcelTime(f, c.date1904)
+		return fmt.Sprintf("%0d%0d.%d", t.Minute(), t.Second(), t.Nanosecond()/1000)
+
+	case "yyyy\\-mm\\-dd":
+		return c.formatToTime("2006\\-01\\-02")
+	case "dd/mm/yy":
+		return c.formatToTime("02/01/06")
+	case "hh:mm:ss":
+		return c.formatToTime("15:04:05")
+	case "dd/mm/yy\\ hh:mm":
+		return c.formatToTime("02/01/06\\ 15:04")
+	case "dd/mm/yyyy hh:mm:ss":
+		return c.formatToTime("02/01/2006 15:04:05")
+	case "yy-mm-dd":
+		return c.formatToTime("06-01-02")
+	case "d-mmm-yyyy":
+		return c.formatToTime("2-Jan-2006")
+	case "m/d/yy":
+		return c.formatToTime("1/2/06")
+	case "m/d/yyyy":
+		return c.formatToTime("1/2/2006")
+	case "dd-mmm-yyyy":
+		return c.formatToTime("02-Jan-2006")
+	case "dd/mm/yyyy":
+		return c.formatToTime("02/01/2006")
+	case "mm/dd/yy hh:mm am/pm":
+		return c.formatToTime("01/02/06 03:04 pm")
+	case "mm/dd/yyyy hh:mm:ss":
+		return c.formatToTime("01/02/2006 15:04:05")
+	case "yyyy-mm-dd hh:mm:ss":
+		return c.formatToTime("2006-01-02 15:04:05")
+	}
+	return c.Value
+}
+
+func (c *Cell) SetStyle(style *Style) int {
+	if c.styles == nil {
+		c.styles = &xlsxStyles{}
+	}
+	index := len(c.styles.Fonts)
+	xFont := xlsxFont{}
+	xFill := xlsxFill{}
+	xBorder := xlsxBorder{}
+	xCellStyleXf := xlsxXf{}
+	xCellXf := xlsxXf{}
+	xFont.Sz.Val = strconv.Itoa(style.Font.Size)
+	xFont.Name.Val = style.Font.Name
+	xFont.Family.Val = strconv.Itoa(style.Font.Family)
+	xFont.Charset.Val = strconv.Itoa(style.Font.Charset)
+	xPatternFill := xlsxPatternFill{}
+	xPatternFill.PatternType = style.Fill.PatternType
+	xPatternFill.FgColor.RGB = style.Fill.FgColor
+	xPatternFill.BgColor.RGB = style.Fill.BgColor
+	xFill.PatternFill = xPatternFill
+	c.styles.Fonts = append(c.styles.Fonts, xFont)
+	c.styles.Fills = append(c.styles.Fills, xFill)
+	c.styles.Borders = append(c.styles.Borders, xBorder)
+	c.styles.CellStyleXfs = append(c.styles.CellStyleXfs, xCellStyleXf)
+	c.styles.CellXfs = append(c.styles.CellXfs, xCellXf)
+	return index
+}

+ 358 - 0
cell_test.go

@@ -0,0 +1,358 @@
+package xlsx
+
+import (
+	. "gopkg.in/check.v1"
+)
+
+type StyleSuite struct {}
+
+var _ = Suite(&StyleSuite{})
+
+
+func (s *StyleSuite) TestNewStyle(c *C){
+	style := NewStyle()
+	c.Assert(style, NotNil)
+}
+
+
+// Test that SetFont correctly updates the Font associated with a Style.
+func (s *StyleSuite) TestSetFont(c *C) {
+	font := NewFont(12, "Calibra")
+	style := Style{}
+	style.SetFont(*font)
+	c.Assert(style.Font.Size, Equals, 12)
+	c.Assert(style.Font.Name, Equals, "Calibra")
+}
+
+
+type FontSuite struct {}
+
+var _ = Suite(&FontSuite{})
+
+func (s *FontSuite) TestNewFont(c *C) {
+	font := NewFont(12, "Verdana")
+	c.Assert(font, NotNil)
+	c.Assert(font.Name, Equals, "Verdana")
+	c.Assert(font.Size, Equals, 12)
+}
+
+type CellSuite struct {}
+
+var _ = Suite(&CellSuite{})
+
+// Test that we can set and get a Value from a Cell
+func (s *CellSuite) TestValueSet(c *C) {
+	// Note, this test is fairly pointless, it serves mostly to
+	// reinforce that this functionality is important, and should
+	// the mechanics of this all change at some point, to remind
+	// us not to lose this.
+	cell := Cell{}
+	cell.Value = "A string"
+	c.Assert(cell.Value, Equals, "A string")
+}
+
+// Test that GetStyle correctly converts the xlsxStyle.Fonts.
+func (s *CellSuite) TestGetStyleWithFonts(c *C) {
+	var cell *Cell
+	var style Style
+	var xStyles *xlsxStyles
+	var fonts []xlsxFont
+	var cellXfs []xlsxXf
+
+	fonts = make([]xlsxFont, 1)
+	fonts[0] = xlsxFont{
+		Sz:   xlsxVal{Val: "10"},
+		Name: xlsxVal{Val: "Calibra"}}
+
+	cellXfs = make([]xlsxXf, 1)
+	cellXfs[0] = xlsxXf{ApplyFont: true, FontId: 0}
+
+	xStyles = &xlsxStyles{Fonts: fonts, CellXfs: cellXfs}
+
+	cell = &Cell{Value: "123", styleIndex: 0, styles: xStyles}
+	style = cell.GetStyle()
+	c.Assert(style, NotNil)
+	c.Assert(style.Font.Size, Equals, 10)
+	c.Assert(style.Font.Name, Equals, "Calibra")
+}
+
+// Test that SetStyle correctly updates the xlsxStyle.Fonts.
+func (s *CellSuite) TestSetStyleWithFonts(c *C) {
+	file := NewFile()
+	sheet := file.AddSheet("Test")
+	row := sheet.AddRow()
+	cell := row.AddCell()
+	font := NewFont(12, "Calibra")
+	style := NewStyle()
+	style.SetFont(*font)
+	cell.SetStyle(style)
+	c.Assert(cell.styleIndex, Equals, 0)
+	c.Assert(cell.styles.Fonts, HasLen, 1)
+	xFont := cell.styles.Fonts[0]
+	c.Assert(xFont.Sz.Val, Equals, "12")
+	c.Assert(xFont.Name.Val, Equals, "Calibra")
+}
+
+
+// Test that GetStyle correctly converts the xlsxStyle.Fills.
+func (s *CellSuite) TestGetStyleWithFills(c *C) {
+	var cell *Cell
+	var style Style
+	var xStyles *xlsxStyles
+	var fills []xlsxFill
+	var cellXfs []xlsxXf
+
+	fills = make([]xlsxFill, 1)
+	fills[0] = xlsxFill{
+		PatternFill: xlsxPatternFill{
+			PatternType: "solid",
+			FgColor:     xlsxColor{RGB: "FF000000"},
+			BgColor:     xlsxColor{RGB: "00FF0000"}}}
+	cellXfs = make([]xlsxXf, 1)
+	cellXfs[0] = xlsxXf{ApplyFill: true, FillId: 0}
+
+	xStyles = &xlsxStyles{Fills: fills, CellXfs: cellXfs}
+
+	cell = &Cell{Value: "123", styleIndex: 0, styles: xStyles}
+	style = cell.GetStyle()
+	fill := style.Fill
+	c.Assert(fill.PatternType, Equals, "solid")
+	c.Assert(fill.BgColor, Equals, "00FF0000")
+	c.Assert(fill.FgColor, Equals, "FF000000")
+}
+
+// Test that SetStyle correctly updates xlsxStyle.Fills.
+func (s *CellSuite) TestSetStyleWithFills(c *C) {
+	file := NewFile()
+	sheet := file.AddSheet("Test")
+	row := sheet.AddRow()
+	cell := row.AddCell()
+	fill := NewFill("solid", "00FF0000", "FF000000")
+	style := NewStyle()
+	style.SetFill(*fill)
+	cell.SetStyle(style)
+	c.Assert(cell.styleIndex, Equals, 0)
+	c.Assert(cell.styles.Fills, HasLen, 1)
+	xFill := cell.styles.Fills[0]
+	xPatternFill := xFill.PatternFill
+	c.Assert(xPatternFill.PatternType, Equals, "solid")
+	c.Assert(xPatternFill.FgColor.RGB, Equals, "00FF0000")
+	c.Assert(xPatternFill.BgColor.RGB, Equals, "FF000000")
+}
+
+
+// Test that GetStyle correctly converts the xlsxStyle.Borders.
+func (s *CellSuite) TestGetStyleWithBorders(c *C) {
+	var cell *Cell
+	var style Style
+	var xStyles *xlsxStyles
+	var borders []xlsxBorder
+	var cellXfs []xlsxXf
+
+	borders = make([]xlsxBorder, 1)
+	borders[0] = xlsxBorder{
+		Left:   xlsxLine{Style: "thin"},
+		Right:  xlsxLine{Style: "thin"},
+		Top:    xlsxLine{Style: "thin"},
+		Bottom: xlsxLine{Style: "thin"}}
+
+	cellXfs = make([]xlsxXf, 1)
+	cellXfs[0] = xlsxXf{ApplyBorder: true, BorderId: 0}
+
+	xStyles = &xlsxStyles{Borders: borders, CellXfs: cellXfs}
+
+	cell = &Cell{Value: "123", styleIndex: 0, styles: xStyles}
+	style = cell.GetStyle()
+	border := style.Border
+	c.Assert(border.Left, Equals, "thin")
+	c.Assert(border.Right, Equals, "thin")
+	c.Assert(border.Top, Equals, "thin")
+	c.Assert(border.Bottom, Equals, "thin")
+}
+
+
+func (s *CellSuite) TestGetNumberFormat(c *C) {
+	var cell *Cell
+	var cellXfs []xlsxXf
+	var numFmt xlsxNumFmt
+	var numFmts []xlsxNumFmt
+	var xStyles *xlsxStyles
+	var numFmtRefTable map[int]xlsxNumFmt
+
+	cellXfs = make([]xlsxXf, 1)
+	cellXfs[0] = xlsxXf{NumFmtId: 0}
+
+	numFmts = make([]xlsxNumFmt, 1)
+	numFmtRefTable = make(map[int]xlsxNumFmt)
+
+	xStyles = &xlsxStyles{NumFmts: numFmts, CellXfs: cellXfs}
+
+	cell = &Cell{Value: "123.123", numFmtRefTable: numFmtRefTable, styleIndex: 0, styles: xStyles}
+
+	numFmt = xlsxNumFmt{NumFmtId: 0, FormatCode: "dd/mm/yy"}
+	numFmts[0] = numFmt
+	numFmtRefTable[0] = numFmt
+	c.Assert(cell.GetNumberFormat(), Equals, "dd/mm/yy")
+}
+
+// We can return a string representation of the formatted data
+func (l *CellSuite) TestFormattedValue(c *C) {
+	var cell, earlyCell, negativeCell, smallCell *Cell
+	var cellXfs []xlsxXf
+	var numFmt xlsxNumFmt
+	var numFmts []xlsxNumFmt
+	var xStyles *xlsxStyles
+	var numFmtRefTable map[int]xlsxNumFmt
+
+	cellXfs = make([]xlsxXf, 1)
+	cellXfs[0] = xlsxXf{NumFmtId: 1}
+
+	numFmts = make([]xlsxNumFmt, 1)
+	numFmtRefTable = make(map[int]xlsxNumFmt)
+
+	xStyles = &xlsxStyles{NumFmts: numFmts, CellXfs: cellXfs}
+	cell = &Cell{Value: "37947.7500001", numFmtRefTable: numFmtRefTable, styleIndex: 0, styles: xStyles}
+	negativeCell = &Cell{Value: "-37947.7500001", numFmtRefTable: numFmtRefTable, styleIndex: 0, styles: xStyles}
+	smallCell = &Cell{Value: "0.007", numFmtRefTable: numFmtRefTable, styleIndex: 0, styles: xStyles}
+	earlyCell = &Cell{Value: "2.1", numFmtRefTable: numFmtRefTable, styleIndex: 0, styles: xStyles}
+	setCode := func(code string) {
+		numFmt = xlsxNumFmt{NumFmtId: 1, FormatCode: code}
+		numFmts[0] = numFmt
+		numFmtRefTable[1] = numFmt
+	}
+
+	setCode("general")
+	c.Assert(cell.FormattedValue(), Equals, "37947.7500001")
+	c.Assert(negativeCell.FormattedValue(), Equals, "-37947.7500001")
+
+	setCode("0")
+	c.Assert(cell.FormattedValue(), Equals, "37947")
+
+	setCode("#,##0") // For the time being we're not doing this
+	// comma formatting, so it'll fall back to
+	// the related non-comma form.
+	c.Assert(cell.FormattedValue(), Equals, "37947")
+
+	setCode("0.00")
+	c.Assert(cell.FormattedValue(), Equals, "37947.75")
+
+	setCode("#,##0.00") // For the time being we're not doing this
+	// comma formatting, so it'll fall back to
+	// the related non-comma form.
+	c.Assert(cell.FormattedValue(), Equals, "37947.75")
+
+	setCode("#,##0 ;(#,##0)")
+	c.Assert(cell.FormattedValue(), Equals, "37947")
+	c.Assert(negativeCell.FormattedValue(), Equals, "(37947)")
+
+	setCode("#,##0 ;[red](#,##0)")
+	c.Assert(cell.FormattedValue(), Equals, "37947")
+	c.Assert(negativeCell.FormattedValue(), Equals, "(37947)")
+
+	setCode("0%")
+	c.Assert(cell.FormattedValue(), Equals, "3794775%")
+
+	setCode("0.00%")
+	c.Assert(cell.FormattedValue(), Equals, "3794775.00%")
+
+	setCode("0.00e+00")
+	c.Assert(cell.FormattedValue(), Equals, "3.794775e+04")
+
+	setCode("##0.0e+0") // This is wrong, but we'll use it for now.
+	c.Assert(cell.FormattedValue(), Equals, "3.794775e+04")
+
+	setCode("mm-dd-yy")
+	c.Assert(cell.FormattedValue(), Equals, "11-22-03")
+
+	setCode("d-mmm-yy")
+	c.Assert(cell.FormattedValue(), Equals, "22-Nov-03")
+	c.Assert(earlyCell.FormattedValue(), Equals, "1-Jan-00")
+
+	setCode("d-mmm")
+	c.Assert(cell.FormattedValue(), Equals, "22-Nov")
+	c.Assert(earlyCell.FormattedValue(), Equals, "1-Jan")
+
+	setCode("mmm-yy")
+	c.Assert(cell.FormattedValue(), Equals, "Nov-03")
+
+	setCode("h:mm am/pm")
+	c.Assert(cell.FormattedValue(), Equals, "6:00 pm")
+	c.Assert(smallCell.FormattedValue(), Equals, "12:14 am")
+
+	setCode("h:mm:ss am/pm")
+	c.Assert(cell.FormattedValue(), Equals, "6:00:00 pm")
+	c.Assert(smallCell.FormattedValue(), Equals, "12:14:47 am")
+
+	setCode("h:mm")
+	c.Assert(cell.FormattedValue(), Equals, "18:00")
+	c.Assert(smallCell.FormattedValue(), Equals, "00:14")
+
+	setCode("h:mm:ss")
+	c.Assert(cell.FormattedValue(), Equals, "18:00:00")
+	// This is wrong, but there's no eary way aroud it in Go right now, AFAICT.
+	c.Assert(smallCell.FormattedValue(), Equals, "00:14:47")
+
+	setCode("m/d/yy h:mm")
+	c.Assert(cell.FormattedValue(), Equals, "11/22/03 18:00")
+	c.Assert(smallCell.FormattedValue(), Equals, "12/30/99 00:14") // Note, that's 1899
+	c.Assert(earlyCell.FormattedValue(), Equals, "1/1/00 02:24")   // and 1900
+
+	setCode("mm:ss")
+	c.Assert(cell.FormattedValue(), Equals, "00:00")
+	c.Assert(smallCell.FormattedValue(), Equals, "14:47")
+
+	setCode("[h]:mm:ss")
+	c.Assert(cell.FormattedValue(), Equals, "18:00:00")
+	c.Assert(smallCell.FormattedValue(), Equals, "14:47")
+
+	setCode("mmss.0") // I'm not sure about these.
+	c.Assert(cell.FormattedValue(), Equals, "00.8640")
+	c.Assert(smallCell.FormattedValue(), Equals, "1447.999997")
+
+	setCode("yyyy\\-mm\\-dd")
+	c.Assert(cell.FormattedValue(), Equals, "2003\\-11\\-22")
+
+	setCode("dd/mm/yy")
+	c.Assert(cell.FormattedValue(), Equals, "22/11/03")
+	c.Assert(earlyCell.FormattedValue(), Equals, "01/01/00")
+
+	setCode("hh:mm:ss")
+	c.Assert(cell.FormattedValue(), Equals, "18:00:00")
+	c.Assert(smallCell.FormattedValue(), Equals, "00:14:47")
+
+	setCode("dd/mm/yy\\ hh:mm")
+	c.Assert(cell.FormattedValue(), Equals, "22/11/03\\ 18:00")
+
+	setCode("yy-mm-dd")
+	c.Assert(cell.FormattedValue(), Equals, "03-11-22")
+
+	setCode("d-mmm-yyyy")
+	c.Assert(cell.FormattedValue(), Equals, "22-Nov-2003")
+	c.Assert(earlyCell.FormattedValue(), Equals, "1-Jan-1900")
+
+	setCode("m/d/yy")
+	c.Assert(cell.FormattedValue(), Equals, "11/22/03")
+	c.Assert(earlyCell.FormattedValue(), Equals, "1/1/00")
+
+	setCode("m/d/yyyy")
+	c.Assert(cell.FormattedValue(), Equals, "11/22/2003")
+	c.Assert(earlyCell.FormattedValue(), Equals, "1/1/1900")
+
+	setCode("dd-mmm-yyyy")
+	c.Assert(cell.FormattedValue(), Equals, "22-Nov-2003")
+
+	setCode("dd/mm/yyyy")
+	c.Assert(cell.FormattedValue(), Equals, "22/11/2003")
+
+	setCode("mm/dd/yy hh:mm am/pm")
+	c.Assert(cell.FormattedValue(), Equals, "11/22/03 06:00 pm")
+
+	setCode("mm/dd/yyyy hh:mm:ss")
+	c.Assert(cell.FormattedValue(), Equals, "11/22/2003 18:00:00")
+	c.Assert(smallCell.FormattedValue(), Equals, "12/30/1899 00:14:47")
+
+	setCode("yyyy-mm-dd hh:mm:ss")
+	c.Assert(cell.FormattedValue(), Equals, "2003-11-22 18:00:00")
+	c.Assert(smallCell.FormattedValue(), Equals, "1899-12-30 00:14:47")
+}
+

+ 48 - 0
contenttypes.go

@@ -0,0 +1,48 @@
+package xlsx
+
+import (
+	"encoding/xml"
+)
+
+type xlsxTypes struct {
+	XMLName xml.Name `xml:"http://schemas.openxmlformats.org/package/2006/content-types Types"`
+
+	Overrides []xlsxOverride `xml:"Override"`
+	Defaults []xlsxDefault `xml:"Default"`
+}
+
+type xlsxOverride struct {
+	PartName    string `xml:",attr"`
+	ContentType string `xml:",attr"`
+}
+
+type xlsxDefault struct {
+	Extension	string `xml:",attr"`
+	ContentType	string `xml:",attr"`
+}
+
+func MakeDefaultContentTypes() (types xlsxTypes) {
+	types.Overrides = make([]xlsxOverride, 7)
+	types.Defaults = make([]xlsxDefault, 2)
+
+	types.Overrides[0].PartName = "/_rels/.rels"
+	types.Overrides[0].ContentType = "application/vnd.openxmlformats-package.relationships+xml"
+	types.Overrides[1].PartName = "/docProps/app.xml"
+	types.Overrides[1].ContentType = "application/vnd.openxmlformats-officedocument.extended-properties+xml"
+	types.Overrides[2].PartName = "/docProps/core.xml"
+	types.Overrides[2].ContentType = "application/vnd.openxmlformats-package.core-properties+xml"
+	types.Overrides[3].PartName = "/xl/_rels/workbook.xml.rels"
+	types.Overrides[3].ContentType = "application/vnd.openxmlformats-package.relationships+xml"
+	types.Overrides[4].PartName = "/xl/sharedStrings.xml"
+	types.Overrides[4].ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"
+	types.Overrides[5].PartName = "/xl/styles.xml"
+	types.Overrides[5].ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"
+	types.Overrides[6].PartName = "/xl/workbook.xml"
+	types.Overrides[6].ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"
+
+	types.Defaults[0].Extension = "rels"
+	types.Defaults[0].ContentType = "application/vnd.openxmlformats-package.relationships+xml"
+	types.Defaults[1].Extension = "xml"
+	types.Defaults[1].ContentType = "application/xml"
+	return
+}

+ 50 - 0
contenttypes_test.go

@@ -0,0 +1,50 @@
+package xlsx
+
+import (
+	"encoding/xml"
+	. "gopkg.in/check.v1"
+)
+
+type ContentTypesSuite struct{}
+
+var _ = Suite(&ContentTypesSuite{})
+
+func (l *ContentTypesSuite) TestMarshalContentTypes(c *C) {
+	var types xlsxTypes = xlsxTypes{}
+	types.Overrides = make([]xlsxOverride, 1)
+	types.Overrides[0] = xlsxOverride{PartName: "/_rels/.rels", ContentType: "application/vnd.openxmlformats-package.relationships+xml"}
+	output, err := xml.MarshalIndent(types, "   ", "   ")
+	stringOutput := xml.Header + string(output)
+	c.Assert(err, IsNil)
+	expectedContentTypes := `<?xml version="1.0" encoding="UTF-8"?>
+   <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
+      <Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"></Override>
+   </Types>`
+	c.Assert(stringOutput, Equals, expectedContentTypes)
+}
+
+func (l *ContentTypesSuite) TestMakeDefaultContentTypes(c *C) {
+	var types xlsxTypes = MakeDefaultContentTypes()
+	c.Assert(len(types.Overrides), Equals, 7)
+	c.Assert(types.Overrides[0].PartName, Equals, "/_rels/.rels")
+	c.Assert(types.Overrides[0].ContentType, Equals, "application/vnd.openxmlformats-package.relationships+xml")
+	c.Assert(types.Overrides[1].PartName, Equals, "/docProps/app.xml")
+	c.Assert(types.Overrides[1].ContentType, Equals, "application/vnd.openxmlformats-officedocument.extended-properties+xml")
+	c.Assert(types.Overrides[2].PartName, Equals, "/docProps/core.xml")
+	c.Assert(types.Overrides[2].ContentType, Equals, "application/vnd.openxmlformats-package.core-properties+xml")
+	c.Assert(types.Overrides[3].PartName, Equals, "/xl/_rels/workbook.xml.rels")
+	c.Assert(types.Overrides[3].ContentType, Equals, "application/vnd.openxmlformats-package.relationships+xml")
+	c.Assert(types.Overrides[4].PartName, Equals, "/xl/sharedStrings.xml")
+	c.Assert(types.Overrides[4].ContentType, Equals, "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml")
+	c.Assert(types.Overrides[5].PartName, Equals, "/xl/styles.xml")
+	c.Assert(types.Overrides[5].ContentType, Equals, "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml")
+	c.Assert(types.Overrides[6].PartName, Equals, "/xl/workbook.xml")
+	c.Assert(types.Overrides[6].ContentType, Equals, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml")
+
+	c.Assert(types.Defaults[0].Extension, Equals, "rels")
+	c.Assert(types.Defaults[0].ContentType, Equals, "application/vnd.openxmlformats-package.relationships+xml")
+	c.Assert(types.Defaults[1].Extension, Equals, "xml")
+	c.Assert(types.Defaults[1].ContentType, Equals, "application/xml")
+
+
+}

+ 190 - 0
file.go

@@ -0,0 +1,190 @@
+package xlsx
+
+import (
+	"archive/zip"
+	"encoding/xml"
+	"fmt"
+	"io"
+	"os"
+	"strconv"
+)
+
+// File is a high level structure providing a slice of Sheet structs
+// to the user.
+type File struct {
+	worksheets     map[string]*zip.File
+	numFmtRefTable map[int]xlsxNumFmt
+	referenceTable *RefTable
+	Date1904       bool
+	styles         *xlsxStyles
+	Sheets        map[string]*Sheet
+}
+
+
+// Create a new File
+func NewFile() (file *File) {
+	file = &File{};
+	file.Sheets = make(map[string]*Sheet)
+	return
+}
+
+// OpenFile() take the name of an XLSX file and returns a populated
+// xlsx.File struct for it.
+func OpenFile(filename string) (*File, error) {
+	var f *zip.ReadCloser
+	f, err := zip.OpenReader(filename)
+	if err != nil {
+		return nil, err
+	}
+	return ReadZip(f)
+}
+
+func (f *File) Save(path string) (err error) {
+	var parts map[string]string
+	var target *os.File
+	var zipWriter *zip.Writer
+
+	parts, err = f.MarshallParts()
+	if err != nil {
+		return
+	}
+
+	target, err = os.Create(path)
+	if err != nil {
+		return
+	}
+
+	zipWriter = zip.NewWriter(target)
+
+	for partName, part := range parts {
+		var writer io.Writer
+		writer, err = zipWriter.Create(partName)
+		if err != nil {
+			return
+		}
+		_, err = writer.Write([]byte(part))
+		if err != nil {
+			return
+		}
+	}
+	err = zipWriter.Close()
+	if err != nil {
+		return
+	}
+
+	return target.Close()
+}
+
+
+// Add a new Sheet, with the provided name, to a File
+func (f *File) AddSheet(sheetName string) (sheet *Sheet) {
+	sheet = &Sheet{}
+	f.Sheets[sheetName] = sheet
+	return sheet
+}
+
+func (f *File) makeWorkbook() xlsxWorkbook {
+	var workbook xlsxWorkbook
+	workbook = xlsxWorkbook{}
+	workbook.FileVersion = xlsxFileVersion{}
+	workbook.FileVersion.AppName = "Go XLSX"
+	workbook.WorkbookPr = xlsxWorkbookPr{BackupFile: false}
+	workbook.BookViews = xlsxBookViews{}
+	workbook.BookViews.WorkBookView = make([]xlsxWorkBookView, 1)
+	workbook.BookViews.WorkBookView[0] = xlsxWorkBookView{}
+	workbook.Sheets = xlsxSheets{}
+	workbook.Sheets.Sheet = make([]xlsxSheet, len(f.Sheets))
+	return workbook
+}
+
+
+func (f *File) MarshallParts() (map[string]string, error) {
+	var parts map[string]string
+	var refTable *RefTable = NewSharedStringRefTable()
+	var workbookRels WorkBookRels = make(WorkBookRels)
+	var err error
+	var workbook xlsxWorkbook
+	var types xlsxTypes = MakeDefaultContentTypes()
+
+	marshal := func(thing interface{}) (string, error) {
+		body, err := xml.MarshalIndent(thing, "  ", "  ")
+		if err != nil {
+			return "", err
+		}
+		return xml.Header + string(body), nil
+	}
+
+
+	parts = make(map[string]string)
+	workbook = f.makeWorkbook()
+	sheetIndex := 1
+
+	for sheetName, sheet := range f.Sheets {
+		xSheet := sheet.makeXLSXSheet(refTable)
+		rId := fmt.Sprintf("rId%d", sheetIndex)
+		sheetId := strconv.Itoa(sheetIndex)
+		sheetPath := fmt.Sprintf("worksheets/sheet%d.xml", sheetIndex)
+		partName := "xl/" + sheetPath
+		types.Overrides = append(
+			types.Overrides,
+			xlsxOverride{
+				PartName: partName,
+				ContentType: "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"})
+		workbookRels[rId] = sheetPath
+		workbook.Sheets.Sheet[sheetIndex - 1] = xlsxSheet{
+			Name: sheetName,
+			SheetId: sheetId,
+			Id: rId}
+		parts[partName], err = marshal(xSheet)
+		if err != nil {
+			return parts, err
+		}
+		sheetIndex++
+	}
+
+	parts["xl/workbook.xml"], err = marshal(workbook)
+	if err != nil {
+		return parts, err
+	}
+
+	parts["_rels/.rels"] = `<?xml version="1.0" encoding="UTF-8"?>
+<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
+  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
+  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
+</Relationships>`
+
+	parts["docProps/app.xml"] = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
+  <TotalTime>0</TotalTime>
+  <Application>Go XLSX</Application>
+</Properties>`
+
+	// TODO - do this properly, modification and revision information
+	parts["docProps/core.xml"] = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></cp:coreProperties>`
+
+	xSST := refTable.makeXLSXSST()
+	parts["xl/sharedStrings.xml"], err = marshal(xSST)
+	if err != nil {
+		return parts, err
+	}
+
+	xWRel := workbookRels.MakeXLSXWorkbookRels()
+
+	parts["xl/_rels/workbook.xml.rels"], err = marshal(xWRel)
+	if err != nil {
+		return parts, err
+	}
+
+	parts["[Content_Types].xml"], err = marshal(types)
+	if err != nil {
+		return parts, err
+	}
+
+	parts["xl/styles.xml"] = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+</styleSheet>`
+
+	return parts, nil
+}

+ 403 - 0
file_test.go

@@ -0,0 +1,403 @@
+package xlsx
+
+import (
+	"encoding/xml"
+	"path/filepath"
+	. "gopkg.in/check.v1"
+)
+
+type FileSuite struct {}
+
+var _ = Suite(&FileSuite{})
+
+// Test we can correctly open a XSLX file and return a xlsx.File
+// struct.
+func (l *FileSuite) TestOpenFile(c *C) {
+	var xlsxFile *File
+	var error error
+
+	xlsxFile, error = OpenFile("testfile.xlsx")
+	c.Assert(error, IsNil)
+	c.Assert(xlsxFile, NotNil)
+}
+
+
+func (l *FileSuite) TestOpenFileWithoutStyleAndSharedStrings(c *C) {
+	var xlsxFile *File
+	var error error
+
+	xlsxFile, error = OpenFile("noStylesAndSharedStringsTest.xlsx")
+	c.Assert(error, IsNil)
+	c.Assert(xlsxFile, NotNil)
+}
+
+// Test that we can correctly extract a reference table from the
+// sharedStrings.xml file embedded in the XLSX file and return a
+// reference table of string values from it.
+func (l *FileSuite) TestReadSharedStringsFromZipFile(c *C) {
+	var xlsxFile *File
+	var err error
+	xlsxFile, err = OpenFile("testfile.xlsx")
+	c.Assert(err, IsNil)
+	c.Assert(xlsxFile.referenceTable, NotNil)
+}
+
+
+// Helper function used to test contents of a given xlsxXf against
+// expectations.
+func testXf(c *C, result, expected *xlsxXf) {
+	c.Assert(result.ApplyAlignment, Equals, expected.ApplyAlignment)
+	c.Assert(result.ApplyBorder, Equals, expected.ApplyBorder)
+	c.Assert(result.ApplyFont, Equals, expected.ApplyFont)
+	c.Assert(result.ApplyFill, Equals, expected.ApplyFill)
+	c.Assert(result.ApplyProtection, Equals, expected.ApplyProtection)
+	c.Assert(result.BorderId, Equals, expected.BorderId)
+	c.Assert(result.FillId, Equals, expected.FillId)
+	c.Assert(result.FontId, Equals, expected.FontId)
+	c.Assert(result.NumFmtId, Equals, expected.NumFmtId)
+}
+
+// We can correctly extract a style table from the style.xml file
+// embedded in the XLSX file and return a styles struct from it.
+func (l *FileSuite) TestReadStylesFromZipFile(c *C) {
+	var xlsxFile *File
+	var err error
+	var fontCount, fillCount, borderCount, cellStyleXfCount, cellXfCount int
+	var font xlsxFont
+	var fill xlsxFill
+	var border xlsxBorder
+	var xf xlsxXf
+
+	xlsxFile, err = OpenFile("testfile.xlsx")
+	c.Assert(err, IsNil)
+	c.Assert(xlsxFile.styles, NotNil)
+
+	fontCount = len(xlsxFile.styles.Fonts)
+	c.Assert(fontCount, Equals, 4)
+
+	font = xlsxFile.styles.Fonts[0]
+	c.Assert(font.Sz.Val, Equals, "11")
+	c.Assert(font.Name.Val, Equals, "Calibri")
+
+	fillCount = len(xlsxFile.styles.Fills)
+	c.Assert(fillCount, Equals, 3)
+
+	fill = xlsxFile.styles.Fills[2]
+	c.Assert(fill.PatternFill.PatternType, Equals, "solid")
+
+	borderCount = len(xlsxFile.styles.Borders)
+	c.Assert(borderCount, Equals, 2)
+
+	border = xlsxFile.styles.Borders[1]
+	c.Assert(border.Left.Style, Equals, "thin")
+	c.Assert(border.Right.Style, Equals, "thin")
+	c.Assert(border.Top.Style, Equals, "thin")
+	c.Assert(border.Bottom.Style, Equals, "thin")
+
+	cellStyleXfCount = len(xlsxFile.styles.CellStyleXfs)
+	c.Assert(cellStyleXfCount, Equals, 20)
+
+	xf = xlsxFile.styles.CellStyleXfs[0]
+	expectedXf := &xlsxXf{
+		ApplyAlignment:  true,
+		ApplyBorder:     true,
+		ApplyFont:       true,
+		ApplyFill:       false,
+		ApplyProtection: true,
+		BorderId:        0,
+		FillId:          0,
+		FontId:          0,
+		NumFmtId:        164}
+	testXf(c, &xf, expectedXf)
+
+	cellXfCount = len(xlsxFile.styles.CellXfs)
+	c.Assert(cellXfCount, Equals, 3)
+
+	xf = xlsxFile.styles.CellXfs[0]
+	expectedXf = &xlsxXf{
+		ApplyAlignment:  false,
+		ApplyBorder:     false,
+		ApplyFont:       false,
+		ApplyFill:       false,
+		ApplyProtection: false,
+		BorderId:        0,
+		FillId:          0,
+		FontId:          0,
+		NumFmtId:        164}
+	testXf(c, &xf, expectedXf)
+}
+
+// We can correctly extract a map of relationship Ids to the worksheet files in
+// which they are contained from the XLSX file.
+func (l *FileSuite) TestReadWorkbookRelationsFromZipFile(c *C) {
+	var xlsxFile *File
+	var err error
+
+	xlsxFile, err = OpenFile("testfile.xlsx")
+	c.Assert(err, IsNil)
+	c.Assert(len(xlsxFile.Sheets), Equals, 3)
+	sheet, ok := xlsxFile.Sheets["Tabelle1"]
+	c.Assert(ok, Equals, true)
+	c.Assert(sheet, NotNil)
+}
+
+func (l *FileSuite) TestGetStyleFromZipFile(c *C) {
+	var xlsxFile *File
+	var err error
+
+	xlsxFile, err = OpenFile("testfile.xlsx")
+	c.Assert(err, IsNil)
+	sheetCount := len(xlsxFile.Sheets)
+	c.Assert(sheetCount, Equals, 3)
+
+	tabelle1 := xlsxFile.Sheets["Tabelle1"]
+
+	row0 := tabelle1.Rows[0]
+	cellFoo := row0.Cells[0]
+	c.Assert(cellFoo.String(), Equals, "Foo")
+	// style := cellFoo.GetStyle()
+	// c.Assert(style.Fill.BgColor, Equals, "FF33CCCC")
+
+	row1 := tabelle1.Rows[1]
+	cellQuuk := row1.Cells[1]
+	c.Assert(cellQuuk.String(), Equals, "Quuk")
+	c.Assert(cellQuuk.GetStyle().Border.Left, Equals, "thin")
+
+	cellBar := row0.Cells[1]
+	c.Assert(cellBar.String(), Equals, "Bar")
+	c.Assert(cellBar.GetStyle().Fill.BgColor, Equals, "")
+}
+
+
+// Test we can create a File object from scratch
+func (l *FileSuite) TestCreateFile(c *C) {
+	var xlsxFile *File
+
+	xlsxFile = NewFile()
+	c.Assert(xlsxFile, NotNil)
+}
+
+// Test that when we open a real XLSX file we create xlsx.Sheet
+// objects for the sheets inside the file and that these sheets are
+// themselves correct.
+func (l *FileSuite) TestCreateSheet(c *C) {
+	var xlsxFile *File
+	var err error
+	var sheet *Sheet
+	var row *Row
+	xlsxFile, err = OpenFile("testfile.xlsx")
+	c.Assert(err, IsNil)
+	c.Assert(xlsxFile, NotNil)
+	sheetLen := len(xlsxFile.Sheets)
+	c.Assert(sheetLen, Equals, 3)
+	sheet = xlsxFile.Sheets["Tabelle1"]
+	rowLen := len(sheet.Rows)
+	c.Assert(rowLen, Equals, 2)
+	row = sheet.Rows[0]
+	c.Assert(len(row.Cells), Equals, 2)
+	cell := row.Cells[0]
+	cellstring := cell.String()
+	c.Assert(cellstring, Equals, "Foo")
+}
+
+// Test that we can add a sheet to a File
+func (l *FileSuite) TestAddSheet(c *C) {
+	var f *File
+
+	f = NewFile()
+	sheet := f.AddSheet("MySheet")
+	c.Assert(sheet, NotNil)
+	c.Assert(len(f.Sheets), Equals, 1)
+	c.Assert(f.Sheets["MySheet"], Equals, sheet)
+}
+
+// Test that we can create a Workbook and marshal it to XML. 
+func (l *FileSuite) TestMarshalWorkbook(c *C) {
+	var f *File
+
+	f = NewFile()
+
+	f.AddSheet("MyFirstSheet")
+	f.AddSheet("MySecondSheet")
+	workbook := f.makeWorkbook()
+	workbook.Sheets.Sheet[0] = xlsxSheet{
+		Name: "MyFirstSheet",
+		SheetId: "1",
+		Id: "rId1"}
+
+	workbook.Sheets.Sheet[1] = xlsxSheet{
+		Name: "MySecondSheet",
+		SheetId: "2",
+		Id: "rId2"}
+
+	expectedWorkbook := `<?xml version="1.0" encoding="UTF-8"?>
+   <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+      <fileVersion appName="Go XLSX"></fileVersion>
+      <workbookPr date1904="false"></workbookPr>
+      <bookViews>
+         <workbookView></workbookView>
+      </bookViews>
+      <sheets>
+         <sheet name="MyFirstSheet" sheetId="1" xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships:id="rId1"></sheet>
+         <sheet name="MySecondSheet" sheetId="2" xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships:id="rId2"></sheet>
+      </sheets>
+      <definedNames></definedNames>
+      <calcPr></calcPr>
+   </workbook>`
+	output, err := xml.MarshalIndent(workbook, "   ", "   ")
+	c.Assert(err, IsNil)
+	stringOutput := xml.Header + string(output)
+	c.Assert(stringOutput, Equals, expectedWorkbook)
+}
+
+
+// Test that we can marshall a File to a collection of xml files
+func (l *FileSuite) TestMarshalFile(c *C) {
+	var f *File
+	f = NewFile()
+	sheet1 := f.AddSheet("MySheet")
+	row1 := sheet1.AddRow()
+	cell1 := row1.AddCell()
+	cell1.Value = "A cell!"
+	sheet2 := f.AddSheet("AnotherSheet")
+	row2 := sheet2.AddRow()
+	cell2 := row2.AddCell()
+	cell2.Value = "A cell!"
+	parts, err := f.MarshallParts()
+	c.Assert(err, IsNil)
+	c.Assert(len(parts), Equals, 10)
+
+	// sheets
+	expectedSheet := `<?xml version="1.0" encoding="UTF-8"?>
+  <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+    <dimension ref="A1:A1"></dimension>
+    <sheetData>
+      <row r="1">
+        <c r="A1" t="s">
+          <v>0</v>
+        </c>
+      </row>
+    </sheetData>
+  </worksheet>`
+	c.Assert(parts["xl/worksheets/sheet1.xml"], Equals, expectedSheet)
+	c.Assert(parts["xl/worksheets/sheet2.xml"], Equals, expectedSheet)
+
+	// .rels.xml
+	expectedRels := `<?xml version="1.0" encoding="UTF-8"?>
+<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+  <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/>
+  <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/>
+  <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/>
+</Relationships>`
+	c.Assert(parts["_rels/.rels"], Equals, expectedRels)
+
+	// app.xml
+	expectedApp := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
+  <TotalTime>0</TotalTime>
+  <Application>Go XLSX</Application>
+</Properties>`
+	c.Assert(parts["docProps/app.xml"], Equals, expectedApp)
+
+	// core.xml
+	expectedCore := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></cp:coreProperties>`
+	c.Assert(parts["docProps/core.xml"], Equals, expectedCore)
+
+	// sharedStrings.xml
+	expectedXLSXSST := `<?xml version="1.0" encoding="UTF-8"?>
+  <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1">
+    <si>
+      <t>A cell!</t>
+    </si>
+  </sst>`
+	c.Assert(parts["xl/sharedStrings.xml"], Equals, expectedXLSXSST)
+
+	// workbook.xml.rels
+	expectedXLSXWorkbookRels := `<?xml version="1.0" encoding="UTF-8"?>
+  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+    <Relationship Id="rId1" Target="worksheets/sheet1.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"></Relationship>
+    <Relationship Id="rId2" Target="worksheets/sheet2.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"></Relationship>
+    <Relationship Id="rId3" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"></Relationship>
+    <Relationship Id="rId4" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"></Relationship>
+  </Relationships>`
+	c.Assert(parts["xl/_rels/workbook.xml.rels"], Equals, expectedXLSXWorkbookRels)
+
+	// workbook.xml
+	expectedWorkbook := `<?xml version="1.0" encoding="UTF-8"?>
+  <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+    <fileVersion appName="Go XLSX"></fileVersion>
+    <workbookPr date1904="false"></workbookPr>
+    <bookViews>
+      <workbookView></workbookView>
+    </bookViews>
+    <sheets>
+      <sheet name="MySheet" sheetId="1" xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships:id="rId1"></sheet>
+      <sheet name="AnotherSheet" sheetId="2" xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships:id="rId2"></sheet>
+    </sheets>
+    <definedNames></definedNames>
+    <calcPr></calcPr>
+  </workbook>`
+	c.Assert(parts["xl/workbook.xml"], Equals, expectedWorkbook)
+
+	// [Content_Types].xml
+	expectedContentTypes := `<?xml version="1.0" encoding="UTF-8"?>
+  <Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
+    <Override PartName="/_rels/.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"></Override>
+    <Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"></Override>
+    <Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"></Override>
+    <Override PartName="/xl/_rels/workbook.xml.rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"></Override>
+    <Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"></Override>
+    <Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"></Override>
+    <Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"></Override>
+    <Override PartName="xl/worksheets/sheet1.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"></Override>
+    <Override PartName="xl/worksheets/sheet2.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"></Override>
+    <Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"></Default>
+    <Default Extension="xml" ContentType="application/xml"></Default>
+  </Types>`
+	c.Assert(parts["[Content_Types].xml"], Equals, expectedContentTypes)
+
+
+	// styles.xml
+	//
+	// For now we only allow simple string data in the
+	// spreadsheet.  Style support will follow.
+	expectedStyles := `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<styleSheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+</styleSheet>`
+	c.Assert(parts["xl/styles.xml"], Equals, expectedStyles)
+}
+
+
+// We can save a File as a valid XLSX file at a given path.
+func (l *FileSuite) TestSaveFile(c *C) {
+	var tmpPath string = c.MkDir()
+	var f *File
+	f = NewFile()
+	sheet1 := f.AddSheet("MySheet")
+	row1 := sheet1.AddRow()
+	cell1 := row1.AddCell()
+	cell1.Value = "A cell!"
+	sheet2 := f.AddSheet("AnotherSheet")
+	row2 := sheet2.AddRow()
+	cell2 := row2.AddCell()
+	cell2.Value = "A cell!"
+	xlsxPath := filepath.Join(tmpPath, "TestSaveFile.xlsx")
+	err := f.Save(xlsxPath)
+	c.Assert(err, IsNil)
+
+	// Let's eat our own dog food
+	xlsxFile, err := OpenFile(xlsxPath)
+	c.Assert(err, IsNil)
+	c.Assert(xlsxFile, NotNil)
+	c.Assert(len(xlsxFile.Sheets), Equals, 2)
+
+	sheet1, ok := xlsxFile.Sheets["MySheet"]
+	c.Assert(ok, Equals, true)
+	c.Assert(len(sheet1.Rows), Equals, 1)
+	row1 = sheet1.Rows[0]
+	c.Assert(len(row1.Cells), Equals, 1)
+	cell1 = row1.Cells[0]
+	c.Assert(cell1.Value, Equals, "A cell!")
+	c.Assert(cell1.String(), Equals, "A cell!")
+}

+ 141 - 302
lib.go

@@ -24,269 +24,6 @@ func (e *XLSXReaderError) Error() string {
 	return e.Err
 }
 
-// Cell is a high level structure intended to provide user access to
-// the contents of Cell within an xlsx.Row.
-type Cell struct {
-	Value          string
-	styleIndex     int
-	styles         *xlsxStyles
-	numFmtRefTable map[int]xlsxNumFmt
-	date1904       bool
-}
-
-// CellInterface defines the public API of the Cell.
-type CellInterface interface {
-	String() string
-}
-
-// String returns the value of a Cell as a string.
-func (c *Cell) String() string {
-	return c.Value
-}
-
-// GetStyle returns the Style associated with a Cell
-func (c *Cell) GetStyle() *Style {
-	style := &Style{}
-
-	if c.styleIndex >= 0 && c.styleIndex < len(c.styles.CellXfs) {
-		xf := c.styles.CellXfs[c.styleIndex]
-		if xf.BorderId >= 0 && xf.BorderId < len(c.styles.Borders) {
-			var border Border
-			border.Left = c.styles.Borders[xf.BorderId].Left.Style
-			border.Right = c.styles.Borders[xf.BorderId].Right.Style
-			border.Top = c.styles.Borders[xf.BorderId].Top.Style
-			border.Bottom = c.styles.Borders[xf.BorderId].Bottom.Style
-			style.Border = border
-		}
-		if xf.FillId >= 0 && xf.FillId < len(c.styles.Fills) {
-			var fill Fill
-			fill.PatternType = c.styles.Fills[xf.FillId].PatternFill.PatternType
-			fill.BgColor = c.styles.Fills[xf.FillId].PatternFill.BgColor.RGB
-			fill.FgColor = c.styles.Fills[xf.FillId].PatternFill.FgColor.RGB
-			style.Fill = fill
-		}
-		if xf.FontId >= 0 && xf.FontId < len(c.styles.Fonts) {
-			font := c.styles.Fonts[xf.FontId]
-			style.Font = Font{}
-			style.Font.Size, _ = strconv.Atoi(font.Sz.Val)
-			style.Font.Name = font.Name.Val
-			style.Font.Family, _ = strconv.Atoi(font.Family.Val)
-			style.Font.Charset, _ = strconv.Atoi(font.Charset.Val)
-		}
-	}
-	return style
-}
-
-// The number format string is returnable from a cell.
-func (c *Cell) GetNumberFormat() string {
-	var numberFormat string = ""
-	if c.styleIndex > 0 && c.styleIndex <= len(c.styles.CellXfs) {
-		xf := c.styles.CellXfs[c.styleIndex-1]
-		numFmt := c.numFmtRefTable[xf.NumFmtId]
-		numberFormat = numFmt.FormatCode
-	}
-	return strings.ToLower(numberFormat)
-}
-
-func (c *Cell) formatToTime(format string) string {
-	f, err := strconv.ParseFloat(c.Value, 64)
-	if err != nil {
-		return err.Error()
-	}
-	return TimeFromExcelTime(f, c.date1904).Format(format)
-}
-
-func (c *Cell) formatToFloat(format string) string {
-	f, err := strconv.ParseFloat(c.Value, 64)
-	if err != nil {
-		return err.Error()
-	}
-	return fmt.Sprintf(format, f)
-}
-
-func (c *Cell) formatToInt(format string) string {
-	f, err := strconv.ParseFloat(c.Value, 64)
-	if err != nil {
-		return err.Error()
-	}
-	return fmt.Sprintf(format, int(f))
-}
-
-// Return the formatted version of the value.
-func (c *Cell) FormattedValue() string {
-	var numberFormat string = c.GetNumberFormat()
-	switch numberFormat {
-	case "general":
-		return c.Value
-	case "0", "#,##0":
-		return c.formatToInt("%d")
-	case "0.00", "#,##0.00", "@":
-		return c.formatToFloat("%.2f")
-	case "#,##0 ;(#,##0)", "#,##0 ;[red](#,##0)":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return err.Error()
-		}
-		if f < 0 {
-			i := int(math.Abs(f))
-			return fmt.Sprintf("(%d)", i)
-		}
-		i := int(f)
-		return fmt.Sprintf("%d", i)
-	case "#,##0.00;(#,##0.00)", "#,##0.00;[red](#,##0.00)":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return err.Error()
-		}
-		if f < 0 {
-			return fmt.Sprintf("(%.2f)", f)
-		}
-		return fmt.Sprintf("%.2f", f)
-	case "0%":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return err.Error()
-		}
-		f = f * 100
-		return fmt.Sprintf("%d%%", int(f))
-	case "0.00%":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return err.Error()
-		}
-		f = f * 100
-		return fmt.Sprintf("%.2f%%", f)
-	case "0.00e+00", "##0.0e+0":
-		return c.formatToFloat("%e")
-	case "mm-dd-yy":
-		return c.formatToTime("01-02-06")
-	case "d-mmm-yy":
-		return c.formatToTime("2-Jan-06")
-	case "d-mmm":
-		return c.formatToTime("2-Jan")
-	case "mmm-yy":
-		return c.formatToTime("Jan-06")
-	case "h:mm am/pm":
-		return c.formatToTime("3:04 pm")
-	case "h:mm:ss am/pm":
-		return c.formatToTime("3:04:05 pm")
-	case "h:mm":
-		return c.formatToTime("15:04")
-	case "h:mm:ss":
-		return c.formatToTime("15:04:05")
-	case "m/d/yy h:mm":
-		return c.formatToTime("1/2/06 15:04")
-	case "mm:ss":
-		return c.formatToTime("04:05")
-	case "[h]:mm:ss":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return err.Error()
-		}
-		t := TimeFromExcelTime(f, c.date1904)
-		if t.Hour() > 0 {
-			return t.Format("15:04:05")
-		}
-		return t.Format("04:05")
-	case "mmss.0":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return err.Error()
-		}
-		t := TimeFromExcelTime(f, c.date1904)
-		return fmt.Sprintf("%0d%0d.%d", t.Minute(), t.Second(), t.Nanosecond()/1000)
-
-	case "yyyy\\-mm\\-dd":
-		return c.formatToTime("2006\\-01\\-02")
-	case "dd/mm/yy":
-		return c.formatToTime("02/01/06")
-	case "hh:mm:ss":
-		return c.formatToTime("15:04:05")
-	case "dd/mm/yy\\ hh:mm":
-		return c.formatToTime("02/01/06\\ 15:04")
-	case "dd/mm/yyyy hh:mm:ss":
-		return c.formatToTime("02/01/2006 15:04:05")
-	case "yy-mm-dd":
-		return c.formatToTime("06-01-02")
-	case "d-mmm-yyyy":
-		return c.formatToTime("2-Jan-2006")
-	case "m/d/yy":
-		return c.formatToTime("1/2/06")
-	case "m/d/yyyy":
-		return c.formatToTime("1/2/2006")
-	case "dd-mmm-yyyy":
-		return c.formatToTime("02-Jan-2006")
-	case "dd/mm/yyyy":
-		return c.formatToTime("02/01/2006")
-	case "mm/dd/yy hh:mm am/pm":
-		return c.formatToTime("01/02/06 03:04 pm")
-	case "mm/dd/yyyy hh:mm:ss":
-		return c.formatToTime("01/02/2006 15:04:05")
-	case "yyyy-mm-dd hh:mm:ss":
-		return c.formatToTime("2006-01-02 15:04:05")
-	}
-	return c.Value
-}
-
-// Row is a high level structure indended to provide user access to a
-// row within a xlsx.Sheet.  An xlsx.Row contains a slice of xlsx.Cell.
-type Row struct {
-	Cells []*Cell
-}
-
-// Sheet is a high level structure intended to provide user access to
-// the contents of a particular sheet within an XLSX file.
-type Sheet struct {
-	Name   string
-	Rows   []*Row
-	MaxRow int
-	MaxCol int
-}
-
-// Style is a high level structure intended to provide user access to
-// the contents of Style within an XLSX file.
-type Style struct {
-	Border Border
-	Fill   Fill
-	Font   Font
-}
-
-// Border is a high level structure intended to provide user access to
-// the contents of Border Style within an Sheet.
-type Border struct {
-	Left   string
-	Right  string
-	Top    string
-	Bottom string
-}
-
-// Fill is a high level structure intended to provide user access to
-// the contents of background and foreground color index within an Sheet.
-type Fill struct {
-	PatternType string
-	BgColor     string
-	FgColor     string
-}
-
-type Font struct {
-	Size    int
-	Name    string
-	Family  int
-	Charset int
-}
-
-// File is a high level structure providing a slice of Sheet structs
-// to the user.
-type File struct {
-	worksheets     map[string]*zip.File
-	numFmtRefTable map[int]xlsxNumFmt
-	referenceTable []string
-	styles         *xlsxStyles
-	Sheets         []*Sheet          // sheet access by index
-	Sheet          map[string]*Sheet // sheet access by name
-	Date1904       bool
-}
-
 // getRangeFromString is an internal helper function that converts
 // XLSX internal range syntax to a pair of integers.  For example,
 // the range string "1:3" yield the upper and lower intergers 1 and 3.
@@ -327,6 +64,85 @@ func lettersToNumeric(letters string) int {
 	return sum
 }
 
+
+// Get the largestDenominator that is a multiple of a basedDenominator
+// and fits at least once into a given numerator.
+func getLargestDenominator(numerator, multiple, baseDenominator, power int) (int, int) {
+	if numerator / multiple == 0 {
+		return 1, power
+	}
+	next, nextPower := getLargestDenominator(
+		numerator, multiple * baseDenominator, baseDenominator, power + 1)
+	if next > multiple {
+		return next, nextPower
+	}
+	return multiple, power
+}
+
+// Convers a list of numbers representing a column into a alphabetic
+// representation, as used in the spreadsheet.
+func formatColumnName(colId []int) string {
+	lastPart := len(colId) - 1
+
+	result := ""
+	for n, part := range(colId) {
+		if n == lastPart {
+			// The least significant number is in the
+			// range 0-25, all other numbers are 1-26,
+			// hence we use a differente offset for the
+			// last part.
+			result += string(part + 65)
+		} else {
+			// Don't output leading 0s, as there is no
+			// representation of 0 in this format.
+			if part > 0 {
+				result += string(part + 64)
+			}
+		}
+	}
+	return result
+}
+
+func smooshBase26Slice(b26 []int) []int {
+	// Smoosh values together, eliminating 0s from all but the
+	// least significant part.
+	lastButOnePart := len(b26) - 2
+	for i := lastButOnePart; i > 0; i-- {
+		part := b26[i]
+		if part == 0 {
+			greaterPart := b26[i-1]
+			if greaterPart > 0 {
+				b26[i-1] = greaterPart - 1
+				b26[i] = 26
+			}
+		}
+	}
+	return b26
+}
+
+func intToBase26(x int) (parts []int) {
+	// Excel column codes are pure evil - in essence they're just
+	// base26, but they don't represent the number 0.
+	b26Denominator, _ := getLargestDenominator(x, 1, 26, 0)
+
+	// This loop terminates because integer division of 1 / 26
+	// returns 0.
+	for d := b26Denominator; d > 0; d = d / 26 {
+		value := x / d
+		remainder := x % d
+		parts = append(parts, value)
+		x = remainder
+	}
+	return parts
+}
+
+// numericToLetters is used to convert a zero based, numeric column
+// indentifier into a character code.
+func numericToLetters(colRef int) string {
+	parts := intToBase26(colRef)
+	return formatColumnName(smooshBase26Slice(parts))
+}
+
 // letterOnlyMapF is used in conjunction with strings.Map to return
 // only the characters A-Z and a-z in a string
 func letterOnlyMapF(rune rune) rune {
@@ -362,6 +178,14 @@ func getCoordsFromCellIDString(cellIDString string) (x, y int, error error) {
 	return x, y, error
 }
 
+// getCellIDStringFromCoords returns the Excel format cell name that
+// represents a pair of zero based cartesian coordinates.
+func getCellIDStringFromCoords(x, y int) string {
+	letterPart := numericToLetters(x);
+	numericPart := y + 1
+	return fmt.Sprintf("%s%d", letterPart, numericPart)
+}
+
 // getMaxMinFromDimensionRef return the zero based cartesian maximum
 // and minimum coordinates from the dimension reference embedded in a
 // XLSX worksheet.  For example, the dimension reference "A1:B2"
@@ -473,9 +297,10 @@ func makeRowFromRaw(rawrow xlsxRow) *Row {
 	return row
 }
 
-// getValueFromCellData attempts to extract a valid value, usable in CSV form from the raw cell value.
-// Note - this is not actually general enough - we should support retaining tabs and newlines.
-func getValueFromCellData(rawcell xlsxC, reftable []string) string {
+// getValueFromCellData attempts to extract a valid value, usable in
+// CSV form from the raw cell value.  Note - this is not actually
+// general enough - we should support retaining tabs and newlines.
+func getValueFromCellData(rawcell xlsxC, reftable *RefTable) string {
 	var value string = ""
 	var data string = rawcell.V
 	if len(data) > 0 {
@@ -486,7 +311,7 @@ func getValueFromCellData(rawcell xlsxC, reftable []string) string {
 			if error != nil {
 				panic(error)
 			}
-			value = reftable[ref]
+			value = reftable.ResolveSharedString(ref)
 		default:
 			value = vval
 		}
@@ -501,7 +326,7 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File) ([]*Row, int, int)
 	var rows []*Row
 	var row *Row
 	var minCol, maxCol, minRow, maxRow, colCount, rowCount int
-	var reftable []string
+	var reftable *RefTable
 	var err error
 	var insertRowIndex, insertColIndex int
 
@@ -591,7 +416,7 @@ func readSheetFromFile(sc chan *indexedSheet, index int, rsheet xlsxSheet, fi *F
 // readSheetsFromZipFile is an internal helper function that loops
 // over the Worksheets defined in the XSLXWorkbook and loads them into
 // Sheet objects stored in the Sheets slice of a xlsx.File struct.
-func readSheetsFromZipFile(f *zip.File, file *File, sheetXMLMap map[string]string) ([]*Sheet, error) {
+func readSheetsFromZipFile(f *zip.File, file *File, sheetXMLMap map[string]string) (map[string]*Sheet, error) {
 	var workbook *xlsxWorkbook
 	var error error
 	var rc io.ReadCloser
@@ -609,7 +434,7 @@ func readSheetsFromZipFile(f *zip.File, file *File, sheetXMLMap map[string]strin
 	}
 	file.Date1904 = workbook.WorkbookPr.Date1904
 	sheetCount = len(workbook.Sheets.Sheet)
-	sheets := make([]*Sheet, sheetCount)
+	sheets := make(map[string]*Sheet, sheetCount)
 	sheetChan := make(chan *indexedSheet, sheetCount)
 	for i, rawsheet := range workbook.Sheets.Sheet {
 		go readSheetFromFile(sheetChan, i, rawsheet, file, sheetXMLMap)
@@ -619,8 +444,7 @@ func readSheetsFromZipFile(f *zip.File, file *File, sheetXMLMap map[string]strin
 		if sheet.Error != nil {
 			return nil, sheet.Error
 		}
-		sheet.Sheet.Name = workbook.Sheets.Sheet[sheet.Index].Name
-		sheets[sheet.Index] = sheet.Sheet
+		sheets[workbook.Sheets.Sheet[sheet.Index].Name] = sheet.Sheet
 	}
 	return sheets, nil
 }
@@ -628,20 +452,19 @@ func readSheetsFromZipFile(f *zip.File, file *File, sheetXMLMap map[string]strin
 // readSharedStringsFromZipFile() is an internal helper function to
 // extract a reference table from the sharedStrings.xml file within
 // the XLSX zip file.
-func readSharedStringsFromZipFile(f *zip.File) ([]string, error) {
+func readSharedStringsFromZipFile(f *zip.File) (*RefTable, error) {
 	var sst *xlsxSST
 	var error error
 	var rc io.ReadCloser
 	var decoder *xml.Decoder
-	var reftable []string
+	var reftable *RefTable
 
 
 	// In a file with no strings it's possible that
 	// sharedStrings.xml doesn't exist.  In this case the value
 	// passed as f will be nil.
 	if f == nil {
-		reftable = make([]string, 0)
-		return reftable, nil
+		return nil, nil
 	}
 	rc, error = f.Open()
 	if error != nil {
@@ -686,12 +509,47 @@ func buildNumFmtRefTable(style *xlsxStyles) map[int]xlsxNumFmt {
 	return refTable
 }
 
+type WorkBookRels map[string]string
+
+func (w *WorkBookRels) MakeXLSXWorkbookRels() xlsxWorkbookRels {
+	relCount := len(*w)
+	xWorkbookRels := xlsxWorkbookRels{}
+	xWorkbookRels.Relationships = make([]xlsxWorkbookRelation, relCount + 2)
+	for k, v := range(*w) {
+		index, err := strconv.Atoi(k[3:len(k)])
+		if err != nil {
+			panic(err.Error())
+		}
+		xWorkbookRels.Relationships[index -1] = xlsxWorkbookRelation{
+			Id: k,
+			Target: v,
+			Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"}
+	}
+
+	relCount++
+	sheetId := fmt.Sprintf("rId%d", relCount)
+	xWorkbookRels.Relationships[relCount -1] = xlsxWorkbookRelation{
+			Id: sheetId,
+			Target: "sharedStrings.xml",
+			Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"}
+
+	relCount++
+	sheetId = fmt.Sprintf("rId%d", relCount)
+	xWorkbookRels.Relationships[relCount -1] = xlsxWorkbookRelation{
+			Id: sheetId,
+			Target: "styles.xml",
+		Type: "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"}
+
+	return xWorkbookRels
+}
+
+
 // readWorkbookRelationsFromZipFile is an internal helper function to
 // extract a map of relationship ID strings to the name of the
 // worksheet.xml file they refer to.  The resulting map can be used to
 // reliably derefence the worksheets in the XLSX file.
-func readWorkbookRelationsFromZipFile(workbookRels *zip.File) (map[string]string, error) {
-	var sheetXMLMap map[string]string
+func readWorkbookRelationsFromZipFile(workbookRels *zip.File) (WorkBookRels, error) {
+	var sheetXMLMap WorkBookRels
 	var wbRelationships *xlsxWorkbookRels
 	var rc io.ReadCloser
 	var decoder *xml.Decoder
@@ -707,7 +565,7 @@ func readWorkbookRelationsFromZipFile(workbookRels *zip.File) (map[string]string
 	if err != nil {
 		return nil, err
 	}
-	sheetXMLMap = make(map[string]string)
+	sheetXMLMap = make(WorkBookRels)
 	for _, rel := range wbRelationships.Relationships {
 		if strings.HasSuffix(rel.Target, ".xml") && rel.Type == "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" {
 			_, filename := path.Split(rel.Target)
@@ -717,16 +575,6 @@ func readWorkbookRelationsFromZipFile(workbookRels *zip.File) (map[string]string
 	return sheetXMLMap, nil
 }
 
-// OpenFile() take the name of an XLSX file and returns a populated
-// xlsx.File struct for it.
-func OpenFile(filename string) (*File, error) {
-	var f *zip.ReadCloser
-	f, err := zip.OpenReader(filename)
-	if err != nil {
-		return nil, err
-	}
-	return ReadZip(f)
-}
 
 // ReadZip() takes a pointer to a zip.ReadCloser and returns a
 // xlsx.File struct populated with its contents.  In most cases
@@ -736,15 +584,15 @@ func ReadZip(f *zip.ReadCloser) (*File, error) {
 	return ReadZipReader(&f.Reader)
 }
 
-// ReadZipReader() can be used to read xlsx in memory without touch filesystem.
+// ReadZipReader() can be used to read an XLSX in memory without
+// touching the filesystem.
 func ReadZipReader(r *zip.Reader) (*File, error) {
 	var err error
 	var file *File
-	var reftable []string
+	var reftable *RefTable
 	var sharedStrings *zip.File
-	var sheetMap map[string]*Sheet
 	var sheetXMLMap map[string]string
-	var sheets []*Sheet
+	var sheets map[string]*Sheet
 	var style *xlsxStyles
 	var styles *zip.File
 	var v *zip.File
@@ -800,14 +648,5 @@ func ReadZipReader(r *zip.Reader) (*File, error) {
 		return nil, readerErr
 	}
 	file.Sheets = sheets
-	sheetMap = make(map[string]*Sheet, len(sheets))
-	for i := 0; i < len(sheets); i++ {
-		sheetMap[sheets[i].Name] = sheets[i]
-	}
-	file.Sheet = sheetMap
 	return file, nil
 }
-
-func NewFile() *File {
-	return &File{}
-}

+ 91 - 447
lib_test.go

@@ -12,431 +12,6 @@ type LibSuite struct{}
 
 var _ = Suite(&LibSuite{})
 
-// Test we can correctly open a XSLX file and return a xlsx.File
-// struct.
-func (l *LibSuite) TestOpenFile(c *C) {
-	var xlsxFile *File
-	var error error
-
-	xlsxFile, error = OpenFile("testfile.xlsx")
-	c.Assert(error, IsNil)
-	c.Assert(xlsxFile, NotNil)
-
-}
-
-func (l *LibSuite) TestOpenFileWithoutStyleAndSharedStrings(c *C) {
-	var xlsxFile *File
-	var error error
-
-	xlsxFile, error = OpenFile("noStylesAndSharedStringsTest.xlsx")
-	c.Assert(error, IsNil)
-	c.Assert(xlsxFile, NotNil)
-
-}
-
-// Test we can create a File object from scratch
-func (l *LibSuite) TestCreateFile(c *C) {
-	var xlsxFile *File
-
-	xlsxFile = NewFile()
-	c.Assert(xlsxFile, NotNil)
-}
-
-// Test that when we open a real XLSX file we create xlsx.Sheet
-// objects for the sheets inside the file and that these sheets are
-// themselves correct.
-func (l *LibSuite) TestCreateSheet(c *C) {
-	var xlsxFile *File
-	var err error
-	var sheet *Sheet
-	var row *Row
-	xlsxFile, err = OpenFile("testfile.xlsx")
-	c.Assert(err, IsNil)
-	c.Assert(xlsxFile, NotNil)
-	sheetLen := len(xlsxFile.Sheets)
-	c.Assert(sheetLen, Equals, 3)
-	sheet = xlsxFile.Sheets[0]
-	rowLen := len(sheet.Rows)
-	c.Assert(rowLen, Equals, 2)
-	row = sheet.Rows[0]
-	c.Assert(len(row.Cells), Equals, 2)
-	cell := row.Cells[0]
-	cellstring := cell.String()
-	c.Assert(cellstring, Equals, "Foo")
-}
-
-func (l *LibSuite) TestGetNumberFormat(c *C) {
-	var cell *Cell
-	var cellXfs []xlsxXf
-	var numFmt xlsxNumFmt
-	var numFmts []xlsxNumFmt
-	var xStyles *xlsxStyles
-	var numFmtRefTable map[int]xlsxNumFmt
-
-	cellXfs = make([]xlsxXf, 1)
-	cellXfs[0] = xlsxXf{NumFmtId: 1}
-
-	numFmts = make([]xlsxNumFmt, 1)
-	numFmtRefTable = make(map[int]xlsxNumFmt)
-
-	xStyles = &xlsxStyles{NumFmts: numFmts, CellXfs: cellXfs}
-
-	cell = &Cell{Value: "123.123", numFmtRefTable: numFmtRefTable, styleIndex: 1, styles: xStyles}
-
-	numFmt = xlsxNumFmt{NumFmtId: 1, FormatCode: "dd/mm/yy"}
-	numFmts[0] = numFmt
-	numFmtRefTable[1] = numFmt
-	c.Assert(cell.GetNumberFormat(), Equals, "dd/mm/yy")
-}
-
-// We can return a string representation of the formatted data
-func (l *LibSuite) TestFormattedValue(c *C) {
-	var cell, earlyCell, negativeCell, smallCell *Cell
-	var cellXfs []xlsxXf
-	var numFmt xlsxNumFmt
-	var numFmts []xlsxNumFmt
-	var xStyles *xlsxStyles
-	var numFmtRefTable map[int]xlsxNumFmt
-
-	cellXfs = make([]xlsxXf, 1)
-	cellXfs[0] = xlsxXf{NumFmtId: 1}
-
-	numFmts = make([]xlsxNumFmt, 1)
-	numFmtRefTable = make(map[int]xlsxNumFmt)
-
-	xStyles = &xlsxStyles{NumFmts: numFmts, CellXfs: cellXfs}
-	cell = &Cell{Value: "37947.7500001", numFmtRefTable: numFmtRefTable, styleIndex: 1, styles: xStyles}
-	negativeCell = &Cell{Value: "-37947.7500001", numFmtRefTable: numFmtRefTable, styleIndex: 1, styles: xStyles}
-	smallCell = &Cell{Value: "0.007", numFmtRefTable: numFmtRefTable, styleIndex: 1, styles: xStyles}
-	earlyCell = &Cell{Value: "2.1", numFmtRefTable: numFmtRefTable, styleIndex: 1, styles: xStyles}
-	setCode := func(code string) {
-		numFmt = xlsxNumFmt{NumFmtId: 1, FormatCode: code}
-		numFmts[0] = numFmt
-		numFmtRefTable[1] = numFmt
-	}
-
-	setCode("general")
-	c.Assert(cell.FormattedValue(), Equals, "37947.7500001")
-	c.Assert(negativeCell.FormattedValue(), Equals, "-37947.7500001")
-
-	setCode("0")
-	c.Assert(cell.FormattedValue(), Equals, "37947")
-
-	setCode("#,##0") // For the time being we're not doing this
-	// comma formatting, so it'll fall back to
-	// the related non-comma form.
-	c.Assert(cell.FormattedValue(), Equals, "37947")
-
-	setCode("0.00")
-	c.Assert(cell.FormattedValue(), Equals, "37947.75")
-
-	setCode("#,##0.00") // For the time being we're not doing this
-	// comma formatting, so it'll fall back to
-	// the related non-comma form.
-	c.Assert(cell.FormattedValue(), Equals, "37947.75")
-
-	setCode("#,##0 ;(#,##0)")
-	c.Assert(cell.FormattedValue(), Equals, "37947")
-	c.Assert(negativeCell.FormattedValue(), Equals, "(37947)")
-
-	setCode("#,##0 ;[red](#,##0)")
-	c.Assert(cell.FormattedValue(), Equals, "37947")
-	c.Assert(negativeCell.FormattedValue(), Equals, "(37947)")
-
-	setCode("0%")
-	c.Assert(cell.FormattedValue(), Equals, "3794775%")
-
-	setCode("0.00%")
-	c.Assert(cell.FormattedValue(), Equals, "3794775.00%")
-
-	setCode("0.00e+00")
-	c.Assert(cell.FormattedValue(), Equals, "3.794775e+04")
-
-	setCode("##0.0e+0") // This is wrong, but we'll use it for now.
-	c.Assert(cell.FormattedValue(), Equals, "3.794775e+04")
-
-	setCode("mm-dd-yy")
-	c.Assert(cell.FormattedValue(), Equals, "11-22-03")
-
-	setCode("d-mmm-yy")
-	c.Assert(cell.FormattedValue(), Equals, "22-Nov-03")
-	c.Assert(earlyCell.FormattedValue(), Equals, "1-Jan-00")
-
-	setCode("d-mmm")
-	c.Assert(cell.FormattedValue(), Equals, "22-Nov")
-	c.Assert(earlyCell.FormattedValue(), Equals, "1-Jan")
-
-	setCode("mmm-yy")
-	c.Assert(cell.FormattedValue(), Equals, "Nov-03")
-
-	setCode("h:mm am/pm")
-	c.Assert(cell.FormattedValue(), Equals, "6:00 pm")
-	c.Assert(smallCell.FormattedValue(), Equals, "12:14 am")
-
-	setCode("h:mm:ss am/pm")
-	c.Assert(cell.FormattedValue(), Equals, "6:00:00 pm")
-	c.Assert(smallCell.FormattedValue(), Equals, "12:14:47 am")
-
-	setCode("h:mm")
-	c.Assert(cell.FormattedValue(), Equals, "18:00")
-	c.Assert(smallCell.FormattedValue(), Equals, "00:14")
-
-	setCode("h:mm:ss")
-	c.Assert(cell.FormattedValue(), Equals, "18:00:00")
-	// This is wrong, but there's no eary way aroud it in Go right now, AFAICT.
-	c.Assert(smallCell.FormattedValue(), Equals, "00:14:47")
-
-	setCode("m/d/yy h:mm")
-	c.Assert(cell.FormattedValue(), Equals, "11/22/03 18:00")
-	c.Assert(smallCell.FormattedValue(), Equals, "12/30/99 00:14") // Note, that's 1899
-	c.Assert(earlyCell.FormattedValue(), Equals, "1/1/00 02:24")   // and 1900
-
-	setCode("mm:ss")
-	c.Assert(cell.FormattedValue(), Equals, "00:00")
-	c.Assert(smallCell.FormattedValue(), Equals, "14:47")
-
-	setCode("[h]:mm:ss")
-	c.Assert(cell.FormattedValue(), Equals, "18:00:00")
-	c.Assert(smallCell.FormattedValue(), Equals, "14:47")
-
-	setCode("mmss.0") // I'm not sure about these.
-	c.Assert(cell.FormattedValue(), Equals, "00.8640")
-	c.Assert(smallCell.FormattedValue(), Equals, "1447.999997")
-
-	setCode("yyyy\\-mm\\-dd")
-	c.Assert(cell.FormattedValue(), Equals, "2003\\-11\\-22")
-
-	setCode("dd/mm/yy")
-	c.Assert(cell.FormattedValue(), Equals, "22/11/03")
-	c.Assert(earlyCell.FormattedValue(), Equals, "01/01/00")
-
-	setCode("hh:mm:ss")
-	c.Assert(cell.FormattedValue(), Equals, "18:00:00")
-	c.Assert(smallCell.FormattedValue(), Equals, "00:14:47")
-
-	setCode("dd/mm/yy\\ hh:mm")
-	c.Assert(cell.FormattedValue(), Equals, "22/11/03\\ 18:00")
-
-	setCode("yy-mm-dd")
-	c.Assert(cell.FormattedValue(), Equals, "03-11-22")
-
-	setCode("d-mmm-yyyy")
-	c.Assert(cell.FormattedValue(), Equals, "22-Nov-2003")
-	c.Assert(earlyCell.FormattedValue(), Equals, "1-Jan-1900")
-
-	setCode("m/d/yy")
-	c.Assert(cell.FormattedValue(), Equals, "11/22/03")
-	c.Assert(earlyCell.FormattedValue(), Equals, "1/1/00")
-
-	setCode("m/d/yyyy")
-	c.Assert(cell.FormattedValue(), Equals, "11/22/2003")
-	c.Assert(earlyCell.FormattedValue(), Equals, "1/1/1900")
-
-	setCode("dd-mmm-yyyy")
-	c.Assert(cell.FormattedValue(), Equals, "22-Nov-2003")
-
-	setCode("dd/mm/yyyy")
-	c.Assert(cell.FormattedValue(), Equals, "22/11/2003")
-
-	setCode("mm/dd/yy hh:mm am/pm")
-	c.Assert(cell.FormattedValue(), Equals, "11/22/03 06:00 pm")
-
-	setCode("mm/dd/yyyy hh:mm:ss")
-	c.Assert(cell.FormattedValue(), Equals, "11/22/2003 18:00:00")
-	c.Assert(smallCell.FormattedValue(), Equals, "12/30/1899 00:14:47")
-
-	setCode("yyyy-mm-dd hh:mm:ss")
-	c.Assert(cell.FormattedValue(), Equals, "2003-11-22 18:00:00")
-	c.Assert(smallCell.FormattedValue(), Equals, "1899-12-30 00:14:47")
-}
-
-// Test that GetStyle correctly converts the xlsxStyle.Fonts.
-func (l *LibSuite) TestGetStyleWithFonts(c *C) {
-	var cell *Cell
-	var style *Style
-	var xStyles *xlsxStyles
-	var fonts []xlsxFont
-	var cellXfs []xlsxXf
-
-	fonts = make([]xlsxFont, 1)
-	fonts[0] = xlsxFont{
-		Sz:   xlsxVal{Val: "10"},
-		Name: xlsxVal{Val: "Calibra"}}
-
-	cellXfs = make([]xlsxXf, 1)
-	cellXfs[0] = xlsxXf{ApplyFont: true, FontId: 0}
-
-	xStyles = &xlsxStyles{Fonts: fonts, CellXfs: cellXfs}
-
-	cell = &Cell{Value: "123", styleIndex: 0, styles: xStyles}
-	style = cell.GetStyle()
-	c.Assert(style, NotNil)
-	c.Assert(style.Font.Size, Equals, 10)
-	c.Assert(style.Font.Name, Equals, "Calibra")
-}
-
-// Test that GetStyle correctly converts the xlsxStyle.Fills.
-func (l *LibSuite) TestGetStyleWithFills(c *C) {
-	var cell *Cell
-	var style *Style
-	var xStyles *xlsxStyles
-	var fills []xlsxFill
-	var cellXfs []xlsxXf
-
-	fills = make([]xlsxFill, 1)
-	fills[0] = xlsxFill{
-		PatternFill: xlsxPatternFill{
-			PatternType: "solid",
-			FgColor:     xlsxColor{RGB: "FF000000"},
-			BgColor:     xlsxColor{RGB: "00FF0000"}}}
-	cellXfs = make([]xlsxXf, 1)
-	cellXfs[0] = xlsxXf{ApplyFill: true, FillId: 0}
-
-	xStyles = &xlsxStyles{Fills: fills, CellXfs: cellXfs}
-
-	cell = &Cell{Value: "123", styleIndex: 0, styles: xStyles}
-	style = cell.GetStyle()
-	fill := style.Fill
-	c.Assert(fill.PatternType, Equals, "solid")
-	c.Assert(fill.BgColor, Equals, "00FF0000")
-	c.Assert(fill.FgColor, Equals, "FF000000")
-}
-
-// Test that GetStyle correctly converts the xlsxStyle.Borders.
-func (l *LibSuite) TestGetStyleWithBorders(c *C) {
-	var cell *Cell
-	var style *Style
-	var xStyles *xlsxStyles
-	var borders []xlsxBorder
-	var cellXfs []xlsxXf
-
-	borders = make([]xlsxBorder, 1)
-	borders[0] = xlsxBorder{
-		Left:   xlsxLine{Style: "thin"},
-		Right:  xlsxLine{Style: "thin"},
-		Top:    xlsxLine{Style: "thin"},
-		Bottom: xlsxLine{Style: "thin"}}
-
-	cellXfs = make([]xlsxXf, 1)
-	cellXfs[0] = xlsxXf{ApplyBorder: true, BorderId: 0}
-
-	xStyles = &xlsxStyles{Borders: borders, CellXfs: cellXfs}
-
-	cell = &Cell{Value: "123", styleIndex: 0, styles: xStyles}
-	style = cell.GetStyle()
-	border := style.Border
-	c.Assert(border.Left, Equals, "thin")
-	c.Assert(border.Right, Equals, "thin")
-	c.Assert(border.Top, Equals, "thin")
-	c.Assert(border.Bottom, Equals, "thin")
-}
-
-// Test that we can correctly extract a reference table from the
-// sharedStrings.xml file embedded in the XLSX file and return a
-// reference table of string values from it.
-func (l *LibSuite) TestReadSharedStringsFromZipFile(c *C) {
-	var xlsxFile *File
-	var err error
-	xlsxFile, err = OpenFile("testfile.xlsx")
-	c.Assert(err, IsNil)
-	c.Assert(xlsxFile.referenceTable, NotNil)
-}
-
-// Helper function used to test contents of a given xlsxXf against
-// expectations.
-func testXf(c *C, result, expected *xlsxXf) {
-	c.Assert(result.ApplyAlignment, Equals, expected.ApplyAlignment)
-	c.Assert(result.ApplyBorder, Equals, expected.ApplyBorder)
-	c.Assert(result.ApplyFont, Equals, expected.ApplyFont)
-	c.Assert(result.ApplyFill, Equals, expected.ApplyFill)
-	c.Assert(result.ApplyProtection, Equals, expected.ApplyProtection)
-	c.Assert(result.BorderId, Equals, expected.BorderId)
-	c.Assert(result.FillId, Equals, expected.FillId)
-	c.Assert(result.FontId, Equals, expected.FontId)
-	c.Assert(result.NumFmtId, Equals, expected.NumFmtId)
-}
-
-// We can correctly extract a style table from the style.xml file
-// embedded in the XLSX file and return a styles struct from it.
-func (l *LibSuite) TestReadStylesFromZipFile(c *C) {
-	var xlsxFile *File
-	var err error
-	var fontCount, fillCount, borderCount, cellStyleXfCount, cellXfCount int
-	var font xlsxFont
-	var fill xlsxFill
-	var border xlsxBorder
-	var xf xlsxXf
-
-	xlsxFile, err = OpenFile("testfile.xlsx")
-	c.Assert(err, IsNil)
-	c.Assert(xlsxFile.styles, NotNil)
-
-	fontCount = len(xlsxFile.styles.Fonts)
-	c.Assert(fontCount, Equals, 4)
-
-	font = xlsxFile.styles.Fonts[0]
-	c.Assert(font.Sz.Val, Equals, "11")
-	c.Assert(font.Name.Val, Equals, "Calibri")
-
-	fillCount = len(xlsxFile.styles.Fills)
-	c.Assert(fillCount, Equals, 3)
-
-	fill = xlsxFile.styles.Fills[2]
-	c.Assert(fill.PatternFill.PatternType, Equals, "solid")
-
-	borderCount = len(xlsxFile.styles.Borders)
-	c.Assert(borderCount, Equals, 2)
-
-	border = xlsxFile.styles.Borders[1]
-	c.Assert(border.Left.Style, Equals, "thin")
-	c.Assert(border.Right.Style, Equals, "thin")
-	c.Assert(border.Top.Style, Equals, "thin")
-	c.Assert(border.Bottom.Style, Equals, "thin")
-
-	cellStyleXfCount = len(xlsxFile.styles.CellStyleXfs)
-	c.Assert(cellStyleXfCount, Equals, 20)
-
-	xf = xlsxFile.styles.CellStyleXfs[0]
-	expectedXf := &xlsxXf{
-		ApplyAlignment:  true,
-		ApplyBorder:     true,
-		ApplyFont:       true,
-		ApplyFill:       false,
-		ApplyProtection: true,
-		BorderId:        0,
-		FillId:          0,
-		FontId:          0,
-		NumFmtId:        164}
-	testXf(c, &xf, expectedXf)
-
-	cellXfCount = len(xlsxFile.styles.CellXfs)
-	c.Assert(cellXfCount, Equals, 3)
-
-	xf = xlsxFile.styles.CellXfs[0]
-	expectedXf = &xlsxXf{
-		ApplyAlignment:  false,
-		ApplyBorder:     false,
-		ApplyFont:       false,
-		ApplyFill:       false,
-		ApplyProtection: false,
-		BorderId:        0,
-		FillId:          0,
-		FontId:          0,
-		NumFmtId:        164}
-	testXf(c, &xf, expectedXf)
-}
-
-// We can correctly extract a map of relationship Ids to the worksheet files in
-// which they are contained from the XLSX file.
-func (l *LibSuite) TestReadWorkbookRelationsFromZipFile(c *C) {
-	var xlsxFile *File
-	var err error
-
-	xlsxFile, err = OpenFile("testfile.xlsx")
-	c.Assert(err, IsNil)
-	sheetCount := len(xlsxFile.Sheet)
-	c.Assert(sheetCount, Equals, 3)
-}
 
 // which they are contained from the XLSX file, even when the
 // worksheet files have arbitrary, non-numeric names.
@@ -446,43 +21,89 @@ func (l *LibSuite) TestReadWorkbookRelationsFromZipFileWithFunnyNames(c *C) {
 
 	xlsxFile, err = OpenFile("testrels.xlsx")
 	c.Assert(err, IsNil)
-	sheetCount := len(xlsxFile.Sheet)
-	c.Assert(sheetCount, Equals, 2)
-	bob := xlsxFile.Sheet["Bob"]
+	bob := xlsxFile.Sheets["Bob"]
 	row1 := bob.Rows[0]
 	cell1 := row1.Cells[0]
 	c.Assert(cell1.String(), Equals, "I am Bob")
 }
 
-func (l *LibSuite) TestGetStyleFromZipFile(c *C) {
-	var xlsxFile *File
-	var err error
 
-	xlsxFile, err = OpenFile("testfile.xlsx")
+// We can marshal WorkBookRels to an xml file
+func (l *LibSuite) TestWorkBookRelsMarshal(c *C) {
+	var rels WorkBookRels = make(WorkBookRels)
+	rels["rId1"] = "worksheets/sheet.xml"
+	expectedXML := `<?xml version="1.0" encoding="UTF-8"?>
+  <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
+    <Relationship Id="rId1" Target="worksheets/sheet.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"></Relationship>
+    <Relationship Id="rId2" Target="sharedStrings.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings"></Relationship>
+    <Relationship Id="rId3" Target="styles.xml" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"></Relationship>
+  </Relationships>`
+	xRels := rels.MakeXLSXWorkbookRels()
+
+	output := bytes.NewBufferString(xml.Header)
+	body, err := xml.MarshalIndent(xRels, "  ", "  ")
 	c.Assert(err, IsNil)
-	sheetCount := len(xlsxFile.Sheet)
-	c.Assert(sheetCount, Equals, 3)
+	c.Assert(body, NotNil)
+	_, err = output.Write(body)
+	c.Assert(err, IsNil)
+	c.Assert(output.String(), Equals, expectedXML)
+}
 
-	tabelle1 := xlsxFile.Sheet["Tabelle1"]
 
-	row0 := tabelle1.Rows[0]
-	cellFoo := row0.Cells[0]
-	c.Assert(cellFoo.String(), Equals, "Foo")
-	c.Assert(cellFoo.GetStyle().Fill.BgColor, Equals, "FF33CCCC")
+// Excel column codes are a special form of base26 that doesn't allow
+// zeros, except in the least significant part of the code.  Test we
+// can smoosh the numbers in a normal base26 representation (presented
+// as a slice of integers) down to this form.
+func (l *LibSuite) TestSmooshBase26Slice(c *C) {
+	input := []int{20, 0, 1}
+	expected := []int{19, 26, 1}
+	c.Assert(smooshBase26Slice(input), DeepEquals, expected)
+}
 
-	row1 := tabelle1.Rows[1]
-	cellQuuk := row1.Cells[1]
-	c.Assert(cellQuuk.String(), Equals, "Quuk")
-	c.Assert(cellQuuk.GetStyle().Border.Left, Equals, "thin")
+// formatColumnName converts slices of base26 integers to alphabetical
+// column names.  Note that the least signifcant character has a
+// different numeric offset (Yuck!)
+func (l *LibSuite) TestFormatColumnName(c *C) {
+	c.Assert(formatColumnName([]int{0}), Equals, "A")
+	c.Assert(formatColumnName([]int{25}), Equals, "Z")
+	c.Assert(formatColumnName([]int{1, 25}), Equals, "AZ")
+	c.Assert(formatColumnName([]int{26, 25}), Equals, "ZZ")
+	c.Assert(formatColumnName([]int{26, 26, 25}), Equals, "ZZZ")
+}
 
-	cellBar := row0.Cells[1]
-	c.Assert(cellBar.String(), Equals, "Bar")
-	c.Assert(cellBar.GetStyle().Fill.BgColor, Equals, "")
+
+// getLargestDenominator returns the largest power of a provided value
+// that can fit within a given value.
+func (l *LibSuite) TestGetLargestDenominator(c *C) {
+	d, p := getLargestDenominator(0, 1, 2, 0)
+	c.Assert(d, Equals, 1)
+	c.Assert(p, Equals, 0)
+	d, p = getLargestDenominator(1, 1, 2, 0)
+	c.Assert(d, Equals, 1)
+	c.Assert(p, Equals, 0)
+	d, p = getLargestDenominator(2, 1, 2, 0)
+	c.Assert(d, Equals, 2)
+	c.Assert(p, Equals, 1)
+	d, p = getLargestDenominator(4, 1, 2, 0)
+	c.Assert(d, Equals, 4)
+	c.Assert(p, Equals, 2)
+	d, p = getLargestDenominator(8, 1, 2, 0)
+	c.Assert(d, Equals, 8)
+	c.Assert(p, Equals, 3)
+	d, p = getLargestDenominator(9, 1, 2, 0)
+	c.Assert(d, Equals, 8)
+	c.Assert(p, Equals, 3)
+	d, p = getLargestDenominator(15,1, 2, 0)
+	c.Assert(d, Equals, 8)
+	c.Assert(p, Equals, 3)
+	d, p = getLargestDenominator(16,1, 2, 0)
+	c.Assert(d, Equals, 16)
+	c.Assert(p, Equals, 4)
 }
 
 func (l *LibSuite) TestLettersToNumeric(c *C) {
 	cases := map[string]int{"A": 0, "G": 6, "z": 25, "AA": 26, "Az": 51,
-		"BA": 52, "Bz": 77, "ZA": 26*26 + 0, "ZZ": 26*26 + 25,
+		"BA": 52, "BZ": 77, "ZA": 26*26 + 0, "ZZ": 26*26 + 25,
 		"AAA": 26*26 + 26 + 0, "AMI": 1022}
 	for input, ans := range cases {
 		output := lettersToNumeric(input)
@@ -490,6 +111,24 @@ func (l *LibSuite) TestLettersToNumeric(c *C) {
 	}
 }
 
+func (l *LibSuite) TestNumericToLetters(c *C) {
+	cases := map[string]int{
+		"A": 0,
+		"G": 6,
+		"Z": 25,
+		"AA": 26,
+		"AZ": 51,
+		"BA": 52,
+		"BZ": 77, "ZA": 26*26, "ZB": 26*26 + 1,
+		"ZZ": 26*26 + 25,
+		"AAA": 26*26 + 26 + 0, "AMI": 1022}
+	for ans, input := range cases {
+		output := numericToLetters(input)
+		c.Assert(output, Equals, ans)
+	}
+
+}
+
 func (l *LibSuite) TestLetterOnlyMapFunction(c *C) {
 	var input string = "ABC123"
 	var output string = strings.Map(letterOnlyMapF, input)
@@ -515,6 +154,11 @@ func (l *LibSuite) TestGetCoordsFromCellIDString(c *C) {
 	c.Assert(y, Equals, 2)
 }
 
+func (l *LibSuite) TestGetCellIDStringFromCoords(c *C){
+	c.Assert(getCellIDStringFromCoords(0, 0), Equals, "A1")
+	c.Assert(getCellIDStringFromCoords(2, 2), Equals, "C3")
+}
+
 func (l *LibSuite) TestGetMaxMinFromDimensionRef(c *C) {
 	var dimensionRef string = "A1:B2"
 	var minx, miny, maxx, maxy int

+ 1 - 1
macExcel_test.go

@@ -16,6 +16,6 @@ func (m *MacExcelSuite) TestMacExcel(c *C) {
 	xlsxFile, err := OpenFile("macExcelTest.xlsx")
 	c.Assert(err, IsNil)
 	c.Assert(xlsxFile, NotNil)
-	s := xlsxFile.Sheets[0].Cell(0, 0).String()
+	s := xlsxFile.Sheets["普通技能"].Cell(0, 0).String()
 	c.Assert(s, Equals, "编号")
 }

+ 3 - 1
macNumbers_test.go

@@ -14,6 +14,8 @@ func (m *MacNumbersSuite) TestMacNumbers(c *C) {
 	xlsxFile, err := OpenFile("macNumbersTest.xlsx")
 	c.Assert(err, IsNil)
 	c.Assert(xlsxFile, NotNil)
-	s := xlsxFile.Sheets[0].Cell(0, 0).String()
+	sheet, ok := xlsxFile.Sheets["主动技能"]
+	c.Assert(ok, Equals, true)
+	s := sheet.Cell(0, 0).String()
 	c.Assert(s, Equals, "编号")
 }

+ 11 - 0
row.go

@@ -0,0 +1,11 @@
+package xlsx
+
+type Row struct {
+	Cells []*Cell
+}
+
+func (r *Row) AddCell() *Cell {
+	cell := &Cell{}
+	r.Cells = append(r.Cells, cell)
+	return cell
+}

+ 20 - 0
row_test.go

@@ -0,0 +1,20 @@
+package xlsx
+
+import (
+	. "gopkg.in/check.v1"
+)
+
+type RowSuite struct {}
+
+var _ = Suite(&RowSuite{})
+
+// Test we can add a new Cell to a Row
+func (r *RowSuite) TestAddCell(c *C){
+	var f *File
+	f = NewFile()
+	sheet := f.AddSheet("MySheet")
+	row := sheet.AddRow()
+	cell := row.AddCell()
+	c.Assert(cell, NotNil)
+	c.Assert(len(row.Cells), Equals, 1)
+}

+ 63 - 17
sharedstrings.go

@@ -1,11 +1,17 @@
 package xlsx
 
+import (
+	"encoding/xml"
+)
+
+
 // xlsxSST directly maps the sst element from the namespace
 // http://schemas.openxmlformats.org/spreadsheetml/2006/main currently
 // I have not checked this for completeness - it does as much as I need.
 type xlsxSST struct {
-	Count       string   `xml:"count,attr"`
-	UniqueCount string   `xml:"uniqueCount,attr"`
+	XMLName     xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main sst"`
+	Count       int   `xml:"count,attr"`
+	UniqueCount int   `xml:"uniqueCount,attr"`
 	SI          []xlsxSI `xml:"si"`
 }
 
@@ -26,36 +32,76 @@ type xlsxR struct {
 	T string `xml:"t"`
 }
 
-// // xlsxT directly maps the t element from the namespace
-// // http://schemas.openxmlformats.org/spreadsheetml/2006/main -
-// // currently I have not checked this for completeness - it does as
-// // much as I need.
-// type xlsxT struct {
-// 	Data string `xml:"chardata"`
-// }
+
+type RefTable struct {
+	indexedStrings []string
+	knownStrings map[string]int
+}
+
+// NewSharedStringRefTable() creates a new, empty RefTable.
+func NewSharedStringRefTable() *RefTable {
+	rt := RefTable{}
+	rt.knownStrings = make(map[string]int)
+	return &rt
+}
 
 // MakeSharedStringRefTable() takes an xlsxSST struct and converts
 // it's contents to an slice of strings used to refer to string values
 // by numeric index - this is the model used within XLSX worksheet (a
 // numeric reference is stored to a shared cell value).
-func MakeSharedStringRefTable(source *xlsxSST) []string {
-	reftable := make([]string, len(source.SI))
-	for i, si := range source.SI {
+func MakeSharedStringRefTable(source *xlsxSST) *RefTable {
+	reftable := NewSharedStringRefTable()
+	for _, si := range source.SI {
 		if len(si.R) > 0 {
+			newString := ""
 			for j := 0; j < len(si.R); j++ {
-				reftable[i] = reftable[i] + si.R[j].T
+				newString = newString + si.R[j].T
 			}
+			reftable.AddString(newString)
 		} else {
-			reftable[i] = si.T
+			reftable.AddString(si.T)
 		}
 	}
 	return reftable
 }
 
-// ResolveSharedString() looks up a string value by numeric index from
+// makeXlsxSST() takes a RefTable and returns and
+// equivalent xlsxSST representation.
+func (rt *RefTable) makeXLSXSST() xlsxSST {
+	sst := xlsxSST{}
+	sst.Count = len(rt.indexedStrings)
+	sst.UniqueCount = sst.Count
+	for _, ref := range rt.indexedStrings {
+		si := xlsxSI{}
+		si.T = ref
+		sst.SI = append(sst.SI, si)
+	}
+	return sst
+}
+
+// Resolvesharedstring() looks up a string value by numeric index from
 // a provided reference table (just a slice of strings in the correct
 // order).  This function only exists to provide clarity or purpose
 // via it's name.
-func ResolveSharedString(reftable []string, index int) string {
-	return reftable[index]
+func (rt *RefTable) ResolveSharedString(index int) string {
+	return rt.indexedStrings[index]
+}
+
+
+// AddString adds a string to the reference table and return it's
+// numeric index.  If the string already exists then it simply returns
+// the existing index.
+func (rt *RefTable) AddString(str string) int {
+	index, ok := rt.knownStrings[str]
+	if ok {
+		return index
+	}
+	rt.indexedStrings = append(rt.indexedStrings, str)
+	index = len(rt.indexedStrings) - 1
+	rt.knownStrings[str] = index
+	return index
+}
+
+func (rt *RefTable) Length() int {
+	return len(rt.indexedStrings)
 }

+ 61 - 7
sharedstrings_test.go

@@ -33,6 +33,22 @@ func (s *SharedStringsSuite) SetUpTest(c *C) {
         </sst>`)
 }
 
+// We can add a new string to the RefTable
+func (s *SharedStringsSuite) TestRefTableAddString(c *C) {
+	refTable := NewSharedStringRefTable()
+	index := refTable.AddString("Foo")
+	c.Assert(index, Equals, 0)
+	c.Assert(refTable.ResolveSharedString(0), Equals, "Foo")
+}
+
+func (s *SharedStringsSuite) TestCreateNewSharedStringRefTable(c *C) {
+	refTable := NewSharedStringRefTable()
+	refTable.AddString("Foo")
+	refTable.AddString("Bar")
+	c.Assert(refTable.ResolveSharedString(0), Equals, "Foo")
+	c.Assert(refTable.ResolveSharedString(1), Equals, "Bar")
+}
+
 // Test we can correctly convert a xlsxSST into a reference table
 // using xlsx.MakeSharedStringRefTable().
 func (s *SharedStringsSuite) TestMakeSharedStringRefTable(c *C) {
@@ -40,18 +56,19 @@ func (s *SharedStringsSuite) TestMakeSharedStringRefTable(c *C) {
 	err := xml.NewDecoder(s.SharedStringsXML).Decode(sst)
 	c.Assert(err, IsNil)
 	reftable := MakeSharedStringRefTable(sst)
-	c.Assert(len(reftable), Equals, 4)
-	c.Assert(reftable[0], Equals, "Foo")
-	c.Assert(reftable[1], Equals, "Bar")
+	c.Assert(reftable.Length(), Equals, 4)
+	c.Assert(reftable.ResolveSharedString(0), Equals, "Foo")
+	c.Assert(reftable.ResolveSharedString(1), Equals, "Bar")
 }
 
-// Test we can correctly resolve a numeric reference in the reference table to a string value using xlsx.ResolveSharedString().
+// Test we can correctly resolve a numeric reference in the reference
+// table to a string value using RefTable.ResolveSharedString().
 func (s *SharedStringsSuite) TestResolveSharedString(c *C) {
 	sst := new(xlsxSST)
 	err := xml.NewDecoder(s.SharedStringsXML).Decode(sst)
 	c.Assert(err, IsNil)
 	reftable := MakeSharedStringRefTable(sst)
-	c.Assert(ResolveSharedString(reftable, 0), Equals, "Foo")
+	c.Assert(reftable.ResolveSharedString(0), Equals, "Foo")
 }
 
 // Test we can correctly unmarshal an the sharedstrings.xml file into
@@ -60,9 +77,46 @@ func (s *SharedStringsSuite) TestUnmarshallSharedStrings(c *C) {
 	sst := new(xlsxSST)
 	err := xml.NewDecoder(s.SharedStringsXML).Decode(sst)
 	c.Assert(err, IsNil)
-	c.Assert(sst.Count, Equals, "4")
-	c.Assert(sst.UniqueCount, Equals, "4")
+	c.Assert(sst.Count, Equals, 4)
+	c.Assert(sst.UniqueCount, Equals, 4)
 	c.Assert(sst.SI, HasLen, 4)
 	si := sst.SI[0]
 	c.Assert(si.T, Equals, "Foo")
 }
+
+// Test we can correctly create the xlsx.xlsxSST struct from a RefTable
+func (s *SharedStringsSuite) TestMakeXLSXSST(c *C) {
+	refTable := NewSharedStringRefTable()
+	refTable.AddString("Foo")
+	refTable.AddString("Bar")
+	sst := refTable.makeXLSXSST()
+	c.Assert(sst, NotNil)
+	c.Assert(sst.Count, Equals, 2)
+	c.Assert(sst.UniqueCount, Equals, 2)
+	c.Assert(sst.SI, HasLen, 2)
+	si := sst.SI[0]
+	c.Assert(si.T, Equals, "Foo")
+}
+
+
+func (s *SharedStringsSuite) TestMarshalSST(c *C) {
+	refTable := NewSharedStringRefTable()
+	refTable.AddString("Foo")
+	sst := refTable.makeXLSXSST()
+
+	output := bytes.NewBufferString(xml.Header)
+	body, err := xml.MarshalIndent(sst, "  ", "  ")
+	c.Assert(err, IsNil)
+	c.Assert(body, NotNil)
+	_, err = output.Write(body)
+	c.Assert(err, IsNil)
+
+	expectedXLSXSST := `<?xml version="1.0" encoding="UTF-8"?>
+  <sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="1" uniqueCount="1">
+    <si>
+      <t>Foo</t>
+    </si>
+  </sst>`
+	c.Assert(output.String(), Equals, expectedXLSXSST)
+}
+

+ 57 - 0
sheet.go

@@ -0,0 +1,57 @@
+package xlsx
+
+import (
+	"fmt"
+	"strconv"
+)
+
+// Sheet is a high level structure intended to provide user access to
+// the contents of a particular sheet within an XLSX file.
+type Sheet struct {
+	Name	string
+	Rows   []*Row
+	MaxRow int
+	MaxCol int
+}
+
+// Add a new Row to a Sheet
+func (s *Sheet) AddRow() *Row {
+	row := &Row{}
+	s.Rows = append(s.Rows, row)
+	if len(s.Rows) > s.MaxRow {
+		s.MaxRow = len(s.Rows)
+	}
+	return row
+}
+
+// Dump sheet to it's XML representation
+func (s *Sheet) makeXLSXSheet(refTable *RefTable) *xlsxWorksheet {
+	worksheet := &xlsxWorksheet{}
+	xSheet := xlsxSheetData{}
+	maxRow := 0
+	maxCell := 0
+	for r, row := range s.Rows {
+		if r > maxRow {
+			maxRow = r
+		}
+		xRow := xlsxRow{}
+		xRow.R = r + 1
+		for c, cell := range row.Cells {
+			if c > maxCell {
+				maxCell = c
+			}
+			xC := xlsxC{}
+			xC.R = fmt.Sprintf("%s%d", numericToLetters(c), r + 1)
+			xC.V = strconv.Itoa(refTable.AddString(cell.Value))
+			xC.T = "s" // Hardcode string type, for now.
+			xRow.C = append(xRow.C, xC)
+		}
+		xSheet.Row = append(xSheet.Row, xRow)
+	}
+	worksheet.SheetData = xSheet
+	dimension := xlsxDimension{}
+	dimension.Ref = fmt.Sprintf("A1:%s%d",
+		numericToLetters(maxCell), maxRow + 1)
+	worksheet.Dimension = dimension
+	return worksheet
+}

+ 79 - 0
sheet_test.go

@@ -0,0 +1,79 @@
+package xlsx
+
+import (
+	"bytes"
+	"encoding/xml"
+	. "gopkg.in/check.v1"
+)
+
+type SheetSuite struct{}
+
+var _ = Suite(&SheetSuite{})
+
+// Test we can add a Row to a Sheet
+func (s *SheetSuite) TestAddRow(c *C) {
+	var f *File
+	f = NewFile()
+	sheet := f.AddSheet("MySheet")
+	row := sheet.AddRow()
+	c.Assert(row, NotNil)
+	c.Assert(len(sheet.Rows), Equals, 1)
+}
+
+func (s *SheetSuite) TestMakeXLSXSheetFromRows(c *C) {
+	file := NewFile()
+	sheet := file.AddSheet("Sheet1")
+	row := sheet.AddRow()
+	cell := row.AddCell()
+	cell.Value = "A cell!"
+	refTable := NewSharedStringRefTable()
+	xSheet:= sheet.makeXLSXSheet(refTable)
+	c.Assert(xSheet.Dimension.Ref, Equals, "A1:A1")
+	c.Assert(xSheet.SheetData.Row, HasLen, 1)
+	xRow := xSheet.SheetData.Row[0]
+	c.Assert(xRow.R, Equals, 1)
+	c.Assert(xRow.Spans, Equals, "")
+	c.Assert(xRow.C, HasLen, 1)
+	xC := xRow.C[0]
+	c.Assert(xC.R, Equals, "A1")
+	c.Assert(xC.S, Equals, 0)
+	c.Assert(xC.T, Equals, "s") // Shared string type
+	c.Assert(xC.V, Equals, "0") // reference to shared string
+	xSST := refTable.makeXLSXSST()
+	c.Assert(xSST.Count, Equals, 1)
+	c.Assert(xSST.UniqueCount, Equals, 1)
+	c.Assert(xSST.SI, HasLen, 1)
+	xSI := xSST.SI[0]
+	c.Assert(xSI.T, Equals, "A cell!")
+}
+
+
+func (s *SheetSuite) TestMarshalSheet(c *C) {
+	file := NewFile()
+	sheet := file.AddSheet("Sheet1")
+	row := sheet.AddRow()
+	cell := row.AddCell()
+	cell.Value = "A cell!"
+	refTable := NewSharedStringRefTable()
+	xSheet:= sheet.makeXLSXSheet(refTable)
+
+	output := bytes.NewBufferString(xml.Header)
+	body, err := xml.MarshalIndent(xSheet, "  ", "  ")
+	c.Assert(err, IsNil)
+	c.Assert(body, NotNil)
+	_, err = output.Write(body)
+	c.Assert(err, IsNil)
+	expectedXLSXSheet := `<?xml version="1.0" encoding="UTF-8"?>
+  <worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+    <dimension ref="A1:A1"></dimension>
+    <sheetData>
+      <row r="1">
+        <c r="A1" t="s">
+          <v>0</v>
+        </c>
+      </row>
+    </sheetData>
+  </worksheet>`
+	c.Assert(output.String(), Equals, expectedXLSXSheet)
+}
+

+ 2 - 1
style.go

@@ -7,12 +7,13 @@
 
 package xlsx
 
+
 // xlsxStyle directly maps the style 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 xlsxStyles struct {
-	Fonts        []xlsxFont   `xml:"fonts>font"`
+	Fonts        []xlsxFont   `xml:"fonts>font,"`
 	Fills        []xlsxFill   `xml:"fills>fill"`
 	Borders      []xlsxBorder `xml:"borders>border"`
 	CellStyleXfs []xlsxXf     `xml:"cellStyleXfs>xf"`

BIN=BIN
testfile.xlsx


+ 28 - 16
workbook.go

@@ -10,6 +10,7 @@ import (
 // xmlxWorkbookRels contains xmlxWorkbookRelations
 // which maps sheet id and sheet XML
 type xlsxWorkbookRels struct {
+	XMLName		xml.Name	`xml:"http://schemas.openxmlformats.org/package/2006/relationships Relationships"`
 	Relationships []xlsxWorkbookRelation `xml:"Relationship"`
 }
 
@@ -25,6 +26,7 @@ type xlsxWorkbookRelation struct {
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxWorkbook struct {
+	XMLName		xml.Name	`xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main workbook"`
 	FileVersion  xlsxFileVersion  `xml:"fileVersion"`
 	WorkbookPr   xlsxWorkbookPr   `xml:"workbookPr"`
 	BookViews    xlsxBookViews    `xml:"bookViews"`
@@ -38,10 +40,10 @@ type xlsxWorkbook struct {
 // - currently I have not checked it for completeness - it does as
 // much as I need.
 type xlsxFileVersion struct {
-	AppName      string `xml:"appName,attr"`
-	LastEdited   string `xml:"lastEdited,attr"`
-	LowestEdited string `xml:"lowestEdited,attr"`
-	RupBuild     string `xml:"rupBuild,attr"`
+	AppName      string `xml:"appName,attr,omitempty"`
+	LastEdited   string `xml:"lastEdited,attr,omitempty"`
+	LowestEdited string `xml:"lowestEdited,attr,omitempty"`
+	RupBuild     string `xml:"rupBuild,attr,omitempty"`
 }
 
 // xlsxWorkbookPr directly maps the workbookPr element from the
@@ -49,10 +51,10 @@ type xlsxFileVersion struct {
 // - currently I have not checked it for completeness - it does as
 // 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"`
+	DefaultThemeVersion string `xml:"defaultThemeVersion,attr,omitempty"`
+	BackupFile	bool `xml:"backupFile,attr,omitempty"`
+	ShowObjects	string `xml:"showObjects,attr,omitempty"`
+	Date1904	bool `xml:"date1904,attr"`
 }
 
 // xlsxBookViews directly maps the bookViews element from the
@@ -68,10 +70,16 @@ type xlsxBookViews struct {
 // - currently I have not checked it for completeness - it does as
 // much as I need.
 type xlsxWorkBookView struct {
-	XWindow      string `xml:"xWindow,attr"`
-	YWindow      string `xml:"yWindow,attr"`
-	WindowWidth  string `xml:"windowWidth,attr"`
-	WindowHeight string `xml:"windowHeight,attr"`
+	ActiveTab	int `xml:"activeTab,attr,omitempty"`
+	FirstSheet	int `xml:"firstSheet,attr,omitempty"`
+	ShowHorizontalScroll bool `xml:"showHorizontalScroll,attr,omitempty"`
+	ShowVerticalScroll bool `xml:"showVerticalScroll,attr,omitempty"`
+	ShowSheetTabs bool `xml:"showSheetTabs,attr,omitempty"`
+	TabRatio	int `xml:"tabRatio,attr,omitempty"`
+	WindowHeight	int `xml:"windowHeight,attr,omitempty"`
+	WindowWidth	int `xml:"windowWidth,attr,omitempty"`
+	XWindow      string `xml:"xWindow,attr,omitempty"`
+	YWindow      string `xml:"yWindow,attr,omitempty"`
 }
 
 // xlsxSheets directly maps the sheets element from the namespace
@@ -87,9 +95,9 @@ type xlsxSheets struct {
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxSheet struct {
-	Name    string `xml:"name,attr"`
-	SheetId string `xml:"sheetId,attr"`
-	Id      string `xml:"id,attr"`
+	Name    string `xml:"name,attr,omitempty"`
+	SheetId string `xml:"sheetId,attr,omitempty"`
+	Id      string `xml:"http://schemas.openxmlformats.org/officeDocument/2006/relationships id,attr,omitempty"`
 }
 
 // xlsxDefinedNames directly maps the definedNames element from the
@@ -115,7 +123,11 @@ type xlsxDefinedName struct {
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxCalcPr struct {
-	CalcId string `xml:"calcId,attr"`
+	CalcId string `xml:"calcId,attr,omitempty"`
+	IterateCount int `xml:"iterateCount,attr,omitempty"`
+	RefMode string `xml:"refMode,attr,omitempty"`
+	Iterate bool `xml:"iterate,attr,omitempty"`
+	IterateDelta float64 `xml:"iterateDelta,attr,omitempty"`
 }
 
 // getWorksheetFromSheet() is an internal helper function to open a

+ 35 - 3
workbook_test.go

@@ -62,9 +62,9 @@ func (w *WorkbookSuite) TestUnmarshallWorkbookXML(c *C) {
 	workBookView := workbook.BookViews.WorkBookView[0]
 	c.Assert(workBookView.XWindow, Equals, "120")
 	c.Assert(workBookView.YWindow, Equals, "75")
-	c.Assert(workBookView.WindowWidth, Equals, "15135")
-	c.Assert(workBookView.WindowHeight, Equals, "7620")
-	c.Assert(workbook.Sheets.Sheet, HasLen, 3)
+	c.Assert(workBookView.WindowWidth, Equals, 15135)
+	c.Assert(workBookView.WindowHeight, Equals, 7620)
+	c.Assert(workbook.Sheets.Sheet, HasLen,  3)
 	sheet := workbook.Sheets.Sheet[0]
 	c.Assert(sheet.Id, Equals, "rId1")
 	c.Assert(sheet.Name, Equals, "Sheet1")
@@ -76,3 +76,35 @@ func (w *WorkbookSuite) TestUnmarshallWorkbookXML(c *C) {
 	c.Assert(dname.Name, Equals, "monitors")
 	c.Assert(workbook.CalcPr.CalcId, Equals, "125725")
 }
+
+
+// Test we can marshall a Workbook to xml
+func (w *WorkbookSuite) TestMarshallWorkbook(c *C) {
+	var workbook *xlsxWorkbook
+	workbook = new(xlsxWorkbook)
+	workbook.FileVersion = xlsxFileVersion{}
+	workbook.FileVersion.AppName = "xlsx"
+	workbook.WorkbookPr = xlsxWorkbookPr{BackupFile: false}
+	workbook.BookViews = xlsxBookViews{}
+	workbook.BookViews.WorkBookView = make([]xlsxWorkBookView, 1)
+	workbook.BookViews.WorkBookView[0] = xlsxWorkBookView{}
+	workbook.Sheets = xlsxSheets{}
+	workbook.Sheets.Sheet = make([]xlsxSheet, 1)
+	workbook.Sheets.Sheet[0] = xlsxSheet{Name: "sheet1", SheetId: "1", Id: "rId2"}
+
+	body, err := xml.MarshalIndent(workbook, "  ", "  ")
+	c.Assert(err, IsNil)
+	expectedWorkbook := `  <workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
+    <fileVersion appName="xlsx"></fileVersion>
+    <workbookPr date1904="false"></workbookPr>
+    <bookViews>
+      <workbookView></workbookView>
+    </bookViews>
+    <sheets>
+      <sheet name="sheet1" sheetId="1" xmlns:relationships="http://schemas.openxmlformats.org/officeDocument/2006/relationships" relationships:id="rId2"></sheet>
+    </sheets>
+    <definedNames></definedNames>
+    <calcPr></calcPr>
+  </workbook>`
+	c.Assert(string(body), Equals, expectedWorkbook)
+}

+ 12 - 6
worksheet.go

@@ -1,10 +1,15 @@
 package xlsx
 
+import (
+	"encoding/xml"
+)
+
 // xlsxWorksheet directly maps the worksheet 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 xlsxWorksheet struct {
+	XMLName xml.Name `xml:"http://schemas.openxmlformats.org/spreadsheetml/2006/main worksheet"`
 	Dimension xlsxDimension `xml:"dimension"`
 	SheetData xlsxSheetData `xml:"sheetData"`
 }
@@ -22,6 +27,7 @@ type xlsxDimension struct {
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxSheetData struct {
+	XMLName xml.Name `xml:"sheetData"`
 	Row []xlsxRow `xml:"row"`
 }
 
@@ -31,19 +37,19 @@ type xlsxSheetData struct {
 // as I need.
 type xlsxRow struct {
 	R     int     `xml:"r,attr"`
-	Spans string  `xml:"spans,attr"`
+	Spans string  `xml:"spans,attr,omitempty"`
 	C     []xlsxC `xml:"c"`
 }
 
 // xlsxC directly maps the c element in the namespace
-// http://schemas.openxmlformats.org/spreadsheetml/2006/main -
+// http://schemas.openxmlformats.org/sprceadsheetml/2006/main -
 // currently I have not checked it for completeness - it does as much
 // as I need.
 type xlsxC struct {
-	R string `xml:"r,attr"`
-	S int    `xml:"s,attr"`
-	T string `xml:"t,attr"`
-	V string `xml:"v"`
+	R string `xml:"r,attr"`  // Cell ID, e.g. A1
+	S int    `xml:"s,attr,omitempty"`  // Style reference.
+	T string `xml:"t,attr"`  // Type.
+	V string  `xml:"v"`       // Value
 }
 
 // get cell

+ 1 - 1
wpsBlankLine_test.go

@@ -14,7 +14,7 @@ func (w *WpsBlankLineSuite) TestWpsBlankLine(c *C) {
 	xlsxFile, err := OpenFile("wpsBlankLineTest.xlsx")
 	c.Assert(err, IsNil)
 	c.Assert(xlsxFile, NotNil)
-	sheet := xlsxFile.Sheets[0]
+	sheet := xlsxFile.Sheets["Sheet1"]
 	row := sheet.Rows[0]
 	cell := row.Cells[0]
 	s := cell.String()