Skip to content

Commit 1c5d050

Browse files
committed
feat(go): add data/table features (visibility, auto-fit, protection, conditional
formats, sort/filters) - Add row/column visibility (hide/show with table:visibility="collapse") - Add column auto-fit (style:use-optimal-column-width) - Add sheet protection (table:protected) and cell protection (style:cell-protect) - Add conditional formatting with calcext namespace support - Add filter criteria and sort keys on database ranges
1 parent 97d6bd0 commit 1c5d050

38 files changed

Lines changed: 4072 additions & 51 deletions

autofilter.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,22 @@ type autoFilter struct {
66
startRow int
77
endCol int
88
endRow int
9+
filters []filterColumn
10+
}
11+
12+
type filterColumn struct {
13+
colIdx int
14+
values []string
15+
}
16+
17+
type FilterCriteria struct {
18+
Column int
19+
Values []string
20+
}
21+
22+
type SortKey struct {
23+
Column int
24+
Descending bool
925
}
1026

1127
func (f *File) SetAutoFilter(sheet, topLeft, bottomRight string) error {
@@ -83,3 +99,101 @@ func (f *File) RemoveAutoFilter(sheet string) error {
8399
}
84100
return ErrAutoFilterNotFound
85101
}
102+
103+
func (f *File) SetFilterCriteria(sheet string, criteria []FilterCriteria) error {
104+
if f.closed {
105+
return ErrFileClosed
106+
}
107+
if f.getSheet(sheet) == nil {
108+
return ErrSheetNotFound
109+
}
110+
111+
for i, af := range f.autoFilters {
112+
if af.sheet == sheet {
113+
f.autoFilters[i].filters = nil
114+
for _, c := range criteria {
115+
f.autoFilters[i].filters = append(f.autoFilters[i].filters, filterColumn{
116+
colIdx: c.Column,
117+
values: c.Values,
118+
})
119+
}
120+
return nil
121+
}
122+
}
123+
return ErrAutoFilterNotFound
124+
}
125+
126+
func (f *File) GetFilterCriteria(sheet string) ([]FilterCriteria, error) {
127+
if f.closed {
128+
return nil, ErrFileClosed
129+
}
130+
if f.getSheet(sheet) == nil {
131+
return nil, ErrSheetNotFound
132+
}
133+
134+
for _, af := range f.autoFilters {
135+
if af.sheet == sheet {
136+
var result []FilterCriteria
137+
for _, fc := range af.filters {
138+
result = append(result, FilterCriteria{
139+
Column: fc.colIdx,
140+
Values: fc.values,
141+
})
142+
}
143+
return result, nil
144+
}
145+
}
146+
return nil, ErrAutoFilterNotFound
147+
}
148+
149+
func (f *File) ClearFilterCriteria(sheet string) error {
150+
if f.closed {
151+
return ErrFileClosed
152+
}
153+
if f.getSheet(sheet) == nil {
154+
return ErrSheetNotFound
155+
}
156+
157+
for i, af := range f.autoFilters {
158+
if af.sheet == sheet {
159+
f.autoFilters[i].filters = nil
160+
return nil
161+
}
162+
}
163+
return ErrAutoFilterNotFound
164+
}
165+
166+
func (f *File) SetSort(sheet string, keys []SortKey) error {
167+
if f.closed {
168+
return ErrFileClosed
169+
}
170+
s := f.getSheet(sheet)
171+
if s == nil {
172+
return ErrSheetNotFound
173+
}
174+
s.sortKeys = keys
175+
return nil
176+
}
177+
178+
func (f *File) GetSort(sheet string) ([]SortKey, error) {
179+
if f.closed {
180+
return nil, ErrFileClosed
181+
}
182+
s := f.getSheet(sheet)
183+
if s == nil {
184+
return nil, ErrSheetNotFound
185+
}
186+
return s.sortKeys, nil
187+
}
188+
189+
func (f *File) RemoveSort(sheet string) error {
190+
if f.closed {
191+
return ErrFileClosed
192+
}
193+
s := f.getSheet(sheet)
194+
if s == nil {
195+
return ErrSheetNotFound
196+
}
197+
s.sortKeys = nil
198+
return nil
199+
}

