Browse Source

Merge pull request #6 from ryho/ryanh/improve_cell_formatting

Improve Cell Fromatting
Ryan Hollis 8 years ago
parent
commit
8b1b09ee33
10 changed files with 942 additions and 276 deletions
  1. 27 182
      cell.go
  2. 23 29
      cell_test.go
  3. 1 0
      col.go
  4. 561 0
      format_code.go
  5. 242 0
      format_code_test.go
  6. 2 2
      lib.go
  7. 1 1
      sheet.go
  8. 6 7
      sheet_test.go
  9. 40 37
      stream_test.go
  10. 39 18
      xmlStyle.go

+ 27 - 182
cell.go

@@ -1,10 +1,10 @@
 package xlsx
 
 import (
+	"errors"
 	"fmt"
 	"math"
 	"strconv"
-	"strings"
 	"time"
 )
 
@@ -35,16 +35,17 @@ func (ct CellType) Ptr() *CellType {
 // Cell is a high level structure intended to provide user access to
 // the contents of Cell within an xlsx.Row.
 type Cell struct {
-	Row      *Row
-	Value    string
-	formula  string
-	style    *Style
-	NumFmt   string
-	date1904 bool
-	Hidden   bool
-	HMerge   int
-	VMerge   int
-	cellType CellType
+	Row          *Row
+	Value        string
+	formula      string
+	style        *Style
+	NumFmt       string
+	parsedNumFmt *parsedNumberFormat
+	date1904     bool
+	Hidden       bool
+	HMerge       int
+	VMerge       int
+	cellType     CellType
 }
 
 // CellInterface defines the public API of the Cell.
@@ -55,7 +56,7 @@ type CellInterface interface {
 
 // NewCell creates a cell and adds it to a row.
 func NewCell(r *Row) *Cell {
-	return &Cell{Row: r}
+	return &Cell{Row: r, NumFmt: "general"}
 }
 
 // Merge with other cells, horizontally and/or vertically.
@@ -210,39 +211,14 @@ func (c *Cell) Int64() (int64, error) {
 // to display values when the storage type is Number and the format type is General. It is not 100% identical to the
 // spec but is as close as you can get using the built in Go formatting tools.
 func (c *Cell) GeneralNumeric() (string, error) {
-	return c.generalNumericScientific(true)
+	return generalNumericScientific(c.Value, true)
 }
 
 // GeneralNumericWithoutScientific returns numbers that are always formatted as numbers, but it does not follow
 // the rules for when XLSX should switch to scientific notation, since sometimes scientific notation is not desired,
 // even if that is how the document is supposed to be formatted.
 func (c *Cell) GeneralNumericWithoutScientific() (string, error) {
-	return c.generalNumericScientific(false)
-}
-
-func (c *Cell) generalNumericScientific(allowScientific bool) (string, error) {
-	if strings.TrimSpace(c.Value) == "" {
-		return "", nil
-	}
-	f, err := strconv.ParseFloat(c.Value, 64)
-	if err != nil {
-		return c.Value, err
-	}
-	if allowScientific {
-		absF := math.Abs(f)
-		// When using General format, numbers that are less than 1e-9 (0.000000001) and greater than or equal to
-		// 1e11 (100,000,000,000) should be shown in scientific notation.
-		// Numbers less than the number after zero, are assumed to be zero.
-		if (absF >= math.SmallestNonzeroFloat64 && absF < minNonScientificNumber) || absF >= maxNonScientificNumber {
-			return strconv.FormatFloat(f, 'E', -1, 64), nil
-		}
-	}
-	// This format (fmt="f", prec=-1) will prevent padding with zeros and will never switch to scientific notation.
-	// However, it will show more than 11 characters for very precise numbers, and this cannot be changed.
-	// You could also use fmt="g", prec=11, which doesn't pad with zeros and allows the correct precision,
-	// but it will use scientific notation on numbers less than 1e-4. That value is hardcoded in Go and cannot be
-	// configured or disabled.
-	return strconv.FormatFloat(f, 'f', -1, 64), nil
+	return generalNumericScientific(c.Value, false)
 }
 
 // SetInt sets a cell's value to an integer.
@@ -359,6 +335,13 @@ func (c *Cell) formatToInt(format string) (string, error) {
 	return fmt.Sprintf(format, int(f)), nil
 }
 
+func (c *Cell) getNumberFormat() *parsedNumberFormat {
+	if c.parsedNumFmt == nil || c.parsedNumFmt.numFmt != c.NumFmt {
+		c.parsedNumFmt = parseFullNumberFormatString(c.NumFmt)
+	}
+	return c.parsedNumFmt
+}
+
 // FormattedValue returns a value, and possibly an error condition
 // from a Cell.  If it is possible to apply a format to the cell
 // value, it will do so, if not then an error will be returned, along
@@ -395,148 +378,10 @@ func (c *Cell) formatToInt(format string) (string, error) {
 // does not support adjusting the precision while not padding with zeros, while also not switching to scientific
 // notation too early.
 func (c *Cell) FormattedValue() (string, error) {
-	var numberFormat = c.GetNumberFormat()
-	if isTimeFormat(numberFormat) {
-		return parseTime(c)
-	}
-	switch numberFormat {
-	case builtInNumFmt[builtInNumFmtIndex_GENERAL]:
-		if c.cellType == CellTypeNumeric {
-			// If the cell type is Numeric, format the string the way it should be shown to the user.
-			val, err := c.GeneralNumeric()
-			if err != nil {
-				return c.Value, nil
-			}
-			return val, nil
-		}
-		return c.Value, nil
-	case builtInNumFmt[builtInNumFmtIndex_STRING]:
-		return c.Value, nil
-	case builtInNumFmt[builtInNumFmtIndex_INT], "#,##0":
-		return c.formatToInt("%d")
-	case builtInNumFmt[builtInNumFmtIndex_FLOAT], "#,##0.00":
-		return c.formatToFloat("%.2f")
-	case "#,##0 ;(#,##0)", "#,##0 ;[red](#,##0)":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return c.Value, err
-		}
-		if f < 0 {
-			i := int(math.Abs(f))
-			return fmt.Sprintf("(%d)", i), nil
-		}
-		i := int(f)
-		return fmt.Sprintf("%d", i), nil
-	case "#,##0.00;(#,##0.00)", "#,##0.00;[red](#,##0.00)":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return c.Value, err
-		}
-		if f < 0 {
-			return fmt.Sprintf("(%.2f)", f), nil
-		}
-		return fmt.Sprintf("%.2f", f), nil
-	case "0%":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return c.Value, err
-		}
-		f = f * 100
-		return fmt.Sprintf("%d%%", int(f)), nil
-	case "0.00%":
-		f, err := strconv.ParseFloat(c.Value, 64)
-		if err != nil {
-			return c.Value, err
-		}
-		f = f * 100
-		return fmt.Sprintf("%.2f%%", f), nil
-	case "0.00e+00", "##0.0e+0":
-		return c.formatToFloat("%e")
+	fullFormat := c.getNumberFormat()
+	returnVal, err := fullFormat.FormatValue(c)
+	if fullFormat.parseEncounteredError {
+		return returnVal, errors.New("invalid number format")
 	}
-	return c.Value, nil
-
-}
-
-// parseTime returns a string parsed using time.Time
-func parseTime(c *Cell) (string, error) {
-	f, err := strconv.ParseFloat(c.Value, 64)
-	if err != nil {
-		return c.Value, err
-	}
-	val := TimeFromExcelTime(f, c.date1904)
-	format := c.GetNumberFormat()
-
-	// Replace Excel placeholders with Go time placeholders.
-	// For example, replace yyyy with 2006. These are in a specific order,
-	// due to the fact that m is used in month, minute, and am/pm. It would
-	// be easier to fix that with regular expressions, but if it's possible
-	// to keep this simple it would be easier to maintain.
-	// Full-length month and days (e.g. March, Tuesday) have letters in them that would be replaced
-	// by other characters below (such as the 'h' in March, or the 'd' in Tuesday) below.
-	// First we convert them to arbitrary characters unused in Excel Date formats, and then at the end,
-	// turn them to what they should actually be.
-	// Based off: http://www.ozgrid.com/Excel/CustomFormats.htm
-	replacements := []struct{ xltime, gotime string }{
-		{"yyyy", "2006"},
-		{"yy", "06"},
-		{"mmmm", "%%%%"},
-		{"dddd", "&&&&"},
-		{"dd", "02"},
-		{"d", "2"},
-		{"mmm", "Jan"},
-		{"mmss", "0405"},
-		{"ss", "05"},
-		{"mm:", "04:"},
-		{":mm", ":04"},
-		{"mm", "01"},
-		{"am/pm", "pm"},
-		{"m/", "1/"},
-		{"%%%%", "January"},
-		{"&&&&", "Monday"},
-	}
-	// It is the presence of the "am/pm" indicator that determins
-	// if this is a 12 hour or 24 hours time format, not the
-	// number of 'h' characters.
-	if is12HourTime(format) {
-		format = strings.Replace(format, "hh", "03", 1)
-		format = strings.Replace(format, "h", "3", 1)
-	} else {
-		format = strings.Replace(format, "hh", "15", 1)
-		format = strings.Replace(format, "h", "15", 1)
-	}
-	for _, repl := range replacements {
-		format = strings.Replace(format, repl.xltime, repl.gotime, 1)
-	}
-	// If the hour is optional, strip it out, along with the
-	// possible dangling colon that would remain.
-	if val.Hour() < 1 {
-		format = strings.Replace(format, "]:", "]", 1)
-		format = strings.Replace(format, "[03]", "", 1)
-		format = strings.Replace(format, "[3]", "", 1)
-		format = strings.Replace(format, "[15]", "", 1)
-	} else {
-		format = strings.Replace(format, "[3]", "3", 1)
-		format = strings.Replace(format, "[15]", "15", 1)
-	}
-	return val.Format(format), nil
-}
-
-// isTimeFormat checks whether an Excel format string represents
-// a time.Time.
-func isTimeFormat(format string) bool {
-	dateParts := []string{
-		"yy", "hh", "h", "am/pm", "AM/PM", "A/P", "a/p", "ss", "mm", ":",
-	}
-	for _, part := range dateParts {
-		if strings.Contains(format, part) {
-			return true
-		}
-	}
-	return false
-}
-
-// is12HourTime checks whether an Excel time format string is a 12
-// hours form.
-func is12HourTime(format string) bool {
-	return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P")
+	return returnVal, err
 }

+ 23 - 29
cell_test.go

@@ -2,6 +2,7 @@ package xlsx
 
 import (
 	"math"
+	"testing"
 	"time"
 
 	. "gopkg.in/check.v1"
@@ -203,11 +204,6 @@ func (l *CellSuite) TestGeneralNumberHandling(c *C) {
 			formattedValueOutput: "-12345678",
 			noExpValueOutput:     "-12345678",
 		},
-		{
-			value:                "",
-			formattedValueOutput: "",
-			noExpValueOutput:     "",
-		},
 	}
 	for _, testCase := range testCases {
 		cell := Cell{
@@ -245,7 +241,7 @@ func (s *CellSuite) TestGetTime(c *C) {
 
 // FormattedValue returns an error for formatting errors
 func (l *CellSuite) TestFormattedValueErrorsOnBadFormat(c *C) {
-	cell := Cell{Value: "Fudge Cake"}
+	cell := Cell{Value: "Fudge Cake", cellType: CellTypeNumeric}
 	cell.NumFmt = "#,##0 ;(#,##0)"
 	value, err := cell.FormattedValue()
 	c.Assert(value, Equals, "Fudge Cake")
@@ -253,14 +249,6 @@ func (l *CellSuite) TestFormattedValueErrorsOnBadFormat(c *C) {
 	c.Assert(err.Error(), Equals, "strconv.ParseFloat: parsing \"Fudge Cake\": invalid syntax")
 }
 
-// FormattedValue returns a string containing error text for formatting errors
-func (l *CellSuite) TestFormattedValueReturnsErrorAsValueForBadFormat(c *C) {
-	cell := Cell{Value: "Fudge Cake"}
-	cell.NumFmt = "#,##0 ;(#,##0)"
-	_, err := cell.FormattedValue()
-	c.Assert(err.Error(), Equals, "strconv.ParseFloat: parsing \"Fudge Cake\": invalid syntax")
-}
-
 // formattedValueChecker removes all the boilerplate for testing Cell.FormattedValue
 // after its change from returning one value (a string) to two values (string, error)
 // This allows all the old one-line asserts in the test to continue to be one
@@ -276,16 +264,22 @@ func (fvc *formattedValueChecker) Equals(cell Cell, expected string) {
 	}
 	fvc.c.Assert(val, Equals, expected)
 }
+func cellsFormattedValueEquals(t *testing.T, cell *Cell, expected string) {
+	val, err := cell.FormattedValue()
+	if err != nil {
+		t.Error(err)
+	}
+	if val != expected {
+		t.Errorf("Expected cell.FormattedValue() to be %v, got %v", expected, val)
+	}
+}
 
 // We can return a string representation of the formatted data
 func (l *CellSuite) TestFormattedValue(c *C) {
-	// XXX TODO, this test should probably be split down, and made
-	// in terms of SafeFormattedValue, as FormattedValue wraps
-	// that function now.
-	cell := Cell{Value: "37947.7500001"}
-	negativeCell := Cell{Value: "-37947.7500001"}
-	smallCell := Cell{Value: "0.007"}
-	earlyCell := Cell{Value: "2.1"}
+	cell := Cell{Value: "37947.7500001", cellType: CellTypeNumeric}
+	negativeCell := Cell{Value: "-37947.7500001", cellType: CellTypeNumeric}
+	smallCell := Cell{Value: "0.007", cellType: CellTypeNumeric}
+	earlyCell := Cell{Value: "2.1", cellType: CellTypeNumeric}
 
 	fvc := formattedValueChecker{c: c}
 
@@ -298,12 +292,12 @@ func (l *CellSuite) TestFormattedValue(c *C) {
 	// don't think FormattedValue() should be doing a numeric conversion on the value
 	// before returning the string.
 	cell.NumFmt = "0"
-	fvc.Equals(cell, "37947")
+	fvc.Equals(cell, "37948")
 
 	cell.NumFmt = "#,##0" // For the time being we're not doing
 	// this comma formatting, so it'll fall back to the related
 	// non-comma form.
-	fvc.Equals(cell, "37947")
+	fvc.Equals(cell, "37948")
 
 	cell.NumFmt = "#,##0.00;(#,##0.00)"
 	fvc.Equals(cell, "37947.75")
@@ -317,17 +311,17 @@ func (l *CellSuite) TestFormattedValue(c *C) {
 	fvc.Equals(cell, "37947.75")
 
 	cell.NumFmt = "#,##0 ;(#,##0)"
-	fvc.Equals(cell, "37947")
+	fvc.Equals(cell, "37948")
 	negativeCell.NumFmt = "#,##0 ;(#,##0)"
-	fvc.Equals(negativeCell, "(37947)")
+	fvc.Equals(negativeCell, "(37948)")
 
 	cell.NumFmt = "#,##0 ;[red](#,##0)"
-	fvc.Equals(cell, "37947")
+	fvc.Equals(cell, "37948")
 	negativeCell.NumFmt = "#,##0 ;[red](#,##0)"
-	fvc.Equals(negativeCell, "(37947)")
+	fvc.Equals(negativeCell, "(37948)")
 
 	negativeCell.NumFmt = "#,##0.00;(#,##0.00)"
-	fvc.Equals(negativeCell, "(-37947.75)")
+	fvc.Equals(negativeCell, "(37947.75)")
 
 	cell.NumFmt = "0%"
 	fvc.Equals(cell, "3794775%")
@@ -657,7 +651,7 @@ func (s *CellSuite) TestIsTimeFormat(c *C) {
 	c.Assert(isTimeFormat("a/p"), Equals, true)
 	c.Assert(isTimeFormat("ss"), Equals, true)
 	c.Assert(isTimeFormat("mm"), Equals, true)
-	c.Assert(isTimeFormat(":"), Equals, true)
+	c.Assert(isTimeFormat(":"), Equals, false)
 	c.Assert(isTimeFormat("z"), Equals, false)
 }
 

+ 1 - 0
col.go

@@ -11,6 +11,7 @@ type Col struct {
 	Collapsed    bool
 	OutlineLevel uint8
 	numFmt       string
+	parsedNumFmt *parsedNumberFormat
 	style        *Style
 }
 

+ 561 - 0
format_code.go

@@ -0,0 +1,561 @@
+package xlsx
+
+import (
+	"errors"
+	"fmt"
+	"math"
+	"strconv"
+	"strings"
+)
+
+// Do not edit these attributes once this struct is created. This struct should only be created by
+// parseFullNumberFormatString() from a number format string. If the format for a cell needs to change, change
+// the number format string and getNumberFormat() will invalidate the old struct and re-parse the string.
+type parsedNumberFormat struct {
+	numFmt                        string
+	isTimeFormat                  bool
+	negativeFormatExpectsPositive bool
+	positiveFormat                *formatOptions
+	negativeFormat                *formatOptions
+	zeroFormat                    *formatOptions
+	textFormat                    *formatOptions
+	parseEncounteredError         bool
+}
+
+type formatOptions struct {
+	isTimeFormat        bool
+	showPercent         bool
+	fullFormatString    string
+	reducedFormatString string
+	prefix              string
+	suffix              string
+}
+
+func (fullFormat *parsedNumberFormat) FormatValue(cell *Cell) (string, error) {
+	if cell.cellType != CellTypeNumeric {
+		textFormat := cell.parsedNumFmt.textFormat
+		// This switch statement is only for String formats
+		switch textFormat.reducedFormatString {
+		case builtInNumFmt[builtInNumFmtIndex_GENERAL]: // General is literally "general"
+			return cell.Value, nil
+		case builtInNumFmt[builtInNumFmtIndex_STRING]: // String is "@"
+			return textFormat.prefix + cell.Value + textFormat.suffix, nil
+		case "":
+			return textFormat.prefix + textFormat.suffix, nil
+		default:
+			return cell.Value, errors.New("invalid or unsupported format")
+		}
+	}
+	if fullFormat.isTimeFormat {
+		return fullFormat.parseTime(cell.Value, cell.date1904)
+	}
+	var numberFormat *formatOptions
+	floatVal, floatErr := strconv.ParseFloat(cell.Value, 64)
+	if floatErr != nil {
+		return cell.Value, floatErr
+	}
+	if floatVal > 0 {
+		numberFormat = fullFormat.positiveFormat
+	} else if floatVal < 0 {
+		if fullFormat.negativeFormatExpectsPositive {
+			floatVal = math.Abs(floatVal)
+		}
+		numberFormat = fullFormat.negativeFormat
+	} else {
+		numberFormat = fullFormat.zeroFormat
+	}
+
+	if numberFormat.showPercent {
+		floatVal = 100 * floatVal
+	}
+
+	// Only the most common format strings are supported here.
+	// Eventually this switch needs to be replaced with a more general solution.
+	// Some of these "supported" formats should have thousand separators, but don't get them since Go fmt
+	// doesn't have a way to request thousands separators.
+	// The only things that should be supported here are in the array formattingCharacters,
+	// everything else has been stripped out before.
+	// The formatting characters can have non-formatting characters mixed in with them and those should be maintained.
+	// However, at this time we fail to parse those formatting codes and they get replaced with "General"
+
+	// This switch statement is only for number formats
+	var formattedNum string
+	switch numberFormat.reducedFormatString {
+	case builtInNumFmt[builtInNumFmtIndex_GENERAL]: // General is literally "general"
+		// prefix, showPercent, and suffix cannot apply to the general format
+		// The logic for showing numbers when the format is "general" is much more complicated than the rest of these.
+		val, err := generalNumericScientific(cell.Value, true)
+		if err != nil {
+			return cell.Value, nil
+		}
+		return val, nil
+	case builtInNumFmt[builtInNumFmtIndex_STRING]: // String is "@"
+		formattedNum = cell.Value
+	case builtInNumFmt[builtInNumFmtIndex_INT], "#,##0": // Int is "0"
+		// Previously this case would cast to int and print with %d, but that will not round the value correctly.
+		formattedNum = fmt.Sprintf("%.0f", floatVal)
+	case "0.0", "#,##0.0":
+		formattedNum = fmt.Sprintf("%.1f", floatVal)
+	case builtInNumFmt[builtInNumFmtIndex_FLOAT], "#,##0.00": // Float is "0.00"
+		formattedNum = fmt.Sprintf("%.2f", floatVal)
+	case "0.000", "#,##0.000":
+		formattedNum = fmt.Sprintf("%.3f", floatVal)
+	case "0.0000", "#,##0.0000":
+		formattedNum = fmt.Sprintf("%.4f", floatVal)
+	case "0.00e+00", "##0.0e+0":
+		formattedNum = fmt.Sprintf("%e", floatVal)
+	case "":
+		// Do nothing.
+	default:
+		return cell.Value, nil
+	}
+	return numberFormat.prefix + formattedNum + numberFormat.suffix, nil
+}
+
+func generalNumericScientific(value string, allowScientific bool) (string, error) {
+	if strings.TrimSpace(value) == "" {
+		return "", nil
+	}
+	f, err := strconv.ParseFloat(value, 64)
+	if err != nil {
+		return value, err
+	}
+	if allowScientific {
+		absF := math.Abs(f)
+		// When using General format, numbers that are less than 1e-9 (0.000000001) and greater than or equal to
+		// 1e11 (100,000,000,000) should be shown in scientific notation.
+		// Numbers less than the number after zero, are assumed to be zero.
+		if (absF >= math.SmallestNonzeroFloat64 && absF < minNonScientificNumber) || absF >= maxNonScientificNumber {
+			return strconv.FormatFloat(f, 'E', -1, 64), nil
+		}
+	}
+	// This format (fmt="f", prec=-1) will prevent padding with zeros and will never switch to scientific notation.
+	// However, it will show more than 11 characters for very precise numbers, and this cannot be changed.
+	// You could also use fmt="g", prec=11, which doesn't pad with zeros and allows the correct precision,
+	// but it will use scientific notation on numbers less than 1e-4. That value is hardcoded in Go and cannot be
+	// configured or disabled.
+	return strconv.FormatFloat(f, 'f', -1, 64), nil
+}
+
+// Format strings are a little strange to compare because empty string needs to be taken as general, and general needs
+// to be compared case insensitively.
+func compareFormatString(fmt1, fmt2 string) bool {
+	if fmt1 == fmt2 {
+		return true
+	}
+	if fmt1 == "" || strings.EqualFold(fmt1, "general") {
+		fmt1 = "general"
+	}
+	if fmt2 == "" || strings.EqualFold(fmt2, "general") {
+		fmt2 = "general"
+	}
+	return fmt1 == fmt2
+}
+
+func parseFullNumberFormatString(numFmt string) *parsedNumberFormat {
+	parsedNumFmt := &parsedNumberFormat{
+		numFmt: numFmt,
+	}
+	if isTimeFormat(numFmt) {
+		// Time formats cannot have multiple groups separated by semicolons, there is only one format.
+		// Strings are unaffected by the time format.
+		parsedNumFmt.isTimeFormat = true
+		parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
+		return parsedNumFmt
+	}
+
+	var fmtOptions []*formatOptions
+	formats, err := splitFormatOnSemicolon(numFmt)
+	if err == nil {
+		for _, formatSection := range formats {
+			parsedFormat, err := parseNumberFormatSection(formatSection)
+			if err != nil {
+				// If an invalid number section is found, fall back to general
+				parsedFormat = fallbackErrorFormat
+				parsedNumFmt.parseEncounteredError = true
+			}
+			fmtOptions = append(fmtOptions, parsedFormat)
+		}
+	} else {
+		fmtOptions = append(fmtOptions, fallbackErrorFormat)
+		parsedNumFmt.parseEncounteredError = true
+	}
+	if len(fmtOptions) > 4 {
+		fmtOptions = []*formatOptions{fallbackErrorFormat}
+		parsedNumFmt.parseEncounteredError = true
+	}
+
+	if len(fmtOptions) == 1 {
+		// If there is only one option, it is used for all
+		parsedNumFmt.positiveFormat = fmtOptions[0]
+		parsedNumFmt.negativeFormat = fmtOptions[0]
+		parsedNumFmt.zeroFormat = fmtOptions[0]
+		if strings.Contains(fmtOptions[0].fullFormatString, "@") {
+			parsedNumFmt.textFormat = fmtOptions[0]
+		} else {
+			parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
+		}
+	} else if len(fmtOptions) == 2 {
+		// If there are two formats, the first is used for positive and zeros, the second gets used as a negative format,
+		// and strings are not formatted.
+		// When negative numbers now have their own format, they should become positive before having the format applied.
+		// The format will contain a negative sign if it is desired, but they may be colored red or wrapped in
+		// parenthesis instead.
+		parsedNumFmt.negativeFormatExpectsPositive = true
+		parsedNumFmt.positiveFormat = fmtOptions[0]
+		parsedNumFmt.negativeFormat = fmtOptions[1]
+		parsedNumFmt.zeroFormat = fmtOptions[0]
+		parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
+	} else if len(fmtOptions) == 3 {
+		// If there are three formats, the first is used for positive, the second gets used as a negative format,
+		// the third is for negative, and strings are not formatted.
+		parsedNumFmt.negativeFormatExpectsPositive = true
+		parsedNumFmt.positiveFormat = fmtOptions[0]
+		parsedNumFmt.negativeFormat = fmtOptions[1]
+		parsedNumFmt.zeroFormat = fmtOptions[2]
+		parsedNumFmt.textFormat, _ = parseNumberFormatSection("general")
+	} else {
+		// With four options, the first is positive, the second is negative, the third is zero, and the fourth is strings
+		// Negative numbers should be still become positive before having the negative formatting applied.
+		parsedNumFmt.negativeFormatExpectsPositive = true
+		parsedNumFmt.positiveFormat = fmtOptions[0]
+		parsedNumFmt.negativeFormat = fmtOptions[1]
+		parsedNumFmt.zeroFormat = fmtOptions[2]
+		parsedNumFmt.textFormat = fmtOptions[3]
+	}
+	return parsedNumFmt
+}
+
+// splitFormatOnSemicolon will split the format string into the format sections
+// This logic to split the different formats on semicolon is fully correct, and will skip all literal semicolons,
+// and will catch all breaking semicolons.
+func splitFormatOnSemicolon(format string) ([]string, error) {
+	var formats []string
+	prevIndex := 0
+	for i := 0; i < len(format); i++ {
+		if format[i] == ';' {
+			formats = append(formats, format[prevIndex:i])
+			prevIndex = i + 1
+		} else if format[i] == '\\' {
+			i++
+		} else if format[i] == '"' {
+			endQuoteIndex := strings.Index(format[i+1:], "\"")
+			if endQuoteIndex == -1 {
+				// This is an invalid format string, fall back to general
+				return nil, errors.New("invalid format string")
+			}
+			i += endQuoteIndex + 1
+		}
+	}
+	return append(formats, format[prevIndex:]), nil
+}
+
+var fallbackErrorFormat = &formatOptions{
+	fullFormatString:    "general",
+	reducedFormatString: "general",
+}
+
+// parseNumberFormatSection takes in individual format and parses out most of the options.
+// Some options are parsed, removed from the string, and set as settings on formatOptions.
+// There remainder of the format string is put in the reducedFormatString attribute, and supported values for these
+// are handled in a switch in the Cell.FormattedValue() function.
+// Ideally more and more of the format string would be parsed out here into settings until there is no remainder string
+// at all.
+// Features that this supports:
+// - Time formats are detected, and marked in the options. Time format strings are handled when doing the formatting.
+//   The logic to detect time formats is currently not correct, and can catch formats that are not time formats as well
+//   as miss formats that are time formats.
+// - Color formats are detected and removed.
+// - Currency annotations are handled properly.
+// - Literal strings wrapped in quotes are handled and put into prefix or suffix.
+// - Numbers that should be percent are detected and marked in the options.
+// - Conditionals are detected and removed, but they are not obeyed. The conditional groups will be used just like the
+//   positive;negative;zero;string format groups. Here is an example of a conditional format: "[Red][<=100];[Blue][>100]"
+// Decoding the actual number formatting portion is out of scope, that is placed into reducedFormatString and is used
+// when formatting the string. The string there will be reduced to only the things in the formattingCharacters array.
+// Everything not in that array has been parsed out and put into formatOptions.
+func parseNumberFormatSection(fullFormat string) (*formatOptions, error) {
+	reducedFormat := strings.TrimSpace(fullFormat)
+
+	// general is the only format that does not use the normal format symbols notations
+	if compareFormatString(reducedFormat, "general") {
+		return &formatOptions{
+			fullFormatString:    "general",
+			reducedFormatString: "general",
+		}, nil
+	}
+
+	prefix, reducedFormat, showPercent1, err := parseLiterals(reducedFormat)
+	if err != nil {
+		return nil, err
+	}
+
+	reducedFormat, suffixFormat := splitFormatAndSuffixFormat(reducedFormat)
+
+	suffix, remaining, showPercent2, err := parseLiterals(suffixFormat)
+	if err != nil {
+		return nil, err
+	}
+	if len(remaining) > 0 {
+		// This paradigm of codes consisting of literals, number formats, then more literals is not always correct, they can
+		// actually be intertwined. Though 99% of the time number formats will not do this.
+		// Excel uses this format string for Social Security Numbers: 000\-00\-0000
+		// and this for US phone numbers: [<=9999999]###\-####;\(###\)\ ###\-####
+		return nil, errors.New("invalid or unsupported format string")
+	}
+
+	return &formatOptions{
+		fullFormatString:    fullFormat,
+		isTimeFormat:        false,
+		reducedFormatString: reducedFormat,
+		prefix:              prefix,
+		suffix:              suffix,
+		showPercent:         showPercent1 || showPercent2,
+	}, nil
+}
+
+// formattingCharacters will be left in the reducedNumberFormat
+// It is important that these be looked for in order so that the slash cases are handled correctly.
+// / (slash) is a fraction format if preceded by 0, #, or ?, otherwise it is not a formatting character
+// E- E+ e- e+ are scientific notation, but E, e, -, + are not formatting characters independently
+// \ (back slash) makes the next character a literal (not formatting)
+// " Anything in double quotes is not a formatting character
+// _ (underscore) skips the width of the next character, so the next character cannot be formatting
+var formattingCharacters = []string{"0/", "#/", "?/", "E-", "E+", "e-", "e+", "0", "#", "?", ".", ",", "@", "*"}
+
+// The following are also time format characters, but since this is only used for detecting, not decoding, they are
+// redundant here: ee, gg, ggg, rr, ss, mm, hh, yyyy, dd, ddd, dddd, mm, mmm, mmmm, mmmmm, ss.0000, ss.000, ss.00, ss.0
+// The .00 type format is very tricky, because it only counts if it comes after ss or s or [ss] or [s]
+// .00 is actually a valid number format by itself.
+var timeFormatCharacters = []string{"m", "d", "yy", "h", "m", "AM/PM", "A/P", "am/pm", "a/p", "r", "g", "e", "b1", "b2", "[hh]", "[h]", "[mm]", "[m]",
+	"s.0000", "s.000", "s.00", "s.0", "s", "[ss].0000", "[ss].000", "[ss].00", "[ss].0", "[ss]", "[s].0000", "[s].000", "[s].00", "[s].0", "[s]"}
+
+func splitFormatAndSuffixFormat(format string) (string, string) {
+	var i int
+	for ; i < len(format); i++ {
+		curReducedFormat := format[i:]
+		var found bool
+		for _, special := range formattingCharacters {
+			if strings.HasPrefix(curReducedFormat, special) {
+				// Skip ahead if the special character was longer than length 1
+				i += len(special) - 1
+				found = true
+				break
+			}
+		}
+		if !found {
+			break
+		}
+	}
+	suffixFormat := format[i:]
+	format = format[:i]
+	return format, suffixFormat
+}
+
+func parseLiterals(format string) (string, string, bool, error) {
+	var prefix string
+	showPercent := false
+	for i := 0; i < len(format); i++ {
+		curReducedFormat := format[i:]
+		switch curReducedFormat[0] {
+		case '\\':
+			// If there is a slash, skip the next character, and add it to the prefix
+			if len(curReducedFormat) > 1 {
+				i++
+				prefix += curReducedFormat[1:2]
+			}
+		case '_':
+			// If there is an underscore, skip the next character, but don't add it to the prefix
+			if len(curReducedFormat) > 1 {
+				i++
+			}
+		case '*':
+			// Asterisks are used to repeat the next character to fill the full cell width.
+			// There isn't really a cell size in this context, so this will be ignored.
+		case '"':
+			// If there is a quote skip to the next quote, and add the quoted characters to the prefix
+			endQuoteIndex := strings.Index(curReducedFormat[1:], "\"")
+			if endQuoteIndex == -1 {
+				return "", "", false, errors.New("invalid formatting code")
+			}
+			prefix = prefix + curReducedFormat[1:endQuoteIndex+1]
+			i += endQuoteIndex + 1
+		case '%':
+			showPercent = true
+			prefix += "%"
+		case '[':
+			// Brackets can be currency annotations (e.g. [$$-409])
+			// color formats (e.g. [color1] through [color56], as well as [red] etc.)
+			// conditionals (e.g. [>100], the valid conditionals are =, >, <, >=, <=, <>)
+			bracketIndex := strings.Index(curReducedFormat, "]")
+			if bracketIndex == -1 {
+				return "", "", false, errors.New("invalid formatting code")
+			}
+			// Currencies in Excel are annotated with this format: [$<Currency String>-<Language Info>]
+			// Currency String is something like $, ¥, €, or £
+			// Language Info is three hexadecimal characters
+			if len(curReducedFormat) > 2 && curReducedFormat[1] == '$' {
+				dashIndex := strings.Index(curReducedFormat, "-")
+				if dashIndex != -1 && dashIndex < bracketIndex {
+					// Get the currency symbol, and skip to the end of the currency format
+					prefix += curReducedFormat[2:dashIndex]
+				} else {
+					return "", "", false, errors.New("invalid formatting code")
+				}
+			}
+			i += bracketIndex
+		case '$', '-', '+', '/', '(', ')', ':', '!', '^', '&', '\'', '~', '{', '}', '<', '>', '=', ' ':
+			// These symbols are allowed to be used as literal without escaping
+			prefix += curReducedFormat[0:1]
+		default:
+			for _, special := range formattingCharacters {
+				if strings.HasPrefix(curReducedFormat, special) {
+					// This means we found the start of the actual number formatting portion, and should return.
+					return prefix, format[i:], showPercent, nil
+				}
+			}
+			// Symbols that don't have meaning and aren't in the exempt literal characters, but be escaped.
+			return "", "", false, errors.New("invalid formatting code")
+		}
+	}
+	return prefix, "", showPercent, nil
+}
+
+// parseTime returns a string parsed using time.Time
+func (fullFormat *parsedNumberFormat) parseTime(value string, date1904 bool) (string, error) {
+	f, err := strconv.ParseFloat(value, 64)
+	if err != nil {
+		return value, err
+	}
+	val := TimeFromExcelTime(f, date1904)
+	format := fullFormat.numFmt
+	// Replace Excel placeholders with Go time placeholders.
+	// For example, replace yyyy with 2006. These are in a specific order,
+	// due to the fact that m is used in month, minute, and am/pm. It would
+	// be easier to fix that with regular expressions, but if it's possible
+	// to keep this simple it would be easier to maintain.
+	// Full-length month and days (e.g. March, Tuesday) have letters in them that would be replaced
+	// by other characters below (such as the 'h' in March, or the 'd' in Tuesday) below.
+	// First we convert them to arbitrary characters unused in Excel Date formats, and then at the end,
+	// turn them to what they should actually be.
+	// Based off: http://www.ozgrid.com/Excel/CustomFormats.htm
+	replacements := []struct{ xltime, gotime string }{
+		{"yyyy", "2006"},
+		{"yy", "06"},
+		{"mmmm", "%%%%"},
+		{"dddd", "&&&&"},
+		{"dd", "02"},
+		{"d", "2"},
+		{"mmm", "Jan"},
+		{"mmss", "0405"},
+		{"ss", "05"},
+		{"mm:", "04:"},
+		{":mm", ":04"},
+		{"mm", "01"},
+		{"am/pm", "pm"},
+		{"m/", "1/"},
+		{"%%%%", "January"},
+		{"&&&&", "Monday"},
+	}
+	// It is the presence of the "am/pm" indicator that determins
+	// if this is a 12 hour or 24 hours time format, not the
+	// number of 'h' characters.
+	if is12HourTime(format) {
+		format = strings.Replace(format, "hh", "03", 1)
+		format = strings.Replace(format, "h", "3", 1)
+	} else {
+		format = strings.Replace(format, "hh", "15", 1)
+		format = strings.Replace(format, "h", "15", 1)
+	}
+	for _, repl := range replacements {
+		format = strings.Replace(format, repl.xltime, repl.gotime, 1)
+	}
+	// If the hour is optional, strip it out, along with the
+	// possible dangling colon that would remain.
+	if val.Hour() < 1 {
+		format = strings.Replace(format, "]:", "]", 1)
+		format = strings.Replace(format, "[03]", "", 1)
+		format = strings.Replace(format, "[3]", "", 1)
+		format = strings.Replace(format, "[15]", "", 1)
+	} else {
+		format = strings.Replace(format, "[3]", "3", 1)
+		format = strings.Replace(format, "[15]", "15", 1)
+	}
+	return val.Format(format), nil
+}
+
+// isTimeFormat checks whether an Excel format string represents a time.Time.
+// This function is now correct, but it can detect time format strings that cannot be correctly handled by parseTime()
+func isTimeFormat(format string) bool {
+	var foundTimeFormatCharacters bool
+	for i := 0; i < len(format); i++ {
+		curReducedFormat := format[i:]
+		switch curReducedFormat[0] {
+		case '\\', '_':
+			// If there is a slash, skip the next character, and add it to the prefix
+			// If there is an underscore, skip the next character, but don't add it to the prefix
+			if len(curReducedFormat) > 1 {
+				i++
+			}
+		case '*':
+			// Asterisks are used to repeat the next character to fill the full cell width.
+			// There isn't really a cell size in this context, so this will be ignored.
+		case '"':
+			// If there is a quote skip to the next quote, and add the quoted characters to the prefix
+			endQuoteIndex := strings.Index(curReducedFormat[1:], "\"")
+			if endQuoteIndex == -1 {
+				// This is not any type of valid format.
+				return false
+			}
+			i += endQuoteIndex + 1
+		case '$', '-', '+', '/', '(', ')', ':', '!', '^', '&', '\'', '~', '{', '}', '<', '>', '=', ' ':
+			// These symbols are allowed to be used as literal without escaping
+		case ',':
+			// This is not documented in the XLSX spec as far as I can tell, but Excel and Numbers will include
+			// commas in number formats without escaping them, so this should be supported.
+		default:
+			foundInThisLoop := false
+			for _, special := range timeFormatCharacters {
+				if strings.HasPrefix(curReducedFormat, special) {
+					foundTimeFormatCharacters = true
+					foundInThisLoop = true
+					i += len(special) - 1
+					break
+				}
+			}
+			if foundInThisLoop {
+				continue
+			}
+			if curReducedFormat[0] == '[' {
+				// For number formats, this code would happen above in a case '[': section.
+				// However, for time formats it must happen after looking for occurrences in timeFormatCharacters
+				// because there are a few time formats that can be wrapped in brackets.
+
+				// Brackets can be currency annotations (e.g. [$$-409])
+				// color formats (e.g. [color1] through [color56], as well as [red] etc.)
+				// conditionals (e.g. [>100], the valid conditionals are =, >, <, >=, <=, <>)
+				bracketIndex := strings.Index(curReducedFormat, "]")
+				if bracketIndex == -1 {
+					// This is not any type of valid format.
+					return false
+				}
+				i += bracketIndex
+				continue
+			}
+			// Symbols that don't have meaning, aren't in the exempt literal characters, and aren't escaped are invalid.
+			// The string could still be a valid number format string.
+			return false
+		}
+	}
+	// If the string doesn't have any time formatting characters, it could technically be a time format, but it
+	// would be a pretty weak time format. A valid time format with no time formatting symbols will also be a number
+	// format with no number formatting symbols, which is essentially a constant string that does not depend on the
+	// cell's value in anyway. The downstream logic will do the right thing in that case if this returns false.
+	return foundTimeFormatCharacters
+}
+
+// is12HourTime checks whether an Excel time format string is a 12
+// hours form.
+func is12HourTime(format string) bool {
+	return strings.Contains(format, "am/pm") || strings.Contains(format, "AM/PM") || strings.Contains(format, "a/p") || strings.Contains(format, "A/P")
+}

+ 242 - 0
format_code_test.go

@@ -0,0 +1,242 @@
+package xlsx
+
+import (
+	"time"
+
+	. "gopkg.in/check.v1"
+)
+
+func (s *CellSuite) TestMoreFormattingFeatures(c *C) {
+
+	cell := Cell{}
+	cell.SetFloat(0)
+	date, err := cell.GetTime(false)
+	c.Assert(err, Equals, nil)
+	c.Assert(date, Equals, time.Date(1899, 12, 30, 0, 0, 0, 0, time.UTC))
+	cell.SetFloat(39813.0)
+	date, err = cell.GetTime(true)
+	c.Assert(err, Equals, nil)
+	c.Assert(date, Equals, time.Date(2013, 1, 1, 0, 0, 0, 0, time.UTC))
+	cell.Value = "d"
+	_, err = cell.GetTime(false)
+	c.Assert(err, NotNil)
+}
+
+func (l *CellSuite) TestFormatStringSupport(c *C) {
+	testCases := []struct {
+		formatString         string
+		value                string
+		formattedValueOutput string
+		cellType             CellType
+		expectError          bool
+	}{
+		{
+			formatString:         `[red]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `[blue]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `[color50]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `[$$-409]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "$19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `[$¥-409]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "¥19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `[$€-409]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "€19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `[$£-409]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "£19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `[$USD-409] 0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "USD 19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `0[$USD-409]`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "19USD",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `-[$USD-409]0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "-USD19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `\[0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "[19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `"["0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "[19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         "_[0",
+			value:                "18.989999999999998",
+			formattedValueOutput: "19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `"asdf"0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "asdf19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `"$"0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "$19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `$0`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "$19",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `%0`, // The percent sign can be anywhere in the format.
+			value:                "18.989999999999998",
+			formattedValueOutput: "%1899",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `$-+/()!^&'~{}<>=: 0 :=><}{~'&^)(/+-$`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "$-+/()!^&'~{}<>=: 19 :=><}{~'&^)(/+-$",
+			cellType:             CellTypeNumeric,
+		},
+		{
+			formatString:         `0;-0;"zero"`,
+			value:                "18.989999999999998",
+			formattedValueOutput: "19",
+			cellType:             CellTypeNumeric,
+		},
+		{ // 2 formats
+			formatString:         `0;(0)`,
+			value:                "0",
+			formattedValueOutput: "0",
+			cellType:             CellTypeNumeric,
+		},
+		{ // 2 formats
+			formatString:         `0;(0)`,
+			value:                "4.1",
+			formattedValueOutput: "4",
+			cellType:             CellTypeNumeric,
+		},
+		{ // 2 formats
+			formatString:         `0;(0)`,
+			value:                "-1",
+			formattedValueOutput: "(1)",
+			cellType:             CellTypeNumeric,
+		},
+		{ // 2 formats
+			formatString:         `0;(0)`,
+			value:                "asdf",
+			formattedValueOutput: "asdf",
+			cellType:             CellTypeNumeric,
+			expectError:          true,
+		},
+		{ // 2 formats
+			formatString:         `0;(0)`,
+			value:                "asdf",
+			formattedValueOutput: "asdf",
+			cellType:             CellTypeString,
+		},
+		{ // 3 formats
+			formatString:         `0;(0);"zero"`,
+			value:                "59.6",
+			formattedValueOutput: "60",
+			cellType:             CellTypeNumeric,
+		},
+		{ // 3 formats
+			formatString:         `0;(0);"zero"`,
+			value:                "-39",
+			formattedValueOutput: "(39)",
+			cellType:             CellTypeNumeric,
+		},
+		{ // 3 formats
+			formatString:         `0;(0);"zero"`,
+			value:                "0",
+			formattedValueOutput: "zero",
+			cellType:             CellTypeNumeric,
+		},
+		{ // 3 formats
+			formatString:         `0;(0);"zero"`,
+			value:                "asdf",
+			formattedValueOutput: "asdf",
+			cellType:             CellTypeNumeric,
+			expectError:          true,
+		},
+		{ // 3 formats
+			formatString:         `0;(0);"zero"`,
+			value:                "asdf",
+			formattedValueOutput: "asdf",
+			cellType:             CellTypeString,
+		},
+		{ // 4 formats, also note that the case of the format is maintained. Format codes should not be lower cased.
+			formatString:         `0;(0);"zero";"Behold: "@`,
+			value:                "asdf",
+			formattedValueOutput: "Behold: asdf",
+			cellType:             CellTypeString,
+		},
+		{ // 4 formats
+			formatString:         `0;(0);"zero";"Behold": @`,
+			value:                "asdf",
+			formattedValueOutput: "Behold: asdf",
+			cellType:             CellTypeString,
+		},
+		{ // 4 formats. This format contains an extra
+			formatString:         `0;(0);"zero";"Behold; "@`,
+			value:                "asdf",
+			formattedValueOutput: "Behold; asdf",
+			cellType:             CellTypeString,
+		},
+	}
+	for _, testCase := range testCases {
+		cell := &Cell{
+			cellType: testCase.cellType,
+			NumFmt:   testCase.formatString,
+			Value:    testCase.value,
+		}
+		val, err := cell.FormattedValue()
+		if err != nil != testCase.expectError {
+			c.Fatal(err, testCase)
+		}
+		if val != testCase.formattedValueOutput {
+			c.Fatalf("Expected %v but got %v", testCase.formattedValueOutput, val)
+		}
+	}
+}

+ 2 - 2
lib.go

@@ -550,7 +550,7 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLi
 				cols[i-1] = col
 				if file.styles != nil {
 					col.style = file.styles.getStyle(rawcol.Style)
-					col.numFmt = file.styles.getNumberFormat(rawcol.Style)
+					col.numFmt, col.parsedNumFmt = file.styles.getNumberFormat(rawcol.Style)
 				}
 			}
 		}
@@ -617,7 +617,7 @@ func readRowsFromSheet(Worksheet *xlsxWorksheet, file *File, sheet *Sheet, rowLi
 				fillCellData(rawcell, reftable, sharedFormulas, cell)
 				if file.styles != nil {
 					cell.style = file.styles.getStyle(rawcell.S)
-					cell.NumFmt = file.styles.getNumberFormat(rawcell.S)
+					cell.NumFmt, cell.parsedNumFmt = file.styles.getNumberFormat(rawcell.S)
 				}
 				cell.date1904 = file.Date1904
 				// Cell is considered hidden if the row or the column of this cell is hidden

+ 1 - 1
sheet.go

@@ -280,7 +280,7 @@ func (s *Sheet) makeXLSXSheet(refTable *RefTable, styles *xlsxStyleSheet) *xlsxW
 			style := cell.style
 			if style != nil {
 				XfId = handleStyleForXLSX(style, xNumFmt.NumFmtId, styles)
-			} else if len(cell.NumFmt) > 0 && s.Cols[c].numFmt != cell.NumFmt {
+			} else if len(cell.NumFmt) > 0 && !compareFormatString(s.Cols[c].numFmt, cell.NumFmt) {
 				XfId = handleNumFmtIdForXLSX(xNumFmt.NumFmtId, styles)
 			}
 

+ 6 - 7
sheet_test.go

@@ -77,20 +77,19 @@ func (s *SheetSuite) TestMakeXLSXSheetWithNumFormats(c *C) {
 
 	c.Assert(styles.CellStyleXfs, IsNil)
 
-	c.Assert(styles.CellXfs.Count, Equals, 5)
+	c.Assert(styles.CellXfs.Count, Equals, 4)
 	c.Assert(styles.CellXfs.Xf[0].NumFmtId, Equals, 0)
-	c.Assert(styles.CellXfs.Xf[1].NumFmtId, Equals, 0)
-	c.Assert(styles.CellXfs.Xf[2].NumFmtId, Equals, 1)
-	c.Assert(styles.CellXfs.Xf[3].NumFmtId, Equals, 14)
-	c.Assert(styles.CellXfs.Xf[4].NumFmtId, Equals, 164)
+	c.Assert(styles.CellXfs.Xf[1].NumFmtId, Equals, 1)
+	c.Assert(styles.CellXfs.Xf[2].NumFmtId, Equals, 14)
+	c.Assert(styles.CellXfs.Xf[3].NumFmtId, Equals, 164)
 	c.Assert(styles.NumFmts.Count, Equals, 1)
 	c.Assert(styles.NumFmts.NumFmt[0].NumFmtId, Equals, 164)
 	c.Assert(styles.NumFmts.NumFmt[0].FormatCode, Equals, "hh:mm:ss")
 
 	// Finally we check that the cell points to the right CellXf /
 	// CellStyleXf.
-	c.Assert(worksheet.SheetData.Row[0].C[0].S, Equals, 1)
-	c.Assert(worksheet.SheetData.Row[0].C[1].S, Equals, 2)
+	c.Assert(worksheet.SheetData.Row[0].C[0].S, Equals, 0)
+	c.Assert(worksheet.SheetData.Row[0].C[1].S, Equals, 1)
 }
 
 // When we create the xlsxSheet we also populate the xlsxStyles struct

+ 40 - 37
stream_test.go

@@ -6,20 +6,25 @@ import (
 	"io"
 	"reflect"
 	"strings"
-	"testing"
+
+	. "gopkg.in/check.v1"
 )
 
 const (
 	TestsShouldMakeRealFiles = false
 )
 
-func TestTestsShouldMakeRealFilesShouldBeFalse(t *testing.T) {
+type StreamSuite struct{}
+
+var _ = Suite(&SheetSuite{})
+
+func (s *StreamSuite) TestTestsShouldMakeRealFilesShouldBeFalse(t *C) {
 	if TestsShouldMakeRealFiles {
 		t.Fatal("TestsShouldMakeRealFiles should only be true for local debugging. Don't forget to switch back before commiting.")
 	}
 }
 
-func TestXlsxStreamWrite(t *testing.T) {
+func (s *StreamSuite) TestXlsxStreamWrite(t *C) {
 	// When shouldMakeRealFiles is set to true this test will make actual XLSX files in the file system.
 	// This is useful to ensure files open in Excel, Numbers, Google Docs, etc.
 	// In case of issues you can use "Open XML SDK 2.5" to diagnose issues in generated XLSX files:
@@ -229,41 +234,39 @@ func TestXlsxStreamWrite(t *testing.T) {
 		},
 	}
 	for i, testCase := range testCases {
-		t.Run(testCase.testName, func(t *testing.T) {
-			var filePath string
-			var buffer bytes.Buffer
-			if TestsShouldMakeRealFiles {
-				filePath = fmt.Sprintf("Workbook%d.xlsx", i)
-			}
-			err := writeStreamFile(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles)
-			if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
-				t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
-			}
-			if testCase.expectedError != nil {
-				return
-			}
-			// read the file back with the xlsx package
-			var bufReader *bytes.Reader
-			var size int64
-			if !TestsShouldMakeRealFiles {
-				bufReader = bytes.NewReader(buffer.Bytes())
-				size = bufReader.Size()
-			}
-			actualSheetNames, actualWorkbookData := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
-			// check if data was able to be read correctly
-			if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
-				t.Fatal("Expected sheet names to be equal")
-			}
-			if !reflect.DeepEqual(actualWorkbookData, testCase.workbookData) {
-				t.Fatal("Expected workbook data to be equal")
-			}
-		})
+		var filePath string
+		var buffer bytes.Buffer
+		if TestsShouldMakeRealFiles {
+			filePath = fmt.Sprintf("Workbook%d.xlsx", i)
+		}
+		err := writeStreamFile(filePath, &buffer, testCase.sheetNames, testCase.workbookData, testCase.headerTypes, TestsShouldMakeRealFiles)
+		if err != testCase.expectedError && err.Error() != testCase.expectedError.Error() {
+			t.Fatalf("Error differs from expected error. Error: %v, Expected Error: %v ", err, testCase.expectedError)
+		}
+		if testCase.expectedError != nil {
+			return
+		}
+		// read the file back with the xlsx package
+		var bufReader *bytes.Reader
+		var size int64
+		if !TestsShouldMakeRealFiles {
+			bufReader = bytes.NewReader(buffer.Bytes())
+			size = bufReader.Size()
+		}
+		actualSheetNames, actualWorkbookData := readXLSXFile(t, filePath, bufReader, size, TestsShouldMakeRealFiles)
+		// check if data was able to be read correctly
+		if !reflect.DeepEqual(actualSheetNames, testCase.sheetNames) {
+			t.Fatal("Expected sheet names to be equal")
+		}
+		if !reflect.DeepEqual(actualWorkbookData, testCase.workbookData) {
+			t.Fatal("Expected workbook data to be equal")
+		}
 	}
 }
 
 // The purpose of TestXlsxStyleBehavior is to ensure that initMaxStyleId has the correct starting value
 // and that the logic in AddSheet() that predicts Style IDs is correct.
-func TestXlsxStyleBehavior(t *testing.T) {
+func (s *StreamSuite) TestXlsxStyleBehavior(t *C) {
 	file := NewFile()
 	sheet, err := file.AddSheet("Sheet 1")
 	if err != nil {
@@ -365,7 +368,7 @@ func writeStreamFile(filePath string, fileBuffer io.Writer, sheetNames []string,
 }
 
 // readXLSXFile will read the file using the xlsx package.
-func readXLSXFile(t *testing.T, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string) {
+func readXLSXFile(t *C, filePath string, fileBuffer io.ReaderAt, size int64, shouldMakeRealFiles bool) ([]string, [][][]string) {
 	var readFile *File
 	var err error
 	if shouldMakeRealFiles {
@@ -400,7 +403,7 @@ func readXLSXFile(t *testing.T, filePath string, fileBuffer io.ReaderAt, size in
 	return sheetNames, actualWorkbookData
 }
 
-func TestAddSheetErrorsAfterBuild(t *testing.T) {
+func (s *StreamSuite) TestAddSheetErrorsAfterBuild(t *C) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
 	err := file.AddSheet("Sheet1", []string{"Header"}, nil)
@@ -422,7 +425,7 @@ func TestAddSheetErrorsAfterBuild(t *testing.T) {
 	}
 }
 
-func TestBuildErrorsAfterBuild(t *testing.T) {
+func (s *StreamSuite) TestBuildErrorsAfterBuild(t *C) {
 	file := NewStreamFileBuilder(bytes.NewBuffer(nil))
 
 	err := file.AddSheet("Sheet1", []string{"Header"}, nil)
@@ -444,7 +447,7 @@ func TestBuildErrorsAfterBuild(t *testing.T) {
 	}
 }
 
-func TestCloseWithNothingWrittenToSheets(t *testing.T) {
+func (s *StreamSuite) TestCloseWithNothingWrittenToSheets(t *C) {
 	buffer := bytes.NewBuffer(nil)
 	file := NewStreamFileBuilder(buffer)
 

+ 39 - 18
xmlStyle.go

@@ -12,7 +12,6 @@ import (
 	"encoding/xml"
 	"fmt"
 	"strconv"
-	"strings"
 	"sync"
 )
 
@@ -58,6 +57,19 @@ var builtInNumFmt = map[int]string{
 	49: "@",
 }
 
+// These are the color annotations from number format codes that contain color names.
+// Also possible are [color1] through [color56]
+var numFmtColorCodes = []string{
+	"[red]",
+	"[black]",
+	"[green]",
+	"[white]",
+	"[blue]",
+	"[magenta]",
+	"[yellow]",
+	"[cyan]",
+}
+
 var builtInNumFmtInv = make(map[string]int, 40)
 
 func init() {
@@ -91,9 +103,10 @@ type xlsxStyleSheet struct {
 
 	theme *theme
 
-	sync.RWMutex   // protects the following
-	styleCache     map[int]*Style
-	numFmtRefTable map[int]xlsxNumFmt
+	sync.RWMutex      // protects the following
+	styleCache        map[int]*Style
+	numFmtRefTable    map[int]xlsxNumFmt
+	parsedNumFmtTable map[string]*parsedNumberFormat
 }
 
 func newXlsxStyleSheet(t *theme) *xlsxStyleSheet {
@@ -220,22 +233,30 @@ func getBuiltinNumberFormat(numFmtId int) string {
 	return builtInNumFmt[numFmtId]
 }
 
-func (styles *xlsxStyleSheet) getNumberFormat(styleIndex int) string {
-	if styles.CellXfs.Xf == nil {
-		return ""
-	}
-	var numberFormat string = ""
-	if styleIndex > -1 && styleIndex <= styles.CellXfs.Count {
-		xf := styles.CellXfs.Xf[styleIndex]
-		if builtin := getBuiltinNumberFormat(xf.NumFmtId); builtin != "" {
-			return builtin
+func (styles *xlsxStyleSheet) getNumberFormat(styleIndex int) (string, *parsedNumberFormat) {
+	var numberFormat string = "general"
+	if styles.CellXfs.Xf != nil {
+		if styleIndex > -1 && styleIndex <= styles.CellXfs.Count {
+			xf := styles.CellXfs.Xf[styleIndex]
+			if builtin := getBuiltinNumberFormat(xf.NumFmtId); builtin != "" {
+				numberFormat = builtin
+			} else {
+				if styles.numFmtRefTable != nil {
+					numFmt := styles.numFmtRefTable[xf.NumFmtId]
+					numberFormat = numFmt.FormatCode
+				}
+			}
 		}
-		if styles.numFmtRefTable != nil {
-			numFmt := styles.numFmtRefTable[xf.NumFmtId]
-			numberFormat = numFmt.FormatCode
+	}
+	parsedFmt, ok := styles.parsedNumFmtTable[numberFormat]
+	if !ok {
+		if styles.parsedNumFmtTable == nil {
+			styles.parsedNumFmtTable = map[string]*parsedNumberFormat{}
 		}
+		parsedFmt = parseFullNumberFormatString(numberFormat)
+		styles.parsedNumFmtTable[numberFormat] = parsedFmt
 	}
-	return strings.ToLower(numberFormat)
+	return numberFormat, parsedFmt
 }
 
 func (styles *xlsxStyleSheet) addFont(xFont xlsxFont) (index int) {
@@ -313,7 +334,7 @@ func (styles *xlsxStyleSheet) addCellXf(xCellXf xlsxXf) (index int) {
 
 // newNumFmt generate a xlsxNumFmt according the format code. When the FormatCode is built in, it will return a xlsxNumFmt with the NumFmtId defined in ECMA document, otherwise it will generate a new NumFmtId greater than 164.
 func (styles *xlsxStyleSheet) newNumFmt(formatCode string) xlsxNumFmt {
-	if formatCode == "" {
+	if compareFormatString(formatCode, "general") {
 		return xlsxNumFmt{NumFmtId: 0, FormatCode: "general"}
 	}
 	// built in NumFmts in xmlStyle.go, traverse from the const.