193 lines
3.6 KiB
Go
193 lines
3.6 KiB
Go
|
|
package spreedsheetx
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"math"
|
|||
|
|
"regexp"
|
|||
|
|
"strings"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// https://support.microsoft.com/en-us/office/excel-specifications-and-limits-1672b34d-7043-467e-8e27-269d656771c3?ui=en-us&rs=en-us&ad=us
|
|||
|
|
// Max index is 16384 XFD
|
|||
|
|
|
|||
|
|
var (
|
|||
|
|
rxColumnName = regexp.MustCompile("^[A-Za-z]{1,3}$")
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
minNumber = 1
|
|||
|
|
maxNumber = 16384
|
|||
|
|
a = 64
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type Column struct {
|
|||
|
|
startName string // 最开始操作的列
|
|||
|
|
endName string // 最远到达的列
|
|||
|
|
current string // 当前列
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func isValidName(name string) bool {
|
|||
|
|
return rxColumnName.MatchString(name) && toNumber(name) <= maxNumber
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func reverse(name string) []rune {
|
|||
|
|
d := []rune(name)
|
|||
|
|
for i, j := 0, len(d)-1; i < j; i, j = i+1, j-1 {
|
|||
|
|
d[i], d[j] = d[j], d[i]
|
|||
|
|
}
|
|||
|
|
return d
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func toNumber(name string) int {
|
|||
|
|
name = strings.ToUpper(name)
|
|||
|
|
switch len(name) {
|
|||
|
|
case 0:
|
|||
|
|
return 0
|
|||
|
|
case 1:
|
|||
|
|
return int(rune(name[0])) - a
|
|||
|
|
default:
|
|||
|
|
number := 0
|
|||
|
|
for i, r := range reverse(name) {
|
|||
|
|
if i == 0 {
|
|||
|
|
number += int(r) - a
|
|||
|
|
} else {
|
|||
|
|
number += (int(r) - a) * int(math.Pow(26, float64(i)))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return number
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func NewColumn(name string) *Column {
|
|||
|
|
name = strings.ToUpper(name)
|
|||
|
|
if !isValidName(name) {
|
|||
|
|
panic("invalid column name")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return &Column{
|
|||
|
|
startName: name,
|
|||
|
|
endName: name,
|
|||
|
|
current: name,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ToFirst 到第一列,总是返回 A 列
|
|||
|
|
func (c *Column) ToFirst() *Column {
|
|||
|
|
c.current = "A"
|
|||
|
|
return c
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Next 当前列的下一列
|
|||
|
|
func (c *Column) Next() (*Column, error) {
|
|||
|
|
return c.RightShift(1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c *Column) Prev() (*Column, error) {
|
|||
|
|
return c.LeftShift(1)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// StartName 返回最开始的列名
|
|||
|
|
func (c Column) StartName() string {
|
|||
|
|
return c.startName
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c *Column) setEndName(name string) *Column {
|
|||
|
|
if c.endName < name || len(c.endName) < len(name) {
|
|||
|
|
c.endName = name
|
|||
|
|
}
|
|||
|
|
return c
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// EndName 返回最远到达的列名
|
|||
|
|
func (c Column) EndName() string {
|
|||
|
|
return c.endName
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Name 当前列名
|
|||
|
|
func (c Column) Name() string {
|
|||
|
|
return c.current
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NameWithRow 带行号的列名,比如:A1
|
|||
|
|
func (c Column) NameWithRow(row int) string {
|
|||
|
|
return fmt.Sprintf("%s%d", c.current, row)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Reset 重置到最开始的列(NewColumn 创建时的列)
|
|||
|
|
func (c *Column) Reset() *Column {
|
|||
|
|
c.current = c.startName
|
|||
|
|
c.endName = c.startName
|
|||
|
|
return c
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// To 跳转到指定的列
|
|||
|
|
func (c *Column) To(name string) (*Column, error) {
|
|||
|
|
name = strings.ToUpper(name)
|
|||
|
|
if !isValidName(name) {
|
|||
|
|
return c, fmt.Errorf("invalid column name %s", name)
|
|||
|
|
}
|
|||
|
|
c.current = name
|
|||
|
|
c.setEndName(name)
|
|||
|
|
return c, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c *Column) RightShift(steps int) (*Column, error) {
|
|||
|
|
if steps <= 0 {
|
|||
|
|
return c, nil
|
|||
|
|
}
|
|||
|
|
return c.shift(steps)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (c *Column) LeftShift(steps int) (*Column, error) {
|
|||
|
|
if steps <= 0 {
|
|||
|
|
return c, nil
|
|||
|
|
}
|
|||
|
|
return c.shift(-steps)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// RightShift 基于当前位置右移多少列
|
|||
|
|
func (c *Column) shift(steps int) (*Column, error) {
|
|||
|
|
if steps == 0 {
|
|||
|
|
return c, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
number := toNumber(c.current)
|
|||
|
|
number += steps
|
|||
|
|
if number > maxNumber {
|
|||
|
|
return c, errors.New("out of max columns")
|
|||
|
|
} else if number < minNumber {
|
|||
|
|
return c, errors.New("out of min columns")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sb := strings.Builder{}
|
|||
|
|
sb.Grow(3) // Max 3 letters
|
|||
|
|
times := 0
|
|||
|
|
for {
|
|||
|
|
times++
|
|||
|
|
quotient := number / 26
|
|||
|
|
remainder := number % 26
|
|||
|
|
if remainder == 0 {
|
|||
|
|
sb.WriteRune('Z')
|
|||
|
|
} else {
|
|||
|
|
sb.WriteRune(rune(a + remainder))
|
|||
|
|
}
|
|||
|
|
if quotient == 0 {
|
|||
|
|
break
|
|||
|
|
} else if quotient <= 26 {
|
|||
|
|
if quotient != 1 || (times >= 1 && remainder != 0) {
|
|||
|
|
sb.WriteRune(rune(a + quotient))
|
|||
|
|
}
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
number = quotient
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
c.current = string(reverse(sb.String()))
|
|||
|
|
if steps > 0 {
|
|||
|
|
// Is right shift
|
|||
|
|
c.setEndName(c.current)
|
|||
|
|
}
|
|||
|
|
return c, nil
|
|||
|
|
}
|