benchmark_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package goods
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
)
9+
10+
func BenchmarkOpenFile(b *testing.B) {
11+
for _, tc := range []struct {
12+
name, path string
13+
}{
14+
{"small", "testdata/sample.ods"},
15+
{"medium", "testdata/golden_formulas.ods"},
16+
{"large", "testdata/golden_large.ods"},
17+
} {
18+
data, err := os.ReadFile(tc.path)
19+
if err != nil {
20+
b.Fatalf("read %s: %v", tc.path, err)
21+
}
22+
b.Run(tc.name, func(b *testing.B) {
23+
b.ReportAllocs()
24+
for i := 0; i < b.N; i++ {
25+
f, err := OpenBytes(data)
26+
if err != nil {
27+
b.Fatal(err)
28+
}
29+
_ = f
30+
}
31+
})
32+
}
33+
}
34+
35+
func BenchmarkWriteToBuffer(b *testing.B) {
36+
for _, tc := range []struct {
37+
name, path string
38+
}{
39+
{"small", "testdata/sample.ods"},
40+
{"medium", "testdata/golden_formulas.ods"},
41+
{"large", "testdata/golden_large.ods"},
42+
} {
43+
data, err := os.ReadFile(tc.path)
44+
if err != nil {
45+
b.Fatalf("read %s: %v", tc.path, err)
46+
}
47+
f, err := OpenBytes(data)
48+
if err != nil {
49+
b.Fatalf("open %s: %v", tc.path, err)
50+
}
51+
b.Run(tc.name, func(b *testing.B) {
52+
b.ReportAllocs()
53+
for i := 0; i < b.N; i++ {
54+
buf, err := f.WriteToBuffer()
55+
if err != nil {
56+
b.Fatal(err)
57+
}
58+
_ = buf
59+
}
60+
})
61+
}
62+
}
63+
64+
func BenchmarkSaveAs(b *testing.B) {
65+
data, err := os.ReadFile("testdata/golden_large.ods")
66+
if err != nil {
67+
b.Fatalf("read: %v", err)
68+
}
69+
f, err := OpenBytes(data)
70+
if err != nil {
71+
b.Fatal(err)
72+
}
73+
tmpDir, err := os.MkdirTemp("", "bench-saveas-*")
74+
if err != nil {
75+
b.Fatal(err)
76+
}
77+
defer os.RemoveAll(tmpDir)
78+
79+
b.ReportAllocs()
80+
b.ResetTimer()
81+
for i := 0; i < b.N; i++ {
82+
path := filepath.Join(tmpDir, fmt.Sprintf("out_%d.ods", i))
83+
if err := f.SaveAs(path); err != nil {
84+
b.Fatal(err)
85+
}
86+
}
87+
}
88+
89+
func BenchmarkRecalcAll(b *testing.B) {
90+
data, err := os.ReadFile("testdata/golden_formulas.ods")
91+
if err != nil {
92+
b.Fatalf("read: %v", err)
93+
}
94+
95+
b.ReportAllocs()
96+
b.ResetTimer()
97+
for i := 0; i < b.N; i++ {
98+
f, err := OpenBytes(data)
99+
if err != nil {
100+
b.Fatal(err)
101+
}
102+
if err := f.RecalcAll(); err != nil {
103+
b.Fatal(err)
104+
}
105+
}
106+
}
107+
108+
func BenchmarkRecalcLarge(b *testing.B) {
109+
f := NewFile()
110+
for row := 1; row <= 1000; row++ {
111+
cellA, err := CoordinatesToCellName(1, row)
112+
if err != nil {
113+
b.Fatal(err)
114+
}
115+
if err := f.SetCellFloat("Sheet1", cellA, float64(row)*1.5); err != nil {
116+
b.Fatal(err)
117+
}
118+
119+
cellB, err := CoordinatesToCellName(2, row)
120+
if err != nil {
121+
b.Fatal(err)
122+
}
123+
formula := fmt.Sprintf("SUM([.A1:.A%d])", row)
124+
if err := f.SetCellFormula("Sheet1", cellB, formula); err != nil {
125+
b.Fatal(err)
126+
}
127+
}
128+
129+
b.ReportAllocs()
130+
b.ResetTimer()
131+
for i := 0; i < b.N; i++ {
132+
if err := f.RecalcAll(); err != nil {
133+
b.Fatal(err)
134+
}
135+
}
136+
}
137+
138+
func BenchmarkRoundTrip(b *testing.B) {
139+
data, err := os.ReadFile("testdata/golden_large.ods")
140+
if err != nil {
141+
b.Fatalf("read: %v", err)
142+
}
143+
144+
b.ReportAllocs()
145+
b.ResetTimer()
146+
for i := 0; i < b.N; i++ {
147+
f, err := OpenBytes(data)
148+
if err != nil {
149+
b.Fatal(err)
150+
}
151+
if err := f.RecalcAll(); err != nil {
152+
b.Fatal(err)
153+
}
154+
buf, err := f.WriteToBuffer()
155+
if err != nil {
156+
b.Fatal(err)
157+
}
158+
_ = buf
159+
}
160+
}
161+
162+
func BenchmarkSetCellValue(b *testing.B) {
163+
b.ReportAllocs()
164+
for i := 0; i < b.N; i++ {
165+
b.StopTimer()
166+
f := NewFile()
167+
b.StartTimer()
168+
169+
for j := 1; j <= 10000; j++ {
170+
cellRef, err := CoordinatesToCellName(1, j)
171+
if err != nil {
172+
b.Fatal(err)
173+
}
174+
if err := f.SetCellValue("Sheet1", cellRef, j); err != nil {
175+
b.Fatal(err)
176+
}
177+
}
178+
}
179+
}
180+
181+
func BenchmarkGetRows(b *testing.B) {
182+
data, err := os.ReadFile("testdata/golden_large.ods")
183+
if err != nil {
184+
b.Fatalf("read: %v", err)
185+
}
186+
f, err := OpenBytes(data)
187+
if err != nil {
188+
b.Fatal(err)
189+
}
190+
191+
b.ReportAllocs()
192+
b.ResetTimer()
193+
for i := 0; i < b.N; i++ {
194+
rows, err := f.GetRows("Sheet1")
195+
if err != nil {
196+
b.Fatal(err)
197+
}
198+
_ = rows
199+
}
200+
}

