From 0bbbd7ca13f748431367b5084fc646243f7a1c45 Mon Sep 17 00:00:00 2001 From: Simon Waldherr Date: Wed, 17 Jul 2024 18:51:17 +0200 Subject: [PATCH] improve table package --- README.md | 2 + table/example_test.go | 94 ++++++++++++++++++- table/table.go | 208 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 289 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index df1ad86..20b9bd9 100644 --- a/README.md +++ b/README.md @@ -56,9 +56,11 @@ each new build gets tested in multiple steps: * [re](https://github.com/SimonWaldherr/golibs#re-----) helps you whenever you have to do something multiple times * [regex](https://github.com/SimonWaldherr/golibs#regex-----) is a layer to speed up your regular expression development * [rss](https://github.com/SimonWaldherr/golibs#rss-----) is a rss feed parser based on Golangs std xml package +* [semver](https://github.com/SimonWaldherr/golibs#semver-----) is a semantic version parsing/checking package * [ssl](https://github.com/SimonWaldherr/golibs#ssl-----) generates ssl certificates for https * [stack](https://github.com/SimonWaldherr/golibs#stack-----) can store your values in stacks and rings * [structs](https://github.com/SimonWaldherr/golibs#structs-----) use structs like maps +* [table](https://github.com/SimonWaldherr/golibs#table-----) prints structs like ASCII or Markdown tables * [xmath](https://github.com/SimonWaldherr/golibs#xmath-----) provides a few mathematical functions like Sum, Median, Harmonic-mean, … * [xtime](https://github.com/SimonWaldherr/golibs#xtime-----) xtime implements a subset of strftime diff --git a/table/example_test.go b/table/example_test.go index ef76001..a383f79 100644 --- a/table/example_test.go +++ b/table/example_test.go @@ -2,6 +2,10 @@ package table import ( "fmt" + "net/http" + "net/http/httptest" + "strings" + "time" ) // Beispiel-Datenstrukturen @@ -26,12 +30,13 @@ func ExampleRenderASCII_noRotation() { asciiTable, _ := RenderASCII(people, TableOption{Rotate: false}) fmt.Println(asciiTable) + // Output: // +------+-----+------------------+ // | Name | Age | Email | // +------+-----+------------------+ - // | John | 42 | john@example.com | - // | Jane | 32 | jane@example.com | + // | John | 42 | john@example.com | + // | Jane | 32 | jane@example.com | // +------+-----+------------------+ } @@ -44,12 +49,13 @@ func ExampleRenderASCII_withRotation() { rotatedASCIITable, _ := RenderASCII(people, TableOption{Rotate: true}) fmt.Println(rotatedASCIITable) + // Output: // +-------+------------------+------------------+ // | | Row 1 | Row 2 | // +-------+------------------+------------------+ // | Name | John | Jane | - // | Age | 42 | 32 | + // | Age | 42 | 32 | // | Email | john@example.com | jane@example.com | // +-------+------------------+------------------+ } @@ -63,9 +69,87 @@ func ExampleRenderMarkdown_noRotation() { markdownTable, _ := RenderMarkdown(people, TableOption{Rotate: false}) fmt.Println(markdownTable) + // Output: // | Name | Age | Email | // |------|-----|------------------| - // | John | 42 | john@example.com | - // | Jane | 32 | jane@example.com | + // | John | 42 | john@example.com | + // | Jane | 32 | jane@example.com | +} + +type TestStruct struct { + Name string + Age int + Salary float64 + StartDate time.Time +} + +func ExampleParseCSVInput() { + csvData := `Name,Age,Salary,StartDate +Alice,30,70000.50,2021-05-01T00:00:00Z +Bob,22,48000.00,2022-07-15T00:00:00Z` + var data []TestStruct + err := ParseCSVInput(strings.NewReader(csvData), &data) + if err != nil { + fmt.Println(err) + } + markdownTable, _ := RenderMarkdown(data, TableOption{Rotate: false}) + fmt.Println(markdownTable) + + // Output: + // | Name | Age | Salary | StartDate | + // |-------|-----|---------|----------------------| + // | Alice | 30 | 70000.5 | 2021-05-01T00:00:00Z | + // | Bob | 22 | 48000 | 2022-07-15T00:00:00Z | +} + +// Header represents a single HTTP header key-value pair +type Header struct { + Key string + Value string +} + +func firstN(s string, n int) string { + i := 0 + for j := range s { + if i == n { + return s[:j] + } + i++ + } + return s +} + +func handler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello, Worldn")) +} + +// ExampleRenderHTTPResponse demonstrates how to render HTTP response headers +func ExampleRenderHTTPResponse() { + req := httptest.NewRequest("GET", "https://github.com", nil) + w := httptest.NewRecorder() + handler(w, req) + resp := w.Result() + + defer resp.Body.Close() + + // Convert headers to a slice of Header structs + var headers []Header + for key, values := range resp.Header { + for _, value := range values { + headers = append(headers, Header{Key: key, Value: firstN(value, 30)}) + } + } + + // Render the headers as a Markdown table + markdownTable, err := RenderMarkdown(headers, TableOption{Rotate: false}) + if err != nil { + fmt.Println("Error:", err) + return + } + fmt.Println(markdownTable) + // Output: + // | Key | Value | + // |--------------|---------------------------| + // | Content-Type | text/plain; charset=utf-8 | } diff --git a/table/table.go b/table/table.go index e5b8173..50f7b34 100644 --- a/table/table.go +++ b/table/table.go @@ -2,20 +2,30 @@ package table import ( "bytes" + "encoding/csv" + "encoding/json" "fmt" + "html" + "io" "reflect" + "strconv" "strings" + "time" ) // TableOption defines the configuration for table output type TableOption struct { Rotate bool + Align []string // alignment options for each column: "left", "center", "right" } // RenderASCII renders the struct slice as an ASCII table func RenderASCII(data interface{}, opt TableOption) (string, error) { var buffer bytes.Buffer - headers, rows := parseStruct(data) + headers, rows, err := parseStruct(data) + if err != nil { + return "", err + } if opt.Rotate { headers, rows = rotate(headers, rows) @@ -30,7 +40,8 @@ func RenderASCII(data interface{}, opt TableOption) (string, error) { } buffer.WriteString("\n|") for i, header := range headers { - buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], header)) + alignment := getColumnAlignment(i, opt.Align, header) + buffer.WriteString(" " + alignText(header, colWidths[i], alignment) + " |") } buffer.WriteString("\n+") for _, width := range colWidths { @@ -42,7 +53,8 @@ func RenderASCII(data interface{}, opt TableOption) (string, error) { for _, row := range rows { buffer.WriteString("|") for i, col := range row { - buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], col)) + alignment := getColumnAlignment(i, opt.Align, col) + buffer.WriteString(" " + alignText(col, colWidths[i], alignment) + " |") } buffer.WriteString("\n") } @@ -58,7 +70,10 @@ func RenderASCII(data interface{}, opt TableOption) (string, error) { // RenderMarkdown renders the struct slice as a Markdown table func RenderMarkdown(data interface{}, opt TableOption) (string, error) { var buffer bytes.Buffer - headers, rows := parseStruct(data) + headers, rows, err := parseStruct(data) + if err != nil { + return "", err + } if opt.Rotate { headers, rows = rotate(headers, rows) @@ -81,7 +96,8 @@ func RenderMarkdown(data interface{}, opt TableOption) (string, error) { for _, row := range rows { buffer.WriteString("|") for i, col := range row { - buffer.WriteString(fmt.Sprintf(" %-*s |", colWidths[i], col)) + alignment := getColumnAlignment(i, opt.Align, col) + buffer.WriteString(fmt.Sprintf(" %s |", alignText(col, colWidths[i], alignment))) } buffer.WriteString("\n") } @@ -89,10 +105,140 @@ func RenderMarkdown(data interface{}, opt TableOption) (string, error) { return buffer.String(), nil } -func parseStruct(data interface{}) ([]string, [][]string) { +// RenderHTML renders the struct slice as an HTML table +func RenderHTML(data interface{}, opt TableOption) (string, error) { + var buffer bytes.Buffer + headers, rows, err := parseStruct(data) + if err != nil { + return "", err + } + + if opt.Rotate { + headers, rows = rotate(headers, rows) + } + + buffer.WriteString("\n\n") + for _, header := range headers { + buffer.WriteString(fmt.Sprintf("", html.EscapeString(header))) + } + buffer.WriteString("\n\n\n") + + for _, row := range rows { + buffer.WriteString("") + for _, col := range row { + buffer.WriteString(fmt.Sprintf("", html.EscapeString(col))) + } + buffer.WriteString("\n") + } + buffer.WriteString("\n
%s
%s
\n") + + return buffer.String(), nil +} + +// ParseJSONInput parses JSON input into a slice of structs +func ParseJSONInput(r io.Reader, v interface{}) error { + decoder := json.NewDecoder(r) + return decoder.Decode(v) +} + +// ParseCSVInput parses CSV input into a slice of structs +func ParseCSVInput(r io.Reader, v interface{}) error { + csvReader := csv.NewReader(r) + records, err := csvReader.ReadAll() + if err != nil { + return err + } + + headers := records[0] + structSlice := reflect.ValueOf(v).Elem() + elemType := structSlice.Type().Elem() + + for _, record := range records[1:] { + elem := reflect.New(elemType).Elem() + for i, header := range headers { + field := elem.FieldByName(header) + if field.IsValid() { + err := setFieldValue(field, record[i]) + if err != nil { + return err + } + } + } + structSlice.Set(reflect.Append(structSlice, elem)) + } + + return nil +} + +func setFieldValue(field reflect.Value, value string) error { + switch field.Kind() { + case reflect.String: + field.SetString(value) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + intValue, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + field.SetInt(intValue) + case reflect.Float32, reflect.Float64: + floatValue, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + field.SetFloat(floatValue) + case reflect.Bool: + boolValue, err := strconv.ParseBool(value) + if err != nil { + return err + } + field.SetBool(boolValue) + case reflect.Struct: + if field.Type() == reflect.TypeOf(time.Time{}) { + timeValue, err := time.Parse(time.RFC3339, value) + if err != nil { + return err + } + field.Set(reflect.ValueOf(timeValue)) + } + default: + return fmt.Errorf("unsupported field type: %s", field.Kind()) + } + return nil +} + +// RenderCSV renders the struct slice as a CSV output +func RenderCSV(data interface{}) (string, error) { + var buffer bytes.Buffer + headers, rows, err := parseStruct(data) + if err != nil { + return "", err + } + + csvWriter := csv.NewWriter(&buffer) + defer csvWriter.Flush() + + if err := csvWriter.Write(headers); err != nil { + return "", err + } + + for _, row := range rows { + if err := csvWriter.Write(row); err != nil { + return "", err + } + } + + return buffer.String(), nil +} + +// parseStruct extracts headers and rows from a slice of structs +func parseStruct(data interface{}) ([]string, [][]string, error) { v := reflect.ValueOf(data) if v.Kind() != reflect.Slice { - return nil, nil + return nil, nil, fmt.Errorf("data is not a slice") + } + + if v.Len() == 0 { + return nil, nil, fmt.Errorf("slice is empty") } var headers []string @@ -101,7 +247,7 @@ func parseStruct(data interface{}) ([]string, [][]string) { for i := 0; i < v.Len(); i++ { elem := v.Index(i) if elem.Kind() != reflect.Struct { - continue + return nil, nil, fmt.Errorf("element is not a struct") } var row []string @@ -110,14 +256,26 @@ func parseStruct(data interface{}) ([]string, [][]string) { if i == 0 { headers = append(headers, elemType.Field(j).Name) } - row = append(row, fmt.Sprintf("%v", elem.Field(j).Interface())) + field := elem.Field(j).Interface() + row = append(row, formatField(field)) } rows = append(rows, row) } - return headers, rows + return headers, rows, nil } +// formatField formats field values, especially date fields according to ISO 8601 +func formatField(field interface{}) string { + switch v := field.(type) { + case time.Time: + return v.Format(time.RFC3339) + default: + return fmt.Sprintf("%v", v) + } +} + +// rotate swaps rows and columns func rotate(headers []string, rows [][]string) ([]string, [][]string) { newHeaders := make([]string, len(rows)) for i := range rows { @@ -144,6 +302,7 @@ func rotate(headers []string, rows [][]string) ([]string, [][]string) { return newHeaders, newRows } +// calculateColumnWidths calculates the width of each column func calculateColumnWidths(headers []string, rows [][]string) []int { colWidths := make([]int, len(headers)) @@ -161,3 +320,32 @@ func calculateColumnWidths(headers []string, rows [][]string) []int { return colWidths } + +// getColumnAlignment returns the alignment for a given column index +func getColumnAlignment(index int, align []string, value string) string { + if index < len(align) { + return align[index] + } + + // Default alignment based on value type + if _, err := fmt.Sscanf(value, "%f", new(float64)); err == nil { + return "right" + } + + return "left" +} + +// Alignment function to align text based on the specified alignment type +func alignText(text string, width int, alignment string) string { + switch alignment { + case "center": + spaces := width - len(text) + left := spaces / 2 + right := spaces - left + return strings.Repeat(" ", left) + text + strings.Repeat(" ", right) + case "right": + return fmt.Sprintf("%*s", width, text) + default: + return fmt.Sprintf("%-*s", width, text) + } +}