Browse Source

Merge pull request #311 from thewraven/master

Read struct from row based on xlsx tags.
Geoffrey J. Teale 8 years ago
parent
commit
b1dcce149d
3 changed files with 411 additions and 0 deletions
  1. 36 0
      example_read_test.go
  2. 132 0
      read.go
  3. 243 0
      read_test.go

+ 36 - 0
example_read_test.go

@@ -0,0 +1,36 @@
+package xlsx
+
+import "fmt"
+
+func ExampleRow_ReadStruct() {
+	//example type
+	type structTest struct {
+		IntVal     int     `xlsx:"0"`
+		StringVal  string  `xlsx:"1"`
+		FloatVal   float64 `xlsx:"2"`
+		IgnoredVal int     `xlsx:"-"`
+		BoolVal    bool    `xlsx:"4"`
+	}
+	structVal := structTest{
+		IntVal:     16,
+		StringVal:  "heyheyhey :)!",
+		FloatVal:   3.14159216,
+		IgnoredVal: 7,
+		BoolVal:    true,
+	}
+	//create a new xlsx file and write a struct
+	//in a new row
+	f := NewFile()
+	sheet, _ := f.AddSheet("TestRead")
+	row := sheet.AddRow()
+	row.WriteStruct(&structVal, -1)
+
+	//read the struct from the same row
+	readStruct := &structTest{}
+	err := row.ReadStruct(readStruct)
+	if err != nil {
+		fmt.Println(readStruct)
+	} else {
+		panic(err)
+	}
+}

+ 132 - 0
read.go

@@ -0,0 +1,132 @@
+package xlsx
+
+import (
+	"errors"
+	"reflect"
+	"strconv"
+	"time"
+)
+
+var (
+	errNilInterface     = errors.New("nil pointer is not a valid argument")
+	errNotStructPointer = errors.New("argument must be a pointer to struct")
+	errInvalidTag       = errors.New(`invalid tag: must have the format xlsx:idx`)
+)
+
+//XLSXUnmarshaler is the interface implemented for types that can unmarshal a Row
+//as a representation of themselves.
+type XLSXUnmarshaler interface {
+	Unmarshal(*Row) error
+}
+
+//ReadStruct reads a struct from r to ptr. Accepts a ptr
+//to struct. This code expects a tag xlsx:"N", where N is the index
+//of the cell to be used. Basic types like int,string,float64 and bool
+//are supported
+func (r *Row) ReadStruct(ptr interface{}) error {
+	if ptr == nil {
+		return errNilInterface
+	}
+	//check if the type implements XLSXUnmarshaler. If so,
+	//just let it do the work.
+	unmarshaller, ok := ptr.(XLSXUnmarshaler)
+	if ok {
+		return unmarshaller.Unmarshal(r)
+	}
+	v := reflect.ValueOf(ptr)
+	if v.Kind() != reflect.Ptr {
+		return errNotStructPointer
+	}
+	v = v.Elem()
+	if v.Kind() != reflect.Struct {
+		return errNotStructPointer
+	}
+	n := v.NumField()
+	for i := 0; i < n; i++ {
+		field := v.Type().Field(i)
+		idx := field.Tag.Get("xlsx")
+		//do a recursive check for the field if it is a struct or a pointer
+		//even if it doesn't have a tag
+		//ignore if it has a - or empty tag
+		isTime := false
+		switch {
+		case idx == "-":
+			continue
+		case field.Type.Kind() == reflect.Ptr || field.Type.Kind() == reflect.Struct:
+			var structPtr interface{}
+			if !v.Field(i).CanSet() {
+				continue
+			}
+			if field.Type.Kind() == reflect.Struct {
+				structPtr = v.Field(i).Addr().Interface()
+			} else {
+				structPtr = v.Field(i).Interface()
+			}
+			//check if the container is a time.Time
+			_, isTime = structPtr.(*time.Time)
+			if isTime {
+				break
+			}
+			err := r.ReadStruct(structPtr)
+			if err != nil {
+				return err
+			}
+			continue
+		case len(idx) == 0:
+			continue
+		}
+		pos, err := strconv.Atoi(idx)
+		if err != nil {
+			return errInvalidTag
+		}
+
+		//check if desired position is not out of bounds
+		if pos > len(r.Cells)-1 {
+			continue
+		}
+		cell := r.Cells[pos]
+		fieldV := v.Field(i)
+		//continue if the field is not settable
+		if !fieldV.CanSet() {
+			continue
+		}
+		if isTime {
+			t, err := cell.GetTime(false)
+			if err != nil {
+				return err
+			}
+			if field.Type.Kind() == reflect.Ptr {
+				fieldV.Set(reflect.ValueOf(&t))
+			} else {
+				fieldV.Set(reflect.ValueOf(t))
+			}
+			continue
+		}
+		switch field.Type.Kind() {
+		case reflect.String:
+			value, err := cell.String()
+			if err != nil {
+				return err
+			}
+			fieldV.SetString(value)
+		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+			value, err := cell.Int64()
+			if err != nil {
+				return err
+			}
+			fieldV.SetInt(value)
+		case reflect.Float64:
+			value, err := cell.Float()
+			if err != nil {
+				return err
+			}
+			fieldV.SetFloat(value)
+		case reflect.Bool:
+			value := cell.Bool()
+			fieldV.SetBool(value)
+		}
+	}
+	value := v.Interface()
+	ptr = &value
+	return nil
+}

