From ac8f0370919753f6e6ec5d15da3a8385d9698b0b Mon Sep 17 00:00:00 2001 From: yyt Date: Sat, 5 Nov 2022 21:59:22 +0800 Subject: [PATCH 1/5] add auto fit columns function --- col.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/col.go b/col.go index 964cb9751f..15c87ca9fe 100644 --- a/col.go +++ b/col.go @@ -512,6 +512,101 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error return err } +// SetColAutoWidth provides a function to autofit from startCol to endCol columns according to their text content +// +// f := excelize.NewFile() +// err := f.SetColAutoWidth("Sheet1", "A", "H") +func (f *File) SetColAutoWidth(sheetName string, startCol, endCol string) error { + startColIdx, err := ColumnNameToNumber(startCol) + if err != nil { + return err + } + + endColIdx, err := ColumnNameToNumber(endCol) + if err != nil { + return err + } + + if startColIdx > endColIdx { + startColIdx, endColIdx = endColIdx, startColIdx + } + + cols, err := f.Cols(sheetName) + if err != nil { + return err + } + + colIdx := 1 + for cols.Next() { + if colIdx >= startColIdx && colIdx <= endColIdx { + rowCells, _ := cols.Rows() + max := defaultColWidth + for i := range rowCells { + rowCell := rowCells[i] + cellWidth := float64(len(rowCell) + 3) // + 3 for margin + if cellWidth > max && cellWidth < MaxColumnWidth { + max = cellWidth + } + } + + name, err := ColumnNumberToName(colIdx) + if err != nil { + return err + } + + if err := f.SetColWidth(sheetName, name, name, float64(max)); err != nil { + return err + } + } + + // fast go away + if colIdx == endColIdx { + break + } + + colIdx++ + } + + return nil +} + +// SetAllColAutoWidth provides a function to autofit all columns according to their text content +// +// f := excelize.NewFile() +// err := f.SetAllColAutoWidth("Sheet1") +func (f *File) SetAllColAutoWidth(sheetName string) error { + cols, err := f.Cols(sheetName) + if err != nil { + return err + } + + colIdx := 1 + for cols.Next() { + rowCells, _ := cols.Rows() + max := defaultColWidth + for i := range rowCells { + rowCell := rowCells[i] + cellWidth := float64(len(rowCell) + 3) // + 3 for margin + if cellWidth > max && cellWidth < MaxColumnWidth { + max = cellWidth + } + } + + name, err := ColumnNumberToName(colIdx) + if err != nil { + return err + } + + if err := f.SetColWidth(sheetName, name, name, float64(max)); err != nil { + return err + } + + colIdx++ + } + + return nil +} + // flatCols provides a method for the column's operation functions to flatten // and check the worksheet columns. func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol { From 3ce74095ac266ecd987e50c543558dffed3a3ab5 Mon Sep 17 00:00:00 2001 From: yyt030 Date: Sun, 6 Nov 2022 13:03:19 +0800 Subject: [PATCH 2/5] add auto fit column width functions --- col.go | 86 +++++++++++++---------------------------------------- col_test.go | 43 +++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/col.go b/col.go index 15c87ca9fe..3155adfa13 100644 --- a/col.go +++ b/col.go @@ -467,20 +467,18 @@ func (f *File) SetColStyle(sheet, columns string, styleID int) error { // f := excelize.NewFile() // err := f.SetColWidth("Sheet1", "A", "H", 20) func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error { - min, err := ColumnNameToNumber(startCol) - if err != nil { - return err - } - max, err := ColumnNameToNumber(endCol) + min, max, err := f.parseColRange(startCol + ":" + endCol) if err != nil { return err } + + return f.setColWidth(sheet, min, max, width) +} + +func (f *File) setColWidth(sheet string, min, max int, width float64) error { if width > MaxColumnWidth { return ErrColumnWidth } - if min > max { - min, max = max, min - } ws, err := f.workSheetReader(sheet) if err != nil { @@ -512,25 +510,22 @@ func (f *File) SetColWidth(sheet, startCol, endCol string, width float64) error return err } -// SetColAutoWidth provides a function to autofit from startCol to endCol columns according to their text content +// AutoFitColWidth provides a function to autofit columns according to +// their text content // -// f := excelize.NewFile() -// err := f.SetColAutoWidth("Sheet1", "A", "H") -func (f *File) SetColAutoWidth(sheetName string, startCol, endCol string) error { - startColIdx, err := ColumnNameToNumber(startCol) - if err != nil { - return err - } - - endColIdx, err := ColumnNameToNumber(endCol) +// For example set column of column H on Sheet1: +// +// err = f.AutoFitColWidth("Sheet1", "H") +// +// Set style of columns C:F on Sheet1: +// +// err = f.AutoFitColWidth("Sheet1", "C:F", style) +func (f *File) AutoFitColWidth(sheetName string, columns string) error { + startColIdx, endColIdx, err := f.parseColRange(columns) if err != nil { return err } - if startColIdx > endColIdx { - startColIdx, endColIdx = endColIdx, startColIdx - } - cols, err := f.Cols(sheetName) if err != nil { return err @@ -544,17 +539,15 @@ func (f *File) SetColAutoWidth(sheetName string, startCol, endCol string) error for i := range rowCells { rowCell := rowCells[i] cellWidth := float64(len(rowCell) + 3) // + 3 for margin + if cellWidth > max && cellWidth < MaxColumnWidth { max = cellWidth + } else if cellWidth >= MaxColumnWidth { + max = MaxColumnWidth } } - name, err := ColumnNumberToName(colIdx) - if err != nil { - return err - } - - if err := f.SetColWidth(sheetName, name, name, float64(max)); err != nil { + if err := f.setColWidth(sheetName, colIdx, colIdx, max); err != nil { return err } } @@ -570,43 +563,6 @@ func (f *File) SetColAutoWidth(sheetName string, startCol, endCol string) error return nil } -// SetAllColAutoWidth provides a function to autofit all columns according to their text content -// -// f := excelize.NewFile() -// err := f.SetAllColAutoWidth("Sheet1") -func (f *File) SetAllColAutoWidth(sheetName string) error { - cols, err := f.Cols(sheetName) - if err != nil { - return err - } - - colIdx := 1 - for cols.Next() { - rowCells, _ := cols.Rows() - max := defaultColWidth - for i := range rowCells { - rowCell := rowCells[i] - cellWidth := float64(len(rowCell) + 3) // + 3 for margin - if cellWidth > max && cellWidth < MaxColumnWidth { - max = cellWidth - } - } - - name, err := ColumnNumberToName(colIdx) - if err != nil { - return err - } - - if err := f.SetColWidth(sheetName, name, name, float64(max)); err != nil { - return err - } - - colIdx++ - } - - return nil -} - // flatCols provides a method for the column's operation functions to flatten // and check the worksheet columns. func flatCols(col xlsxCol, cols []xlsxCol, replacer func(fc, c xlsxCol) xlsxCol) []xlsxCol { diff --git a/col_test.go b/col_test.go index f786335709..227e8272be 100644 --- a/col_test.go +++ b/col_test.go @@ -1,6 +1,7 @@ package excelize import ( + "math/rand" "path/filepath" "testing" @@ -347,6 +348,48 @@ func TestColWidth(t *testing.T) { convertRowHeightToPixels(0) } +func TestAutoFitColWidth(t *testing.T) { + randStr := func(n int) string { + var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) + } + + f := NewFile() + // Test null data columns + assert.NoError(t, f.AutoFitColWidth("Sheet1", "A")) + width, err := f.GetColWidth("Sheet1", "A") + assert.Equal(t, defaultColWidth, width) + assert.NoError(t, err) + + // Test some data + for _, max := range []int{1, 10, 100, 1000} { + f.SetCellValue("Sheet1", "B2", randStr(max/2)) + f.SetCellValue("Sheet1", "B3", randStr(max)) + assert.NoError(t, f.AutoFitColWidth("Sheet1", "B:A")) + width, err = f.GetColWidth("Sheet1", "A") + assert.Equal(t, defaultColWidth, width) + assert.NoError(t, err) + + width, err = f.GetColWidth("Sheet1", "B") + if float64(max) < defaultColWidth { + assert.Equal(t, defaultColWidth, width) + } else if max > MaxColumnWidth { + assert.Equal(t, float64(MaxColumnWidth), width) + } else { + assert.Equal(t, float64(max+3), width) + } + assert.NoError(t, err) + } + + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAutoColWidth.xlsx"))) + convertRowHeightToPixels(0) +} + func TestGetColStyle(t *testing.T) { f := NewFile() styleID, err := f.GetColStyle("Sheet1", "A") From 06f23574658296134af3c778b3fe8ba111901494 Mon Sep 17 00:00:00 2001 From: yyt030 Date: Sun, 6 Nov 2022 14:02:00 +0800 Subject: [PATCH 3/5] update test case for auto fit all columns --- col_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/col_test.go b/col_test.go index 227e8272be..b774c7f63e 100644 --- a/col_test.go +++ b/col_test.go @@ -386,6 +386,23 @@ func TestAutoFitColWidth(t *testing.T) { assert.NoError(t, err) } + // Test All columns + f.SetCellValue("sheet1", "C2", randStr(10)) + f.SetCellValue("sheet1", "D2", randStr(20)) + f.SetCellValue("sheet1", "E2", randStr(30)) + assert.NoError(t, f.AutoFitColWidth("Sheet1", "XFD:D")) + width, err = f.GetColWidth("Sheet1", "C") + assert.Equal(t, defaultColWidth, width) + assert.NoError(t, err) + + width, err = f.GetColWidth("Sheet1", "D") + assert.Equal(t, float64(23), width) + assert.NoError(t, err) + + width, err = f.GetColWidth("Sheet1", "E") + assert.Equal(t, float64(33), width) + assert.NoError(t, err) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAutoColWidth.xlsx"))) convertRowHeightToPixels(0) } From 551e77897ef3a0afc2bfc395780f13f28ca29355 Mon Sep 17 00:00:00 2001 From: yyt030 Date: Sat, 12 Nov 2022 18:12:35 +0800 Subject: [PATCH 4/5] update function description --- col.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/col.go b/col.go index 33c06155c0..065f893801 100644 --- a/col.go +++ b/col.go @@ -520,6 +520,8 @@ func (f *File) setColWidth(sheet string, min, max int, width float64) error { // AutoFitColWidth provides a function to autofit columns according to // their text content +// Note: this only works on the column with cells which not contains +// formula cell and style with a number format. // // For example set column of column H on Sheet1: // From ca026978f2638be94cba3352c038f3023c51b978 Mon Sep 17 00:00:00 2001 From: yyt030 Date: Tue, 15 Nov 2022 23:33:08 +0800 Subject: [PATCH 5/5] calculate sbcs and mbcs and refactor the test case --- col.go | 41 +++++++++++++++++++------ col_test.go | 87 ++++++++++++++++++++++------------------------------- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/col.go b/col.go index 065f893801..a34e942302 100644 --- a/col.go +++ b/col.go @@ -17,6 +17,7 @@ import ( "math" "strconv" "strings" + "unicode/utf8" "github.com/mohae/deepcopy" ) @@ -519,7 +520,7 @@ func (f *File) setColWidth(sheet string, min, max int, width float64) error { } // AutoFitColWidth provides a function to autofit columns according to -// their text content +// their text content with default font size and font. // Note: this only works on the column with cells which not contains // formula cell and style with a number format. // @@ -529,8 +530,8 @@ func (f *File) setColWidth(sheet string, min, max int, width float64) error { // // Set style of columns C:F on Sheet1: // -// err = f.AutoFitColWidth("Sheet1", "C:F", style) -func (f *File) AutoFitColWidth(sheetName string, columns string) error { +// err = f.AutoFitColWidth("Sheet1", "C:F") +func (f *File) AutoFitColWidth(sheetName, columns string) error { startColIdx, endColIdx, err := f.parseColRange(columns) if err != nil { return err @@ -545,24 +546,44 @@ func (f *File) AutoFitColWidth(sheetName string, columns string) error { for cols.Next() { if colIdx >= startColIdx && colIdx <= endColIdx { rowCells, _ := cols.Rows() - max := defaultColWidth + var max int for i := range rowCells { rowCell := rowCells[i] - cellWidth := float64(len(rowCell) + 3) // + 3 for margin - if cellWidth > max && cellWidth < MaxColumnWidth { + // Single Byte Character Set(SBCS) is 1 holds + // Multi-Byte Character System(MBCS) is 2 holds + var cellLenSBCS, cellLenMBCS int + for ii := range rowCell { + if rowCell[ii] < 0x80 { + cellLenSBCS++ + } + } + + runeLen := utf8.RuneCountInString(rowCell) + cellLenMBCS = runeLen - cellLenSBCS + + cellWidth := cellLenSBCS + cellLenMBCS*2 + if cellWidth > max { max = cellWidth - } else if cellWidth >= MaxColumnWidth { - max = MaxColumnWidth } } - if err := f.setColWidth(sheetName, colIdx, colIdx, max); err != nil { + // The ratio of 1.123 is the best approximation I tried my best to + // find. + actualMax := float64(max) * 1.123 + + if actualMax < defaultColWidth { + actualMax = defaultColWidth + } else if actualMax >= MaxColumnWidth { + actualMax = MaxColumnWidth + } + + if err := f.setColWidth(sheetName, colIdx, colIdx, actualMax); err != nil { return err } } - // fast go away + // fast go away. if colIdx == endColIdx { break } diff --git a/col_test.go b/col_test.go index 72b404fee0..3977e5a287 100644 --- a/col_test.go +++ b/col_test.go @@ -1,9 +1,10 @@ package excelize import ( - "math/rand" "path/filepath" + "strings" "testing" + "unicode/utf8" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -362,62 +363,46 @@ func TestColWidth(t *testing.T) { } func TestAutoFitColWidth(t *testing.T) { - randStr := func(n int) string { - var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - - b := make([]rune, n) - for i := range b { - b[i] = letterRunes[rand.Intn(len(letterRunes))] + getCellLen := func(s string) int { + var cellLenSBCS, cellLenMBCS int + for i := range s { + if s[i] < 0x80 { + cellLenSBCS++ + } } - return string(b) - } - f := NewFile() - // Test null data columns - assert.NoError(t, f.AutoFitColWidth("Sheet1", "A")) - width, err := f.GetColWidth("Sheet1", "A") - assert.Equal(t, defaultColWidth, width) - assert.NoError(t, err) + runeLen := utf8.RuneCountInString(s) + cellLenMBCS = runeLen - cellLenSBCS - // Test some data - for _, max := range []int{1, 10, 100, 1000} { - f.SetCellValue("Sheet1", "B2", randStr(max/2)) - f.SetCellValue("Sheet1", "B3", randStr(max)) - assert.NoError(t, f.AutoFitColWidth("Sheet1", "B:A")) - width, err = f.GetColWidth("Sheet1", "A") - assert.Equal(t, defaultColWidth, width) - assert.NoError(t, err) + return cellLenSBCS + cellLenMBCS*2 + } - width, err = f.GetColWidth("Sheet1", "B") - if float64(max) < defaultColWidth { - assert.Equal(t, defaultColWidth, width) - } else if max > MaxColumnWidth { - assert.Equal(t, float64(MaxColumnWidth), width) - } else { - assert.Equal(t, float64(max+3), width) + for _, c := range []string{"", "A", "a", "a你好", "你好", "あなた"} { + f := NewFile() + for i := 1; i <= 10; i++ { + colN, err := ColumnNumberToName(i) + assert.NoError(t, err) + + v := strings.Repeat(c, i) + assert.NoError(t, f.SetCellValue("Sheet1", colN+"1", v)) + + assert.NoError(t, f.AutoFitColWidth("Sheet1", colN)) + + got, err := f.GetColWidth("Sheet1", colN) + assert.CallerInfo() + assert.NoError(t, err) + + actualLen := float64(getCellLen(v)) * 1.123 + if actualLen < defaultColWidth { + assert.Equal(t, defaultColWidth, got) + } else if actualLen >= MaxColumnWidth { + assert.Equal(t, float64(MaxColumnWidth), got) + } else { + assert.Equal(t, actualLen, got) + } } - assert.NoError(t, err) + assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAutoColWidth.xlsx"))) } - - // Test All columns - f.SetCellValue("sheet1", "C2", randStr(10)) - f.SetCellValue("sheet1", "D2", randStr(20)) - f.SetCellValue("sheet1", "E2", randStr(30)) - assert.NoError(t, f.AutoFitColWidth("Sheet1", "XFD:D")) - width, err = f.GetColWidth("Sheet1", "C") - assert.Equal(t, defaultColWidth, width) - assert.NoError(t, err) - - width, err = f.GetColWidth("Sheet1", "D") - assert.Equal(t, float64(23), width) - assert.NoError(t, err) - - width, err = f.GetColWidth("Sheet1", "E") - assert.Equal(t, float64(33), width) - assert.NoError(t, err) - - assert.NoError(t, f.SaveAs(filepath.Join("test", "TestAutoColWidth.xlsx"))) - convertRowHeightToPixels(0) } func TestGetColStyle(t *testing.T) {