| ... | ... |
@@ -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) |