Skip to content

Commit

Permalink
Add range with traversal
Browse files Browse the repository at this point in the history
  • Loading branch information
chonla committed Nov 9, 2020
1 parent a343002 commit 3573d27
Show file tree
Hide file tree
Showing 10 changed files with 490 additions and 156 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Cell Walker is a go package for virtually traversing Excel cell by cell's name.

## Example

```
```go
package main

import (
Expand All @@ -14,7 +14,30 @@ import (
)

func main() {
// Walk from a cell to other cell
fmt.Println(cellwalker.At("B3").Right().Below().String()) // C4

// Jump from a cell to other cell
fmt.Println(cellwalker.At("C2").ColumnOffset(5).RowOffset(10).String()) // H12

// Too far jump from a cell to other cell will hit the limit of boundary
fmt.Println(cellwalker.At("ZZZ2").ColumnOffset(5).RowOffset(10).String()) // XFD12

// Range walking apply other boundary to walker
cellwalker.Within("C2:H3").At("C3").Right().Below().String()) // C4

// Too far jump in a new boundary
cellwalker.Within("C2:H3").At("ZZZ2").ColumnOffset(5).RowOffset(10).String()) // H3

// Range traversal
result1 := cellwalker.Within("B3:E5").At("C4") // Define range and initial cell position
result2 := result1.Tour() // result2 = D4
result3 := result2.Tour() // result3 = E4
result4 := result3.Tour() // result4 = B5
result5 := result4.Tour() // result5 = C5
result6 := result5.Tour() // result6 = D5
result7 := result6.Tour() // result7 = E5
result8 := result7.Tour() // nil
}
```

Expand Down
69 changes: 69 additions & 0 deletions cell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package cellwalker

import (
"fmt"
"regexp"
"strconv"
"strings"
)

// Cell represents a cell in Excel
type Cell struct {
column int
row int
}

func newCell(col, row int) *Cell {
if col > ColumnsLimit {
col = ColumnsLimit
} else {
if col < 1 {
col = 1
}
}
if row < 1 {
row = 1
} else {
if row > RowsLimit {
row = RowsLimit
}
}
return &Cell{
column: col,
row: row,
}
}

func newCellByID(cellID string) *Cell {
cleanCellID := strings.ToUpper(cellID)
re := regexp.MustCompile(`^([A-Z]+)([0-9]*)$`)
match := re.FindStringSubmatch(cleanCellID)

col := match[1]
row, err := strconv.ParseInt(fmt.Sprintf("0%s", match[2]), 10, 32)
if err != nil || row == 0 {
row = 1
}

return newCell(ColumnNameToIndex(col), int(row))
}

// String representation of Cell
func (c *Cell) String() string {
return fmt.Sprintf("%s%d", ColumnIndexToName(c.column), c.row)
}

// Clone creates a copy of Cell
func (c *Cell) Clone() *Cell {
return newCell(c.column, c.row)
}

// ColumnIndex returns column number
func (c *Cell) ColumnIndex() int {
return c.column
}

// RowIndex returns row number
func (c *Cell) RowIndex() int {
return c.row
}
13 changes: 13 additions & 0 deletions cell_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cellwalker

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestCreateCell(t *testing.T) {
result := newCell(1, 1).String()

assert.Equal(t, result, "A1")
}
191 changes: 90 additions & 101 deletions cellwalker.go
Original file line number Diff line number Diff line change
@@ -1,168 +1,157 @@
package cellwalker

import (
"fmt"
"math"
"regexp"
"strconv"
"strings"
)

// RowsLimit https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3
const (
RowsLimit = 1048576
ColumnsLimit = 16384
)

// CellWalker struct
type CellWalker struct {
column int
row int
position *Cell
boundary *Range
}

func newCellWalker(col int, row int) *CellWalker {
if col > ColumnsLimit {
col = ColumnsLimit
} else {
if col < 1 {
col = 1
}
}
if row < 1 {
row = 1
} else {
if row > RowsLimit {
row = RowsLimit
}
}
func newCellWalker(cell *Cell, boundary *Range) *CellWalker {
return &CellWalker{
column: col,
row: row,
position: cell.Clone(),
boundary: boundary.Clone(),
}
}

// At initializes CellWalker by specify initial cell to start
func At(cellID string) *CellWalker {
cleanCellID := strings.ToUpper(cellID)
re := regexp.MustCompile(`^([A-Z]+)([0-9]*)$`)
match := re.FindStringSubmatch(cleanCellID)

col := match[1]
row, err := strconv.ParseInt(fmt.Sprintf("0%s", match[2]), 10, 32)
if err != nil || row == 0 {
row = 1
}

return newCellWalker(ColumnNameToIndex(col), int(row))
return newCellWalker(newCellByID(cellID), Sheet())
}

// ColumnIndexToName converts column index to default excel name
func ColumnIndexToName(id int) string {
name := ""
dividend := id
modulo := 0

for dividend > 0 {
modulo = (dividend - 1) % 26
name = fmt.Sprintf("%c%s", rune(modulo+'A'), name)
dividend = (dividend - modulo) / 26
}
return name
}

// ColumnNameToIndex converts default excel column name to index, 1-based index
// name must be uppercase start from A, B, C, ..., Z, AA, AB, ... ZZ, AAA, ..., ZZZ, ...
func ColumnNameToIndex(name string) int {
index := 0
for colCharIndex, colCharLen := 0, len(name); colCharIndex < colCharLen; colCharIndex++ {
charNum := int(name[colCharIndex]-'A') + 1
digitNum := (colCharLen - (colCharIndex + 1))
columnWeight := int(math.Pow(26.0, float64(digitNum)))
columnValue := charNum * columnWeight
index += columnValue
}
return index
func (c *CellWalker) String() string {
return c.position.String()
}

// String representation of Cell
func (c *CellWalker) String() string {
return fmt.Sprintf("%s%d", ColumnIndexToName(c.column), c.row)
// Clone creates a clone of cellwalker
func (c *CellWalker) Clone() *CellWalker {
return &CellWalker{
position: c.position,
boundary: c.boundary,
}
}

// Above to move up one row
func (c *CellWalker) Above() *CellWalker {
rowAbove := c.row - 1
if rowAbove < 1 {
rowAbove = 1
if c.CanMoveUp() {
rowAbove := c.position.RowIndex() - 1
return newCellWalker(newCell(c.position.ColumnIndex(), rowAbove), c.boundary)
}
return newCellWalker(c.column, rowAbove)
return c.Clone()
}

// Below to move down one row
func (c *CellWalker) Below() *CellWalker {
rowBeneath := c.row + 1
if rowBeneath > RowsLimit {
rowBeneath = RowsLimit
if c.CanMoveDown() {
rowBeneath := c.position.RowIndex() + 1
return newCellWalker(newCell(c.position.ColumnIndex(), rowBeneath), c.boundary)
}
return newCellWalker(c.column, rowBeneath)
return c.Clone()
}

// Right to move right one column
func (c *CellWalker) Right() *CellWalker {
rowRight := c.column + 1
if rowRight > ColumnsLimit {
rowRight = ColumnsLimit
if c.CanMoveRight() {
columnRight := c.position.ColumnIndex() + 1
return newCellWalker(newCell(columnRight, c.position.RowIndex()), c.boundary)
}
return newCellWalker(rowRight, c.row)
return c.Clone()
}

// Left to move left one column
func (c *CellWalker) Left() *CellWalker {
rowLeft := c.column - 1
if rowLeft < 1 {
rowLeft = 1
if c.CanMoveLeft() {
columnLeft := c.position.ColumnIndex() - 1
return newCellWalker(newCell(columnLeft, c.position.RowIndex()), c.boundary)
}
return c.Clone()
}

// LeftMost to move leftmost column
func (c *CellWalker) LeftMost() *CellWalker {
if c.CanMoveLeft() {
leftMost := c.boundary.LeftIndex()
return newCellWalker(newCell(leftMost, c.position.RowIndex()), c.boundary)
}
return c.Clone()
}

// Rightmost to move rightmost column
func (c *CellWalker) Rightmost() *CellWalker {
if c.CanMoveRight() {
rightMost := c.boundary.RightIndex()
return newCellWalker(newCell(rightMost, c.position.RowIndex()), c.boundary)
}
return c.Clone()
}

// TopMost to move topmost column
func (c *CellWalker) TopMost() *CellWalker {
if c.CanMoveUp() {
topMost := c.boundary.TopIndex()
return newCellWalker(newCell(c.position.ColumnIndex(), topMost), c.boundary)
}
return newCellWalker(rowLeft, c.row)
return c.Clone()
}

// Bottommost to move bottommost column
func (c *CellWalker) Bottommost() *CellWalker {
if c.CanMoveDown() {
bottomMost := c.boundary.BottomIndex()
return newCellWalker(newCell(c.position.ColumnIndex(), bottomMost), c.boundary)
}
return c.Clone()
}

// CanMoveLeft determines if it is at the left most cell
func (c *CellWalker) CanMoveLeft() bool {
return c.column > 1
return c.position.ColumnIndex() > c.boundary.left
}

// CanMoveRight determines if it is at the right most cell
func (c *CellWalker) CanMoveRight() bool {
return c.column < ColumnsLimit
return c.position.ColumnIndex() < c.boundary.right
}

// CanMoveUp determines if it is at the up most cell
func (c *CellWalker) CanMoveUp() bool {
return c.row > 1
return c.position.RowIndex() > c.boundary.top
}

// CanMoveDown determines if it is at the bottom most cell
func (c *CellWalker) CanMoveDown() bool {
return c.row < RowsLimit
return c.position.RowIndex() < c.boundary.bottom
}

// Column jump to a given colName
// Column jumps to a given colName
func (c *CellWalker) Column(colName string) *CellWalker {
colIndex := ColumnNameToIndex(colName)

return newCellWalker(colIndex, c.row)
return newCellWalker(newCell(colIndex, c.position.RowIndex()), c.boundary)
}

// Row jump to a given row
// Row jumps to a given row
func (c *CellWalker) Row(row int) *CellWalker {
return newCellWalker(c.column, row)
return newCellWalker(newCell(c.position.ColumnIndex(), row), c.boundary)
}

// ColumnOffset return a cell with a given offset distance to column
// ColumnOffset returns a cell with a given offset distance to column
func (c *CellWalker) ColumnOffset(offset int) *CellWalker {
return newCellWalker(c.column+offset, c.row)
return newCellWalker(newCell(c.position.ColumnIndex()+offset, c.position.RowIndex()), c.boundary)
}

// RowOffset return a cell with a given offset distance to row
// RowOffset returns a cell with a given offset distance to row
func (c *CellWalker) RowOffset(offset int) *CellWalker {
return newCellWalker(c.column, c.row+offset)
return newCellWalker(newCell(c.position.ColumnIndex(), c.position.RowIndex()+offset), c.boundary)
}

// Tour traverses position to Right column first then first column of next row if hit the boundary edge.
// Return nil if cannot make a further move
func (c *CellWalker) Tour() *CellWalker {
if c.CanMoveRight() {
return c.Right()
}
if c.CanMoveDown() {
return c.Below().LeftMost()
}
return nil
}
Loading

0 comments on commit 3573d27

Please sign in to comment.