cell.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,46 @@ func (f *File) GetSheetDimension(sheet string) (string, error) {
329329
return fmt.Sprintf("A1:%s", endCell), nil
330330
}
331331

332+
func (f *File) SetRowValues(sheet string, row int, values []any) error {
333+
if f.closed {
334+
return sheetErr(sheet, ErrFileClosed)
335+
}
336+
s := f.getSheet(sheet)
337+
if s == nil {
338+
return sheetErr(sheet, ErrSheetNotFound)
339+
}
340+
if row < 1 {
341+
return sheetErr(sheet, ErrRowOutOfRange)
342+
}
343+
344+
for i, v := range values {
345+
valueType, rawValue := detectCellType(v)
346+
s.setCellValue(i+1, row, valueType, rawValue)
347+
}
348+
f.triggerRecalc(sheet)
349+
return nil
350+
}
351+
352+
func (f *File) AppendRows(sheet string, rows [][]any) error {
353+
if f.closed {
354+
return sheetErr(sheet, ErrFileClosed)
355+
}
356+
s := f.getSheet(sheet)
357+
if s == nil {
358+
return sheetErr(sheet, ErrSheetNotFound)
359+
}
360+
361+
startRow := s.maxRow + 1
362+
for i, rowValues := range rows {
363+
for j, v := range rowValues {
364+
valueType, rawValue := detectCellType(v)
365+
s.setCellValue(j+1, startRow+i, valueType, rawValue)
366+
}
367+
}
368+
f.triggerRecalc(sheet)
369+
return nil
370+
}
371+
332372
func (s *sheet) setCellValue(col, row int, valueType CellType, rawValue string) {
333373
r := s.getOrCreateRow(row)
334374

0 commit comments

Comments
 (0)