package xlsx import ( "bytes" "encoding/base64" "fmt" "math" "strconv" "strings" "time" ) const ( maxNonScientificNumber = 1e11 minNonScientificNumber = 1e-9 ) // CellType is an int type for storing metadata about the data type in the cell. type CellType int // These are the cell types from the ST_CellType spec const ( CellTypeString CellType = iota // CellTypeStringFormula is a specific format for formulas that return string values. Formulas that return numbers // and booleans are stored as those types. CellTypeStringFormula CellTypeNumeric CellTypeBool // CellTypeInline is not respected on save, all inline string cells will be saved as SharedStrings // when saving to an XLSX file. This the same behavior as that found in Excel. CellTypeInline CellTypeError // d (Date): Cell contains a date in the ISO 8601 format. // That is the only mention of this format in the XLSX spec. // Date seems to be unused by the current version of Excel, it stores dates as Numeric cells with a date format string. // For now these cells will have their value output directly. It is unclear if the value is supposed to be parsed // into a number and then formatted using the formatting or not. CellTypeDate ) func (ct CellType) Ptr() *CellType { return &ct } func (ct *CellType) fallbackTo(cellData string, fallback CellType) CellType { if ct != nil { switch *ct { case CellTypeNumeric: if _, err := strconv.ParseFloat(cellData, 64); err == nil { return *ct } default: } } return fallback } // 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 RichText []RichTextRun formula string style *Style NumFmt string parsedNumFmt *parsedNumberFormat date1904 bool Hidden bool HMerge int VMerge int cellType CellType DataValidation *xlsxDataValidation Hyperlink Hyperlink num int modified bool origValue string origNumFmt string origRichText []RichTextRun } // Return a representation of the Cell as a slice of bytes func (c Cell) MarshalBinary() ([]byte, error) { // bs uses base64 to avoid directly encoding newlines and other bad values bs := func(s string) string { return base64.StdEncoding.EncodeToString([]byte(s)) } var b bytes.Buffer // We can omit the Row pointer, because we know this information when we unmarshal. // We can omit the parsedNumFmt because this is created on demand anyway. // We can omit the DataValidation because we store this separately with a derived key // We can omit the Style because we store this separately with a derived key // // String values all contain fixed prefixes to avoid issues with empty strings. fmt.Fprintln(&b, bs("V"+c.Value), bs("F"+c.formula), bs("N"+c.NumFmt), c.date1904, c.Hidden, c.HMerge, c.VMerge, c.cellType, bs("HDS"+c.Hyperlink.DisplayString), bs("HL"+c.Hyperlink.Link), bs("HTT"+c.Hyperlink.Tooltip), c.num) return b.Bytes(), nil } // Read a slice of bytes, produced by MarshalBinary, into a Cell func (c *Cell) UnmarshalBinary(data []byte) error { ubs := func(s string) string { decoded, err := base64.StdEncoding.DecodeString(s) if err != nil { panic(err) } return string(decoded) } b := bytes.NewBuffer(data) var value, formula, numfmt, hds, hl, htt string _, err := fmt.Fscanln(b, &value, &formula, &numfmt, &c.date1904, &c.Hidden, &c.HMerge, &c.VMerge, &c.cellType, &hds, &hl, &htt, &c.num) c.Value = strings.TrimPrefix(ubs(value), "V") c.formula = strings.TrimPrefix(ubs(formula), "F") c.NumFmt = strings.TrimPrefix(ubs(numfmt), "N") c.Hyperlink.DisplayString = strings.TrimPrefix(ubs(hds), "HDS") c.Hyperlink.Link = strings.TrimPrefix(ubs(hl), "HL") c.Hyperlink.Tooltip = strings.TrimPrefix(ubs(htt), "HTT") return err } // Modified returns True if a cell has been modified since it was last persisted. func (c *Cell) Modified() bool { if c == nil { return false } rtEq := func(a, b []RichTextRun) bool { if len(a) != len(b) { return false } for i := 0; i < len(a); i++ { if a[i] != b[i] { return false } } return true } return c.modified || c.Value != c.origValue || c.NumFmt != c.origNumFmt || !rtEq(c.RichText, c.origRichText) } // Return a string repersenting a Cell in a way that can be used by the CellStore func (c *Cell) key() string { return fmt.Sprintf("%s:%06d:%06d", c.Row.Sheet.Name, c.Row.num, c.num) } // Hyperlink is a structure to store link information // in-workbook links to cells or defined names are stored in Location // external links are stores in Link type Hyperlink struct { DisplayString string Link string Tooltip string Location string } // CellInterface defines the public API of the Cell. type CellInterface interface { String() string FormattedValue() string } // NewCell creates a cell with a reference to its parent Row. In most // cases you shouldn't call this, but rather call Row.AddCell. func newCell(r *Row, num int) *Cell { cell := &Cell{Row: r, num: num} return cell } func (c *Cell) updatable() { if c.Row != nil && c.Row.cellStoreRow != nil { c.Row.cellStoreRow.CellUpdatable(c) } } // Merge with other cells, horizontally and/or vertically. func (c *Cell) Merge(hcells, vcells int) { c.updatable() c.HMerge = hcells c.VMerge = vcells c.modified = true } // Type returns the CellType of a cell. See CellType constants for more details. func (c *Cell) Type() CellType { return c.cellType } // SetString sets the value of a cell to a string. func (c *Cell) SetString(s string) { c.updatable() c.Value = s c.RichText = nil c.formula = "" c.cellType = CellTypeString c.modified = true } // SetRichText sets the value of a cell to a set of the rich text. func (c *Cell) SetRichText(r []RichTextRun) { c.updatable() c.Value = "" c.RichText = append([]RichTextRun(nil), r...) c.formula = "" c.cellType = CellTypeString c.modified = true } // String returns the value of a Cell as a string. If you'd like to // see errors returned from formatting then please use // Cell.FormattedValue() instead. func (c *Cell) String() string { // To preserve the String() interface we'll throw away errors. // Note that using FormattedValue is therefore strongly // preferred. value, _ := c.FormattedValue() return value } // SetFloat sets the value of a cell to a float. func (c *Cell) SetFloat(n float64) { c.updatable() c.SetValue(n) } // IsTime returns true if the cell stores a time value. func (c *Cell) IsTime() bool { c.getNumberFormat() return c.parsedNumFmt.isTimeFormat } // GetTime returns the value of a Cell as a time.Time func (c *Cell) GetTime(date1904 bool) (t time.Time, err error) { f, err := c.Float() if err != nil { return t, err } return TimeFromExcelTime(f, date1904), nil } /* The following are samples of format samples. * "0.00e+00" * "0", "#,##0" * "0.00", "#,##0.00", "@" * "#,##0 ;(#,##0)", "#,##0 ;[red](#,##0)" * "#,##0.00;(#,##0.00)", "#,##0.00;[red](#,##0.00)" * "0%", "0.00%" * "0.00e+00", "##0.0e+0" */ // SetFloatWithFormat sets the value of a cell to a float and applies // formatting to the cell. func (c *Cell) SetFloatWithFormat(n float64, format string) { c.updatable() c.SetValue(n) c.NumFmt = format c.formula = "" } // SetCellFormat set cell value format func (c *Cell) SetFormat(format string) { c.updatable() c.NumFmt = format c.modified = true } // DateTimeOptions are additional options for exporting times type DateTimeOptions struct { // Location allows calculating times in other timezones/locations Location *time.Location // ExcelTimeFormat is the string you want excel to use to format the datetime ExcelTimeFormat string } var ( DefaultDateFormat = builtInNumFmt[14] DefaultDateTimeFormat = builtInNumFmt[22] DefaultDateOptions = DateTimeOptions{ Location: timeLocationUTC, ExcelTimeFormat: DefaultDateFormat, } DefaultDateTimeOptions = DateTimeOptions{ Location: timeLocationUTC, ExcelTimeFormat: DefaultDateTimeFormat, } ) // SetDate sets the value of a cell to a float. func (c *Cell) SetDate(t time.Time) { c.updatable() c.SetDateWithOptions(t, DefaultDateOptions) } func (c *Cell) SetDateTime(t time.Time) { c.updatable() c.SetDateWithOptions(t, DefaultDateTimeOptions) } // SetDateWithOptions allows for more granular control when exporting dates and times func (c *Cell) SetDateWithOptions(t time.Time, options DateTimeOptions) { c.updatable() _, offset := t.In(options.Location).Zone() t = time.Unix(t.Unix()+int64(offset), int64(t.Nanosecond())) c.SetDateTimeWithFormat(TimeToExcelTime(t.In(timeLocationUTC), c.date1904), options.ExcelTimeFormat) c.modified = true } func (c *Cell) SetDateTimeWithFormat(n float64, format string) { c.updatable() c.Value = strconv.FormatFloat(n, 'f', -1, 64) c.NumFmt = format c.formula = "" c.cellType = CellTypeNumeric c.modified = true } // Float returns the value of cell as a number. func (c *Cell) Float() (float64, error) { f, err := strconv.ParseFloat(c.Value, 64) if err != nil { return math.NaN(), err } return f, nil } // SetInt64 sets a cell's value to a 64-bit integer. func (c *Cell) SetInt64(n int64) { c.updatable() c.SetValue(n) } // Int64 returns the value of cell as 64-bit integer. func (c *Cell) Int64() (int64, error) { f, err := strconv.ParseInt(c.Value, 10, 64) if err != nil { return -1, err } return f, nil } // GeneralNumeric returns the value of the cell as a string. It is formatted very closely to the the XLSX spec for how // 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 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 generalNumericScientific(c.Value, false) } // SetInt sets a cell's value to an integer. func (c *Cell) SetInt(n int) { c.updatable() c.SetValue(n) } // SetHyperlink sets this cell to contain the given hyperlink, displayText and tooltip. // If the displayText or tooltip are an empty string, they will not be set. // The hyperlink provided must be a valid URL starting with http:// or https:// or // excel will not recognize it as an external link. All other hyperlink formats will be // treated as internal link between sheets. Official format in form of `#Sheet!A123`. // Maximum number of hyperlinks per sheet is 65530, according to specification. func (c *Cell) SetHyperlink(hyperlink string, displayText string, tooltip string) { c.updatable() h := strings.ToLower(hyperlink) if strings.HasPrefix(h, "http:") || strings.HasPrefix(h, "https://") { c.Hyperlink = Hyperlink{Link: hyperlink} } else { c.Hyperlink = Hyperlink{Link: hyperlink, Location: hyperlink} } c.SetString(hyperlink) c.Row.Sheet.addRelation(RelationshipTypeHyperlink, hyperlink, RelationshipTargetModeExternal) if displayText != "" { c.Hyperlink.DisplayString = displayText c.SetString(displayText) } if tooltip != "" { c.Hyperlink.Tooltip = tooltip } } // SetValue sets a cell's value to any type. func (c *Cell) SetValue(n interface{}) { c.updatable() switch t := n.(type) { case time.Time: c.SetDateTime(t) case int, int8, int16, int32, int64: c.SetNumeric(fmt.Sprintf("%d", n)) case float64: // When formatting floats, do not use fmt.Sprintf("%v", n), this will cause numbers below 1e-4 to be printed in // scientific notation. Scientific notation is not a valid way to store numbers in XML. // Also not not use fmt.Sprintf("%f", n), this will cause numbers to be stored as X.XXXXXX. Which means that // numbers will lose precision and numbers with fewer significant digits such as 0 will be stored as 0.000000 // which causes tests to fail. c.SetNumeric(strconv.FormatFloat(t, 'f', -1, 64)) case float32: c.SetNumeric(strconv.FormatFloat(float64(t), 'f', -1, 32)) case string: c.SetString(t) case []byte: c.SetString(string(t)) case bool: c.SetBool(t) case nil: c.SetString("") default: c.SetString(fmt.Sprintf("%v", n)) } } // SetNumeric sets a cell's value to a number func (c *Cell) SetNumeric(s string) { c.updatable() c.Value = s c.NumFmt = builtInNumFmt[builtInNumFmtIndex_GENERAL] c.formula = "" c.cellType = CellTypeNumeric c.modified = true } // Int returns the value of cell as integer. // Has max 53 bits of precision // See: float64(int64(math.MaxInt)) func (c *Cell) Int() (int, error) { f, err := strconv.ParseFloat(c.Value, 64) if err != nil { return -1, err } return int(f), nil } // SetBool sets a cell's value to a boolean. func (c *Cell) SetBool(b bool) { c.updatable() if b { c.Value = "1" } else { c.Value = "0" } c.cellType = CellTypeBool c.modified = true } // Bool returns a boolean from a cell's value. // TODO: Determine if the current return value is // appropriate for types other than CellTypeBool. func (c *Cell) Bool() bool { // If bool, just return the value. if c.cellType == CellTypeBool { return c.Value == "1" } // If numeric, base it on a non-zero. if c.cellType == CellTypeNumeric { return c.Value != "0" } // Return whether there's an empty string. return c.Value != "" } // SetFormula sets the format string for a cell. func (c *Cell) SetFormula(formula string) { c.updatable() c.formula = formula c.cellType = CellTypeNumeric c.modified = true } func (c *Cell) SetStringFormula(formula string) { c.updatable() c.formula = formula c.cellType = CellTypeStringFormula c.modified = true } // Formula returns the formula string for the cell. func (c *Cell) Formula() string { return c.formula } // GetStyle returns the Style associated with a Cell func (c *Cell) GetStyle() *Style { if c.style == nil { c.style = NewStyle() } return c.style } // SetStyle sets the style of a cell. func (c *Cell) SetStyle(style *Style) { c.updatable() c.style = style c.modified = true } // GetNumberFormat returns the number format string for a cell. func (c *Cell) GetNumberFormat() string { return c.NumFmt } func (c *Cell) formatToFloat(format string) (string, error) { f, err := strconv.ParseFloat(c.Value, 64) if err != nil { return c.Value, err } return fmt.Sprintf(format, f), nil } func (c *Cell) formatToInt(format string) (string, error) { f, err := strconv.ParseFloat(c.Value, 64) if err != nil { return c.Value, err } return fmt.Sprintf(format, int(f)), nil } // getNumberFormat will update the parsedNumFmt struct if it has become out of date, since a cell's NumFmt string is a // public field that could be edited by clients. 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 // with the raw value of the Cell. func (c *Cell) FormattedValue() (string, error) { fullFormat := c.getNumberFormat() returnVal, err := fullFormat.FormatValue(c) if fullFormat.parseEncounteredError != nil { return returnVal, *fullFormat.parseEncounteredError } return returnVal, err } // SetDataValidation set data validation func (c *Cell) SetDataValidation(dd *xlsxDataValidation) { c.updatable() c.DataValidation = dd c.modified = true } // GetCoordinates returns a pair of integers representing the // cartesian coorindates of the Cell within the Sheet. The // coordinates are zero based and a returned in order x,y where x is // the Column number and y is the Row number. If you need to convert // these numbers to a Excel cellID (i.e. B15) then please see the // GetCellIDStringFromCoords function. func (c *Cell) GetCoordinates() (int, int) { return c.num, c.Row.num }