Skip to content

Commit

Permalink
Functions to copy and remove sheets (#281)
Browse files Browse the repository at this point in the history
* Add funcs to remove sheet from workbook
- RemoveSheet added to remove sheet by index
- RemoveSheetByName added to remove sheet by its name
* Add `CopyRelationship` func to `Relationships`
* Add funcs `CopySheet`, `CopySheetByName`
- `CopySheet` copies sheet at the specified index
- `CopySheetByName` copies sheet with the specified name
* Make `CopyRelationship` return copied rel and bool flag
* Add func `CopyOverride` to `ContentTypes`
* Add test for `CopyRelationship`
* Add tests for sheet removing funcs
* Add test for `CopyOverride`
* Add tests for `CopySheet`, `CopySheetByName`
  • Loading branch information
nkryuchkov authored and gunnsth committed May 12, 2019
1 parent 06b1c96 commit 1783b65
Show file tree
Hide file tree
Showing 7 changed files with 346 additions and 0 deletions.
21 changes: 21 additions & 0 deletions common/contenttypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,24 @@ func (c ContentTypes) RemoveOverride(path string) {
}
}
}

// CopyOverride copies override content type for a given `path` and puts it with a path `newPath`.
func (c ContentTypes) CopyOverride(path, newPath string) {
if !strings.HasPrefix(path, "/") {
path = "/" + path
}

if !strings.HasPrefix(newPath, "/") {
newPath = "/" + newPath
}

for i := range c.x.Override {
if c.x.Override[i].PartNameAttr == path {
copied := *c.x.Override[i]

copied.PartNameAttr = newPath

c.x.Override = append(c.x.Override, &copied)
}
}
}
19 changes: 19 additions & 0 deletions common/contenttypes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,22 @@ func TestContentTypesUnmarshal(t *testing.T) {

testhelper.CompareGoldenXML(t, "contenttypes.xml", got.Bytes())
}

func TestCopyOverride(t *testing.T) {
ct := common.NewContentTypes()
ct.AddOverride("/foo/bar.xml", "application/xml")

lenBefore := len(ct.X().Override)

ct.CopyOverride("foo/bar.xml", "foo/bar2.xml")

if len(ct.X().Override) != (lenBefore + 1) {
t.Errorf("expected override len %d, got %d", lenBefore+1, len(ct.X().Override))
}

copyIdx := len(ct.X().Override) - 1

if ct.X().Override[copyIdx].PartNameAttr != "/foo/bar2.xml" {
t.Errorf("expected \"/foo/bar2.xml\" PartNameAttr, go %s", ct.X().Override[copyIdx].PartNameAttr)
}
}
35 changes: 35 additions & 0 deletions common/relationships.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ func NewRelationships() Relationships {
return Relationships{x: relationships.NewRelationships()}
}

// NewRelationshipsCopy creates a new relationships wrapper as a copy of passed in instance.
func NewRelationshipsCopy(rels Relationships) Relationships {
copiedBody := *rels.x
return Relationships{x: &copiedBody}
}

// X returns the underlying raw XML data.
func (r Relationships) X() *relationships.Relationships {
return r.x
Expand Down Expand Up @@ -97,6 +103,35 @@ func (r Relationships) Remove(rel Relationship) bool {
return false
}

// CopyRelationship copies the relationship.
func (r Relationships) CopyRelationship(idAttr string) (Relationship, bool) {
for i := range r.x.Relationship {
if r.x.Relationship[i].IdAttr == idAttr {
copied := *r.x.Relationship[i]

nextID := len(r.x.Relationship) + 1
used := map[string]struct{}{}

// identify IDs in use
for _, exRel := range r.x.Relationship {
used[exRel.IdAttr] = struct{}{}
}
// find the next ID that is unused
for _, ok := used[fmt.Sprintf("rId%d", nextID)]; ok; _, ok = used[fmt.Sprintf("rId%d", nextID)] {
nextID++
}

copied.IdAttr = fmt.Sprintf("rId%d", nextID)

r.x.Relationship = append(r.x.Relationship, &copied)

return Relationship{&copied}, true
}
}

return Relationship{}, false
}

// Hyperlink is just an appropriately configured relationship.
type Hyperlink Relationship

Expand Down
29 changes: 29 additions & 0 deletions common/relationships_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,32 @@ func TestRelationshipsRemoval(t *testing.T) {
t.Errorf("expected 0, got %d", len(r.Relationships()))
}
}

