... | ... |
@@ -1,4 +1,7 @@ |
1 | 1 |
*.csv |
2 | 2 |
*.xlsx |
3 | 3 |
.idea/** |
4 |
-.idea |
|
4 |
+.idea |
|
5 |
+crosscompile.sh |
|
6 |
+makefile |
|
7 |
+csv2xlsx |
... | ... |
@@ -12,11 +12,11 @@ license below), you may download the binary. |
12 | 12 |
|
13 | 13 |
Here are the SHA-256 checksums for the binaries: |
14 | 14 |
|
15 |
- 724ffe87ca8b81173faf3219015fb3e529ef357399b5827bafe564b8c8d87970 csv2xlsx_386.exe |
|
16 |
- 7068b39ac35b2a419fbde39871253170c4666a2617154027247e43d32faff6a5 csv2xlsx_amd64.exe |
|
17 |
- 49b1ed81c1d3dc15ef24618a97892bda91216103466db3cfb8b8811c7cf5ed33 csv2xlsx_linux_386 |
|
18 |
- d4a01f8ae47c7c6315e828df06070e93202b4448e98bf957799d3be389be7209 csv2xlsx_linux_amd64 |
|
19 |
- dcbdb99e29552afcd26755a0bbd993180e8e1ad5ce21bd6f8f351cebed9bf6c5 csv2xlsx_osx |
|
15 |
+ 07edbff0609058b31bbbfdce532f0b83919da029555970c12af5fa52b0c2f9d1 csv2xlsx_386.exe |
|
16 |
+ 818041bde85552ea4930152987c478f581614e7768e272af28b2bdd1b4940ed7 csv2xlsx_amd64.exe |
|
17 |
+ a1c4b4a84e467f878c9ae413e732f697d75fa8ced49c5adea22f9fab251ff3c9 csv2xlsx_linux_386 |
|
18 |
+ 955a8d4de854ab0c5fd7c7e676c61f61c3f59712a8da101a1db1c71ebc622bb0 csv2xlsx_linux_amd64 |
|
19 |
+ 61ed47ca548ec7773080ff4a059dc2e8a04f345c45b8c0456ef7013dbf9d0047 csv2xlsx_osx |
|
20 | 20 |
|
21 | 21 |
|
22 | 22 |
### Usage |
... | ... |
@@ -28,12 +28,16 @@ Please see below for a list of command line options. |
28 | 28 |
|
29 | 29 |
``` |
30 | 30 |
-? display usage information |
31 |
+ -abortonerror |
|
32 |
+ abort program on first invalid cell data type |
|
31 | 33 |
-colsep string |
32 | 34 |
column separator (default '|') (default "|") |
33 | 35 |
-columns string |
34 | 36 |
column range to use (see below) |
35 | 37 |
-dateformat string |
36 |
- format for date cells (default YYYY-MM-DD) (default "2006-01-02") |
|
38 |
+ format for CSV date cells (default YYYY-MM-DD) (default "2006-01-02") |
|
39 |
+ -exceldateformat string |
|
40 |
+ Excel format for date cells (default as in Excel) |
|
37 | 41 |
-h display usage information |
38 | 42 |
-help |
39 | 43 |
display usage information |
... | ... |
@@ -53,7 +57,8 @@ Please see below for a list of command line options. |
53 | 57 |
use first row as titles (will force string type) (default true) |
54 | 58 |
|
55 | 59 |
Column ranges are a comma-separated list of numbers (e.g. 1,4,8,16), intervals (e.g. 0-4,18-32) or a combination. |
56 |
- Each comma group can take a type specifiers for the column, one of "text", "number", "date" or "standard", |
|
60 |
+ Each comma group can take a type specifiers for the column, |
|
61 |
+ one of "text", "number", "integer", "currency", date" or "standard", |
|
57 | 62 |
separated from numbers with a colon (e.g. 0:text,3-16:number,17:date) |
58 | 63 |
``` |
59 | 64 |
|
... | ... |
@@ -66,6 +71,18 @@ the code or want to contribute, please do not hesitate to do so. I'd really like |
66 | 71 |
As my spare time for coding is limited to some hours around midnight a week, so please have some patience with my answers. |
67 | 72 |
I am still amazed what you can accomplish within less than 200 lines of code in terms of making my admin part of life easier. :-) |
68 | 73 |
|
74 |
+### Changelog |
|
75 |
+ |
|
76 |
+ 2017-08-03 0.0.1 |
|
77 |
+ Initial commit. First, ugly version |
|
78 |
+ |
|
79 |
+ 2017-08-04 0.1.2 |
|
80 |
+ Refactored code to improve readability, added options |
|
81 |
+ --abortonerror |
|
82 |
+ --exceldateformat |
|
83 |
+ --silent |
|
84 |
+ Added datatypes integer, currency |
|
85 |
+ Prints version info on Usage or with --version |
|
69 | 86 |
|
70 | 87 |
### License |
71 | 88 |
|
... | ... |
@@ -10,9 +10,34 @@ import ( |
10 | 10 |
"os" |
11 | 11 |
"strconv" |
12 | 12 |
"strings" |
13 |
+ "path/filepath" |
|
13 | 14 |
"time" |
14 | 15 |
) |
15 | 16 |
|
17 |
+var ( |
|
18 |
+ parmCols string |
|
19 |
+ parmRows string |
|
20 |
+ parmSheet string |
|
21 |
+ parmInFile string |
|
22 |
+ parmOutFile string |
|
23 |
+ parmColSep string |
|
24 |
+ parmRowSep string |
|
25 |
+ parmDateFormat string |
|
26 |
+ parmExcelDateFormat string |
|
27 |
+ parmUseTitles bool |
|
28 |
+ parmSilent bool |
|
29 |
+ parmHelp bool |
|
30 |
+ parmAbortOnError bool |
|
31 |
+ parmShowVersion bool |
|
32 |
+ rowRangeParsed map[int]string |
|
33 |
+ colRangeParsed map[int]string |
|
34 |
+ workBook *xlsx.File |
|
35 |
+ workSheet *xlsx.Sheet |
|
36 |
+ rightAligned *xlsx.Style |
|
37 |
+ buildTimestamp string |
|
38 |
+ versionInfo string |
|
39 |
+) |
|
40 |
+ |
|
16 | 41 |
// parseCommaGroup parses a single comma group (x or x-y), |
17 | 42 |
// optionally followed by :datatype (used only for columns right now) |
18 | 43 |
// It returns a map with row or column index as key and the datatype as value |
... | ... |
@@ -68,47 +93,39 @@ func parseRangeString(rangeStr string) map[int]string { |
68 | 93 |
return result |
69 | 94 |
} |
70 | 95 |
|
71 |
-// the main entry function |
|
72 |
-func main() { |
|
73 |
- var ( |
|
74 |
- parmCols string |
|
75 |
- parmRows string |
|
76 |
- parmSheet string |
|
77 |
- parmInFile string |
|
78 |
- parmOutFile string |
|
79 |
- parmColSep string |
|
80 |
- parmRowSep string |
|
81 |
- parmDateFormat string |
|
82 |
- parmUseTitles bool |
|
83 |
- parmSilent bool |
|
84 |
- parmHelp bool |
|
85 |
- err error |
|
86 |
- rowRangeParsed map[int]string |
|
87 |
- colRangeParsed map[int]string |
|
88 |
- workBook *xlsx.File |
|
89 |
- workSheet *xlsx.Sheet |
|
90 |
- ) |
|
91 |
- |
|
96 |
+// ParseCommandLine defines and parses command line flags and checks for usage info flags. |
|
97 |
+// The function exits the program, if the input file does not exist |
|
98 |
+func parseCommandLine() { |
|
92 | 99 |
flag.StringVar(&parmInFile, "infile", "", "full pathname of input file (CSV file)") |
93 | 100 |
flag.StringVar(&parmOutFile, "outfile", "", "full pathname of output file (.xlsx file)") |
94 |
- flag.StringVar(&parmDateFormat, "dateformat", "2006-01-02", "format for date cells (default YYYY-MM-DD)") |
|
101 |
+ flag.StringVar(&parmDateFormat, "dateformat", "2006-01-02", "format for CSV date cells (default YYYY-MM-DD)") |
|
102 |
+ flag.StringVar(&parmExcelDateFormat, "exceldateformat", "", "Excel format for date cells (default as in Excel)") |
|
95 | 103 |
flag.StringVar(&parmCols, "columns", "", "column range to use (see below)") |
96 | 104 |
flag.StringVar(&parmRows, "rows", "", "list of line numbers to use (1,2,8 or 1,3-14,28)") |
97 | 105 |
flag.StringVar(&parmSheet, "sheet", "fromCSV", "tab name of the Excel sheet") |
98 | 106 |
flag.StringVar(&parmColSep, "colsep", "|", "column separator (default '|') ") |
99 | 107 |
flag.StringVar(&parmRowSep, "rowsep", "\n", "row separator (default LF) ") |
100 | 108 |
flag.BoolVar(&parmUseTitles, "usetitles", true, "use first row as titles (will force string type)") |
109 |
+ flag.BoolVar(&parmAbortOnError, "abortonerror", false, "abort program on first invalid cell data type") |
|
101 | 110 |
flag.BoolVar(&parmSilent, "silent", false, "do not display progress messages") |
102 | 111 |
flag.BoolVar(&parmHelp, "help", false, "display usage information") |
103 | 112 |
flag.BoolVar(&parmHelp, "h", false, "display usage information") |
104 | 113 |
flag.BoolVar(&parmHelp, "?", false, "display usage information") |
114 |
+ flag.BoolVar(&parmShowVersion, "version", false, "display version information") |
|
105 | 115 |
flag.Parse() |
106 | 116 |
|
117 |
+ if parmShowVersion { |
|
118 |
+ fmt.Println("Version ", versionInfo, ", Build timestamp ", buildTimestamp) |
|
119 |
+ os.Exit(0) |
|
120 |
+ } |
|
121 |
+ |
|
107 | 122 |
if parmHelp { |
123 |
+ fmt.Printf("You are running verion %s of %s\n\n", versionInfo, filepath.Base(os.Args[0])) |
|
108 | 124 |
flag.Usage() |
109 | 125 |
fmt.Println(` |
110 | 126 |
Column ranges are a comma-separated list of numbers (e.g. 1,4,8,16), intervals (e.g. 0-4,18-32) or a combination. |
111 |
- Each comma group can take a type specifiers for the column, one of "text", "number", "date" or "standard", |
|
127 |
+ Each comma group can take a type specifiers for the column, |
|
128 |
+ one of "text", "number", "integer", "currency", date" or "standard", |
|
112 | 129 |
separated from numbers with a colon (e.g. 0:text,3-16:number,17:date) |
113 | 130 |
`) |
114 | 131 |
os.Exit(1) |
... | ... |
@@ -117,76 +134,144 @@ func main() { |
117 | 134 |
fmt.Println("Input file does not exist, exiting.") |
118 | 135 |
os.Exit(1) |
119 | 136 |
} |
137 |
+} |
|
120 | 138 |
|
121 |
- // first read csv file to allow using actual row and col counts as option defaults |
|
122 |
- f, err := os.Open(parmInFile) |
|
139 |
+// loadInputFile reads the complete input file into a matrix of strings. |
|
140 |
+// currently there is not need for gigabyte files, but maybe this should be done streaming. |
|
141 |
+// in addition, we need row and column counts first to set the default ranges later on in the program flow. |
|
142 |
+func loadInputFile(filename string) (rows [][]string) { |
|
143 |
+ f, err := os.Open(filename) |
|
144 |
+ defer f.Close() |
|
123 | 145 |
if err != nil { |
124 | 146 |
fmt.Println(err) |
125 | 147 |
os.Exit(1) |
126 | 148 |
} |
149 |
+ // use csv reader to read entire file |
|
127 | 150 |
r := csv.NewReader(bufio.NewReader(f)) |
128 | 151 |
r.Comma = []rune(parmColSep)[0] |
129 | 152 |
r.LazyQuotes = true |
130 |
- |
|
131 |
- rows, err := r.ReadAll() |
|
153 |
+ rows, err = r.ReadAll() |
|
132 | 154 |
if err != nil { |
133 | 155 |
fmt.Println(err) |
134 |
- os.Exit(1) |
|
156 |
+ os.Exit(2) |
|
135 | 157 |
} |
158 |
+ // if we get here, we have file data, so no need for an error value. |
|
159 |
+ return rows |
|
160 |
+} |
|
136 | 161 |
|
162 |
+// setRangeInformation uses the input file's row and column count to set the default ranges |
|
163 |
+// for lines and columns. of course we could leave this out by improving the parser function |
|
164 |
+// at parseRangeString to allow something like line 34- (instead of 34-999). It's on the list ... |
|
165 |
+func setRangeInformation(rowCount, colCount int) { |
|
166 |
+ // now we can set the default ranges for lines and columns |
|
137 | 167 |
if parmRows == "" { |
138 |
- parmRows = fmt.Sprintf("0-%d", len(rows)) |
|
168 |
+ parmRows = fmt.Sprintf("0-%d", rowCount) |
|
139 | 169 |
} |
140 | 170 |
if parmCols == "" { |
141 |
- colCount := len(rows[0]) |
|
142 | 171 |
parmCols = fmt.Sprintf("0-%d", colCount) |
143 | 172 |
} |
144 |
- |
|
145 | 173 |
// will bail out on parse error, see declaration |
146 | 174 |
rowRangeParsed = parseRangeString(parmRows) |
147 | 175 |
colRangeParsed = parseRangeString(parmCols) |
176 |
+} |
|
177 |
+ |
|
178 |
+// writeCellContents is basically a boring comparison which data type should be written |
|
179 |
+// to the spreadsheet cell. if the function encounters invalid values for the data type, |
|
180 |
+// it outputs an error message and ignores the value |
|
181 |
+func writeCellContents(cell *xlsx.Cell, colString, colType string, rownum, colnum int) bool { |
|
182 |
+ success := true |
|
183 |
+ switch colType { |
|
184 |
+ case "text": |
|
185 |
+ cell.SetString(colString) |
|
186 |
+ case "number": |
|
187 |
+ case "currency": |
|
188 |
+ floatVal, err := strconv.ParseFloat(colString, 64) |
|
189 |
+ if err != nil { |
|
190 |
+ fmt.Println(fmt.Sprintf("Cell (%d,%d) is not a valid number, value: %s", rownum, colnum, colString)) |
|
191 |
+ success = false |
|
192 |
+ } else { |
|
193 |
+ cell.SetStyle(rightAligned) |
|
194 |
+ if colType == "currency" { |
|
195 |
+ cell.SetFloatWithFormat(floatVal, "#,##0.00;[red](#,##0.00)") |
|
196 |
+ } else { |
|
197 |
+ cell.SetFloat(floatVal) |
|
198 |
+ } |
|
199 |
+ } |
|
200 |
+ case "integer": |
|
201 |
+ intVal, err := strconv.ParseInt(colString, 10, 64) |
|
202 |
+ if err != nil { |
|
203 |
+ fmt.Println(fmt.Sprintf("Cell (%d,%d) is not a valid integer, value: %s", rownum, colnum, colString)) |
|
204 |
+ success = false |
|
205 |
+ } else { |
|
206 |
+ cell.SetStyle(rightAligned) |
|
207 |
+ cell.SetInt64(intVal) |
|
208 |
+ cell.NumFmt = "#0" |
|
209 |
+ } |
|
210 |
+ case "date": |
|
211 |
+ dt, err := time.Parse(parmDateFormat, colString) |
|
212 |
+ if err != nil { |
|
213 |
+ fmt.Println(fmt.Sprintf("Cell (%d,%d) is not a valid date, value: %s", rownum, colnum, colString)) |
|
214 |
+ success = false |
|
215 |
+ } else { |
|
216 |
+ cell.SetDateTime(dt) |
|
217 |
+ if parmExcelDateFormat != "" { |
|
218 |
+ cell.NumFmt = parmExcelDateFormat |
|
219 |
+ } |
|
220 |
+ } |
|
221 |
+ default: |
|
222 |
+ cell.SetValue(colString) |
|
223 |
+ } |
|
224 |
+ return success |
|
225 |
+} |
|
148 | 226 |
|
227 |
+// processDataColumns processes a row from the csv input file and writes a cell for each column |
|
228 |
+// that should be processed (is in column range, which means it is a key in the colRangeParsed map. |
|
229 |
+// if the abortOnError option is set, the function exits the program on the first data type error. |
|
230 |
+func processDataColumns(excelRow *xlsx.Row, rownum int, csvLine []string) { |
|
231 |
+ if !parmSilent { |
|
232 |
+ fmt.Println(fmt.Sprintf("Processing csvLine %d (%d cols)", rownum, len(csvLine))) |
|
233 |
+ } |
|
234 |
+ for colnum := 0; colnum < len(csvLine); colnum++ { |
|
235 |
+ colType, processColumn := colRangeParsed[colnum] |
|
236 |
+ if processColumn { |
|
237 |
+ cell := excelRow.AddCell() |
|
238 |
+ if rownum == 0 && parmUseTitles { |
|
239 |
+ // special case for the title row |
|
240 |
+ cell.SetString(csvLine[colnum]) |
|
241 |
+ if colType == "number" || colType == "currency" { |
|
242 |
+ cell.SetStyle(rightAligned) |
|
243 |
+ } |
|
244 |
+ } else { |
|
245 |
+ // if the user wanted drama (--abortonerror), exit on first error |
|
246 |
+ ok := writeCellContents(cell, csvLine[colnum], colType, rownum, colnum) |
|
247 |
+ if !ok && parmAbortOnError { |
|
248 |
+ os.Exit(3) |
|
249 |
+ } |
|
250 |
+ } |
|
251 |
+ } |
|
252 |
+ } |
|
253 |
+} |
|
254 |
+ |
|
255 |
+// the main entry function |
|
256 |
+func main() { |
|
257 |
+ // preflight stuff |
|
258 |
+ parseCommandLine() |
|
259 |
+ rows := loadInputFile(parmInFile) |
|
260 |
+ setRangeInformation(len(rows), len(rows[0])) |
|
261 |
+ |
|
262 |
+ // excel stuff, create file, add worksheet, define a right-aligned style |
|
149 | 263 |
workBook = xlsx.NewFile() |
150 | 264 |
workSheet, _ = workBook.AddSheet(parmSheet) |
265 |
+ rightAligned = &xlsx.Style{} |
|
266 |
+ rightAligned.Alignment = xlsx.Alignment{Horizontal: "right"} |
|
151 | 267 |
|
268 |
+ // loop thru line and column ranges and process data cells |
|
152 | 269 |
for rownum := 0; rownum < len(rows); rownum++ { |
153 |
- _, ok := rowRangeParsed[rownum] |
|
154 |
- if ok { |
|
270 |
+ _, processLine := rowRangeParsed[rownum] |
|
271 |
+ if processLine { |
|
155 | 272 |
line := rows[rownum] |
156 | 273 |
excelRow := workSheet.AddRow() |
157 |
- if !parmSilent { |
|
158 |
- fmt.Println(fmt.Sprintf("Processing line %d (%d cols)", rownum, len(line))) |
|
159 |
- } |
|
160 |
- for colnum := 0; colnum < len(line); colnum++ { |
|
161 |
- colType, ok := colRangeParsed[colnum] |
|
162 |
- if ok { |
|
163 |
- cell := excelRow.AddCell() |
|
164 |
- if rownum == 0 && parmUseTitles { |
|
165 |
- cell.SetString(line[colnum]) |
|
166 |
- } else { |
|
167 |
- switch colType { |
|
168 |
- case "text": |
|
169 |
- cell.SetString(line[colnum]) |
|
170 |
- case "number": |
|
171 |
- floatVal, err := strconv.ParseFloat(line[colnum], 64) |
|
172 |
- if err != nil { |
|
173 |
- fmt.Println(fmt.Sprintf("Cell (%d,%d) is not a valid number, value: %s", rownum, colnum, line[colnum])) |
|
174 |
- } else { |
|
175 |
- cell.SetFloat(floatVal) |
|
176 |
- } |
|
177 |
- case "date": |
|
178 |
- dt, err := time.Parse(parmDateFormat, line[colnum]) |
|
179 |
- if err != nil { |
|
180 |
- fmt.Println(fmt.Sprintf("Cell (%d,%d) is not a valid date, value: %s", rownum, colnum, line[colnum])) |
|
181 |
- } else { |
|
182 |
- cell.SetDateTime(dt) |
|
183 |
- } |
|
184 |
- default: |
|
185 |
- cell.SetValue(line[colnum]) |
|
186 |
- } |
|
187 |
- } |
|
188 |
- } |
|
189 |
- } |
|
274 |
+ processDataColumns(excelRow, rownum, line) |
|
190 | 275 |
} |
191 | 276 |
} |
192 | 277 |
workBook.Save(parmOutFile) |