Showing 8 changed files with 179 additions and 74 deletions
+4 -1
.gitignore
... ...
@@ -1,4 +1,7 @@
1 1
 *.csv
2 2
 *.xlsx
3 3
 .idea/**
4
-.idea
4
+.idea
5
+crosscompile.sh
6
+makefile
7
+csv2xlsx
+24 -7
README.md
... ...
@@ -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
 
+151 -66
csv2xlsx.go
... ...
@@ -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)
BIN
csv2xlsx_386.exe
Binary file not shown.
BIN
csv2xlsx_amd64.exe
Binary file not shown.
BIN
csv2xlsx_linux_386
Binary file not shown.
BIN
csv2xlsx_linux_amd64
Binary file not shown.
BIN
csv2xlsx_osx
Binary file not shown.