func TestCopyRelationship(t *testing.T) {
r := common.NewRelationships()
r.AddRelationship("foo1", "http://bar")
r.AddRelationship("foo2", "http://bar")
r.AddRelationship("foo3", "http://bar")

if len(r.Relationships()) != 3 {
t.Errorf("expected 3, got %d", len(r.Relationships()))
}

copied, ok := r.CopyRelationship(r.Relationships()[1].ID())
if !ok {
t.Errorf("expected true, got %v", ok)
}

if len(r.Relationships()) != 4 {
t.Errorf("expected 4, got %d", len(r.Relationships()))
}

if got := copied.Target(); got != "foo2" {
t.Errorf("expected foo2, got %s", got)
}

_, ok = r.CopyRelationship("qweqwe")
if ok {
t.Errorf("expected false, got %v", ok)
}
}
Binary file added spreadsheet/testdata/sheets.xlsx
Binary file not shown.
128 changes: 128 additions & 0 deletions spreadsheet/workbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,134 @@ func (wb *Workbook) AddSheet() Sheet {
return Sheet{wb, rs, ws}
}

// RemoveSheet removes the sheet with the given index from the workbook.
func (wb *Workbook) RemoveSheet(ind int) error {
if wb.SheetCount() <= ind {
return ErrorNotFound
}

for _, r := range wb.wbRels.Relationships() {
if r.ID() == wb.x.Sheets.Sheet[ind].IdAttr {
wb.wbRels.Remove(r)
break
}
}

wb.ContentTypes.RemoveOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet,
unioffice.WorksheetContentType, ind+1))

copy(wb.xws[ind:], wb.xws[ind+1:])
wb.xws = wb.xws[:len(wb.xws)-1]

removed := wb.x.Sheets.Sheet[ind]

copy(wb.x.Sheets.Sheet[ind:], wb.x.Sheets.Sheet[ind+1:])
wb.x.Sheets.Sheet = wb.x.Sheets.Sheet[:len(wb.x.Sheets.Sheet)-1]

// fix sheet IDs by decrementing each one after the removed sheet
for i := range wb.x.Sheets.Sheet {
if wb.x.Sheets.Sheet[i].SheetIdAttr > removed.SheetIdAttr {
wb.x.Sheets.Sheet[i].SheetIdAttr--
}
}

copy(wb.xwsRels[ind:], wb.xwsRels[ind+1:])
wb.xwsRels = wb.xwsRels[:len(wb.xwsRels)-1]

copy(wb.comments[ind:], wb.comments[ind+1:])
wb.comments = wb.comments[:len(wb.comments)-1]

return nil
}

// RemoveSheetByName removes the sheet with the given name from the workbook.
func (wb *Workbook) RemoveSheetByName(name string) error {
sheetInd := -1
for i, s := range wb.Sheets() {
if name == s.Name() {
sheetInd = i
break
}
}

if sheetInd == -1 {
return ErrorNotFound
}

return wb.RemoveSheet(sheetInd)
}

// CopySheet copies the existing sheet at index `ind` and puts its copy with the name `copiedSheetName`.
func (wb *Workbook) CopySheet(ind int, copiedSheetName string) (Sheet, error) {
if wb.SheetCount() <= ind {
return Sheet{}, ErrorNotFound
}

var copiedRel common.Relationship
for _, r := range wb.wbRels.Relationships() {
if r.ID() == wb.x.Sheets.Sheet[ind].IdAttr {
var ok bool
if copiedRel, ok = wb.wbRels.CopyRelationship(r.ID()); !ok {
return Sheet{}, ErrorNotFound
}

break
}
}

wb.ContentTypes.CopyOverride(unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet,
unioffice.WorksheetContentType, ind+1), unioffice.AbsoluteFilename(unioffice.DocTypeSpreadsheet,
unioffice.WorksheetContentType, len(wb.ContentTypes.X().Override)))

copiedWs := *wb.xws[ind]
wb.xws = append(wb.xws, &copiedWs)

var nextSheetID uint32 = 0
for _, s := range wb.x.Sheets.Sheet {
if s.SheetIdAttr > nextSheetID {
nextSheetID = s.SheetIdAttr
}
}
nextSheetID++

copiedSheet := *wb.x.Sheets.Sheet[ind]
copiedSheet.IdAttr = copiedRel.ID()
copiedSheet.NameAttr = copiedSheetName
copiedSheet.SheetIdAttr = nextSheetID

wb.x.Sheets.Sheet = append(wb.x.Sheets.Sheet, &copiedSheet)

copiedXwsRel := common.NewRelationshipsCopy(wb.xwsRels[ind])
wb.xwsRels = append(wb.xwsRels, copiedXwsRel)

copiedCommentsPtr := wb.comments[ind]
if copiedCommentsPtr == nil {
wb.comments = append(wb.comments, nil)
} else {
copiedComments := *copiedCommentsPtr
wb.comments = append(wb.comments, &copiedComments)
}

