diff --git a/.gitignore b/.gitignore index fb0f8ba..8ed9d14 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,8 @@ json2excel # Ignore test data -*.json \ No newline at end of file +*.json + +# Ignore Mac DS_Store files +.DS_Store +**/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index 52af935..f0ad8e2 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,8 @@ It expects that the json input is formatted as an object containing an array of | freezeTopRow | true | Freezes the first row of the Excel worksheet | | autofilter | true | Sets the auto filter on the first row | | autowidth | true | Automatically set the column width to fit contents | + +## Advanced configuration + +Advanced settings can be provided via a top level json key named "config". Below are options that can be set via this config key: +- key-order \ No newline at end of file diff --git a/cmd/.DS_Store b/cmd/.DS_Store deleted file mode 100644 index 3dd2bda..0000000 Binary files a/cmd/.DS_Store and /dev/null differ diff --git a/cmd/main/main.go b/cmd/main/main.go index 576c91b..0869527 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -7,6 +7,7 @@ import ( "log" "os" "reflect" + "strings" "time" "unicode/utf8" @@ -16,9 +17,16 @@ import ( // Initial concept from https://stackoverflow.com/q/68621039 +type Config struct { + keyOrder map[int]string +} + +var config Config + func main() { //jsonFile := "test.json" parentNode := "input" + configNode := "config" //worksheetName := "Sheet2" //outputFilename := "test.xlsx" @@ -55,6 +63,91 @@ func main() { // Truncate worksheetName to the maximum 31 characters worksheetName = TruncateString(worksheetName, 31) + // Read the json input file + if fileExists(inputJson) { + s, err = os.ReadFile(inputJson) + if err != nil { + panic(err) + } + } else { + fmt.Printf("Input JSON file '%s' does not exist.\n", inputJson) + os.Exit(1) + } + + // Unmarshal the json into an orderedmap to preserve the ordering of json structure + o := orderedmap.New() + err = json.Unmarshal([]byte(s), &o) + if err != nil { + error := fmt.Sprintf("JSON Unmarshal error %s\n", err) + panic(error) + } + + // Assume that our content is within the first top-level key + topLevel := o.Keys() + + // Check for config embedded in json + if strings.EqualFold(topLevel[0], configNode) && len(topLevel) > 1 { + fmt.Printf("Found configNode as toplevel json key, setting parentNode as '%s'\n", topLevel[1]) + parentNode = topLevel[1] + + config.keyOrder = make(map[int]string) + + // Get a reference to the top level node we specified earlier + configInterface, ok := o.Get(configNode) + if !ok { + fmt.Printf("Missing key for multitype array when reading embedded config") + } + + // Get an interface that we can work with to access the sub elements + //configSlice := configInterface + //fmt.Printf("%v\n", configSlice) + + // Get the keys for the first element so we know what config options have been specified + configMap := configInterface.(orderedmap.OrderedMap) + configKeys := configMap.Keys() + + // Parse each key into our config struct + for _, key := range configKeys { + if strings.EqualFold(key, "key-order") { + fmt.Printf("Found config element for key-order\n") + e, _ := configMap.Get(key) + for i, e := range strings.Split(e.(string), ",") { + config.keyOrder[i] = e + } + fmt.Printf("Column order is now : '%v'\n", config.keyOrder) + } else if strings.EqualFold(key, "overwrite-file") { + fmt.Printf("Found config element for overwriting output file\n") + e, _ := configMap.Get(key) + overwriteFile = e.(bool) + } + } + } else if strings.EqualFold(topLevel[0], configNode) { + error := "Only found config in first level of json keys" + panic(error) + } else { + fmt.Printf("Detected toplevel json key as: '%s'\n", topLevel[0]) + parentNode = topLevel[0] + } + + // Get a reference to the top level node we specified earlier + vislice, ok := o.Get(parentNode) + if !ok { + fmt.Printf("Missing key for multitype array") + } + + // Get an interface that we can work with to access the sub elements + vslice := vislice.([]interface{}) + + // Check that the first element is what we expected + if _, ok := vslice[0].(orderedmap.OrderedMap); !ok { + error := fmt.Sprintf("Type of first vslice element is not an ordered map. It appears to be '%v'\n", reflect.TypeOf(vslice[0])) + panic(error) + } + // Get the keys for the first element so we know what the column names will be + columnMap := vslice[0].(orderedmap.OrderedMap) + //fmt.Printf("First vslice element is an ordered map\n") + columnNames := columnMap.Keys() + // Check if xlsx file exists already, and if it does then open and append data if fileExists(outputFilename) { if overwriteFile { @@ -75,48 +168,6 @@ func main() { xlsx = createWorkbook(worksheetName, outputFilename) } - // Read the json input file - if fileExists(inputJson) { - s, err = os.ReadFile(inputJson) - if err != nil { - panic(err) - } - } else { - fmt.Printf("Input JSON file '%s' does not exist.\n", inputJson) - os.Exit(1) - } - - // Unmarshal the json into an orderedmap to preserve the ordering of json structure - o := orderedmap.New() - err = json.Unmarshal([]byte(s), &o) - if err != nil { - fmt.Printf("JSON Unmarshal error %s\n", err) - } - - // Assume that our content is within the first top-level key - topLevel := o.Keys() - fmt.Printf("Detected toplevel json key as: '%s'\n", topLevel[0]) - parentNode = topLevel[0] - - // Get a reference to the top level node we specified earlier - vislice, ok := o.Get(parentNode) - if !ok { - fmt.Printf("Missing key for multitype array") - } - - // Get an interface that we can work with to access the sub elements - vslice := vislice.([]interface{}) - - // Check that the first element is what we expected - if _, ok := vslice[0].(orderedmap.OrderedMap); !ok { - error := fmt.Sprintf("Type of first vslice element is not an ordered map. It appears to be '%v'\n", reflect.TypeOf(vslice[0])) - panic(error) - } - // Get the keys for the first element so we know what the column names will be - columnMap := vslice[0].(orderedmap.OrderedMap) - fmt.Printf("First vslice element is an ordered map") - columnNames := columnMap.Keys() - // Run code to add column names to the first row of the workbook createHeadingRow(xlsx, worksheetName, columnNames) @@ -186,23 +237,37 @@ func main() { // Each iteration should start back at column 1 column = 1 - // Print each key-value pair contained in this slice + // Get the key-value pairs contained in this slice vmap := v.(orderedmap.OrderedMap) k := vmap.Keys() - for j := range k { - //a = string(asciiValue) - //cell = a + strconv.Itoa(2+i) - cell, _ = excelize.CoordinatesToCellName(column, row) + if len(config.keyOrder) > 0 { + // If we have a specified order for the values then use that - e, _ := vmap.Get(k[j]) - //fmt.Printf("Setting cell %s to value %v\n", cell, e) - xlsx.SetCellValue(worksheetName, cell, e) + for j := 0; j < len(config.keyOrder); j++ { + cell, _ = excelize.CoordinatesToCellName(column, row) - // Move to the next column - //asciiValue++ - column++ + e, _ := vmap.Get(config.keyOrder[j]) + //fmt.Printf("Setting cell %s to value %v\n", cell, e) + xlsx.SetCellValue(worksheetName, cell, e) + + // Move to the next column + column++ + } + } else { + // Otherwise use the order the json was in + for j := range k { + cell, _ = excelize.CoordinatesToCellName(column, row) + + e, _ := vmap.Get(k[j]) + //fmt.Printf("Setting cell %s to value %v\n", cell, e) + xlsx.SetCellValue(worksheetName, cell, e) + + // Move to the next column + column++ + } } + //fmt.Printf("k: %v\n", k) // Move to next row @@ -327,14 +392,32 @@ func createHeadingRow(xlsx *excelize.File, worksheetName string, columnNames []s row := 1 column := 1 - // Add the header row - for i := 0; i < len(columnNames); i++ { - cell, _ = excelize.CoordinatesToCellName(column, row) - fmt.Printf("Setting cell %s to value %s\n", cell, columnNames[i]) - xlsx.SetCellValue(worksheetName, cell, columnNames[i]) - //xlsx.SetCellStyle(worksheetName, cell, cell, headerStyle) - column++ + if len(config.keyOrder) > 0 { + fmt.Printf("Setting heading order as per config key-order\n") + // Check that the number of specified columns matches input data + if len(config.keyOrder) != len(columnNames) { + error := fmt.Sprintf("Column order specified in json key-order but mismatch found in json data. %d specifed columns does not match %d found columns.", len(config.keyOrder), len(columnNames)) + panic(error) + } + + // Iterate the map and add the columns as per that order + for i := 0; i < len(config.keyOrder); i++ { + cell, _ = excelize.CoordinatesToCellName(column, row) + fmt.Printf("Setting cell %s to value %s at index %d\n", cell, config.keyOrder[i], i) + xlsx.SetCellValue(worksheetName, cell, config.keyOrder[i]) + column++ + } + } else { + // Add the header row + for i := 0; i < len(columnNames); i++ { + cell, _ = excelize.CoordinatesToCellName(column, row) + fmt.Printf("Setting cell %s to value %s\n", cell, columnNames[i]) + xlsx.SetCellValue(worksheetName, cell, columnNames[i]) + //xlsx.SetCellStyle(worksheetName, cell, cell, headerStyle) + column++ + } } + } // Taken from https://github.com/qax-os/excelize/issues/92#issuecomment-821578446 @@ -352,7 +435,7 @@ func SetColAutoWidth(xlsx *excelize.File, sheetName string) error { largestWidth = cellWidth } } - fmt.Printf("SetColAutoWidth calculated largest width is '%d'\n", largestWidth) + fmt.Printf("SetColAutoWidth calculated largest width for column index '%d' is '%d'\n", idx, largestWidth) name, err := excelize.ColumnNumberToName(idx + 1) if err != nil { return err diff --git a/internal/.DS_Store b/internal/.DS_Store deleted file mode 100644 index 5008ddf..0000000 Binary files a/internal/.DS_Store and /dev/null differ