Skip to content

Commit

Permalink
Fix qax-os#533, add support overlapped mergecells
Browse files Browse the repository at this point in the history
  • Loading branch information
xuri committed Dec 14, 2019
1 parent 4c433c5 commit da0d2ff
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 138 deletions.
3 changes: 0 additions & 3 deletions adjust.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,9 +206,6 @@ func (f *File) areaRefToCoordinates(ref string) ([]int, error) {
return coordinates, err
}
coordinates[2], coordinates[3], err = CellNameToCoordinates(lastCell)
if err != nil {
return coordinates, err
}
return coordinates, err
}

Expand Down
86 changes: 24 additions & 62 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,63 +412,6 @@ func (f *File) SetCellHyperLink(sheet, axis, link, linkType string) error {
return nil
}

// MergeCell provides a function to merge cells by given coordinate area and
// sheet name. For example create a merged cell of D3:E9 on Sheet1:
//
// err := f.MergeCell("Sheet1", "D3", "E9")
//
// If you create a merged cell that overlaps with another existing merged cell,
// those merged cells that already exist will be removed.
func (f *File) MergeCell(sheet, hcell, vcell string) error {
coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell)
if err != nil {
return err
}
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]

if x1 == x2 && y1 == y2 {
return err
}

// Correct the coordinate area, such correct C1:B3 to B1:C3.
if x2 < x1 {
x1, x2 = x2, x1
}

if y2 < y1 {
y1, y2 = y2, y1
}

hcell, _ = CoordinatesToCellName(x1, y1)
vcell, _ = CoordinatesToCellName(x2, y2)

xlsx, err := f.workSheetReader(sheet)
if err != nil {
return err
}
if xlsx.MergeCells != nil {
ref := hcell + ":" + vcell
// Delete the merged cells of the overlapping area.
for _, cellData := range xlsx.MergeCells.Cells {
cc := strings.Split(cellData.Ref, ":")
if len(cc) != 2 {
return fmt.Errorf("invalid area %q", cellData.Ref)
}
c1, _ := checkCellInArea(hcell, cellData.Ref)
c2, _ := checkCellInArea(vcell, cellData.Ref)
c3, _ := checkCellInArea(cc[0], ref)
c4, _ := checkCellInArea(cc[1], ref)
if !(!c1 && !c2 && !c3 && !c4) {
return nil
}
}
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref})
} else {
xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: hcell + ":" + vcell}}}
}
return err
}

