spreadsheet: add helpers and example for number/date/time formatting

This commit is contained in:
Todd 2017-09-05 18:42:39 -04:00
parent 70051ae509
commit 10498401d6
10 changed files with 247 additions and 79 deletions

View File

@ -0,0 +1,60 @@
// Copyright 2017 Baliance. All rights reserved.
package main
import (
"log"
"time"
"baliance.com/gooxml/spreadsheet"
)
func main() {
ss := spreadsheet.New()
sheet := ss.AddSheet()
row := sheet.AddRow()
cell := row.AddCell()
// If no formatting/styles are applied, then the 'general' format is used.
cell.SetNumber(1.234)
// You can also apply a format at the same time you are setting a number.
cell = row.AddCell()
cell.SetNumberWithStyle(0.95, spreadsheet.StandardFormatPercent)
// But that involves a few lookups, so if you're creating many, many cells
// it wil be faster to
cell = row.AddCell()
// create the style
dateStyle := ss.StyleSheet.AddCellStyle()
// set its format
dateStyle.SetNumberFormatStandard(spreadsheet.StandardFormatDate)
// and apply it to a cell
cell.SetDate(time.Now())
cell.SetStyle(dateStyle)
// It's even faster if repeatedly applying a style to apply the style index
// directly. This is probably not worth the hassle most of the time, and
// will generate the same content as calling setXWithStyle
cs := ss.StyleSheet.AddCellStyle()
cs.SetNumberFormatStandard(spreadsheet.StandardFormatTime)
idx := cs.Index()
for i := 0; i < 5; i++ {
cell = row.AddCell()
cell.SetDate(time.Now())
cell.SetStyleIndex(idx)
}
// completely custom number formats can also be used
customStyle := ss.StyleSheet.AddCellStyle()
customStyle.SetNumberFormat("$#,##0.00")
cell = row.AddCell()
cell.SetNumber(1.234)
cell.SetStyle(customStyle)
if err := ss.Validate(); err != nil {
log.Fatalf("error validating sheet: %s", err)
}
ss.SaveToFile("number-date-time-formats.xlsx")
}

View File

