├── .circleci └── config.yml ├── .gitignore ├── Copyright ├── LICENSE ├── README.md ├── doc.go ├── go.mod ├── go.sum ├── read.go ├── read_1.20_test.go ├── read_test.go ├── reader.go ├── reader_test.go ├── value.go ├── value_test.go ├── write.go ├── write_test.go ├── writer.go └── writer_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Use the latest 2.1 version of CircleCI pipeline process engine. 2 | # See: https://circleci.com/docs/2.0/configuration-reference 3 | version: 2.1 4 | 5 | # Define a job to be invoked later in a workflow. 6 | # See: https://circleci.com/docs/2.0/configuration-reference/#jobs 7 | jobs: 8 | build: 9 | working_directory: ~/repo 10 | # Specify the execution environment. You can specify an image from Dockerhub or use one of our Convenience Images from CircleCI's Developer Hub. 11 | # See: https://circleci.com/docs/2.0/configuration-reference/#docker-machine-macos-windows-executor 12 | docker: 13 | - image: cimg/go:1.18 14 | # Add steps to the job 15 | # See: https://circleci.com/docs/2.0/configuration-reference/#steps 16 | steps: 17 | - checkout 18 | - run: 19 | name: Install Dependencies 20 | command: go mod download 21 | - run: 22 | name: Run tests 23 | command: | 24 | go test -coverprofile=coverage.txt -covermode=atomic -v ./... 25 | - run: 26 | name: Upload codecov 27 | command: | 28 | bash <(curl -s https://codecov.io/bash) 29 | # Invoke jobs via workflows 30 | # See: https://circleci.com/docs/2.0/configuration-reference/#workflows 31 | workflows: 32 | sample: # This is the name of the workflow, feel free to change it to better match your workflow. 33 | # Inside the workflow, you define the jobs you want to run. 34 | jobs: 35 | - build -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea 15 | logs 16 | .log 17 | .gz 18 | .DS_Store 19 | vendor -------------------------------------------------------------------------------- /Copyright: -------------------------------------------------------------------------------- 1 | Copyright $today.year exl Author. All Rights Reserved. 2 | Licensed under the Apache License, Version 2.0 (the "License"); 3 | you may not use this file except in compliance with the License. 4 | You may obtain a copy of the License at 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | Unless required by applicable law or agreed to in writing, software 7 | distributed under the License is distributed on an "AS IS" BASIS, 8 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | See the License for the specific language governing permissions and 10 | limitations under the License. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # exl 2 | Excel binding to struct written in Go.(Only supports Go1.18+) 3 | 4 | [![CircleCI](https://circleci.com/gh/go-the-way/exl/tree/main.svg?style=shield)](https://circleci.com/gh/go-the-way/exl/tree/main) 5 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/go-the-way/exl) 6 | [![codecov](https://codecov.io/gh/go-the-way/exl/branch/main/graph/badge.svg?token=8MAR3J959H)](https://codecov.io/gh/go-the-way/exl) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-the-way/exl)](https://goreportcard.com/report/github.com/go-the-way/exl) 8 | [![GoDoc](https://pkg.go.dev/badge/github.com/go-the-way/exl?status.svg)](https://pkg.go.dev/github.com/go-the-way/exl?tab=doc) 9 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#microsoft-excel) 10 | 11 | ## usage 12 | 13 | ### Read Excel 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/go-the-way/exl" 22 | ) 23 | 24 | type ReadExcel struct { 25 | ID int `excel:"ID"` 26 | Name string `excel:"Name"` 27 | } 28 | 29 | func (*ReadExcel) ReadConfigure(rc *exl.ReadConfig) {} 30 | 31 | func main() { 32 | if models, err := exl.ReadFile[*ReadExcel]("/to/path.xlsx"); err != nil { 33 | fmt.Println("read excel err:" + err.Error()) 34 | } else { 35 | fmt.Printf("read excel num: %d\n", len(models)) 36 | } 37 | } 38 | ``` 39 | 40 | ### Write Excel 41 | 42 | ```go 43 | package main 44 | 45 | import ( 46 | "fmt" 47 | 48 | "github.com/go-the-way/exl" 49 | ) 50 | 51 | type WriteExcel struct { 52 | ID int `excel:"ID"` 53 | Name string `excel:"Name"` 54 | } 55 | 56 | func (m *WriteExcel) WriteConfigure(wc *exl.WriteConfig) {} 57 | 58 | func main() { 59 | if err := exl.Write("/to/path.xlsx", []*WriteExcel{{100, "apple"}, {200, "pear"}}); err != nil { 60 | fmt.Println("write excel err:" + err.Error()) 61 | } else { 62 | fmt.Println("write excel done") 63 | } 64 | } 65 | ``` 66 | 67 | ## Writer 68 | 69 | ```go 70 | package main 71 | 72 | import ( 73 | "fmt" 74 | 75 | "github.com/go-the-way/exl" 76 | ) 77 | 78 | func main() { 79 | w := exl.NewWriter() 80 | if err := w.Write("int", []int{1, 2}); err != nil { 81 | fmt.Println(err) 82 | return 83 | } 84 | if err := w.Write("float", []float64{1.1, 2.2}); err != nil { 85 | fmt.Println(err) 86 | return 87 | } 88 | if err := w.Write("string", []string{"hello", "world"}); err != nil { 89 | fmt.Println(err) 90 | return 91 | } 92 | if err := w.Write("map", []map[string]string{{"id":"1000","name":"hello"},{"id":"2000","name":"world"}}); err != nil { 93 | fmt.Println(err) 94 | return 95 | } 96 | if err := w.Write("structWithField", []struct{ID int}{{1000},{2000}}); err != nil { 97 | fmt.Println(err) 98 | return 99 | } 100 | if err := w.Write("structWithTag", []struct{ID int `excel:"编号"`}{{1000},{2000}}); err != nil { 101 | fmt.Println(err) 102 | return 103 | } 104 | if err := w.Write("structWithTagAndIgnore", []struct{ 105 | ID int `excel:"编号"` 106 | Extra int `excel:"-"` 107 | Name string `excel:"名称"` 108 | }{{1000,0,"Coco"},{2000,0,"Apple"}}); err != nil { 109 | fmt.Println(err) 110 | return 111 | } 112 | if err := w.SaveTo("dist.xlsx"); err != nil { 113 | fmt.Println(err) 114 | return 115 | } 116 | } 117 | ``` 118 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | // Package exl 13 | // 14 | // Excel binding to struct written in Go.(Only supports Go1.18+) 15 | package exl 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-the-way/exl 2 | 3 | go 1.18 4 | 5 | require github.com/tealeg/xlsx/v3 v3.3.4 6 | 7 | require ( 8 | github.com/frankban/quicktest v1.14.6 // indirect 9 | github.com/google/btree v1.1.2 // indirect 10 | github.com/google/go-cmp v0.6.0 // indirect 11 | github.com/kr/pretty v0.3.1 // indirect 12 | github.com/kr/text v0.2.0 // indirect 13 | github.com/peterbourgon/diskv/v3 v3.0.1 // indirect 14 | github.com/rogpeppe/fastuuid v1.2.0 // indirect 15 | github.com/rogpeppe/go-internal v1.9.0 // indirect 16 | github.com/shabbyrobe/xmlwriter v0.0.0-20230525083848-85336ec334fa // indirect 17 | golang.org/x/text v0.14.0 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 3 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 4 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 5 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 6 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 7 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 8 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 9 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 10 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 11 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 12 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 13 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 14 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 15 | github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= 16 | github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= 17 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 18 | github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= 19 | github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= 20 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 21 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 22 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 23 | github.com/shabbyrobe/xmlwriter v0.0.0-20230525083848-85336ec334fa h1:qSlczoKLzv10e3zbEqwwDvjhai++MGDuns4ZoOKaE+U= 24 | github.com/shabbyrobe/xmlwriter v0.0.0-20230525083848-85336ec334fa/go.mod h1:tKYSeHyJGYz7eoZMlzrRDQSfdYPYt0UduMr8b97Mmaw= 25 | github.com/tealeg/xlsx/v3 v3.3.4 h1:+ekdnOtVHfCGadxXXuQv1JK9uXSweMpqICsHJ9macR4= 26 | github.com/tealeg/xlsx/v3 v3.3.4/go.mod h1:KV4FTFtvGy0TBlOivJLZu/YNZk6e0Qtk7eOSglWksuA= 27 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 28 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 29 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 30 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 31 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= 32 | -------------------------------------------------------------------------------- /read.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | 14 | import ( 15 | "encoding" 16 | "errors" 17 | "fmt" 18 | "io" 19 | "reflect" 20 | "time" 21 | 22 | "github.com/tealeg/xlsx/v3" 23 | ) 24 | 25 | type ( 26 | ReadConfigurator interface{ ReadConfigure(rc *ReadConfig) } 27 | RowUnmarshalErrorHandlerFunc func(*xlsx.Cell, *reflect.Value, FieldInfo) 28 | UnusedColumnsHandlerFunc func(*xlsx.Cell, *reflect.Value, FieldInfo) 29 | ReadConfig struct { 30 | // The tag name to use when looking for fields in the target struct. 31 | // Defaults to "excel". 32 | TagName string 33 | // Name of the worksheet to be read. Takes precedence over SheetIndex. 34 | // Defaults to "" 35 | SheetName string 36 | // The index of the worksheet to be read. 37 | // Defaults to 0, the first worksheet. 38 | SheetIndex int 39 | // The row index at which the column headers are read from. 40 | // Zero-based, defaults to 0. 41 | HeaderRowIndex int 42 | // Start the data reading at this row. 43 | // The header row counts as row. 44 | // Zero-based, defaults to 1. 45 | DataStartRowIndex int 46 | // Configure the default string unmarshaler to trim space after reading a cell. 47 | // Does not impact any other default unmarshaler, 48 | // but is available to custom unmarshalers via ExcelUnmarshalParameters.TrimSpace. 49 | // Defaults to false. 50 | TrimSpace bool 51 | // Fallback date formats for date parsing. 52 | // If an Excel cell is to be unmarshalled into a date, 53 | // and that cell is either not formatted as Date or contains raw text 54 | // (which can happen if Excel does not correctly recognize the date format) 55 | // then these formats are used in the order specified to try and parse 56 | // the raw cell value into a date. 57 | // There are no fallback formats configured by default. 58 | FallbackDateFormats []string 59 | // Skip reading columns for which no target field is found. 60 | // Defaults to true. 61 | SkipUnknownColumns bool 62 | // Skip reading columns, if there is a target field, 63 | // but the target type is unsupported 64 | // or caused an error when determining the unmarshaler to use. 65 | // Defaults to false. 66 | SkipUnknownTypes bool 67 | // Configure how errors during unmarshalling are handled. 68 | // Unmarshalling errors are e.g. invalid number formats in the cell, 69 | // date parsing with invalid input, 70 | // or attempting to unmarshal non-numeric text into a numeric field. 71 | // Defaults to UnmarshalErrorAbort. 72 | UnmarshalErrorHandling UnmarshalErrorHandling 73 | // If UnmarshalErrorHandling is configured as UnmarshalErrorCollect, 74 | // this option limits the number of errors which are collected before 75 | // parsing is aborted. 76 | // Configure a limit of 0 to collect all errors, without upper limit. 77 | // Defaults to 10. 78 | MaxUnmarshalErrors uint64 79 | // Handler function for unmarshal errors during row parsing. 80 | // Takes precedence over all UnmarshalErrorHandling except 81 | // UnmarshalErrorIgnore. 82 | // Defaults to nil. 83 | RowUnmarshalErrorHandler RowUnmarshalErrorHandlerFunc 84 | // Handler function for columns not present in struct. 85 | // Defaults to nil. 86 | UnusedColumnsHandler UnusedColumnsHandlerFunc 87 | } 88 | UnmarshalErrorHandling uint8 89 | FieldError struct { 90 | RowIndex int // 0-based row index. Printed as 1-based row number in error text. 91 | ColumnIndex int // 0-based column index. 92 | ColumnHeader string 93 | Err error 94 | } 95 | ContentError struct { 96 | FieldErrors []FieldError 97 | LimitReached bool 98 | } 99 | ) 100 | 101 | var ( 102 | // Ensure FieldError implements the error interface 103 | _ error = FieldError{} 104 | // Ensure FieldError can be unwrapped 105 | _ interface { 106 | Unwrap() error 107 | } = FieldError{} 108 | // Ensure ContentError implements the error interface 109 | _ error = ContentError{} 110 | ) 111 | 112 | // Error implements error. 113 | func (e FieldError) Error() string { 114 | return fmt.Sprintf("error unmarshalling column \"%s\" in row %d: %s", e.ColumnHeader, e.RowIndex+1, e.Err.Error()) 115 | } 116 | 117 | // Unwrap 118 | // Error implements the anonymous unwrap interface used by errors.Unwrap and others. 119 | func (e FieldError) Unwrap() error { 120 | return e.Err 121 | } 122 | 123 | // Error implements error. 124 | func (e ContentError) Error() string { 125 | if e.LimitReached { 126 | return fmt.Sprintf("too many (%d) errors reading data from Excel", len(e.FieldErrors)) 127 | } else { 128 | return fmt.Sprintf("%d errors reading data from Excel", len(e.FieldErrors)) 129 | } 130 | } 131 | 132 | // Unwrap 133 | // Error implements the anonymous unwrap interface used by errors.Unwrap and others. 134 | func (e ContentError) Unwrap() []error { 135 | // Slice needs to be type-adjusted 136 | errs := make([]error, len(e.FieldErrors)) 137 | for i, v := range e.FieldErrors { 138 | errs[i] = v 139 | } 140 | return errs 141 | } 142 | 143 | const ( 144 | // UnmarshalErrorIgnore 145 | // Ignore any errors during unmarshalling 146 | UnmarshalErrorIgnore UnmarshalErrorHandling = iota 147 | // UnmarshalErrorAbort 148 | // Abort reading when encountering the first unmarshalling error 149 | UnmarshalErrorAbort 150 | // UnmarshalErrorCollect 151 | // Collect unmarshalling errors up to a limit, but continue reading. 152 | // Collected errors are returned as one error at the end, of type 153 | UnmarshalErrorCollect 154 | ) 155 | 156 | var ( 157 | defaultReadConfig = func() *ReadConfig { 158 | return &ReadConfig{ 159 | TagName: "excel", 160 | DataStartRowIndex: 1, 161 | SkipUnknownColumns: true, 162 | UnmarshalErrorHandling: UnmarshalErrorAbort, 163 | MaxUnmarshalErrors: 10, 164 | } 165 | } 166 | ErrSheetIndexOutOfRange = errors.New("exl: sheet index out of range") 167 | ErrHeaderRowIndexOutOfRange = errors.New("exl: header row index out of range") 168 | ErrDataStartRowIndexOutOfRange = errors.New("exl: data start row index out of range") 169 | ErrNoUnmarshaler = errors.New("no unmarshaler") 170 | ErrNoDestinationField = errors.New("no destination field with matching tag") 171 | ) 172 | 173 | func readStrings(maxCol int, row *xlsx.Row) []string { 174 | ls := make([]string, maxCol) 175 | for i := 0; i < maxCol; i++ { 176 | ls[i] = row.GetCell(i).Value 177 | } 178 | return ls 179 | } 180 | 181 | func GetUnmarshalFunc(destField reflect.Value) UnmarshalExcelFunc { 182 | if destField.CanInterface() { 183 | 184 | inf := getFieldInterface(destField) 185 | if inf != nil { 186 | 187 | // Prefer ExcelUnmarshaler, if implemented 188 | if _, ok := inf.(ExcelUnmarshaler); ok { 189 | return UnmarshalExcelUnmarshaler 190 | } 191 | 192 | // Then handle specific types with special implementation 193 | if destField.Type() == reflect.TypeOf(time.Time{}) { 194 | return UnmarshalTime 195 | } 196 | 197 | // Then utilize TextUnmarshaler, e.g. for things like decimal.Decimal 198 | if _, ok := inf.(encoding.TextUnmarshaler); ok { 199 | return UnmarshalTextUnmarshaler 200 | } 201 | 202 | } 203 | } 204 | 205 | // And for primitive types, use custom unmarshalling func 206 | kind := destField.Type().Kind() 207 | isPointer := false 208 | if kind == reflect.Ptr { 209 | kind = destField.Type().Elem().Kind() 210 | isPointer = true 211 | } 212 | unmarshalFunc, ok := DefaultUnmarshalFuncs[kind] 213 | if ok { 214 | if isPointer { 215 | return func(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 216 | reflect.New(destField.Type()) 217 | return unmarshalPointer(destValue, cell, params, unmarshalFunc) 218 | } 219 | } 220 | return unmarshalFunc 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func unmarshalPointer(destPointer reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters, unmarshalFunc UnmarshalExcelFunc) error { 227 | // Create new pointer to the field value, 228 | // as the pointer may be nil 229 | elemType := destPointer.Type().Elem() 230 | destPointer.Set(reflect.New(elemType)) 231 | 232 | // Unmarshal into that new value 233 | destValue := destPointer.Elem() 234 | return unmarshalFunc(destValue, cell, params) 235 | } 236 | 237 | // Read opens an xlsx file from the given io.Reader. 238 | // Each row is parsed and unmarshalled into a slice of `T`. 239 | // Note that this function needs to read the reader entirely 240 | // into memory to determine the size, otherwise the zip reader cannot be called. 241 | // Use one of the other `Read*` methods to avoid reading the whole file into memory 242 | // before parsing starts - the excel library will copy the file content into memory anyways. 243 | func Read[T ReadConfigurator](reader io.Reader, filterFunc ...func(t T) (add bool)) ([]T, error) { 244 | // since io.Reader does not provide a size, we have to read it all to get the size 245 | if bytes, err := io.ReadAll(reader); err != nil { 246 | return []T(nil), err 247 | } else { 248 | return ReadBinary(bytes, filterFunc...) 249 | } 250 | } 251 | 252 | // ReadReaderAt opens an xlsx file at the given file path. 253 | // Each row is parsed and unmarshalled into a slice of `T`. 254 | func ReadReaderAt[T ReadConfigurator](reader io.ReaderAt, size int64, filterFunc ...func(t T) (add bool)) ([]T, error) { 255 | f, err := xlsx.OpenReaderAt(reader, size) 256 | if err != nil { 257 | return nil, err 258 | } 259 | return ReadParsed[T](f, filterFunc...) 260 | } 261 | 262 | // ReadFile opens an xlsx file at the given file path. 263 | // Each row is parsed and unmarshalled into a slice of `T`. 264 | func ReadFile[T ReadConfigurator](file string, filterFunc ...func(t T) (add bool)) ([]T, error) { 265 | f, err := xlsx.OpenFile(file) 266 | if err != nil { 267 | return nil, err 268 | } 269 | return ReadParsed[T](f, filterFunc...) 270 | } 271 | 272 | // ReadBinary opens an xlsx file from the provided bytes. 273 | // Each row is parsed and unmarshalled into a slice of `T`. 274 | func ReadBinary[T ReadConfigurator](bytes []byte, filterFunc ...func(t T) (add bool)) ([]T, error) { 275 | f, err := xlsx.OpenBinary(bytes) 276 | if err != nil { 277 | return nil, err 278 | } 279 | return ReadParsed[T](f, filterFunc...) 280 | } 281 | 282 | type FieldInfo struct { 283 | reflectFieldIndex int 284 | Header string 285 | unmarshalFunc UnmarshalExcelFunc 286 | } 287 | 288 | // ReadParsed opens an already parsed xlsx file directly. 289 | // Each row is parsed and unmarshalled into a slice of `T`. 290 | func ReadParsed[T ReadConfigurator](f *xlsx.File, filterFunc ...func(t T) (add bool)) ([]T, error) { 291 | var t T 292 | rc := defaultReadConfig() 293 | t.ReadConfigure(rc) 294 | sidx := rc.SheetIndex 295 | if len(rc.SheetName) > 0 { 296 | for idx, s := range f.Sheets { 297 | if s.Name == rc.SheetName { 298 | sidx = idx 299 | break 300 | } 301 | } 302 | } 303 | if sidx < 0 || sidx > len(f.Sheet)-1 { 304 | return nil, ErrSheetIndexOutOfRange 305 | } 306 | sheet := f.Sheets[sidx] 307 | if rc.HeaderRowIndex < 0 || rc.HeaderRowIndex > sheet.MaxRow-1 { 308 | return nil, ErrHeaderRowIndexOutOfRange 309 | } 310 | if rc.DataStartRowIndex < 0 || rc.DataStartRowIndex > sheet.MaxRow-1 { 311 | return nil, ErrDataStartRowIndexOutOfRange 312 | } 313 | headerRow, _ := sheet.Row(rc.HeaderRowIndex) 314 | maxCol := sheet.MaxCol 315 | headers := readStrings(maxCol, headerRow) 316 | 317 | // Key: Header / Tag name 318 | // Value: Reflection field index 319 | tagToFieldMap := make(map[string]int) 320 | // Key: Column Index 321 | // Value: Unmarshalling Info 322 | columnFields := make([]FieldInfo, len(headers)) 323 | 324 | typ := reflect.TypeOf(t).Elem() 325 | for i := 0; i < typ.NumField(); i++ { 326 | if ta := typ.Field(i).Tag; ta != "" { 327 | if tt, have := ta.Lookup(rc.TagName); have { 328 | tagToFieldMap[tt] = i 329 | } 330 | } 331 | } 332 | 333 | { 334 | val := reflect.New(typ).Elem() 335 | 336 | for columnIndex, header := range headers { 337 | reflectFieldIndex, have := tagToFieldMap[header] 338 | if !have { 339 | if rc.SkipUnknownColumns { 340 | // Skip reading this field 341 | columnFields[columnIndex] = FieldInfo{ 342 | reflectFieldIndex: reflectFieldIndex, 343 | Header: header, 344 | unmarshalFunc: nil, 345 | } 346 | continue 347 | } else { 348 | return nil, fmt.Errorf("%w for column \"%s\" at index %d", ErrNoDestinationField, header, columnIndex) 349 | } 350 | } 351 | 352 | field := val.Field(reflectFieldIndex) 353 | 354 | unmarshaler := GetUnmarshalFunc(field) 355 | if unmarshaler == nil { 356 | if rc.SkipUnknownTypes { 357 | // Skip reading this field 358 | columnFields[columnIndex] = FieldInfo{ 359 | reflectFieldIndex: reflectFieldIndex, 360 | Header: header, 361 | unmarshalFunc: nil, 362 | } 363 | continue 364 | } else { 365 | return nil, fmt.Errorf("%w for column \"%s\" at index %d", ErrNoUnmarshaler, header, columnIndex) 366 | } 367 | } 368 | 369 | columnFields[columnIndex] = FieldInfo{ 370 | reflectFieldIndex: reflectFieldIndex, 371 | Header: header, 372 | unmarshalFunc: unmarshaler, 373 | } 374 | } 375 | } 376 | 377 | unmarshalConfig := &ExcelUnmarshalParameters{ 378 | TrimSpace: rc.TrimSpace, 379 | Date1904: f.Date1904, 380 | FallbackDateFormats: rc.FallbackDateFormats, 381 | } 382 | 383 | collectedErrors := make([]FieldError, 0) 384 | 385 | ts := make([]T, 0) 386 | for rowIndex := 0; rowIndex < sheet.MaxRow; rowIndex++ { 387 | if rowIndex >= rc.DataStartRowIndex { 388 | val := reflect.New(typ).Elem() 389 | if row, _ := sheet.Row(rowIndex); row != nil { 390 | 391 | for columnIndex, fi := range columnFields { 392 | // If there is no unmarshal function, 393 | // this field has been skipped by previous logic. 394 | // e.g. no destination field, or unknown type. 395 | if fi.unmarshalFunc == nil { 396 | if rc.UnusedColumnsHandler != nil { 397 | rc.UnusedColumnsHandler(row.GetCell(columnIndex), &val, fi) 398 | } 399 | continue 400 | } 401 | cell := row.GetCell(columnIndex) 402 | 403 | destField := val.Field(fi.reflectFieldIndex) 404 | err := fi.unmarshalFunc(destField, cell, unmarshalConfig) 405 | if err != nil && rc.UnmarshalErrorHandling != UnmarshalErrorIgnore { 406 | if rc.RowUnmarshalErrorHandler != nil { 407 | rc.RowUnmarshalErrorHandler(cell, &val, fi) 408 | continue 409 | } 410 | fer := FieldError{ 411 | RowIndex: rowIndex, 412 | ColumnIndex: columnIndex, 413 | ColumnHeader: fi.Header, 414 | Err: err, 415 | } 416 | if rc.UnmarshalErrorHandling == UnmarshalErrorAbort { 417 | return nil, fer 418 | } else { 419 | collectedErrors = append(collectedErrors, fer) 420 | if rc.MaxUnmarshalErrors > 0 && uint64(len(collectedErrors)) >= rc.MaxUnmarshalErrors { 421 | return nil, ContentError{ 422 | FieldErrors: collectedErrors, 423 | LimitReached: true, 424 | } 425 | } 426 | } 427 | } 428 | } 429 | nT := val.Addr().Interface().(T) 430 | add := true 431 | if filterFunc != nil && len(filterFunc) > 0 { 432 | for _, fF := range filterFunc { 433 | if fF != nil { 434 | add = fF(nT) 435 | if !add { 436 | break 437 | } 438 | } 439 | } 440 | } 441 | if add { 442 | ts = append(ts, nT) 443 | } 444 | } 445 | } 446 | } 447 | if len(collectedErrors) > 0 { 448 | return nil, ContentError{ 449 | FieldErrors: collectedErrors, 450 | LimitReached: false, 451 | } 452 | } 453 | return ts, nil 454 | } 455 | 456 | // ReadExcel walk func from excel 457 | func ReadExcel(file string, sheetIndex int, walk func(index int, rows *xlsx.Row)) error { 458 | f, err := xlsx.OpenFile(file) 459 | if err != nil { 460 | return err 461 | } 462 | sheet := f.Sheets[sheetIndex] 463 | for i := 0; i < sheet.MaxRow; i++ { 464 | if row, _ := sheet.Row(i); row != nil { 465 | walk(i, row) 466 | } 467 | } 468 | return nil 469 | } 470 | -------------------------------------------------------------------------------- /read_1.20_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing percissions and 12 | // limitations under the License. 13 | 14 | //go:build go1.20 15 | 16 | package exl 17 | 18 | import ( 19 | "errors" 20 | "testing" 21 | ) 22 | 23 | // TestContentErrorIs tests unwrapping of errors with potentially more than one wrapped error. 24 | // This is only supported starting go 1.20 (when errors.Join() was added). 25 | func TestContentErrorIs(t *testing.T) { 26 | errUnit := errors.New("unit test error") 27 | contentError := ContentError{ 28 | FieldErrors: []FieldError{ 29 | { 30 | Err: errUnit, 31 | }, 32 | }, 33 | } 34 | 35 | if !errors.Is(contentError, errUnit) { 36 | t.Error("ContentError unwrapping failed") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /read_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing percissions and 12 | // limitations under the License. 13 | package exl 14 | 15 | import ( 16 | "errors" 17 | "fmt" 18 | "os" 19 | "path" 20 | "reflect" 21 | "strings" 22 | "testing" 23 | "time" 24 | 25 | "github.com/tealeg/xlsx/v3" 26 | ) 27 | 28 | type ( 29 | readTmp struct { 30 | Name1 string `excel:"Name1"` 31 | Name2 string `excel:"Name2"` 32 | Name3 string `excel:"Name3"` 33 | Name4 string `excel:"Name4"` 34 | Name5 string `excel:"Name5"` 35 | } 36 | readErrorTmp struct { 37 | Name1 string `excel:"Name1"` 38 | Name2 int `excel:"Name2"` 39 | ErrorsCount int 40 | } 41 | readUnusedTmp struct { 42 | Name1 string `excel:"Name1"` 43 | Name2 string `excel:"Name2"` 44 | Count int 45 | } 46 | readSheetIndexOutOfRange struct{} 47 | readHeaderRowIndexOutOfRange struct{} 48 | readDataStartRowIndexOutOfRange struct{} 49 | readSheetNameIndexOutOfRange struct{} 50 | ) 51 | 52 | func (t *readTmp) ReadConfigure(rc *ReadConfig) { 53 | rc.TrimSpace = true 54 | rc.SheetName = "Sheet1" 55 | } 56 | 57 | func countUnmarshalErrors(cell *xlsx.Cell, val *reflect.Value, fi FieldInfo) { 58 | countF := val.FieldByName("ErrorsCount") 59 | countF.SetInt(countF.Int() + 1) 60 | } 61 | 62 | func (t *readErrorTmp) ReadConfigure(rc *ReadConfig) { 63 | rc.RowUnmarshalErrorHandler = countUnmarshalErrors 64 | } 65 | 66 | func countUnusedColumns(cell *xlsx.Cell, val *reflect.Value, fi FieldInfo) { 67 | countF := val.FieldByName("Count") 68 | countF.SetInt(countF.Int() + 1) 69 | } 70 | 71 | func (t *readUnusedTmp) ReadConfigure(rc *ReadConfig) { 72 | rc.UnusedColumnsHandler = countUnusedColumns 73 | } 74 | 75 | func (t *readSheetIndexOutOfRange) ReadConfigure(rc *ReadConfig) { 76 | rc.SheetIndex = -1 77 | } 78 | 79 | func (t *readSheetNameIndexOutOfRange) ReadConfigure(rc *ReadConfig) { 80 | rc.SheetName = "Some" 81 | rc.SheetIndex = -1 82 | } 83 | 84 | func (t *readHeaderRowIndexOutOfRange) ReadConfigure(rc *ReadConfig) { 85 | rc.HeaderRowIndex = -1 86 | } 87 | 88 | func (t *readDataStartRowIndexOutOfRange) ReadConfigure(rc *ReadConfig) { 89 | rc.DataStartRowIndex = -1 90 | } 91 | 92 | func TestFieldErrorError(t *testing.T) { 93 | fieldError := FieldError{ 94 | RowIndex: 2, 95 | ColumnIndex: 7, 96 | ColumnHeader: "ColumnX", 97 | Err: errors.New("unit test error"), 98 | } 99 | 100 | equal(t, "error unmarshalling column \"ColumnX\" in row 3: unit test error", fieldError.Error()) 101 | } 102 | 103 | func TestFieldErrorIs(t *testing.T) { 104 | errUnit := errors.New("unit test error") 105 | fieldError := FieldError{ 106 | Err: errUnit, 107 | } 108 | 109 | if !errors.Is(fieldError, errUnit) { 110 | t.Error("FieldError unwrapping failed") 111 | } 112 | } 113 | 114 | func TestFieldErrorUnwrap(t *testing.T) { 115 | errUnit := errors.New("unit test error") 116 | fieldError := FieldError{ 117 | Err: errUnit, 118 | } 119 | 120 | unwrapped := fieldError.Unwrap() 121 | equal(t, errUnit, unwrapped) 122 | } 123 | 124 | func TestContentErrorError(t *testing.T) { 125 | t.Run("with limit reached", func(t *testing.T) { 126 | contentError := ContentError{ 127 | FieldErrors: []FieldError{ 128 | {}, {}, 129 | }, 130 | LimitReached: true, 131 | } 132 | equal(t, "too many (2) errors reading data from Excel", contentError.Error()) 133 | }) 134 | 135 | t.Run("without limit reached", func(t *testing.T) { 136 | contentError := ContentError{ 137 | FieldErrors: []FieldError{ 138 | {}, {}, 139 | }, 140 | LimitReached: false, 141 | } 142 | equal(t, "2 errors reading data from Excel", contentError.Error()) 143 | }) 144 | } 145 | 146 | func TestContentErrorUnwrap(t *testing.T) { 147 | errUnit1 := errors.New("unit test error 1") 148 | errUnit2 := errors.New("unit test error 2") 149 | contentError := ContentError{ 150 | FieldErrors: []FieldError{ 151 | { 152 | Err: errUnit1, 153 | }, 154 | { 155 | Err: errUnit2, 156 | }, 157 | }, 158 | } 159 | 160 | expected := []error{ 161 | FieldError{ 162 | Err: errUnit1, 163 | }, 164 | FieldError{ 165 | Err: errUnit2, 166 | }, 167 | } 168 | unwrapped := contentError.Unwrap() 169 | equal(t, expected, unwrapped) 170 | } 171 | 172 | type customUnmarshalledString string 173 | 174 | func (s *customUnmarshalledString) UnmarshalExcel(cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 175 | if cell.Value == "error please" { 176 | return errors.New("excel unmarshalled: unit test error") 177 | } else { 178 | *s = customUnmarshalledString("excel unmarshalled: " + cell.Value) 179 | return nil 180 | } 181 | } 182 | 183 | type textUnmarshalledString string 184 | 185 | func (s *textUnmarshalledString) UnmarshalText(text []byte) error { 186 | strValue := string(text) 187 | if strValue == "error please" { 188 | return errors.New("text unmarshalled: unit test error") 189 | } else { 190 | *s = textUnmarshalledString("text unmarshalled: " + strValue) 191 | return nil 192 | } 193 | } 194 | 195 | func TestGetUnmarshalFunc(t *testing.T) { 196 | type TestStruct struct { 197 | ExcelUnmarshalled customUnmarshalledString 198 | TextUnmarshalled textUnmarshalledString 199 | TimeUnmarshalled time.Time 200 | PrimitiveUnmarshalled string 201 | PrimitivePointerUnmarshalled *string 202 | } 203 | 204 | testStruct := &TestStruct{} 205 | val := reflect.ValueOf(testStruct).Elem() 206 | 207 | // Test cell with a value to be unmarshalled, 208 | // using a date value so the time unmarshaler can use this. 209 | // Every other unmarshaler will just use the raw string value 210 | successfulCell := &xlsx.Cell{ 211 | Value: "12000", 212 | NumFmt: xlsx.DefaultDateTimeFormat, 213 | } 214 | // Test cell with a specific value which causes the dummy unmarshalers 215 | // to explicitly cause errors, and the string unmarshaler to error out due to a formatting issue. 216 | errorCell := &xlsx.Cell{ 217 | Value: "error please", 218 | NumFmt: "<><><>error<><><>", 219 | } 220 | 221 | params := &ExcelUnmarshalParameters{} 222 | 223 | t.Run("ExcelUnmarshaler", func(t *testing.T) { 224 | field := val.FieldByName("ExcelUnmarshalled") 225 | unmarshaler := GetUnmarshalFunc(field) 226 | if unmarshaler == nil { 227 | t.Fatal("expected an unmarshaler func, got nil") 228 | } 229 | 230 | t.Run("successful", func(t *testing.T) { 231 | err := unmarshaler(field, successfulCell, params) 232 | if err != nil { 233 | t.Error("unexpected error:", err) 234 | } 235 | equal(t, customUnmarshalledString("excel unmarshalled: 12000"), testStruct.ExcelUnmarshalled) 236 | }) 237 | t.Run("error", func(t *testing.T) { 238 | err := unmarshaler(field, errorCell, params) 239 | equal(t, "excel unmarshalled: unit test error", err.Error()) 240 | }) 241 | }) 242 | 243 | t.Run("TextUnmarshaler", func(t *testing.T) { 244 | field := val.FieldByName("TextUnmarshalled") 245 | unmarshaler := GetUnmarshalFunc(field) 246 | if unmarshaler == nil { 247 | t.Fatal("expected an unmarshaler func, got nil") 248 | } 249 | 250 | t.Run("successful", func(t *testing.T) { 251 | err := unmarshaler(field, successfulCell, params) 252 | if err != nil { 253 | t.Error("unexpected error:", err) 254 | } 255 | equal(t, textUnmarshalledString("text unmarshalled: 12000"), testStruct.TextUnmarshalled) 256 | }) 257 | t.Run("error", func(t *testing.T) { 258 | err := unmarshaler(field, errorCell, params) 259 | equal(t, "text unmarshalled: unit test error", err.Error()) 260 | }) 261 | }) 262 | 263 | t.Run("Time", func(t *testing.T) { 264 | field := val.FieldByName("TimeUnmarshalled") 265 | unmarshaler := GetUnmarshalFunc(field) 266 | if unmarshaler == nil { 267 | t.Fatal("expected an unmarshaler func, got nil") 268 | } 269 | 270 | t.Run("successful", func(t *testing.T) { 271 | err := unmarshaler(field, successfulCell, params) 272 | if err != nil { 273 | t.Error("unexpected error:", err) 274 | } 275 | equal(t, time.Date(1932, time.November, 7, 0, 0, 0, 0, time.UTC), testStruct.TimeUnmarshalled) 276 | }) 277 | t.Run("error", func(t *testing.T) { 278 | err := unmarshaler(field, errorCell, params) 279 | equal(t, "error parsing cell as date/time value: no recognized format", err.Error()) 280 | }) 281 | }) 282 | t.Run("Primitive", func(t *testing.T) { 283 | field := val.FieldByName("PrimitiveUnmarshalled") 284 | unmarshaler := GetUnmarshalFunc(field) 285 | if unmarshaler == nil { 286 | t.Fatal("expected an unmarshaler func, got nil") 287 | } 288 | 289 | t.Run("successful", func(t *testing.T) { 290 | err := unmarshaler(field, successfulCell, params) 291 | if err != nil { 292 | t.Error("unexpected error:", err) 293 | } 294 | equal(t, "12000", testStruct.PrimitiveUnmarshalled) 295 | }) 296 | t.Run("error", func(t *testing.T) { 297 | err := unmarshaler(field, errorCell, params) 298 | equal(t, "error formatting string value: invalid formatting code: unsupported or unescaped characters", err.Error()) 299 | }) 300 | }) 301 | t.Run("Primitive Pointer", func(t *testing.T) { 302 | field := val.FieldByName("PrimitivePointerUnmarshalled") 303 | unmarshaler := GetUnmarshalFunc(field) 304 | if unmarshaler == nil { 305 | t.Fatal("expected an unmarshaler func, got nil") 306 | } 307 | 308 | t.Run("successful", func(t *testing.T) { 309 | err := unmarshaler(field, successfulCell, params) 310 | if err != nil { 311 | t.Error("unexpected error:", err) 312 | } 313 | expected := "12000" 314 | equal(t, &expected, testStruct.PrimitivePointerUnmarshalled) 315 | }) 316 | t.Run("error", func(t *testing.T) { 317 | err := unmarshaler(field, errorCell, params) 318 | equal(t, "error formatting string value: invalid formatting code: unsupported or unescaped characters", err.Error()) 319 | }) 320 | }) 321 | } 322 | 323 | func TestReadFileErr(t *testing.T) { 324 | if _, err := ReadFile[*readTmp](""); err == nil { 325 | t.Error("test failed") 326 | } 327 | testFile := "tmp.xlsx" 328 | defer func() { _ = os.Remove(testFile) }() 329 | _ = Write(testFile, []*writeTmp{{}}) 330 | if _, err := ReadFile[*readSheetIndexOutOfRange](testFile); err != ErrSheetIndexOutOfRange { 331 | t.Error("test failed") 332 | } 333 | if _, err := ReadFile[*readHeaderRowIndexOutOfRange](testFile); err != ErrHeaderRowIndexOutOfRange { 334 | t.Error("test failed") 335 | } 336 | if _, err := ReadFile[*readDataStartRowIndexOutOfRange](testFile); err != ErrDataStartRowIndexOutOfRange { 337 | t.Error("test failed") 338 | } 339 | if _, err := ReadFile[*readSheetNameIndexOutOfRange](testFile); err != ErrSheetIndexOutOfRange { 340 | t.Error("test failed") 341 | } 342 | } 343 | 344 | type _reader struct{} 345 | 346 | func (*_reader) Read([]byte) (n int, err error) { 347 | return 0, errors.New("read: unit test error") 348 | } 349 | 350 | func (*_reader) ReadAt([]byte, int64) (n int, err error) { 351 | return 0, errors.New("readat: unit test error") 352 | } 353 | 354 | func TestReadBinary(t *testing.T) { 355 | t.Run("error reading empty slice", func(t *testing.T) { 356 | _, err := ReadBinary[*readTmp]([]byte{}) 357 | equal(t, "zip: not a valid zip file", err.Error()) 358 | }) 359 | t.Run("error (not panic) reading nil", func(t *testing.T) { 360 | _, err := ReadBinary[*readTmp](nil) 361 | equal(t, "zip: not a valid zip file", err.Error()) 362 | }) 363 | t.Run("success reading valid file", func(t *testing.T) { 364 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 365 | 366 | // Shortened test data, see tests for ReadFile for full test 367 | 368 | data := [][]string{ 369 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 370 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 371 | } 372 | 373 | if err := WriteExcel(testFile, data); err != nil { 374 | t.Error("test failed: " + err.Error()) 375 | } 376 | 377 | bytes, err := os.ReadFile(testFile) 378 | if err != nil { 379 | t.Error("test failed: " + err.Error()) 380 | } 381 | 382 | if models, err := ReadBinary[*readTmp](bytes); err != nil { 383 | t.Error("test failed: " + err.Error()) 384 | } else if len(models) != len(data)-1 { 385 | t.Error("test failed") 386 | } else { 387 | for i, m := range models { 388 | d := data[i+1] 389 | if d[0] != m.Name1 { 390 | t.Error("test failed: Name1 not equal") 391 | } 392 | if d[1] != m.Name2 { 393 | t.Error("test failed: Name2 not equal") 394 | } 395 | if d[2] != m.Name3 { 396 | t.Error("test failed: Name3 not equal") 397 | } 398 | if d[3] != m.Name4 { 399 | t.Error("test failed: Name4 not equal") 400 | } 401 | if d[4] != m.Name5 { 402 | t.Error("test failed: Name5 not equal") 403 | } 404 | } 405 | } 406 | }) 407 | } 408 | 409 | func TestRead(t *testing.T) { 410 | t.Run("error reading empty reader", func(t *testing.T) { 411 | _, err := Read[*readTmp](strings.NewReader("")) 412 | equal(t, "zip: not a valid zip file", err.Error()) 413 | }) 414 | t.Run("error reading with read error", func(t *testing.T) { 415 | _, err := Read[*readTmp](&_reader{}) 416 | equal(t, "read: unit test error", err.Error()) 417 | }) 418 | t.Run("success reading valid file", func(t *testing.T) { 419 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 420 | 421 | // Shortened test data, see tests for ReadFile for full test 422 | 423 | data := [][]string{ 424 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 425 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 426 | } 427 | 428 | if err := WriteExcel(testFile, data); err != nil { 429 | t.Error("test failed: " + err.Error()) 430 | } 431 | 432 | reader, err := os.Open(testFile) 433 | if err != nil { 434 | t.Error("test failed: " + err.Error()) 435 | } 436 | defer reader.Close() 437 | 438 | if models, err := Read[*readTmp](reader); err != nil { 439 | t.Error("test failed: " + err.Error()) 440 | } else if len(models) != len(data)-1 { 441 | t.Error("test failed") 442 | } else { 443 | for i, m := range models { 444 | d := data[i+1] 445 | if d[0] != m.Name1 { 446 | t.Error("test failed: Name1 not equal") 447 | } 448 | if d[1] != m.Name2 { 449 | t.Error("test failed: Name2 not equal") 450 | } 451 | if d[2] != m.Name3 { 452 | t.Error("test failed: Name3 not equal") 453 | } 454 | if d[3] != m.Name4 { 455 | t.Error("test failed: Name4 not equal") 456 | } 457 | if d[4] != m.Name5 { 458 | t.Error("test failed: Name5 not equal") 459 | } 460 | } 461 | } 462 | }) 463 | } 464 | 465 | func TestReadReaderAt(t *testing.T) { 466 | t.Run("error reading empty reader", func(t *testing.T) { 467 | _, err := ReadReaderAt[*readTmp](strings.NewReader(""), 0) 468 | equal(t, "zip: not a valid zip file", err.Error()) 469 | }) 470 | t.Run("error reading with read error", func(t *testing.T) { 471 | _, err := ReadReaderAt[*readTmp](&_reader{}, 0) 472 | equal(t, "readat: unit test error", err.Error()) 473 | }) 474 | t.Run("success reading valid file", func(t *testing.T) { 475 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 476 | 477 | // Shortened test data, see tests for ReadFile for full test 478 | 479 | data := [][]string{ 480 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 481 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 482 | } 483 | 484 | if err := WriteExcel(testFile, data); err != nil { 485 | t.Error("test failed: " + err.Error()) 486 | } 487 | 488 | reader, err := os.Open(testFile) 489 | if err != nil { 490 | t.Error("test failed: " + err.Error()) 491 | } 492 | defer reader.Close() 493 | fi, err := os.Stat(testFile) 494 | if err != nil { 495 | t.Error("test failed: " + err.Error()) 496 | } 497 | 498 | if models, err := ReadReaderAt[*readTmp](reader, fi.Size()); err != nil { 499 | t.Error("test failed: " + err.Error()) 500 | } else if len(models) != len(data)-1 { 501 | t.Error("test failed") 502 | } else { 503 | for i, m := range models { 504 | d := data[i+1] 505 | if d[0] != m.Name1 { 506 | t.Error("test failed: Name1 not equal") 507 | } 508 | if d[1] != m.Name2 { 509 | t.Error("test failed: Name2 not equal") 510 | } 511 | if d[2] != m.Name3 { 512 | t.Error("test failed: Name3 not equal") 513 | } 514 | if d[3] != m.Name4 { 515 | t.Error("test failed: Name4 not equal") 516 | } 517 | if d[4] != m.Name5 { 518 | t.Error("test failed: Name5 not equal") 519 | } 520 | } 521 | } 522 | }) 523 | } 524 | 525 | func TestReadFile(t *testing.T) { 526 | 527 | t.Run("error reading missing file", func(t *testing.T) { 528 | testFile := path.Join(t.TempDir(), "does_not_exist.xlsx") 529 | _, err := ReadFile[*readTmp](testFile) 530 | // Concrete error message may vary between systems, 531 | // only verifying first part from xlsx library 532 | equal(t, "OpenFile", strings.Split(err.Error(), ":")[0]) 533 | }) 534 | 535 | t.Run("error reading with sheet index out of range", func(t *testing.T) { 536 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 537 | err := Write(testFile, []*writeTmp{{}}) 538 | if err != nil { 539 | t.Fatal(err.Error()) 540 | } 541 | 542 | _, err = ReadFile[*readSheetIndexOutOfRange](testFile) 543 | equal(t, ErrSheetIndexOutOfRange, err) 544 | }) 545 | 546 | t.Run("error reading with header row index out of range", func(t *testing.T) { 547 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 548 | err := Write(testFile, []*writeTmp{{}}) 549 | if err != nil { 550 | t.Fatal(err.Error()) 551 | } 552 | 553 | _, err = ReadFile[*readHeaderRowIndexOutOfRange](testFile) 554 | equal(t, ErrHeaderRowIndexOutOfRange, err) 555 | }) 556 | 557 | t.Run("error reading with start row index out of range", func(t *testing.T) { 558 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 559 | err := Write(testFile, []*writeTmp{{}}) 560 | if err != nil { 561 | t.Fatal(err.Error()) 562 | } 563 | 564 | _, err = ReadFile[*readDataStartRowIndexOutOfRange](testFile) 565 | equal(t, ErrDataStartRowIndexOutOfRange, err) 566 | }) 567 | 568 | t.Run("successful reading", func(t *testing.T) { 569 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 570 | 571 | data := [][]string{ 572 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 573 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 574 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 575 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 576 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 577 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 578 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 579 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 580 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 581 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 582 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 583 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 584 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 585 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 586 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 587 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 588 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 589 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 590 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 591 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 592 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 593 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 594 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 595 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 596 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 597 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 598 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 599 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 600 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 601 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 602 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 603 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 604 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 605 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 606 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 607 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 608 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 609 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 610 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 611 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 612 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 613 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 614 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 615 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 616 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 617 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 618 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 619 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 620 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 621 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 622 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 623 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 624 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 625 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 626 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 627 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 628 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 629 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 630 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 631 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 632 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 633 | } 634 | if err := WriteExcel(testFile, data); err != nil { 635 | t.Error("test failed: " + err.Error()) 636 | } 637 | if models, err := ReadFile[*readTmp](testFile); err != nil { 638 | t.Error("test failed: " + err.Error()) 639 | } else if len(models) != len(data)-1 { 640 | t.Error("test failed") 641 | } else { 642 | for i, m := range models { 643 | d := data[i+1] 644 | if d[0] != m.Name1 { 645 | t.Error("test failed: Name1 not equal") 646 | } 647 | if d[1] != m.Name2 { 648 | t.Error("test failed: Name2 not equal") 649 | } 650 | if d[2] != m.Name3 { 651 | t.Error("test failed: Name3 not equal") 652 | } 653 | if d[3] != m.Name4 { 654 | t.Error("test failed: Name4 not equal") 655 | } 656 | if d[4] != m.Name5 { 657 | t.Error("test failed: Name5 not equal") 658 | } 659 | } 660 | } 661 | }) 662 | 663 | } 664 | 665 | func TestReadTrimSpace(t *testing.T) { 666 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 667 | 668 | data := [][]string{ 669 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 670 | {"Name1 ", "Name2", "Name3", "Name4", "Name5"}, 671 | {"Name11", "Name22 ", "Name33", "Name44", "Name55"}, 672 | {"Name111", "Name222 ", "Name333 ", "Name444", "Name555"}, 673 | } 674 | if err := WriteExcel(testFile, data); err != nil { 675 | t.Error("test failed: " + err.Error()) 676 | } 677 | 678 | if models, err := ReadFile[*readTmp](testFile); err != nil { 679 | t.Error("test failed: " + err.Error()) 680 | } else if models[0].Name1 != "Name1" || models[1].Name2 != "Name22" || models[2].Name3 != "Name333" { 681 | t.Error("test failed") 682 | } 683 | } 684 | 685 | func TestHandleUnusedColumns(t *testing.T) { 686 | testFile := "tmp.xlsx" 687 | defer func() { _ = os.Remove(testFile) }() 688 | data := [][]string{ 689 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 690 | {"Name1 ", "Name2", "Name3", "Name4", "Name5"}, 691 | } 692 | if err := WriteExcel(testFile, data); err != nil { 693 | t.Error("test failed: " + err.Error()) 694 | } 695 | 696 | models, err := ReadFile[*readUnusedTmp](testFile) 697 | if err != nil { 698 | t.Error("test failed: " + err.Error()) 699 | } 700 | if len(models) != 1 || models[0].Count != 3 { 701 | t.Error("test failed") 702 | } 703 | } 704 | 705 | func TestHandleUnmarshalErrors(t *testing.T) { 706 | testFile := "tmp.xlsx" 707 | defer func() { _ = os.Remove(testFile) }() 708 | data := [][]string{ 709 | {"Name1", "Name2"}, 710 | {"Name1 ", "Something"}, 711 | {"Name1 ", "22"}, 712 | } 713 | if err := WriteExcel(testFile, data); err != nil { 714 | t.Error("test failed: " + err.Error()) 715 | } 716 | 717 | models, err := ReadFile[*readErrorTmp](testFile) 718 | if err != nil { 719 | t.Error("test failed: " + err.Error()) 720 | } 721 | if len(models) != 2 || models[0].ErrorsCount != 1 || models[1].ErrorsCount != 0 { 722 | t.Error("test failed") 723 | } 724 | } 725 | 726 | type missingColumnsAllowed struct { 727 | Name1 string `excel:"Name1"` 728 | } 729 | 730 | func (*missingColumnsAllowed) ReadConfigure(rc *ReadConfig) { 731 | rc.SkipUnknownColumns = true 732 | } 733 | 734 | type missingColumnsNotAllowed struct { 735 | Name1 string `excel:"Name1"` 736 | } 737 | 738 | func (*missingColumnsNotAllowed) ReadConfigure(rc *ReadConfig) { 739 | rc.SkipUnknownColumns = false 740 | } 741 | 742 | func TestReadSkipColumns(t *testing.T) { 743 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 744 | 745 | data := [][]string{ 746 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 747 | {"Name1 ", "Name2", "Name3", "Name4", "Name5"}, 748 | {"Name11", "Name22 ", "Name33", "Name44", "Name55"}, 749 | {"Name111", "Name222 ", "Name333 ", "Name444", "Name555"}, 750 | } 751 | if err := WriteExcel(testFile, data); err != nil { 752 | t.Error("test failed: " + err.Error()) 753 | } 754 | 755 | t.Run("allow missing columns", func(t *testing.T) { 756 | if _, err := ReadFile[*missingColumnsAllowed](testFile); err != nil { 757 | t.Error("test failed:", err) 758 | } 759 | }) 760 | t.Run("disallow missing columns", func(t *testing.T) { 761 | _, err := ReadFile[*missingColumnsNotAllowed](testFile) 762 | if err == nil { 763 | t.Error("test failed: expected error, got nil") 764 | } else { 765 | equal(t, "no destination field with matching tag for column \"Name2\" at index 1", err.Error()) 766 | } 767 | }) 768 | } 769 | 770 | type missingTypesAllowed struct { 771 | // Using error as field type, 772 | // as error is an interface and thus 773 | // not unmarshalable without concrete type 774 | Name1 error `excel:"Name1"` 775 | } 776 | 777 | func (*missingTypesAllowed) ReadConfigure(rc *ReadConfig) { 778 | rc.SkipUnknownTypes = true 779 | } 780 | 781 | type missingTypesNotAllowed struct { 782 | // Using error as field type, 783 | // as error is an interface and thus 784 | // not unmarshalable without concrete type 785 | Name1 error `excel:"Name1"` 786 | } 787 | 788 | func (*missingTypesNotAllowed) ReadConfigure(rc *ReadConfig) { 789 | rc.SkipUnknownTypes = false 790 | } 791 | 792 | func TestReadSkipTypes(t *testing.T) { 793 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 794 | 795 | data := [][]string{ 796 | {"Name1"}, 797 | {"Name1 Content"}, 798 | } 799 | if err := WriteExcel(testFile, data); err != nil { 800 | t.Error("test failed: " + err.Error()) 801 | } 802 | 803 | t.Run("allow missing unmarshalers", func(t *testing.T) { 804 | if _, err := ReadFile[*missingTypesAllowed](testFile); err != nil { 805 | t.Error("test failed:", err) 806 | } 807 | }) 808 | t.Run("disallow missing unmarshalers", func(t *testing.T) { 809 | _, err := ReadFile[*missingTypesNotAllowed](testFile) 810 | if err == nil { 811 | t.Error("test failed: expected error, got nil") 812 | } else { 813 | equal(t, "no unmarshaler for column \"Name1\" at index 0", err.Error()) 814 | } 815 | }) 816 | } 817 | 818 | type ignoreUnmarshalErrors struct { 819 | Name1 customUnmarshalledString `excel:"Name1"` 820 | } 821 | 822 | func (*ignoreUnmarshalErrors) ReadConfigure(rc *ReadConfig) { 823 | rc.UnmarshalErrorHandling = UnmarshalErrorIgnore 824 | } 825 | 826 | type abortUnmarshalErrors struct { 827 | Name1 customUnmarshalledString `excel:"Name1"` 828 | } 829 | 830 | func (*abortUnmarshalErrors) ReadConfigure(rc *ReadConfig) { 831 | rc.UnmarshalErrorHandling = UnmarshalErrorAbort 832 | } 833 | 834 | type collectUnmarshalErrors struct { 835 | Name1 customUnmarshalledString `excel:"Name1"` 836 | } 837 | 838 | func (*collectUnmarshalErrors) ReadConfigure(rc *ReadConfig) { 839 | rc.UnmarshalErrorHandling = UnmarshalErrorCollect 840 | rc.MaxUnmarshalErrors = 2 841 | } 842 | 843 | type collectUnmarshalErrorsUnlimited struct { 844 | Name1 customUnmarshalledString `excel:"Name1"` 845 | } 846 | 847 | func (*collectUnmarshalErrorsUnlimited) ReadConfigure(rc *ReadConfig) { 848 | rc.UnmarshalErrorHandling = UnmarshalErrorCollect 849 | rc.MaxUnmarshalErrors = 0 850 | } 851 | 852 | func TestUnmarshalErrors(t *testing.T) { 853 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 854 | 855 | data := [][]string{ 856 | {"Name1"}, 857 | {"error please"}, 858 | {"error please"}, 859 | {"error please"}, 860 | } 861 | if err := WriteExcel(testFile, data); err != nil { 862 | t.Error("test failed: " + err.Error()) 863 | } 864 | 865 | t.Run("ignore unmarshal errors", func(t *testing.T) { 866 | model, err := ReadFile[*ignoreUnmarshalErrors](testFile) 867 | if err != nil { 868 | t.Error("test failed:", err) 869 | } 870 | equal(t, customUnmarshalledString(""), model[0].Name1) 871 | }) 872 | t.Run("abort at first error", func(t *testing.T) { 873 | model, err := ReadFile[*abortUnmarshalErrors](testFile) 874 | if err == nil { 875 | t.Error("test failed: expected error, got nil") 876 | } else { 877 | equal(t, FieldError{ 878 | RowIndex: 1, 879 | ColumnIndex: 0, 880 | ColumnHeader: "Name1", 881 | Err: errors.New("excel unmarshalled: unit test error"), 882 | }, err) 883 | if model != nil { 884 | t.Error("test failed: expected nil result, got:", model) 885 | } 886 | } 887 | }) 888 | t.Run("collect errors limited", func(t *testing.T) { 889 | model, err := ReadFile[*collectUnmarshalErrors](testFile) 890 | if err == nil { 891 | t.Error("test failed: expected error, got nil") 892 | } else { 893 | equal(t, ContentError{ 894 | FieldErrors: []FieldError{ 895 | { 896 | RowIndex: 1, 897 | ColumnIndex: 0, 898 | ColumnHeader: "Name1", 899 | Err: errors.New("excel unmarshalled: unit test error"), 900 | }, 901 | { 902 | RowIndex: 2, 903 | ColumnIndex: 0, 904 | ColumnHeader: "Name1", 905 | Err: errors.New("excel unmarshalled: unit test error"), 906 | }, 907 | }, 908 | LimitReached: true, 909 | }, err) 910 | if model != nil { 911 | t.Error("test failed: expected nil result, got:", model) 912 | } 913 | } 914 | }) 915 | t.Run("collect errors unlimited", func(t *testing.T) { 916 | model, err := ReadFile[*collectUnmarshalErrorsUnlimited](testFile) 917 | if err == nil { 918 | t.Error("test failed: expected error, got nil") 919 | } else { 920 | equal(t, ContentError{ 921 | FieldErrors: []FieldError{ 922 | { 923 | RowIndex: 1, 924 | ColumnIndex: 0, 925 | ColumnHeader: "Name1", 926 | Err: errors.New("excel unmarshalled: unit test error"), 927 | }, 928 | { 929 | RowIndex: 2, 930 | ColumnIndex: 0, 931 | ColumnHeader: "Name1", 932 | Err: errors.New("excel unmarshalled: unit test error"), 933 | }, 934 | { 935 | RowIndex: 3, 936 | ColumnIndex: 0, 937 | ColumnHeader: "Name1", 938 | Err: errors.New("excel unmarshalled: unit test error"), 939 | }, 940 | }, 941 | LimitReached: false, 942 | }, err) 943 | if model != nil { 944 | t.Error("test failed: expected nil result, got:", model) 945 | } 946 | } 947 | }) 948 | } 949 | 950 | func TestReadFilterFunc(t *testing.T) { 951 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 952 | 953 | data := [][]string{ 954 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 955 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 956 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 957 | } 958 | if err := WriteExcel(testFile, data); err != nil { 959 | t.Error("test failed: " + err.Error()) 960 | } 961 | { 962 | if models, err := ReadFile[*readTmp](testFile, func(t *readTmp) (add bool) { 963 | return true 964 | }); err != nil { 965 | t.Error("test failed: " + err.Error()) 966 | } else if len(models) != 2 { 967 | t.Error("test failed") 968 | } 969 | } 970 | { 971 | if models, err := ReadFile[*readTmp](testFile, func(t *readTmp) (add bool) { 972 | return false 973 | }); err != nil { 974 | t.Error("test failed: " + err.Error()) 975 | } else if len(models) != 0 { 976 | t.Error("test failed") 977 | } 978 | } 979 | { 980 | if models, err := ReadFile[*readTmp](testFile, func(t *readTmp) (add bool) { 981 | return t.Name1 == "Name11" 982 | }); err != nil { 983 | t.Error("test failed: " + err.Error()) 984 | } else if len(models) != 1 { 985 | t.Error("test failed") 986 | } 987 | } 988 | } 989 | 990 | func TestReadExcel(t *testing.T) { 991 | if err := ReadExcel("", 0, nil); err == nil { 992 | t.Error("test failed") 993 | } 994 | } 995 | 996 | func testBasic(t *testing.T, testNum int) { 997 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 998 | 999 | data := make([][]string, testNum) 1000 | for i := range data { 1001 | data[i] = []string{fmt.Sprintf("%d", i)} 1002 | } 1003 | if err := WriteExcel(testFile, data); err != nil { 1004 | t.Fatal(err.Error()) 1005 | } 1006 | if err := ReadExcel(testFile, 0, func(index int, rows *xlsx.Row) { 1007 | equal(t, fmt.Sprintf("%d", index), rows.GetCell(0).Value) 1008 | }); err != nil { 1009 | t.Fatal(err.Error()) 1010 | } 1011 | } 1012 | 1013 | func TestBasic(t *testing.T) { 1014 | testBasic(t, 10) 1015 | testBasic(t, 100) 1016 | testBasic(t, 10000) 1017 | } 1018 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | -------------------------------------------------------------------------------- /reader_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | 14 | import ( 15 | "encoding" 16 | "errors" 17 | "fmt" 18 | "reflect" 19 | "strings" 20 | "time" 21 | 22 | "github.com/tealeg/xlsx/v3" 23 | ) 24 | 25 | var ErrNegativeUInt = errors.New("negative integer provided for unsigned field") 26 | var ErrOverflow = errors.New("numeric overflow, number is too large for this field") 27 | var ErrNoRecognizedFormat = errors.New("no recognized format") 28 | 29 | // ErrCannotCastUnmarshaler is returned in case a field technically implements an unmarshaler interface, 30 | // but casting to it at runtime failed for some reason. 31 | var ErrCannotCastUnmarshaler = errors.New("cannot cast to unmarshaler interface") 32 | 33 | var DefaultUnmarshalFuncs = map[reflect.Kind]UnmarshalExcelFunc{ 34 | reflect.String: UnmarshalString, 35 | reflect.Bool: UnmarshalBool, 36 | reflect.Int: UnmarshalInt, 37 | reflect.Int8: UnmarshalInt, 38 | reflect.Int16: UnmarshalInt, 39 | reflect.Int32: UnmarshalInt, 40 | reflect.Int64: UnmarshalInt, 41 | reflect.Uint: UnmarshalUInt, 42 | reflect.Uintptr: UnmarshalUInt, 43 | reflect.Uint8: UnmarshalUInt, 44 | reflect.Uint16: UnmarshalUInt, 45 | reflect.Uint32: UnmarshalUInt, 46 | reflect.Uint64: UnmarshalUInt, 47 | reflect.Float32: UnmarshalFloat, 48 | reflect.Float64: UnmarshalFloat, 49 | } 50 | 51 | type ExcelUnmarshalParameters struct { 52 | // See ReadConfig.TrimSpace 53 | TrimSpace bool 54 | // See xlsx.File.Date1904 55 | Date1904 bool 56 | // See ReadConfig.FallbackDateFormats 57 | FallbackDateFormats []string 58 | } 59 | 60 | type ExcelUnmarshaler interface { 61 | UnmarshalExcel(cell *xlsx.Cell, params *ExcelUnmarshalParameters) error 62 | } 63 | 64 | type UnmarshalExcelFunc func(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error 65 | 66 | func UnmarshalString(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 67 | str, err := cell.FormattedValue() 68 | if err != nil { 69 | return fmt.Errorf("error formatting string value: %w", err) 70 | } 71 | if params.TrimSpace { 72 | str = strings.TrimSpace(str) 73 | } 74 | destValue.SetString(str) 75 | return nil 76 | } 77 | 78 | func UnmarshalBool(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 79 | destValue.SetBool(cell.Bool()) 80 | return nil 81 | } 82 | 83 | func UnmarshalInt(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 84 | val, err := cell.Int64() 85 | if err != nil { 86 | return fmt.Errorf("error parsing cell as integer value: %w", err) 87 | } 88 | if destValue.OverflowInt(val) { 89 | return ErrOverflow 90 | } 91 | destValue.SetInt(val) 92 | return nil 93 | } 94 | 95 | func UnmarshalUInt(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 96 | val, err := cell.Int64() 97 | if err != nil { 98 | return fmt.Errorf("error parsing cell as integer value: %w", err) 99 | } 100 | if val < 0 { 101 | return ErrNegativeUInt 102 | } 103 | uval := uint64(val) 104 | if destValue.OverflowUint(uval) { 105 | return ErrOverflow 106 | } 107 | destValue.SetUint(uval) 108 | return nil 109 | } 110 | 111 | func UnmarshalFloat(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 112 | val, err := cell.Float() 113 | if err != nil { 114 | return fmt.Errorf("error parsing cell as float value: %w", err) 115 | } 116 | if destValue.OverflowFloat(val) { 117 | return ErrOverflow 118 | } 119 | destValue.SetFloat(val) 120 | return nil 121 | } 122 | 123 | func UnmarshalTime(destValue reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 124 | var val time.Time 125 | if cell.IsTime() { 126 | var err error 127 | val, err = cell.GetTime(params.Date1904) 128 | if err != nil { 129 | var ok bool 130 | val, ok = unmarshalTimeFallback(cell.Value, params.FallbackDateFormats) 131 | if !ok { 132 | return fmt.Errorf("error parsing cell as date/time value: %w", err) 133 | } 134 | } 135 | } else { 136 | var ok bool 137 | val, ok = unmarshalTimeFallback(cell.Value, params.FallbackDateFormats) 138 | if !ok { 139 | return fmt.Errorf("error parsing cell as date/time value: %w", ErrNoRecognizedFormat) 140 | } 141 | } 142 | destValue.Set(reflect.ValueOf(val)) 143 | return nil 144 | } 145 | 146 | func unmarshalTimeFallback(value string, formats []string) (time.Time, bool) { 147 | for _, format := range formats { 148 | val, err := time.Parse(format, value) 149 | if err == nil { 150 | return val, true 151 | } 152 | } 153 | return time.Time{}, false 154 | } 155 | 156 | func getFieldInterface(destField reflect.Value) any { 157 | destFieldPointer := destField 158 | 159 | // Same logic as json.Unmarshal is using: 160 | // If the field has a named type and is addressable, 161 | // start with its address, so that if the type has pointer methods, 162 | // we find them. 163 | // Usually unmarshaler implementations are on the pointer type, 164 | // so that they can actually write back to the field when called. 165 | if destField.Kind() != reflect.Pointer && destField.Type().Name() != "" && destField.CanAddr() { 166 | destFieldPointer = destField.Addr() 167 | } 168 | 169 | return destFieldPointer.Interface() 170 | } 171 | 172 | func UnmarshalExcelUnmarshaler(destField reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 173 | unmarshaler, ok := getFieldInterface(destField).(ExcelUnmarshaler) 174 | if !ok { 175 | // This should not happen at runtime, 176 | // as we have already cast successfully to get here 177 | return ErrCannotCastUnmarshaler 178 | } 179 | 180 | return unmarshaler.UnmarshalExcel(cell, params) 181 | } 182 | 183 | func UnmarshalTextUnmarshaler(destField reflect.Value, cell *xlsx.Cell, params *ExcelUnmarshalParameters) error { 184 | unmarshaler, ok := getFieldInterface(destField).(encoding.TextUnmarshaler) 185 | if !ok { 186 | // This should not happen at runtime, 187 | // as we have alread cast successfully to get here 188 | return ErrCannotCastUnmarshaler 189 | } 190 | 191 | return unmarshaler.UnmarshalText([]byte(cell.Value)) 192 | } 193 | -------------------------------------------------------------------------------- /value_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | 14 | import ( 15 | "errors" 16 | "math" 17 | "reflect" 18 | "testing" 19 | "time" 20 | 21 | "github.com/tealeg/xlsx/v3" 22 | ) 23 | 24 | type _model struct { 25 | S string 26 | B bool 27 | I int64 28 | I8 int8 29 | U uint64 30 | U8 uint8 31 | F float64 32 | F32 float32 33 | T time.Time 34 | EU customUnmarshalledString 35 | TU textUnmarshalledString 36 | 37 | A any 38 | } 39 | 40 | func TestUnmarshalString(t *testing.T) { 41 | model := &_model{} 42 | destField := reflect.ValueOf(model).Elem().FieldByName("S") 43 | cell := &xlsx.Cell{} 44 | 45 | t.Run("string cell", func(t *testing.T) { 46 | cell.SetValue("string value") 47 | err := UnmarshalString(destField, cell, &ExcelUnmarshalParameters{}) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | equal(t, "string value", model.S) 52 | }) 53 | 54 | t.Run("formatted cell", func(t *testing.T) { 55 | cell.SetFloatWithFormat(17.3, "0.00e+00") 56 | err := UnmarshalString(destField, cell, &ExcelUnmarshalParameters{}) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | equal(t, "1.730000e+01", model.S) 61 | }) 62 | 63 | t.Run("don't trim space if not configured", func(t *testing.T) { 64 | cell.SetValue(" string value ") 65 | err := UnmarshalString(destField, cell, &ExcelUnmarshalParameters{ 66 | TrimSpace: false, 67 | }) 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | equal(t, " string value ", model.S) 72 | }) 73 | 74 | t.Run("trim space if configured", func(t *testing.T) { 75 | cell.SetValue(" string value ") 76 | err := UnmarshalString(destField, cell, &ExcelUnmarshalParameters{ 77 | TrimSpace: true, 78 | }) 79 | if err != nil { 80 | t.Fatal(err) 81 | } 82 | equal(t, "string value", model.S) 83 | }) 84 | } 85 | 86 | func TestUnmarshalBool(t *testing.T) { 87 | model := &_model{} 88 | destField := reflect.ValueOf(model).Elem().FieldByName("B") 89 | cell := &xlsx.Cell{} 90 | 91 | t.Run("true cell", func(t *testing.T) { 92 | cell.SetBool(true) 93 | err := UnmarshalBool(destField, cell, &ExcelUnmarshalParameters{}) 94 | if err != nil { 95 | t.Fatal(err) 96 | } 97 | equal(t, true, model.B) 98 | }) 99 | 100 | t.Run("false cell", func(t *testing.T) { 101 | cell.SetBool(false) 102 | err := UnmarshalBool(destField, cell, &ExcelUnmarshalParameters{}) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | equal(t, false, model.B) 107 | }) 108 | } 109 | 110 | func TestUnmarshalInt(t *testing.T) { 111 | model := &_model{} 112 | destField := reflect.ValueOf(model).Elem().FieldByName("I") 113 | cell := &xlsx.Cell{} 114 | 115 | t.Run("positive integer cell", func(t *testing.T) { 116 | cell.SetValue(123) 117 | err := UnmarshalInt(destField, cell, &ExcelUnmarshalParameters{}) 118 | if err != nil { 119 | t.Fatal(err) 120 | } 121 | equal(t, int64(123), model.I) 122 | }) 123 | 124 | t.Run("negative integer cell", func(t *testing.T) { 125 | cell.SetValue(-123) 126 | err := UnmarshalInt(destField, cell, &ExcelUnmarshalParameters{}) 127 | if err != nil { 128 | t.Fatal(err) 129 | } 130 | equal(t, int64(-123), model.I) 131 | }) 132 | 133 | t.Run("text cell", func(t *testing.T) { 134 | cell.SetValue("123") 135 | err := UnmarshalInt(destField, cell, &ExcelUnmarshalParameters{}) 136 | if err != nil { 137 | t.Fatal(err) 138 | } 139 | equal(t, int64(123), model.I) 140 | }) 141 | 142 | t.Run("float cell", func(t *testing.T) { 143 | cell.SetValue(123.7) 144 | err := UnmarshalInt(destField, cell, &ExcelUnmarshalParameters{}) 145 | if err == nil { 146 | t.Fatal("expected format error") 147 | } 148 | }) 149 | 150 | t.Run("overflow", func(t *testing.T) { 151 | destFieldOverflow := reflect.ValueOf(model).Elem().FieldByName("I8") 152 | cell.SetValue(math.MaxInt8 + 1) 153 | err := UnmarshalInt(destFieldOverflow, cell, &ExcelUnmarshalParameters{}) 154 | if err == nil { 155 | t.Fatal("expected overflow error") 156 | } 157 | }) 158 | } 159 | 160 | func TestUnmarshalUInt(t *testing.T) { 161 | model := &_model{} 162 | destField := reflect.ValueOf(model).Elem().FieldByName("U") 163 | cell := &xlsx.Cell{} 164 | 165 | t.Run("positive integer cell", func(t *testing.T) { 166 | cell.SetValue(123) 167 | err := UnmarshalUInt(destField, cell, &ExcelUnmarshalParameters{}) 168 | if err != nil { 169 | t.Fatal(err) 170 | } 171 | equal(t, uint64(123), model.U) 172 | }) 173 | 174 | t.Run("negative integer cell: error", func(t *testing.T) { 175 | cell.SetValue(-123) 176 | err := UnmarshalUInt(destField, cell, &ExcelUnmarshalParameters{}) 177 | if err == nil { 178 | t.Fatal("expected error") 179 | } 180 | }) 181 | 182 | t.Run("text cell", func(t *testing.T) { 183 | cell.SetValue("123") 184 | err := UnmarshalUInt(destField, cell, &ExcelUnmarshalParameters{}) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | equal(t, uint64(123), model.U) 189 | }) 190 | 191 | t.Run("float cell", func(t *testing.T) { 192 | cell.SetValue(123.7) 193 | err := UnmarshalUInt(destField, cell, &ExcelUnmarshalParameters{}) 194 | if err == nil { 195 | t.Fatal("expected format error") 196 | } 197 | }) 198 | 199 | t.Run("overflow", func(t *testing.T) { 200 | destFieldOverflow := reflect.ValueOf(model).Elem().FieldByName("U8") 201 | cell.SetValue(math.MaxUint8 + 1) 202 | err := UnmarshalUInt(destFieldOverflow, cell, &ExcelUnmarshalParameters{}) 203 | if err == nil { 204 | t.Fatal("expected overflow error") 205 | } 206 | }) 207 | } 208 | 209 | func TestUnmarshalFloat(t *testing.T) { 210 | model := &_model{} 211 | destField := reflect.ValueOf(model).Elem().FieldByName("F") 212 | cell := &xlsx.Cell{} 213 | 214 | t.Run("positive float cell", func(t *testing.T) { 215 | cell.SetValue(123.7) 216 | err := UnmarshalFloat(destField, cell, &ExcelUnmarshalParameters{}) 217 | if err != nil { 218 | t.Fatal(err) 219 | } 220 | equal(t, 123.7, model.F) 221 | }) 222 | 223 | t.Run("negative float cell", func(t *testing.T) { 224 | cell.SetValue(-123.7) 225 | err := UnmarshalFloat(destField, cell, &ExcelUnmarshalParameters{}) 226 | if err != nil { 227 | t.Fatal(err) 228 | } 229 | equal(t, -123.7, model.F) 230 | }) 231 | 232 | t.Run("text cell with float value", func(t *testing.T) { 233 | cell.SetValue("123.7") 234 | err := UnmarshalFloat(destField, cell, &ExcelUnmarshalParameters{}) 235 | if err != nil { 236 | t.Fatal(err) 237 | } 238 | equal(t, 123.7, model.F) 239 | }) 240 | 241 | t.Run("int cell", func(t *testing.T) { 242 | cell.SetValue(123) 243 | err := UnmarshalFloat(destField, cell, &ExcelUnmarshalParameters{}) 244 | if err != nil { 245 | t.Fatal(err) 246 | } 247 | equal(t, 123.0, model.F) 248 | }) 249 | 250 | t.Run("int cell", func(t *testing.T) { 251 | destFieldOverflow := reflect.ValueOf(model).Elem().FieldByName("F32") 252 | cell.SetValue(math.MaxFloat64) 253 | err := UnmarshalFloat(destFieldOverflow, cell, &ExcelUnmarshalParameters{}) 254 | if err == nil { 255 | t.Fatal("expected overflow error") 256 | } 257 | }) 258 | 259 | t.Run("text cell with non-numeric value returns error", func(t *testing.T) { 260 | cell.SetValue("not a number") 261 | err := UnmarshalFloat(destField, cell, &ExcelUnmarshalParameters{}) 262 | if err == nil { 263 | t.Fatal("expected error, got nil") 264 | } 265 | equal(t, "error parsing cell as float value: strconv.ParseFloat: parsing \"not a number\": invalid syntax", err.Error()) 266 | }) 267 | } 268 | 269 | func TestUnmarshalTime(t *testing.T) { 270 | model := &_model{} 271 | destField := reflect.ValueOf(model).Elem().FieldByName("T") 272 | cell := &xlsx.Cell{} 273 | 274 | // NOTE: Testing with too accurate of a date may result in floating point errors. 275 | testTime := time.Date(2023, time.November, 13, 14, 15, 0, 0, time.UTC) 276 | testTimeFormatted := testTime.Format(time.RFC3339) 277 | 278 | t.Run("date cell", func(t *testing.T) { 279 | cell.SetDate(testTime) 280 | err := UnmarshalTime(destField, cell, &ExcelUnmarshalParameters{}) 281 | if err != nil { 282 | t.Fatal(err) 283 | } 284 | equal(t, testTime, model.T) 285 | }) 286 | 287 | t.Run("date cell with fallback value", func(t *testing.T) { 288 | cell.SetDate(testTime) 289 | cell.Value = testTimeFormatted 290 | err := UnmarshalTime(destField, cell, &ExcelUnmarshalParameters{ 291 | FallbackDateFormats: []string{time.RFC3339}, 292 | }) 293 | if err != nil { 294 | t.Fatal(err) 295 | } 296 | equal(t, testTime, model.T) 297 | }) 298 | 299 | t.Run("date cell with unparsable value", func(t *testing.T) { 300 | cell.SetDate(testTime) 301 | cell.Value = "rubbish" 302 | err := UnmarshalTime(destField, cell, &ExcelUnmarshalParameters{}) 303 | if err == nil { 304 | t.Fatal("expected error, got nil") 305 | } 306 | }) 307 | 308 | t.Run("text cell with fallback value", func(t *testing.T) { 309 | cell.SetString(testTimeFormatted) 310 | err := UnmarshalTime(destField, cell, &ExcelUnmarshalParameters{ 311 | FallbackDateFormats: []string{time.RFC3339}, 312 | }) 313 | if err != nil { 314 | t.Fatal(err) 315 | } 316 | equal(t, testTime, model.T) 317 | }) 318 | 319 | t.Run("text cell with unparsable value", func(t *testing.T) { 320 | cell.SetString("rubbish") 321 | err := UnmarshalTime(destField, cell, &ExcelUnmarshalParameters{}) 322 | if err == nil { 323 | t.Fatal("expected error, got nil") 324 | } 325 | }) 326 | } 327 | 328 | func TestUnmarshalExcelUnmarshaler(t *testing.T) { 329 | model := &_model{} 330 | destField := reflect.ValueOf(model).Elem().FieldByName("EU") 331 | cell := &xlsx.Cell{} 332 | 333 | t.Run("successful unmarshalling", func(t *testing.T) { 334 | cell.SetString("unit test value") 335 | err := UnmarshalExcelUnmarshaler(destField, cell, &ExcelUnmarshalParameters{}) 336 | if err != nil { 337 | t.Fatal(err) 338 | } 339 | equal(t, customUnmarshalledString("excel unmarshalled: unit test value"), model.EU) 340 | }) 341 | 342 | t.Run("unsuccessful unmarshalling returns error", func(t *testing.T) { 343 | cell.SetString("error please") // Trigger an error in the unmarshaller 344 | err := UnmarshalExcelUnmarshaler(destField, cell, &ExcelUnmarshalParameters{}) 345 | equal(t, errors.New("excel unmarshalled: unit test error"), err) 346 | }) 347 | 348 | t.Run("catch wrong type", func(t *testing.T) { 349 | wrongField := reflect.ValueOf(model).Elem().FieldByName("TU") 350 | err := UnmarshalExcelUnmarshaler(wrongField, cell, &ExcelUnmarshalParameters{}) 351 | equal(t, ErrCannotCastUnmarshaler, err) 352 | }) 353 | } 354 | 355 | func TestUnmarshalTextUnmarshaler(t *testing.T) { 356 | model := &_model{} 357 | destField := reflect.ValueOf(model).Elem().FieldByName("TU") 358 | cell := &xlsx.Cell{} 359 | 360 | t.Run("successful unmarshalling", func(t *testing.T) { 361 | cell.SetString("unit test value") 362 | err := UnmarshalTextUnmarshaler(destField, cell, &ExcelUnmarshalParameters{}) 363 | if err != nil { 364 | t.Fatal(err) 365 | } 366 | equal(t, textUnmarshalledString("text unmarshalled: unit test value"), model.TU) 367 | }) 368 | 369 | t.Run("unsuccessful unmarshalling returns error", func(t *testing.T) { 370 | cell.SetString("error please") // Trigger an error in the unmarshaller 371 | err := UnmarshalTextUnmarshaler(destField, cell, &ExcelUnmarshalParameters{}) 372 | equal(t, errors.New("text unmarshalled: unit test error"), err) 373 | }) 374 | 375 | t.Run("catch wrong type", func(t *testing.T) { 376 | wrongField := reflect.ValueOf(model).Elem().FieldByName("EU") 377 | err := UnmarshalTextUnmarshaler(wrongField, cell, &ExcelUnmarshalParameters{}) 378 | equal(t, ErrCannotCastUnmarshaler, err) 379 | }) 380 | } 381 | 382 | func equal(t *testing.T, expected, actual any) { 383 | t.Helper() 384 | if !reflect.DeepEqual(expected, actual) { 385 | t.Errorf("test failed, expected \"%v\", got \"%v\"", expected, actual) 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /write.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | 14 | import ( 15 | "io" 16 | "reflect" 17 | 18 | "github.com/tealeg/xlsx/v3" 19 | ) 20 | 21 | type ( 22 | WriteConfigurator interface{ WriteConfigure(wc *WriteConfig) } 23 | WriteConfig struct { 24 | // Name of the Sheet created to hold the data. 25 | // Defaults to "Sheet1". 26 | SheetName string 27 | // Name of the tag on the data struct to configure column headers 28 | // and whether to ignore any fields. 29 | // Defaults to "excel". 30 | TagName string 31 | // If true, fields without the tag defined via TagName are ignored. 32 | // They are not written to the output file, 33 | // and will also not write a header. 34 | // Defaults to "false". 35 | IgnoreFieldsWithoutTag bool 36 | } 37 | ) 38 | 39 | var defaultWriteConfig = func() *WriteConfig { 40 | return &WriteConfig{SheetName: "Sheet1", TagName: "excel", IgnoreFieldsWithoutTag: false} 41 | } 42 | 43 | func write(sheet *xlsx.Sheet, data []any) { 44 | r := sheet.AddRow() 45 | for _, cell := range data { 46 | r.AddCell().SetValue(cell) 47 | } 48 | } 49 | 50 | // Write defines write []T to excel file 51 | // 52 | // params: file,excel file full path 53 | // 54 | // params: typed parameter T, must be implements exl.Bind 55 | func Write[T WriteConfigurator](file string, ts []T) error { 56 | f := xlsx.NewFile() 57 | write0(f, ts) 58 | return f.Save(file) 59 | } 60 | 61 | // WriteTo defines write to []T to excel file 62 | // 63 | // params: w, the dist writer 64 | // 65 | // params: typed parameter T, must be implements exl.Bind 66 | func WriteTo[T WriteConfigurator](w io.Writer, ts []T) error { 67 | f := xlsx.NewFile() 68 | write0(f, ts) 69 | return f.Write(w) 70 | } 71 | 72 | func write0[T WriteConfigurator](f *xlsx.File, ts []T) { 73 | wc := defaultWriteConfig() 74 | tT := new(T) 75 | // Always configure writes, even if the provided data is empty. 76 | // If not done this way, empty files could have different headers 77 | // compared to files with content, because the write config would not run. 78 | (*tT).WriteConfigure(wc) 79 | if sheet, _ := f.AddSheet(wc.SheetName); sheet != nil { 80 | typ := reflect.TypeOf(tT).Elem().Elem() 81 | numField := typ.NumField() 82 | header := make([]any, 0, numField) 83 | ignoreField := make([]bool, numField) 84 | for i := 0; i < numField; i++ { 85 | fe := typ.Field(i) 86 | name := fe.Name 87 | if tt, have := fe.Tag.Lookup(wc.TagName); have { 88 | name = tt 89 | } else if wc.IgnoreFieldsWithoutTag { 90 | ignoreField[i] = true 91 | continue 92 | } 93 | header = append(header, name) 94 | } 95 | // write header 96 | write(sheet, header) 97 | if len(ts) > 0 { 98 | // write data 99 | for _, t := range ts { 100 | data := make([]any, 0, numField) 101 | for i := 0; i < numField; i++ { 102 | if !ignoreField[i] { 103 | data = append(data, reflect.ValueOf(t).Elem().Field(i).Interface()) 104 | } 105 | } 106 | write(sheet, data) 107 | } 108 | } 109 | } 110 | } 111 | 112 | // WriteExcel defines write [][]string to excel 113 | // 114 | // params: file, excel file pull path 115 | // 116 | // params: data, write data to excel 117 | func WriteExcel(file string, data [][]string) error { 118 | f := xlsx.NewFile() 119 | writeExcel0(f, data) 120 | return f.Save(file) 121 | } 122 | 123 | // WriteExcelTo defines write [][]string to excel 124 | // 125 | // params: w, the dist writer 126 | // 127 | // params: data, write data to excel 128 | func WriteExcelTo(w io.Writer, data [][]string) error { 129 | f := xlsx.NewFile() 130 | writeExcel0(f, data) 131 | return f.Write(w) 132 | } 133 | 134 | func writeExcel0(f *xlsx.File, data [][]string) { 135 | sheet, _ := f.AddSheet("Sheet1") 136 | for _, row := range data { 137 | r := sheet.AddRow() 138 | for _, cell := range row { 139 | r.AddCell().SetString(cell) 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /write_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | 14 | import ( 15 | "os" 16 | "path" 17 | "strings" 18 | "testing" 19 | ) 20 | 21 | type writeTmp struct { 22 | Name1 string `excel:"Name1"` 23 | Name2 string `excel:"Name2"` 24 | Name3 string `excel:"Name3"` 25 | Name4 string `excel:"Name4"` 26 | Name5 string `excel:"Name5"` 27 | } 28 | 29 | type writeReadTmp writeTmp 30 | 31 | func (*writeReadTmp) WriteConfigure(_ *WriteConfig) {} 32 | func (*writeReadTmp) ReadConfigure(_ *ReadConfig) {} 33 | func (*writeTmp) WriteConfigure(_ *WriteConfig) {} 34 | 35 | // Row type which ignored fields without tag. 36 | type writeWithIgnore struct { 37 | Name1 string // This is ignored, should not be written 38 | Name2 string `excel:"Name2"` 39 | } 40 | 41 | func (*writeWithIgnore) WriteConfigure(wc *WriteConfig) { 42 | wc.IgnoreFieldsWithoutTag = true 43 | } 44 | 45 | // Row type for negative test, when a field does not have a tag 46 | // but the write configuration is set to write it anyway. 47 | type writeWithoutIgnore struct { 48 | Name1 string // This is NOT ignored, should be written 49 | Name2 string `excel:"Name2"` 50 | } 51 | 52 | func (*writeWithoutIgnore) WriteConfigure(wc *WriteConfig) { 53 | wc.IgnoreFieldsWithoutTag = false 54 | } 55 | 56 | // Row type used below to inspect headers of otherwise empty files. 57 | // The error message is checked to make sure ignored columns don't get a header. 58 | type readNoFields struct { 59 | } 60 | 61 | func (*readNoFields) ReadConfigure(rc *ReadConfig) { 62 | rc.SkipUnknownColumns = false 63 | // "Read" the header to not get an error message immediately 64 | rc.DataStartRowIndex = 0 65 | } 66 | 67 | func TestWriteErr(t *testing.T) { 68 | testFile := "tmp.xlsx" 69 | defer func() { _ = os.Remove(testFile) }() 70 | if err := Write(testFile, []*writeTmp{}); err != nil { 71 | t.Error("test failed") 72 | } 73 | } 74 | 75 | func TestWrite(t *testing.T) { 76 | testFile := "tmp.xlsx" 77 | defer func() { _ = os.Remove(testFile) }() 78 | data := []*writeTmp{ 79 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 80 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 81 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 82 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 83 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 84 | } 85 | if err := Write(testFile, data); err != nil { 86 | t.Error("test failed: " + err.Error()) 87 | } 88 | if models, err := ReadFile[*writeReadTmp](testFile); err != nil { 89 | t.Error("test failed: " + err.Error()) 90 | } else if len(models) != len(data) { 91 | t.Error("test failed") 92 | } else { 93 | for i, m := range models { 94 | d := data[i] 95 | if d.Name1 != m.Name1 { 96 | t.Error("test failed: Name1 not equal") 97 | } 98 | if d.Name2 != m.Name2 { 99 | t.Error("test failed: Name2 not equal") 100 | } 101 | if d.Name3 != m.Name3 { 102 | t.Error("test failed: Name3 not equal") 103 | } 104 | if d.Name4 != m.Name4 { 105 | t.Error("test failed: Name4 not equal") 106 | } 107 | if d.Name5 != m.Name5 { 108 | t.Error("test failed: Name5 not equal") 109 | } 110 | } 111 | } 112 | } 113 | 114 | func TestWriteTo(t *testing.T) { 115 | data := []*writeTmp{ 116 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 117 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 118 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 119 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 120 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 121 | } 122 | testFile := "tmp.xlsx" 123 | file, err := os.Create(testFile) 124 | defer func() { _ = file.Close(); _ = os.Remove(testFile) }() 125 | if err != nil { 126 | t.Error("test failed: " + err.Error()) 127 | } 128 | if err = WriteTo(file, data); err != nil { 129 | t.Error("test failed: " + err.Error()) 130 | } 131 | if models, err := ReadFile[*writeReadTmp](testFile); err != nil { 132 | t.Error("test failed: " + err.Error()) 133 | } else if len(models) != len(data) { 134 | t.Error("test failed") 135 | } else { 136 | for i, m := range models { 137 | d := data[i] 138 | if d.Name1 != m.Name1 { 139 | t.Error("test failed: Name1 not equal") 140 | } 141 | if d.Name2 != m.Name2 { 142 | t.Error("test failed: Name2 not equal") 143 | } 144 | if d.Name3 != m.Name3 { 145 | t.Error("test failed: Name3 not equal") 146 | } 147 | if d.Name4 != m.Name4 { 148 | t.Error("test failed: Name4 not equal") 149 | } 150 | if d.Name5 != m.Name5 { 151 | t.Error("test failed: Name5 not equal") 152 | } 153 | } 154 | } 155 | } 156 | 157 | func TestWriteExcelTo(t *testing.T) { 158 | data := []*writeTmp{ 159 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 160 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 161 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 162 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 163 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 164 | } 165 | testFile := "tmp.xlsx" 166 | file, err := os.Create(testFile) 167 | defer func() { _ = file.Close(); _ = os.Remove(testFile) }() 168 | if err != nil { 169 | t.Error("test failed: " + err.Error()) 170 | } 171 | if err = WriteExcelTo(file, [][]string{ 172 | {"Name1", "Name2", "Name3", "Name4", "Name5"}, 173 | {"Name11", "Name22", "Name33", "Name44", "Name55"}, 174 | {"Name111", "Name222", "Name333", "Name444", "Name555"}, 175 | {"Name1111", "Name2222", "Name3333", "Name4444", "Name5555"}, 176 | {"Name11111", "Name22222", "Name33333", "Name44444", "Name55555"}, 177 | {"Name111111", "Name222222", "Name333333", "Name444444", "Name555555"}, 178 | }); err != nil { 179 | t.Error("test failed: " + err.Error()) 180 | } 181 | if models, err := ReadFile[*writeReadTmp](testFile); err != nil { 182 | t.Error("test failed: " + err.Error()) 183 | } else if len(models) != len(data) { 184 | t.Error("test failed") 185 | } else { 186 | for i, m := range models { 187 | d := data[i] 188 | if d.Name1 != m.Name1 { 189 | t.Error("test failed: Name1 not equal") 190 | } 191 | if d.Name2 != m.Name2 { 192 | t.Error("test failed: Name2 not equal") 193 | } 194 | if d.Name3 != m.Name3 { 195 | t.Error("test failed: Name3 not equal") 196 | } 197 | if d.Name4 != m.Name4 { 198 | t.Error("test failed: Name4 not equal") 199 | } 200 | if d.Name5 != m.Name5 { 201 | t.Error("test failed: Name5 not equal") 202 | } 203 | } 204 | } 205 | } 206 | 207 | func TestWriteIgnoringFieldsWithoutTag(t *testing.T) { 208 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 209 | 210 | data := []*writeWithIgnore{ 211 | {"Name11", "Name22"}, 212 | {"Name111", "Name222"}, 213 | {"Name1111", "Name2222"}, 214 | {"Name11111", "Name22222"}, 215 | {"Name111111", "Name222222"}, 216 | } 217 | if err := Write(testFile, data); err != nil { 218 | t.Error("test failed: " + err.Error()) 219 | } 220 | if models, err := ReadFile[*writeReadTmp](testFile); err != nil { 221 | t.Error("test failed: " + err.Error()) 222 | } else if len(models) != len(data) { 223 | t.Error("test failed") 224 | } else { 225 | for i, m := range models { 226 | d := data[i] 227 | // Name2 should not be in the excel file, so also not read 228 | if m.Name1 != "" { 229 | t.Error("test failed: Name1 not empty") 230 | } 231 | if d.Name2 != m.Name2 { 232 | t.Error("test failed: Name2 not equal") 233 | } 234 | } 235 | } 236 | } 237 | 238 | func TestWriteNotIgnoringFieldsWithoutTag(t *testing.T) { 239 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 240 | 241 | data := []*writeWithoutIgnore{ 242 | {"Name11", "Name22"}, 243 | {"Name111", "Name222"}, 244 | {"Name1111", "Name2222"}, 245 | {"Name11111", "Name22222"}, 246 | {"Name111111", "Name222222"}, 247 | } 248 | if err := Write(testFile, data); err != nil { 249 | t.Error("test failed: " + err.Error()) 250 | } 251 | if models, err := ReadFile[*writeReadTmp](testFile); err != nil { 252 | t.Error("test failed: " + err.Error()) 253 | } else if len(models) != len(data) { 254 | t.Error("test failed") 255 | } else { 256 | for i, m := range models { 257 | d := data[i] 258 | // Should not be ignored, even though it does not have a tag 259 | if d.Name1 != m.Name1 { 260 | t.Error("test failed: Name1 not equal") 261 | } 262 | if d.Name2 != m.Name2 { 263 | t.Error("test failed: Name2 not equal") 264 | } 265 | } 266 | } 267 | } 268 | 269 | // Ensure that for empty data sets, the write configuration is still done, 270 | // so that custom tag names (if configured) are not ignored, 271 | // and ignored fields (if configured) don't get headers. 272 | func TestWriteWithoutRowsConfiguresHeader(t *testing.T) { 273 | testFile := path.Join(t.TempDir(), "tmp.xlsx") 274 | 275 | // Write data without content, which should write only a header 276 | data := []*writeWithIgnore{} 277 | if err := Write(testFile, data); err != nil { 278 | t.Error("test failed: " + err.Error()) 279 | } 280 | 281 | // Read the file without any fields in the struct, 282 | // which will give us an error message for the first column, 283 | // which should be "Name2" as "Name1" is ignored and should not be in the file. 284 | _, err := ReadFile[*readNoFields](testFile) 285 | if err == nil { 286 | t.Error("test failed, expected error") 287 | } 288 | if !strings.Contains(err.Error(), "Name2") { 289 | t.Error("test failed, expected message for second column, got: " + err.Error()) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | 14 | import ( 15 | "errors" 16 | "fmt" 17 | "io" 18 | "reflect" 19 | 20 | "github.com/tealeg/xlsx/v3" 21 | ) 22 | 23 | // Writer define a writer for exl 24 | type Writer struct { 25 | file *xlsx.File 26 | mapHeader []reflect.Value 27 | ignore map[int]struct{} 28 | } 29 | 30 | // NewWriter returns new exl writer 31 | func NewWriter(options ...xlsx.FileOption) *Writer { 32 | w := &Writer{file: xlsx.NewFile(options...)} 33 | w.reset() 34 | return w 35 | } 36 | 37 | // Write or append the param data into sheet 38 | func (w *Writer) Write(sheet string, data any) error { 39 | if sht, ok := w.file.Sheet[sheet]; ok { 40 | w.reset() 41 | return w.writeSheet(sht, data) 42 | } 43 | if sht, err := w.file.AddSheet(sheet); err != nil { 44 | return err 45 | } else { 46 | w.reset() 47 | return w.writeSheet(sht, data) 48 | } 49 | } 50 | 51 | // SaveTo the buffered binary into dist file 52 | func (w *Writer) SaveTo(path string) (err error) { return w.file.Save(path) } 53 | 54 | // WriteTo the buffered binary into new writer 55 | func (w *Writer) WriteTo(dw io.Writer) (n int, err error) { return 0, w.file.Write(dw) } 56 | 57 | func (w *Writer) writeSheet(sheet *xlsx.Sheet, data any) (err error) { 58 | value := w.deepValue(reflect.ValueOf(data)) 59 | vk := value.Type().Kind() 60 | switch vk { 61 | case reflect.Array, reflect.Slice: 62 | w.writeArrayOrSlice(sheet, value) 63 | return nil 64 | } 65 | return errors.New(fmt.Sprintf("not supported type: %v", vk)) 66 | } 67 | 68 | func (w *Writer) writeArrayOrSlice(sheet *xlsx.Sheet, value reflect.Value) { 69 | arrLen := value.Len() 70 | var header *reflect.Value 71 | if arrLen > 0 { 72 | dv := w.deepValue(value.Index(0)) 73 | header = &dv 74 | } 75 | w.setHeaderRow(sheet.AddRow(), w.deepType(value.Type().Elem()), header) 76 | for i := 0; i < arrLen; i++ { 77 | w.setDataRow(sheet.AddRow(), w.deepValue(value.Index(i))) 78 | } 79 | } 80 | 81 | func (w *Writer) setHeaderRow(row *xlsx.Row, typ reflect.Type, value *reflect.Value) { 82 | vk := typ.Kind() 83 | 84 | if vk == reflect.Map && value != nil { 85 | for _, _mk := range value.MapKeys() { 86 | mk := w.deepValue(_mk) 87 | w.mapHeader = append(w.mapHeader, mk) 88 | w.addCell(row, mk) 89 | } 90 | return 91 | } 92 | 93 | if vk == reflect.Struct { 94 | for i := 0; i < typ.NumField(); i++ { 95 | field := typ.Field(i) 96 | excelTag, _ := field.Tag.Lookup("excel") 97 | if excelTag == "" { 98 | row.AddCell().SetString(field.Name) 99 | } else if excelTag != "-" { 100 | row.AddCell().SetString(excelTag) 101 | } 102 | if excelTag == "-" { 103 | w.ignore[i] = struct{}{} 104 | } 105 | } 106 | return 107 | } 108 | 109 | row.AddCell().SetString("Unnamed") 110 | } 111 | 112 | func (w *Writer) setDataRow(row *xlsx.Row, value reflect.Value) { 113 | vk := value.Kind() 114 | 115 | if vk == reflect.Map { 116 | for _, k := range w.mapHeader { 117 | v := value.MapIndex(k) 118 | w.addCell(row, v) 119 | } 120 | return 121 | } 122 | 123 | if vk == reflect.Struct { 124 | da := value.Type() 125 | for i := 0; i < da.NumField(); i++ { 126 | if _, ok := w.ignore[i]; !ok { 127 | w.addCell(row, w.deepValue(value.Field(i))) 128 | } 129 | } 130 | return 131 | } 132 | 133 | w.addCell(row, value) 134 | } 135 | 136 | func (w *Writer) reset() { 137 | w.mapHeader = make([]reflect.Value, 0) 138 | w.ignore = make(map[int]struct{}, 0) 139 | } 140 | 141 | func (w *Writer) deepType(typ reflect.Type) reflect.Type { 142 | if typ.Kind() == reflect.Ptr { 143 | return w.deepType(typ.Elem()) 144 | } 145 | return typ 146 | } 147 | 148 | func (w *Writer) deepValue(value reflect.Value) reflect.Value { 149 | if value.Type().Kind() == reflect.Ptr { 150 | return w.deepValue(value.Elem()) 151 | } 152 | return value 153 | } 154 | 155 | func (w *Writer) addCell(row *xlsx.Row, value reflect.Value) { 156 | if value.CanInterface() { 157 | row.AddCell().SetValue(value.Interface()) 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /writer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 exl Author. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // Unless required by applicable law or agreed to in writing, software 7 | // distributed under the License is distributed on an "AS IS" BASIS, 8 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 9 | // See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | package exl 13 | 14 | import ( 15 | "bytes" 16 | "os" 17 | "testing" 18 | ) 19 | 20 | func TestWriter(t *testing.T) { 21 | defer os.Remove("out.xlsx") 22 | w := NewWriter() 23 | type testCase struct { 24 | name string 25 | data any 26 | } 27 | var ptr = 10 28 | var tcs = []testCase{ 29 | {"notSupport", 1000}, 30 | {"int", []int{1, 2}}, 31 | {"int\\ / ? * [ ]", []int{1, 2}}, 32 | {"int", []int{11, 22}}, 33 | {"float", []float32{1.1, 2.2}}, 34 | {"string", []string{"coco", "yoyo"}}, 35 | {"boolean", []bool{true, false}}, 36 | {"map", []map[string]any{{"key": "kk", "val": "vv"}}}, 37 | {"ptr", []*map[string]any{{"key": "kkk", "val": "vvv"}}}, 38 | {"struct", []struct { 39 | ID int `excel:"编号"` 40 | Name string `excel:"名称"` 41 | Extra bool `excel:"-"` 42 | Age int `excel:"年龄"` 43 | Addr string 44 | IDPtr *int 45 | }{ 46 | {10, "Apple", false, 25, "Addr1", &ptr}, 47 | {20, "Pear", true, 26, "Addr2", &ptr}, 48 | {30, "Banana", true, 30, "Addr3", &ptr}, 49 | }}, 50 | } 51 | for _, tc := range tcs { 52 | t.Run(tc.name, func(t *testing.T) { 53 | w.Write(tc.name, tc.data) 54 | }) 55 | } 56 | _ = w.SaveTo("out.xlsx") 57 | _, _ = w.WriteTo(&bytes.Buffer{}) 58 | } 59 | --------------------------------------------------------------------------------