// SetSheetRow writes an array to row by given worksheet name, starting
// coordinate and a pointer to array type 'slice'. For example, writes an
// array to row 6 start with the cell B6 on Sheet1:
Expand Down Expand Up @@ -601,7 +544,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error
axis = strings.ToUpper(axis)
if xlsx.MergeCells != nil {
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
ok, err := checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref)
ok, err := f.checkCellInArea(axis, xlsx.MergeCells.Cells[i].Ref)
if err != nil {
return axis, err
}
Expand All @@ -615,7 +558,7 @@ func (f *File) mergeCellsParser(xlsx *xlsxWorksheet, axis string) (string, error

// checkCellInArea provides a function to determine if a given coordinate is
// within an area.
func checkCellInArea(cell, area string) (bool, error) {
func (f *File) checkCellInArea(cell, area string) (bool, error) {
col, row, err := CellNameToCoordinates(cell)
if err != nil {
return false, err
Expand All @@ -625,11 +568,30 @@ func checkCellInArea(cell, area string) (bool, error) {
if len(rng) != 2 {
return false, err
}
coordinates, err := f.areaRefToCoordinates(area)
if err != nil {
return false, err
}

return cellInRef([]int{col, row}, coordinates), err
}

firstCol, firstRow, _ := CellNameToCoordinates(rng[0])
lastCol, lastRow, _ := CellNameToCoordinates(rng[1])
// cellInRef provides a function to determine if a given range is within an
// range.
func cellInRef(cell, ref []int) bool {
return cell[0] >= ref[0] && cell[0] <= ref[2] && cell[1] >= ref[1] && cell[1] <= ref[3]
}

return col >= firstCol && col <= lastCol && row >= firstRow && row <= lastRow, err
// isOverlap find if the given two rectangles overlap or not.
func isOverlap(rect1, rect2 []int) bool {
return cellInRef([]int{rect1[0], rect1[1]}, rect2) ||
cellInRef([]int{rect1[2], rect1[1]}, rect2) ||
cellInRef([]int{rect1[0], rect1[3]}, rect2) ||
cellInRef([]int{rect1[2], rect1[3]}, rect2) ||
cellInRef([]int{rect2[0], rect2[1]}, rect1) ||
cellInRef([]int{rect2[2], rect2[1]}, rect1) ||
cellInRef([]int{rect2[0], rect2[3]}, rect1) ||
cellInRef([]int{rect2[2], rect2[3]}, rect1)
}

// getSharedForumula find a cell contains the same formula as another cell,
Expand Down
37 changes: 8 additions & 29 deletions cell_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
)

func TestCheckCellInArea(t *testing.T) {
f := NewFile()
expectedTrueCellInAreaList := [][2]string{
{"c2", "A1:AAZ32"},
{"B9", "A1:B9"},
Expand All @@ -19,7 +20,7 @@ func TestCheckCellInArea(t *testing.T) {
for _, expectedTrueCellInArea := range expectedTrueCellInAreaList {
cell := expectedTrueCellInArea[0]
area := expectedTrueCellInArea[1]
ok, err := checkCellInArea(cell, area)
ok, err := f.checkCellInArea(cell, area)
assert.NoError(t, err)
assert.Truef(t, ok,
"Expected cell %v to be in area %v, got false\n", cell, area)
Expand All @@ -34,13 +35,17 @@ func TestCheckCellInArea(t *testing.T) {
for _, expectedFalseCellInArea := range expectedFalseCellInAreaList {
cell := expectedFalseCellInArea[0]
area := expectedFalseCellInArea[1]
ok, err := checkCellInArea(cell, area)
ok, err := f.checkCellInArea(cell, area)
assert.NoError(t, err)
assert.Falsef(t, ok,
"Expected cell %v not to be inside of area %v, but got true\n", cell, area)
}

ok, err := checkCellInArea("AA0", "Z0:AB1")
ok, err := f.checkCellInArea("A1", "A:B")
assert.EqualError(t, err, `cannot convert cell "A" to coordinates: invalid cell name "A"`)
assert.False(t, ok)

ok, err = f.checkCellInArea("AA0", "Z0:AB1")
assert.EqualError(t, err, `cannot convert cell "AA0" to coordinates: invalid cell name "AA0"`)
assert.False(t, ok)
}
Expand Down Expand Up @@ -94,32 +99,6 @@ func TestGetCellFormula(t *testing.T) {
f.GetCellFormula("Sheet", "A1")
}

func TestMergeCell(t *testing.T) {
f, err := OpenFile(filepath.Join("test", "Book1.xlsx"))
if !assert.NoError(t, err) {
t.FailNow()
}
assert.EqualError(t, f.MergeCell("Sheet1", "A", "B"), `cannot convert cell "A" to coordinates: invalid cell name "A"`)
f.MergeCell("Sheet1", "D9", "D9")
f.MergeCell("Sheet1", "D9", "E9")
f.MergeCell("Sheet1", "H14", "G13")
f.MergeCell("Sheet1", "C9", "D8")
f.MergeCell("Sheet1", "F11", "G13")
f.MergeCell("Sheet1", "H7", "B15")
f.MergeCell("Sheet1", "D11", "F13")
f.MergeCell("Sheet1", "G10", "K12")
f.SetCellValue("Sheet1", "G11", "set value in merged cell")
f.SetCellInt("Sheet1", "H11", 100)
f.SetCellValue("Sheet1", "I11", float64(0.5))
f.SetCellHyperLink("Sheet1", "J11", "https://github.com/360EntSecGroup-Skylar/excelize", "External")
f.SetCellFormula("Sheet1", "G12", "SUM(Sheet1!B19,Sheet1!C19)")
f.GetCellValue("Sheet1", "H11")
f.GetCellValue("Sheet2", "A6") // Merged cell ref is single coordinate.
f.GetCellFormula("Sheet1", "G12")

assert.NoError(t, f.SaveAs(filepath.Join("test", "TestMergeCell.xlsx")))
}

func ExampleFile_SetCellFloat() {
f := NewFile()
var x = 3.14159265
Expand Down
159 changes: 130 additions & 29 deletions cellmerged.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,102 @@

package excelize

import "strings"
import (
"fmt"
"strings"
)

// MergeCell provides a function to merge cells by given coordinate area and
// sheet name. For example create a merged cell of D3:E9 on Sheet1:
//
// err := f.MergeCell("Sheet1", "D3", "E9")
//
// If you create a merged cell that overlaps with another existing merged cell,
// those merged cells that already exist will be removed.
//
// B1(x1,y1) D1(x2,y1)
// +--------------------------------+
// | |
// | |
// A4(x3,y3) | C4(x4,y3) |
// +-----------------------------+ |
// | | | |
// | | | |
// | |B5(x1,y2) | D5(x2,y2)|
// | +--------------------------------+
// | |
// | |
// |A8(x3,y4) C8(x4,y4)|
// +-----------------------------+
//
func (f *File) MergeCell(sheet, hcell, vcell string) error {
rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell)
if err != nil {
return err
}
// Correct the coordinate area, such correct C1:B3 to B1:C3.
if rect1[2] < rect1[0] {
rect1[0], rect1[2] = rect1[2], rect1[0]
}

if rect1[3] < rect1[1] {
rect1[1], rect1[3] = rect1[3], rect1[1]
}

hcell, _ = CoordinatesToCellName(rect1[0], rect1[1])
vcell, _ = CoordinatesToCellName(rect1[2], rect1[3])

// GetMergeCells provides a function to get all merged cells from a worksheet
// currently.
func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
var mergeCells []MergeCell
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return mergeCells, err
return err
}
ref := hcell + ":" + vcell
if xlsx.MergeCells != nil {
mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells))
for i := 0; i < len(xlsx.MergeCells.Cells); i++ {
cellData := xlsx.MergeCells.Cells[i]
if cellData == nil {
continue
}
cc := strings.Split(cellData.Ref, ":")
if len(cc) != 2 {
return fmt.Errorf("invalid area %q", cellData.Ref)
}

for i := range xlsx.MergeCells.Cells {
ref := xlsx.MergeCells.Cells[i].Ref
axis := strings.Split(ref, ":")[0]
val, _ := f.GetCellValue(sheet, axis)
mergeCells = append(mergeCells, []string{ref, val})
rect2, err := f.areaRefToCoordinates(cellData.Ref)
if err != nil {
return err
}

// Delete the merged cells of the overlapping area.
if isOverlap(rect1, rect2) {
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells[:i], xlsx.MergeCells.Cells[i+1:]...)
i--

if rect1[0] > rect2[0] {
rect1[0], rect2[0] = rect2[0], rect1[0]
}

if rect1[2] < rect2[2] {
rect1[2], rect2[2] = rect2[2], rect1[2]
}

if rect1[1] > rect2[1] {
rect1[1], rect2[1] = rect2[1], rect1[1]
}

if rect1[3] < rect2[3] {
rect1[3], rect2[3] = rect2[3], rect1[3]
}
hcell, _ = CoordinatesToCellName(rect1[0], rect1[1])
vcell, _ = CoordinatesToCellName(rect1[2], rect1[3])
ref = hcell + ":" + vcell
}
}
xlsx.MergeCells.Cells = append(xlsx.MergeCells.Cells, &xlsxMergeCell{Ref: ref})
} else {
xlsx.MergeCells = &xlsxMergeCells{Cells: []*xlsxMergeCell{{Ref: ref}}}
}

return mergeCells, err
return err
}

// UnmergeCell provides a function to unmerge a given coordinate area.
Expand All @@ -44,36 +118,41 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error {
if err != nil {
return err
}
coordinates, err := f.areaRefToCoordinates(hcell + ":" + vcell)
rect1, err := f.areaRefToCoordinates(hcell + ":" + vcell)
if err != nil {
return err
}
x1, y1, x2, y2 := coordinates[0], coordinates[1], coordinates[2], coordinates[3]

if x2 < x1 {
x1, x2 = x2, x1
if rect1[2] < rect1[0] {
rect1[0], rect1[2] = rect1[2], rect1[0]
}
if y2 < y1 {
y1, y2 = y2, y1
if rect1[3] < rect1[1] {
rect1[1], rect1[3] = rect1[3], rect1[1]
}
hcell, _ = CoordinatesToCellName(x1, y1)
vcell, _ = CoordinatesToCellName(x2, y2)
hcell, _ = CoordinatesToCellName(rect1[0], rect1[1])
vcell, _ = CoordinatesToCellName(rect1[2], rect1[3])

// return nil since no MergeCells in the sheet
if xlsx.MergeCells == nil {
return nil
}

ref := hcell + ":" + vcell
i := 0
for _, cellData := range xlsx.MergeCells.Cells {
if cellData == nil {
continue
}
cc := strings.Split(cellData.Ref, ":")
c1, _ := checkCellInArea(hcell, cellData.Ref)
c2, _ := checkCellInArea(vcell, cellData.Ref)
c3, _ := checkCellInArea(cc[0], ref)
c4, _ := checkCellInArea(cc[1], ref)
// skip the overlapped mergecell
if c1 || c2 || c3 || c4 {
if len(cc) != 2 {
return fmt.Errorf("invalid area %q", cellData.Ref)
}

rect2, err := f.areaRefToCoordinates(cellData.Ref)
if err != nil {
return err
}

if isOverlap(rect1, rect2) {
continue
}
xlsx.MergeCells.Cells[i] = cellData
Expand All @@ -83,6 +162,28 @@ func (f *File) UnmergeCell(sheet string, hcell, vcell string) error {
return nil
}

// GetMergeCells provides a function to get all merged cells from a worksheet
// currently.
func (f *File) GetMergeCells(sheet string) ([]MergeCell, error) {
var mergeCells []MergeCell
xlsx, err := f.workSheetReader(sheet)
if err != nil {
return mergeCells, err
}
if xlsx.MergeCells != nil {
mergeCells = make([]MergeCell, 0, len(xlsx.MergeCells.Cells))

for i := range xlsx.MergeCells.Cells {
ref := xlsx.MergeCells.Cells[i].Ref
axis := strings.Split(ref, ":")[0]
val, _ := f.GetCellValue(sheet, axis)
mergeCells = append(mergeCells, []string{ref, val})
}
}

return mergeCells, err
}

// MergeCell define a merged cell data.
// It consists of the following structure.
// example: []string{"D4:E10", "cell value"}
Expand Down
Loading

0 comments on commit da0d2ff

Please sign in to comment.