@ -69,6 +69,12 @@ func (c Cell) SetNumber(v float64) {
c.x.TAttr = sml.ST_CellTypeN
}
// SetNumberWithStyle sets a number and applies a standard format to the cell.
func (c Cell) SetNumberWithStyle(v float64, f StandardFormat) {
c.SetNumber(v)
c.SetStyle(c.w.StyleSheet.GetOrCreateStandardNumberFormat(f))
}
// SetBool sets the cell type to boolean and the value to the given boolean
// value.
func (c Cell) SetBool(v bool) {
@ -108,7 +114,8 @@ func (c Cell) SetTime(d time.Time) {
// SetDate sets the cell value to a date. It's stored as the number of days past
// th sheet epoch. When we support v5 strict, we can store an ISO 8601 date
// string directly, however that's not allowed with v5 transitional (even
// though it works in Excel).
// though it works in Excel). The cell is not styled via this method, so it will
// display as a number. SetDateWithStyle should normally be used instead.
func (c Cell) SetDate(d time.Time) {
d = asUTC(d)
epoch := c.w.Epoch()
@ -122,10 +129,32 @@ func (c Cell) SetDate(d time.Time) {
c.x.V = gooxml.Stringf("%d", int(delta.Hours()/24))
}
// SetStyle applies a style to the cell. This style is referenced in the generated XML
// via CellStyle.Index().
// SetDateWithStyle sets a date with the default date style applied.
func (c Cell) SetDateWithStyle(d time.Time) {
c.SetDate(d)
for _, cs := range c.w.StyleSheet.CellStyles() {
// found an existing number format
if cs.HasNumberFormat() && cs.NumberFormat() == uint32(StandardFormatDate) {
c.SetStyle(cs)
return
}
}
// need to create a new format
cs := c.w.StyleSheet.AddCellStyle()
cs.SetNumberFormatStandard(StandardFormatDate)
c.SetStyle(cs)
}
// SetStyle applies a style to the cell. This style is referenced in the
// generated XML via CellStyle.Index().
func (c Cell) SetStyle(cs CellStyle) {
c.x.SAttr = gooxml.Uint32(cs.Index())
c.SetStyleIndex(cs.Index())
}
// SetStyleIndex directly sets a style index to the cell. This should only be
// called with an index retrieved from CellStyle.Index()
func (c Cell) SetStyleIndex(idx uint32) {
c.x.SAttr = gooxml.Uint32(idx)
}
func (c Cell) GetValue() (string, error) {

View File

@ -15,10 +15,26 @@ import (
// CellStyle is a formatting style for a cell. CellStyles are spreadsheet global
// and can be applied to cells across sheets.
type CellStyle struct {
wb *Workbook
xf *sml.CT_Xf
xfs *sml.CT_CellXfs
}
// HasNumberFormat returns true if the cell style has a number format applied.
func (cs CellStyle) HasNumberFormat() bool {
return cs.xf.NumFmtIdAttr != nil && cs.xf.ApplyNumberFormatAttr != nil &&
*cs.xf.ApplyNumberFormatAttr
}
// NumberFormat returns the number format that the cell style uses, or zero if
// it is not set.
func (cs CellStyle) NumberFormat() uint32 {
if cs.xf.NumFmtIdAttr == nil {
return 0
}
return *cs.xf.NumFmtIdAttr
}
// ClearNumberFormat removes any number formatting from the style.
func (cs CellStyle) ClearNumberFormat() {
cs.xf.NumFmtIdAttr = nil
@ -32,6 +48,15 @@ func (cs CellStyle) SetNumberFormatStandard(s StandardFormat) {
cs.xf.ApplyNumberFormatAttr = gooxml.Bool(true)
}
func (cs CellStyle) SetNumberFormat(s string) {
//SetNumberFormat
nf := cs.wb.StyleSheet.AddNumberFormat()
nf.SetCode(s)
cs.xf.ApplyNumberFormatAttr = gooxml.Bool(true)
cs.xf.NumFmtIdAttr = gooxml.Uint32(nf.ID())
}
// Wrapped returns true if the cell will wrap text.
func (cs CellStyle) Wrapped() bool {
if cs.xf.Alignment == nil {

View File

@ -7,74 +7,42 @@
package spreadsheet
// Extracted from ECMA-376 Part 1 Section 18.8.30
/*
0 General
1 0
2 0.00
3 #,##0
4 #,##0.00
9 0%
10 0.00%
11 0.00E+00
12 # ?/?
13 # ??/??
14 mm-dd-yy
15 d-mmm-yy
16 d-mmm
17 mmm-yy
18 h:mm AM/PM
19 h:mm:ss AM/PM
20 h:mm
21 h:mm:ss
22 m/d/yy h:mm
37 #,##0 ;(#,##0)
38 #,##0 ;[Red](#,##0)
39 #,##0.00;(#,##0.00)
40 #,##0.00;[Red](#,##0.00)
45 mm:ss
46 [h]:mm:ss
47 mmss.0
48 ##0.0E+0
49 @
*/
// StandardFormat is a standard ECMA 376 number format.
type StandardFormat uint32
// StandardFormat constants
// StandardFormat constants, extracted from ECMA-376 Part 1 Section 18.8.30
const (
StandardFormatGeneral StandardFormat = 0
StandardFormat0 StandardFormat = 0
StandardFormat1 StandardFormat = 1
StandardFormat2 StandardFormat = 2
StandardFormat3 StandardFormat = 3
StandardFormat4 StandardFormat = 4
StandardFormatPercent StandardFormat = 9
StandardFormat9 StandardFormat = 9
StandardFormat10 StandardFormat = 10
StandardFormat11 StandardFormat = 11
StandardFormat12 StandardFormat = 12
StandardFormat13 StandardFormat = 13
StandardFormatDate StandardFormat = 14
StandardFormat14 StandardFormat = 14
StandardFormat15 StandardFormat = 15
StandardFormat16 StandardFormat = 16
StandardFormat17 StandardFormat = 17
StandardFormat18 StandardFormat = 18
StandardFormatTime StandardFormat = 19
StandardFormat19 StandardFormat = 19
StandardFormat20 StandardFormat = 20
StandardFormat21 StandardFormat = 21
StandardFormatDateTime StandardFormat = 22
StandardFormat22 StandardFormat = 22
StandardFormat37 StandardFormat = 37
StandardFormat38 StandardFormat = 38
StandardFormat39 StandardFormat = 39
StandardFormat40 StandardFormat = 40
StandardFormat45 StandardFormat = 45
StandardFormat46 StandardFormat = 46
StandardFormat47 StandardFormat = 47
StandardFormat48 StandardFormat = 48
StandardFormat49 StandardFormat = 49
StandardFormatGeneral StandardFormat = 0 // General
StandardFormat0 StandardFormat = 0 // General
StandardFormat1 StandardFormat = 1 // 0
StandardFormat2 StandardFormat = 2 // 0.00
StandardFormat3 StandardFormat = 3 // #,##0
StandardFormat4 StandardFormat = 4 // #,##0.00
StandardFormatPercent StandardFormat = 9 // 0%
StandardFormat9 StandardFormat = 9 // 0%
StandardFormat10 StandardFormat = 10 // 0.00%
StandardFormat11 StandardFormat = 11 // 0.00E+00
StandardFormat12 StandardFormat = 12 // # ?/?
StandardFormat13 StandardFormat = 13 // # ??/??
StandardFormatDate StandardFormat = 14 // mm-dd-yy
StandardFormat14 StandardFormat = 14 // mm-dd-yy
StandardFormat15 StandardFormat = 15 // d-mmm-yy
StandardFormat16 StandardFormat = 16 // d-mmm
StandardFormat17 StandardFormat = 17 // mmm-yy
StandardFormat18 StandardFormat = 18 // h:mm AM/PM
StandardFormatTime StandardFormat = 19 // h:mm:ss AM/PM
StandardFormat19 StandardFormat = 19 // h:mm:ss AM/PM
StandardFormat20 StandardFormat = 20 // h:mm
StandardFormat21 StandardFormat = 21 // h:mm:ss
StandardFormatDateTime StandardFormat = 22 // m/d/yy h:mm
StandardFormat22 StandardFormat = 22 // m/d/yy h:mm
StandardFormat37 StandardFormat = 37 // #,##0 ;(#,##0)
StandardFormat38 StandardFormat = 38 // #,##0 ;[Red](#,##0)
StandardFormat39 StandardFormat = 39 // #,##0.00;(#,##0.00)
StandardFormat40 StandardFormat = 40 // #,##0.00;[Red](#,##0.00)
StandardFormat45 StandardFormat = 45 // mm:ss
StandardFormat46 StandardFormat = 46 // [h]:mm:ss
StandardFormat47 StandardFormat = 47 // mmss.0
StandardFormat48 StandardFormat = 48 // ##0.0E+0
StandardFormat49 StandardFormat = 49 // @
)

View File

@ -21,7 +21,7 @@ func New() *Workbook {
wb.AppProperties = common.NewAppProperties()
wb.CoreProperties = common.NewCoreProperties()
wb.StyleSheet = NewStyleSheet()
wb.StyleSheet = NewStyleSheet(wb)
wb.Rels = common.NewRelationships()
wb.wbRels = common.NewRelationships()

View File

@ -0,0 +1,34 @@
// Copyright 2017 Baliance. All rights reserved.
//
// Use of this source code is governed by the terms of the Affero GNU General
// Public License version 3.0 as published by the Free Software Foundation and
// appearing in the file LICENSE included in the packaging of this file. A
// commercial license can be purchased by contacting sales@baliance.com.
package spreadsheet
import sml "baliance.com/gooxml/schema/schemas.openxmlformats.org/spreadsheetml"
// NumberFormat is a number formatting string that can be applied to a cell
// style.
type NumberFormat struct {
wb *Workbook
x *sml.CT_NumFmt
}
// X returns the inner wrapped XML type.
func (n NumberFormat) X() *sml.CT_NumFmt {
return n.x
}
// SetCode sets the number format code.
func (n NumberFormat) SetCode(f string) {
n.x.FormatCodeAttr = f
}
// ID returns the number format ID. This is not an index as there are some
// predefined number formats which can be used in cell styles and don't need a
// corresponding NumberFormat.
func (n NumberFormat) ID() uint32 {
return n.x.NumFmtIdAttr
}

View File

@ -16,11 +16,12 @@ import (
// StyleSheet is a document style sheet.
type StyleSheet struct {
x *sml.StyleSheet
wb *Workbook
x *sml.StyleSheet
}
// NewStyleSheet constructs a new default stylesheet.
func NewStyleSheet() StyleSheet {
func NewStyleSheet(wb *Workbook) StyleSheet {
ss := sml.NewStyleSheet()
b := NewBorders()
ss.Borders = b.X()
@ -52,7 +53,7 @@ func NewStyleSheet() StyleSheet {
ss.Fonts = sml.NewCT_Fonts()
s := StyleSheet{ss}
s := StyleSheet{wb, ss}
fnt := s.AddFont()
fnt.SetName("Calibri")
fnt.SetSize(11)
@ -66,7 +67,7 @@ func NewStyleSheet() StyleSheet {
return s
}
// X returns the innter XML entity for a stylesheet.
// X returns the inner XML entity for a stylesheet.
func (s StyleSheet) X() *sml.StyleSheet {
return s.x
}
@ -106,10 +107,61 @@ func (s StyleSheet) AddCellStyle() CellStyle {
xf := sml.NewCT_Xf()
s.x.CellXfs.Xf = append(s.x.CellXfs.Xf, xf)
s.x.CellXfs.CountAttr = gooxml.Uint32(uint32(len(s.x.CellXfs.Xf)))
return CellStyle{xf, s.x.CellXfs}
return CellStyle{s.wb, xf, s.x.CellXfs}
}
// CellStyles returns the list of defined cell styles
func (s StyleSheet) CellStyles() []CellStyle {
ret := []CellStyle{}
for _, xf := range s.x.CellXfs.Xf {
ret = append(ret, CellStyle{s.wb, xf, s.x.CellXfs})
}
return ret
}
// GetOrCreateStandardNumberFormat gets or creates a cell style with a given
// standard format. This should only be used when you want to perform
// number/date/time formatting only. Manipulating the style returned will cause
// all cells using style returned from this for a given format to be formatted.
func (s StyleSheet) GetOrCreateStandardNumberFormat(f StandardFormat) CellStyle {
for _, cs := range s.CellStyles() {
// found an existing number format
if cs.HasNumberFormat() && cs.NumberFormat() == uint32(f) {
return cs
}
}
// need to create a new format
cs := s.AddCellStyle()
cs.SetNumberFormatStandard(f)
return cs
}
// AddNumberFormat adds a new blank number format to the stylesheet.
func (s StyleSheet) AddNumberFormat() NumberFormat {
if s.x.NumFmts == nil {
s.x.NumFmts = sml.NewCT_NumFmts()
}
nf := sml.NewCT_NumFmt()
// start our IDs at 200 so we can ensure we don't conflict with any
// pre-defined formats
nf.NumFmtIdAttr = uint32(200 + len(s.x.NumFmts.NumFmt))
s.x.NumFmts.NumFmt = append(s.x.NumFmts.NumFmt, nf)
s.x.NumFmts.CountAttr = gooxml.Uint32(uint32(len(s.x.NumFmts.NumFmt)))
return NumberFormat{s.wb, nf}
}
// Fills returns a Fills object that can be used to add/create/edit fills.
func (s StyleSheet) Fills() Fills {
return Fills{s.x.Fills}
}
/*
func (s StyleSheet) NumberFormats() {
if s.x.NumFmts == nil {
return
}
for _, nfmt := range s.x.NumFmts.NumFmt {
}
}
*/

View File

@ -25,7 +25,7 @@ func TestStyleSheetUnmarshal(t *testing.T) {
t.Fatalf("error reading styles.xml")
}
dec := xml.NewDecoder(f)
r := spreadsheet.NewStyleSheet()
r := spreadsheet.NewStyleSheet(nil)
if err := dec.Decode(r.X()); err != nil {
t.Errorf("error decoding styles.xml: %s", err)
}
@ -40,7 +40,7 @@ func TestStyleSheetUnmarshal(t *testing.T) {
}
func TestStyleSheetFonts(t *testing.T) {
ss := spreadsheet.NewStyleSheet()
ss := spreadsheet.NewStyleSheet(nil)
fc := len(ss.Fonts())
ft := ss.AddFont()

View File

@ -267,7 +267,7 @@ func (wb *Workbook) onNewRelationship(decMap *zippkg.DecodeMap, target, typ stri
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.xws))
case gooxml.StylesType:
wb.StyleSheet = NewStyleSheet()
wb.StyleSheet = NewStyleSheet(wb)
decMap.AddTarget(target, wb.StyleSheet.X())
case gooxml.ThemeType: