10 Commits
v0.1 ... v0.1.3

Author SHA1 Message Date
4bffb9cbfc do some error checking before asserting orderedmap 2023-04-18 09:20:36 +10:00
c500216105 add log message 2023-04-17 13:17:45 +10:00
e0c13681a8 remove unnecessary mac files 2023-02-13 13:13:22 +11:00
b1b3b8834b bugfix bold top row 2023-02-13 13:12:12 +11:00
13201c144a fix autofilter range 2023-02-13 13:09:49 +11:00
cee94f2141 code refactor 2023-02-13 13:08:10 +11:00
bc71a24a83 set document properties 2023-02-13 12:47:06 +11:00
061bb83861 add some docs 2023-02-13 12:40:56 +11:00
78ba74c709 intial docs 2023-02-13 08:42:39 +11:00
f2446daff1 add autowidth 2023-02-11 17:15:31 +11:00
3 changed files with 174 additions and 55 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -0,0 +1,16 @@
# json2excel
This is a basic utility to take json and convert it into an OpenXml format xlsx file, compatible with Excel.
It expects that the json input is formatted as an object containing an array of objects. Column names are generated based on the keys in the first nested object.
## Command line options
| Parameter | Default Value | Description |
|---------------|---------------|---------------------------|
| inputJson | input.json | File path of the input json to process |
| outputFilename | output.xlsx | File path of the output file |
| worksheetName | Sheet1 | Set the name of the worksheet, will be truncated to 31 characters |
| boldTopRow | true | Sets the top row of the worksheet to bold |
| 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 |

View File

@@ -6,6 +6,9 @@ import (
"fmt"
"log"
"os"
"reflect"
"time"
"unicode/utf8"
"github.com/iancoleman/orderedmap"
"github.com/xuri/excelize/v2"
@@ -26,6 +29,8 @@ func main() {
var boldTopRow bool
var freezeTopRow bool
var autoFilter bool
var autoWidth bool
var overwriteFile bool
// Process command line arguments
flag.StringVar(&inputJson, "inputJson", "./input.json", "Full path to input json data file")
@@ -34,61 +39,40 @@ func main() {
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.BoolVar(&overwriteFile, "overwrite", false, "Set to true to overwrite existing file rather than modifying in place")
flag.Parse()
var xlsx *excelize.File
var s []byte
var sheetIndex int
//var sheetIndex int
var err error
var cell string
var row, column int
// TODO - truncate worksheetName to the maximum 31 characters
// Truncate worksheetName to the maximum 31 characters
worksheetName = TruncateString(worksheetName, 31)
// 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 overwriteFile {
// Delete existing file
fmt.Printf("Overwrite flag is set, removing existing file '%s'\n", outputFilename)
err = os.Remove(outputFilename)
if err != nil {
fmt.Printf("Error creating worksheet '%s' : %s\n", worksheetName, err)
panic(err)
}
// Set active worksheet
fmt.Printf("Setting active sheet to index %d", sheetIndex)
xlsx.SetActiveSheet(sheetIndex)
// Create new file
xlsx = createWorkbook(worksheetName, outputFilename)
} else {
xlsx = modifyWorkbook(worksheetName, outputFilename)
}
} 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)
}
}
xlsx = createWorkbook(worksheetName, outputFilename)
}
// Read the json input file
@@ -119,41 +103,41 @@ func main() {
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()
fmt.Printf("Creating excel workbook with following headings : '%v'\n", columnNames)
// Run code to add column names to the first row of the workbook
createHeadingRow(xlsx, worksheetName, 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: boldTopRow,
Bold: true,
},
})
if err2 != nil {
fmt.Printf("Error generating header style : '%s'\n", err2)
}
row = 1
column = 1
// Set the style
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++
if boldTopRow {
err = xlsx.SetRowStyle(worksheetName, 1, 1, headerStyle)
if err != nil {
fmt.Printf("Error setting header style : '%s'\n", err)
}
}
// Freeze top row if requested, see https://xuri.me/excelize/en/utils.html#SetPanes
@@ -176,7 +160,7 @@ func main() {
// Handle autofilter
if autoFilter {
// cell is still a reference to the last cell in the header row
cell, _ = excelize.CoordinatesToCellName(len(columnNames), 1)
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)
@@ -225,6 +209,14 @@ func main() {
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)
@@ -260,6 +252,117 @@ func fileExists(filename string) bool {
return !info.IsDir()
}
// From https://dev.to/takakd/go-safe-truncate-string-9h0#comment-1hp14
func TruncateString(str string, length int) string {
if length <= 0 {
return ""
}
if utf8.RuneCountInString(str) < length {
return str
}
return string([]rune(str)[:length])
}
func createWorkbook(worksheetName string, outputFilename string) *excelize.File {
fmt.Printf("Creating output spreadsheet '%s'\n", outputFilename)
var err error
// 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)
}
}
// Set the document properties
err = xlsx.SetDocProps(&excelize.DocProperties{
Creator: "json2excel",
Created: time.Now().Format(time.RFC3339),
})
if err != nil {
fmt.Printf("Error setting document properties: %s\n", err)
}
return xlsx
}
func modifyWorkbook(worksheetName string, outputFilename string) *excelize.File {
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)
}
return xlsx
}
func createHeadingRow(xlsx *excelize.File, worksheetName string, columnNames []string) {
fmt.Printf("Creating excel workbook with following headings : '%v'\n", columnNames)
var cell string
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++
}
}
// Taken from https://github.com/qax-os/excelize/issues/92#issuecomment-821578446
func SetColAutoWidth(xlsx *excelize.File, sheetName string) error {
// Autofit all columns according to their text content
cols, err := xlsx.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
}
}
fmt.Printf("SetColAutoWidth calculated largest width is '%d'\n", largestWidth)
name, err := excelize.ColumnNumberToName(idx + 1)
if err != nil {
return err
}
xlsx.SetColWidth(sheetName, name, name, float64(largestWidth))
}
// No errors at this point
return nil
}
func TestUnmarshalJSON() {
s := `{
"number": 4,