package main import ( "encoding/json" "flag" "fmt" "log" "os" "unicode/utf8" "github.com/iancoleman/orderedmap" "github.com/xuri/excelize/v2" ) // Initial concept from https://stackoverflow.com/q/68621039 func main() { //jsonFile := "test.json" parentNode := "input" //worksheetName := "Sheet2" //outputFilename := "test.xlsx" // Command line arguments var inputJson string var worksheetName string var outputFilename string var boldTopRow bool var freezeTopRow bool var autoFilter bool var autoWidth bool // Process command line arguments flag.StringVar(&inputJson, "inputJson", "./input.json", "Full path to input json data file") flag.StringVar(&outputFilename, "outputFilename", "./output.xlsx", "Filename for excel worksheet output") flag.StringVar(&worksheetName, "worksheetName", "Sheet1", "Label to set on worksheet") flag.BoolVar(&boldTopRow, "bold-toprow", true, "Sets the top row of the worksheet to bold") flag.BoolVar(&freezeTopRow, "freeze-toprow", true, "Freezes the first row of the Excel worksheet") flag.BoolVar(&autoFilter, "autofilter", true, "Sets the auto filter on the first row") flag.BoolVar(&autoWidth, "autowidth", true, "Automatically set the column width to fit contents") flag.Parse() var xlsx *excelize.File var s []byte var sheetIndex int var err error var cell string var row, column int // TODO - truncate worksheetName to the maximum 31 characters // Check if xlsx file exists already, and if it does then open and append data if fileExists(outputFilename) { fmt.Printf("Output spreadsheet '%s' already exists.\n", outputFilename) xlsx, err = excelize.OpenFile(outputFilename) if err != nil { fmt.Println(err) os.Exit(1) } // Since we have an existing workbook, check if the sheet we want to write to already exists sheetFound := false for index, name := range xlsx.GetSheetMap() { if name == worksheetName { fmt.Printf("Found worksheet '%s' at index '%d'\n", worksheetName, index) sheetFound = true } } if !sheetFound { // Create the sheet fmt.Printf("Creating worksheet '%s'\n", worksheetName) sheetIndex, err = xlsx.NewSheet(worksheetName) if err != nil { fmt.Printf("Error creating worksheet '%s' : %s\n", worksheetName, err) } // Set active worksheet fmt.Printf("Setting active sheet to index %d", sheetIndex) xlsx.SetActiveSheet(sheetIndex) } } else { fmt.Printf("Creating output spreadsheet '%s'\n", outputFilename) // Create the file xlsx = excelize.NewFile() // Rename the default Sheet1 to this worksheet name if worksheetName != "Sheet1" { fmt.Printf("Renaming default worksheet to '%s'\n", worksheetName) err = xlsx.SetSheetName("Sheet1", worksheetName) if err != nil { fmt.Printf("Error setting sheet name to '%s': %s\n", worksheetName, err) } } } // 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{}) // Get the keys for the first element so we know what the column names will be columnMap := vslice[0].(orderedmap.OrderedMap) columnNames := columnMap.Keys() fmt.Printf("Creating excel workbook with following headings : '%v'\n", columnNames) // Set the style for the header values // Just handling bold for now but we can do other styles too as per https://xuri.me/excelize/en/style.html#NewStyle fmt.Printf("Bolding top row : %v\n", boldTopRow) headerStyle, err2 := xlsx.NewStyle(&excelize.Style{ Font: &excelize.Font{ Bold: true, }, }) if err2 != nil { fmt.Printf("Error generating header style : '%s'\n", err2) } row = 1 column = 1 // Set the style if boldTopRow { err = xlsx.SetRowStyle(worksheetName, row, row, headerStyle) if err != nil { fmt.Printf("Error setting header style : '%s'\n", err) } } // 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++ } // Freeze top row if requested, see https://xuri.me/excelize/en/utils.html#SetPanes if freezeTopRow { err = xlsx.SetPanes(worksheetName, &excelize.Panes{ Freeze: true, Split: false, XSplit: 0, YSplit: 1, TopLeftCell: "A2", ActivePane: "bottomLeft", Panes: []excelize.PaneOptions{ {SQRef: "A2", ActiveCell: "A2", Pane: "bottomLeft"}, }, }) if err != nil { fmt.Printf("Error freezing top row : '%s'\n", err) } } // Handle autofilter if autoFilter { // cell is still a reference to the last cell in the header row filterRange := "A1:" + cell fmt.Printf("Setting autofilter to range '%s'\n", filterRange) // As per docs any filters applied need to be manually processed by us (eg hiding rows with blanks) err = xlsx.AutoFilter(worksheetName, filterRange, nil) if err != nil { fmt.Printf("Error setting autofilter : '%s'\n", err) } } // Now process the remaining data in our json input // Set starting row for data row = 2 // Iterate the whole slice to get the data and add to the worksheet fmt.Printf("Adding %d rows of data to spreadsheet.\n", len(vslice)) for i, v := range vslice { // Print the contents each slice //fmt.Printf("'%d' : '%v'\n", i, v) //fmt.Printf("Slice element %d\n", i) _ = i // Each iteration should start back at column 1 column = 1 // Print each key-value pair 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) 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 //asciiValue++ column++ } //fmt.Printf("k: %v\n", k) // Move to next row row++ } // Perform any post processing now that the data exists if autoWidth { err = SetColAutoWidth(xlsx, worksheetName) if err != nil { fmt.Printf("Error setting auto width : '%s'\n", err) } } // Close off the file if err := xlsx.SaveAs(outputFilename); err != nil { log.Fatal(err) return } fmt.Printf("Excel file '%s' generated sucessfully.\n", outputFilename) /* //Test https://gitlab.com/c0b/go-ordered-json var om *ordered.OrderedMap = ordered.NewOrderedMap() err = json.Unmarshal([]byte(file), om) if err != nil { fmt.Printf("error: '%s'\n", err) } iter := om.EntriesIter() for { pair, ok := iter() if !ok { break } fmt.Printf("%-12s: %v\n", pair.Key, pair.Value) } */ } func fileExists(filename string) bool { info, err := os.Stat(filename) if os.IsNotExist(err) { return false } return !info.IsDir() } // Taken from https://github.com/qax-os/excelize/issues/92#issuecomment-821578446 func SetColAutoWidth(f *excelize.File, sheetName string) error { // Autofit all columns according to their text content cols, err := f.GetCols(sheetName) if err != nil { return err } for idx, col := range cols { largestWidth := 0 for _, rowCell := range col { cellWidth := utf8.RuneCountInString(rowCell) + 2 // + 2 for margin if cellWidth > largestWidth { largestWidth = cellWidth } } name, err := excelize.ColumnNumberToName(idx + 1) if err != nil { return err } f.SetColWidth(sheetName, name, name, float64(largestWidth)) } // No errors at this point return nil } func TestUnmarshalJSON() { s := `{ "number": 4, "string": "x", "z": 1, "a": "should not break with unclosed { character in value", "b": 3, "slice": [ "1", 1 ], "orderedmap": { "e": 1, "a { nested key with brace": "with a }}}} }} {{{ brace value", "after": { "link": "test {{{ with even deeper nested braces }" } }, "test\"ing": 9, "after": 1, "multitype_array": [ "test", 1, { "map": "obj", "it" : 5, ":colon in key": "colon: in value" }, [{"inner": "map"}] ], "should not break with { character in key": 1 }` o := orderedmap.New() err := json.Unmarshal([]byte(s), &o) if err != nil { fmt.Printf("JSON Unmarshal error", err) } // Check the root keys expectedKeys := []string{ "number", "string", "z", "a", "b", "slice", "orderedmap", "test\"ing", "after", "multitype_array", "should not break with { character in key", } k := o.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Unmarshal root key order", i, k[i], "!=", expectedKeys[i]) } } // Check nested maps are converted to orderedmaps // nested 1 level deep expectedKeys = []string{ "e", "a { nested key with brace", "after", } vi, ok := o.Get("orderedmap") if !ok { fmt.Printf("Missing key for nested map 1 deep") } v := vi.(orderedmap.OrderedMap) k = v.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 1 deep ", i, k[i], "!=", expectedKeys[i]) } } // nested 2 levels deep expectedKeys = []string{ "link", } vi, ok = v.Get("after") if !ok { fmt.Printf("Missing key for nested map 2 deep") } v = vi.(orderedmap.OrderedMap) k = v.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 2 deep", i, k[i], "!=", expectedKeys[i]) } } // multitype array expectedKeys = []string{ "map", "it", ":colon in key", } vislice, ok := o.Get("multitype_array") if !ok { fmt.Printf("Missing key for multitype array") } vslice := vislice.([]interface{}) vmap := vslice[2].(orderedmap.OrderedMap) k = vmap.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 2 deep", i, k[i], "!=", expectedKeys[i]) } } // nested map 3 deep vislice, _ = o.Get("multitype_array") vslice = vislice.([]interface{}) expectedKeys = []string{"inner"} vinnerslice := vslice[3].([]interface{}) vinnermap := vinnerslice[0].(orderedmap.OrderedMap) k = vinnermap.Keys() for i := range k { if k[i] != expectedKeys[i] { fmt.Printf("Key order for nested map 3 deep", i, k[i], "!=", expectedKeys[i]) } } }