return Sheet{wb, &copiedSheet, &copiedWs}, nil
}

// CopySheetByName copies the existing sheet with the name `name` and puts its copy with the name `copiedSheetName`.
func (wb *Workbook) CopySheetByName(name, copiedSheetName string) (Sheet, error) {
sheetInd := -1
for i, s := range wb.Sheets() {
if name == s.Name() {
sheetInd = i
break
}
}

if sheetInd == -1 {
return Sheet{}, ErrorNotFound
}

return wb.CopySheet(sheetInd, copiedSheetName)
}

// SaveToFile writes the workbook out to a file.
func (wb *Workbook) SaveToFile(path string) error {
f, err := os.Create(path)
Expand Down
114 changes: 114 additions & 0 deletions spreadsheet/workbook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,117 @@ func TestOpenOrderedSheets(t *testing.T) {
}

}

func TestRemoveSheet(t *testing.T) {
wb, err := spreadsheet.Open("./testdata/sheets.xlsx")
defer wb.Close()
if err != nil {
t.Fatalf("error opening workbook: %s", err)
}

wasCount := wb.SheetCount()

if err := wb.RemoveSheet(15); err == nil {
t.Fatalf("invalid sheet index, expected error %v, got nil", spreadsheet.ErrorNotFound)
}

if err := wb.RemoveSheet(2); err != nil {
t.Fatalf("expected no error, got %v", err)
}

if err := wb.Validate(); err != nil {
t.Fatalf("produced invalid workbook: %v", err)
}

if wb.SheetCount() != (wasCount - 1) {
t.Fatalf("expected sheets count %d, got %d", wasCount-1, wb.SheetCount())
}
}

func TestRemoveSheetByName(t *testing.T) {
wb, err := spreadsheet.Open("./testdata/sheets.xlsx")
defer wb.Close()
if err != nil {
t.Fatalf("error opening workbook: %s", err)
}

wasCount := wb.SheetCount()

if err := wb.RemoveSheetByName("Sheet156"); err == nil {
t.Fatalf("invalid sheet name, expected error %v, got nil", spreadsheet.ErrorNotFound)
}

if err := wb.RemoveSheetByName("Sheet2"); err != nil {
t.Fatalf("expected no error, got %v", err)
}

if err := wb.Validate(); err != nil {
t.Fatalf("produced invalid workbook: %v", err)
}

if wb.SheetCount() != (wasCount - 1) {
t.Fatalf("expected sheets count %d, got %d", wasCount-1, wb.SheetCount())
}
}

func TestCopySheet(t *testing.T) {
wb, err := spreadsheet.Open("./testdata/sheets.xlsx")
defer wb.Close()
if err != nil {
t.Fatalf("error opening workbook: %s", err)
}

wasCount := wb.SheetCount()

if _, err := wb.CopySheet(15, "Copied Sheet"); err == nil {
t.Fatalf("invalid sheet index, expected error %v, got nil", spreadsheet.ErrorNotFound)
}

copiedSheet, err := wb.CopySheet(1, "Copied Sheet")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

if err := wb.Validate(); err != nil {
t.Fatalf("produced invalid workbook: %v", err)
}

if copiedSheet.Name() != "Copied Sheet" {
t.Fatalf("invalid name in the copied sheet, expected \"Copied Sheet\", got \"%s\"", copiedSheet.Name())
}

if wb.SheetCount() != (wasCount + 1) {
t.Fatalf("expected sheets count %d, got %d", wasCount+1, wb.SheetCount())
}
}

func TestCopySheetByName(t *testing.T) {
wb, err := spreadsheet.Open("./testdata/sheets.xlsx")
defer wb.Close()
if err != nil {
t.Fatalf("error opening workbook: %s", err)
}

wasCount := wb.SheetCount()

if _, err := wb.CopySheetByName("Sheet156", "Copied Sheet"); err == nil {
t.Fatalf("invalid sheet name, expected error %v, got nil", spreadsheet.ErrorNotFound)
}

copiedSheet, err := wb.CopySheetByName("Sheet2", "Copied Sheet")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}

if err := wb.Validate(); err != nil {
t.Fatalf("produced invalid workbook: %v", err)
}

if copiedSheet.Name() != "Copied Sheet" {
t.Fatalf("invalid name in the copied sheet, expected \"Copied Sheet\", got \"%s\"", copiedSheet.Name())
}

if wb.SheetCount() != (wasCount + 1) {
t.Fatalf("expected sheets count %d, got %d", wasCount+1, wb.SheetCount())
}
}

0 comments on commit 1783b65

Please sign in to comment.