+ 243 - 0
read_test.go

@@ -0,0 +1,243 @@
+package xlsx
+
+import (
+	"errors"
+	"fmt"
+	"time"
+
+	. "gopkg.in/check.v1"
+)
+
+type ReadSuite struct{}
+
+var _ = Suite(&ReadSuite{})
+
+var (
+	errorNoPair         = errors.New("Integer to be unmarshaled is not a pair")
+	errorNotEnoughCells = errors.New("Row has not enough cells")
+)
+
+type pairUnmarshaler int
+
+func (i *pairUnmarshaler) Unmarshal(row *Row) error {
+	if len(row.Cells) == 0 {
+		return errorNotEnoughCells
+	}
+	cellInt, err := row.Cells[0].Int()
+	if err != nil {
+		return err
+	}
+	if cellInt%2 != 0 {
+		return errorNoPair
+	}
+	*i = pairUnmarshaler(cellInt)
+	return nil
+}
+
+type structUnmarshaler struct {
+	private bool
+	custom  string
+	normal  int
+}
+
+func (s *structUnmarshaler) Unmarshal(r *Row) error {
+	if len(r.Cells) < 3 {
+		return errorNotEnoughCells
+	}
+	s.private = r.Cells[0].Bool()
+	var err error
+	s.normal, err = r.Cells[2].Int()
+	if err != nil {
+		return err
+	}
+	currency, err := r.Cells[1].String()
+	if err != nil {
+		return err
+	}
+	s.custom = fmt.Sprintf("$ %s", currency)
+	return nil
+}
+
+func (r *RowSuite) TestInterface(c *C) {
+	var p pairUnmarshaler
+	var s structUnmarshaler
+	f := NewFile()
+	sheet, _ := f.AddSheet("TestReadTime")
+	row := sheet.AddRow()
+	values := []interface{}{1, "500", true}
+	row.WriteSlice(&values, -1)
+	errPair := row.ReadStruct(&p)
+	err := row.ReadStruct(&s)
+	c.Assert(errPair, Equals, errorNoPair)
+	c.Assert(err, Equals, nil)
+	var empty pairUnmarshaler
+	c.Assert(p, Equals, empty)
+	c.Assert(s.normal, Equals, 1)
+	c.Assert(s.private, Equals, true)
+	c.Assert(s.custom, Equals, "$ 500")
+}
+
+func (r *RowSuite) TestTime(c *C) {
+	type Timer struct {
+		Initial time.Time `xlsx:"0"`
+		Final   time.Time `xlsx:"1"`
+	}
+	initial := time.Date(1990, 12, 30, 10, 30, 30, 0, time.UTC)
+	t := Timer{
+		Initial: initial,
+		Final:   initial.Add(time.Hour * 24),
+	}
+	f := NewFile()
+	sheet, _ := f.AddSheet("TestReadTime")
+	row := sheet.AddRow()
+	row.AddCell().SetDateTime(t.Initial)
+	ctime2 := row.AddCell()
+	ctime2.SetDate(t.Final)
+	t2 := Timer{}
+	err := row.ReadStruct(&t2)
+	if err != nil {
+		c.Error(err)
+		c.FailNow()
+	}
+	//removing ns precition
+	t2.Initial = t2.Initial.Add(time.Duration(-1 * t2.Initial.Nanosecond()))
+	t2.Final = t2.Final.Add(time.Duration(-1 * t2.Final.Nanosecond()))
+	c.Assert(t2.Initial, Equals, t.Initial)
+	c.Assert(t2.Final, Equals, t.Final)
+}
+
+func (r *RowSuite) TestEmbedStruct(c *C) {
+	type Embed struct {
+		privateVal bool   `xlsx:"0"`
+		IgnoredVal int    `xlsx:"-"`
+		VisibleVal string `xlsx:"2"`
+	}
+	type structTest struct {
+		Embed
+		FinalVal string `xlsx:"3"`
+	}
+	f := NewFile()
+	sheet, _ := f.AddSheet("TestRead")
+	row := sheet.AddRow()
+	v := structTest{
+		Embed: Embed{
+			privateVal: true,
+			IgnoredVal: 10,
+			VisibleVal: "--This is a test value--",
+		},
+		FinalVal: "--end of struct",
+	}
+	values := []string{
+		fmt.Sprint(v.privateVal),
+		fmt.Sprint(v.IgnoredVal),
+		fmt.Sprint(v.VisibleVal),
+		fmt.Sprint(v.FinalVal),
+	}
+	row.WriteSlice(&values, -1)
+	for _, cell := range row.Cells {
+		v, _ := cell.String()
+		c.Log(v)
+	}
+	read := new(structTest)
+	err := row.ReadStruct(read)
+	if err != nil {
+		c.Error(err)
+		c.FailNow()
+	}
+	c.Assert(read.privateVal, Equals, false)
+	c.Assert(read.VisibleVal, Equals, v.VisibleVal)
+	c.Assert(read.IgnoredVal, Equals, 0)
+	c.Assert(read.FinalVal, Equals, v.FinalVal)
+}
+
+func (r *RowSuite) TestReadStructPrivateFields(c *C) {
+	type nested struct {
+		IgnoredVal int    `xlsx:"-"`
+		VisibleVal string `xlsx:"6"`
+		privateVal bool   `xlsx:"7"`
+	}
+	type structTest struct {
+		IntVal     int16   `xlsx:"0"`
+		StringVal  string  `xlsx:"1"`
+		FloatVal   float64 `xlsx:"2"`
+		IgnoredVal int     `xlsx:"-"`
+		BoolVal    bool    `xlsx:"4"`
+		Nested     nested
+	}
+	val := structTest{
+		IntVal:     16,
+		StringVal:  "heyheyhey :)!",
+		FloatVal:   3.14159216,
+		IgnoredVal: 7,
+		BoolVal:    true,
+		Nested: nested{
+			privateVal: true,
+			IgnoredVal: 90,
+			VisibleVal: "Hello",
+		},
+	}
+	writtenValues := []string{
+		fmt.Sprint(val.IntVal), val.StringVal, fmt.Sprint(val.FloatVal),
+		fmt.Sprint(val.IgnoredVal), fmt.Sprint(val.BoolVal),
+		fmt.Sprint(val.Nested.IgnoredVal), val.Nested.VisibleVal,
+		fmt.Sprint(val.Nested.privateVal),
+	}
+	f := NewFile()
+	sheet, _ := f.AddSheet("TestRead")
+	row := sheet.AddRow()
+	row.WriteSlice(&writtenValues, -1)
+	for i, cell := range row.Cells {
+		str, _ := cell.String()
+		c.Log(i, " ", str)
+	}
+	readStruct := structTest{}
+	err := row.ReadStruct(&readStruct)
+	if err != nil {
+		c.Error(err)
+		c.FailNow()
+	}
+	c.Assert(err, Equals, nil)
+	c.Assert(readStruct.IntVal, Equals, val.IntVal)
+	c.Assert(readStruct.StringVal, Equals, val.StringVal)
+	c.Assert(readStruct.IgnoredVal, Equals, 0)
+	c.Assert(readStruct.FloatVal, Equals, val.FloatVal)
+	c.Assert(readStruct.BoolVal, Equals, val.BoolVal)
+	c.Assert(readStruct.Nested.IgnoredVal, Equals, 0)
+	c.Assert(readStruct.Nested.VisibleVal, Equals, "Hello")
+	c.Assert(readStruct.Nested.privateVal, Equals, false)
+}
+
+func (r *RowSuite) TestReadStruct(c *C) {
+	type structTest struct {
+		IntVal     int8    `xlsx:"0"`
+		StringVal  string  `xlsx:"1"`
+		FloatVal   float64 `xlsx:"2"`
+		IgnoredVal int     `xlsx:"-"`
+		BoolVal    bool    `xlsx:"4"`
+	}
+	structVal := structTest{
+		IntVal:     10,
+		StringVal:  "heyheyhey :)!",
+		FloatVal:   3.14159216,
+		IgnoredVal: 7,
+		BoolVal:    true,
+	}
+	f := NewFile()
+	sheet, _ := f.AddSheet("TestRead")
+	row := sheet.AddRow()
+	row.WriteStruct(&structVal, -1)
+	for i, cell := range row.Cells {
+		str, _ := cell.String()
+		c.Log(i, " ", str)
+	}
+	readStruct := &structTest{}
+	err := row.ReadStruct(readStruct)
+	c.Log(readStruct)
+	c.Log(structVal)
+	c.Assert(err, Equals, nil)
+	c.Assert(readStruct.IntVal, Equals, structVal.IntVal)
+	c.Assert(readStruct.StringVal, Equals, structVal.StringVal)
+	c.Assert(readStruct.IgnoredVal, Equals, 0)
+	c.Assert(readStruct.FloatVal, Equals, structVal.FloatVal)
+	c.Assert(readStruct.BoolVal, Equals, structVal.BoolVal)
+}