From f04f55fc74ff45a55abae203c34b1cfeb991725f Mon Sep 17 00:00:00 2001 From: Mickael Stanislas Date: Mon, 29 Jan 2024 09:56:02 +0100 Subject: [PATCH] feat(print): improve formatter (#3) --- print/README.md | 31 +++++++++++ print/example/go.mod | 3 ++ print/example/main.go | 10 ++++ print/formatter.go | 107 ++++++++++++++++++++++++++++++++++++++ print/formatter_test.go | 110 ++++++++++++++++++++++++++++++++++++++++ print/go.sum | 0 print/print.go | 81 +++++++++++++++-------------- 7 files changed, 304 insertions(+), 38 deletions(-) create mode 100644 print/README.md create mode 100644 print/example/go.mod create mode 100644 print/example/main.go create mode 100644 print/formatter.go create mode 100644 print/formatter_test.go create mode 100644 print/go.sum diff --git a/print/README.md b/print/README.md new file mode 100644 index 0000000..31058ec --- /dev/null +++ b/print/README.md @@ -0,0 +1,31 @@ +# Print package + +## Description + +`print` is a supercharged version of the `tabwriter` package. It provides a `SetHeader` method to set a header for the table, and a `AddFields` method to add lines to the table. It also provides a `PrintTable` method to print the table to a writer. + +`print` don't use external dependencies. + +## Usage + +A working example can be found in the `example` folder. + +```go +package main + +import "github.com/orange-cloudavenue/common-go/print" + +func main() { + p := print.New() + p.SetHeader("String", "Int", "Bool", "Slice", "Map", "Struct", "Array") + p.AddFields("I'm a string", 1, true, []string{"a", "b", "c"}, map[string]string{"a": "1", "b": "2", "c": "3"}, struct{ key1, key2, key3 string }{"a", "b", "c"}, [3]string{"a", "b", "c"}) + p.PrintTable() +} +``` + +## Output + +```sh +STRING INT BOOL SLICE MAP STRUCT ARRAY +I'm a string 1 Enabled a,b,c a:1,b:2,c:3 key1:a,key2:b,key3:c a,b,c +``` diff --git a/print/example/go.mod b/print/example/go.mod new file mode 100644 index 0000000..9e03226 --- /dev/null +++ b/print/example/go.mod @@ -0,0 +1,3 @@ +module github.com/orange-cloudavenue/common-go/print/example + +go 1.20 diff --git a/print/example/main.go b/print/example/main.go new file mode 100644 index 0000000..1dc93e6 --- /dev/null +++ b/print/example/main.go @@ -0,0 +1,10 @@ +package main + +import "github.com/orange-cloudavenue/common-go/print" + +func main() { + p := print.New() + p.SetHeader("String", "Int", "Bool", "Slice", "Map", "Struct", "Array") + p.AddFields("I'm a string", 1, true, []string{"a", "b", "c"}, map[string]string{"a": "1", "b": "2", "c": "3"}, struct{ key1, key2, key3 string }{"a", "b", "c"}, [3]string{"a", "b", "c"}) + p.PrintTable() +} diff --git a/print/formatter.go b/print/formatter.go new file mode 100644 index 0000000..97c2d25 --- /dev/null +++ b/print/formatter.go @@ -0,0 +1,107 @@ +package print + +import ( + "fmt" + "reflect" + "strings" +) + +type ( + fmtFormat string +) + +const ( + // String fmtFormat + String fmtFormat = "%s" + // Int fmtFormat + Int fmtFormat = "%d" + // Float fmtFormat + Float fmtFormat = "%f" + // Bool fmtFormat + Bool fmtFormat = "%s" // Use %s for bool values instead of %t because AddFields() converts bool to string + // Default fmtFormat + Default fmtFormat = "%v" + + // Tab fmtFormat + Tab fmtFormat = "\t" + // NewLine fmtFormat + NewLine fmtFormat = "\n" +) + +// String +func (f fmtFormat) String() string { + return string(f) +} + +// formatEntry is a function that formats the given value based on its type. +// It returns the formatted string representation of the value. +func formatEntry(value any) (fs string) { + switch value.(type) { + case string: + fs += String.String() + case int, int8, int16, int32, int64: + fs += Int.String() + case float32, float64: + fs += Float.String() + case bool: + fs += Bool.String() + default: + fs += Default.String() + } + + return fs + Tab.String() +} + +// format is a function that takes in variadic arguments of any type and returns a formatted string. +// It concatenates the formatted string representation of each argument using the formatEntry function, +// and appends a newline character at the end. +func format(fields ...any) (fs string) { + fs = "" + for _, field := range fields { + fs += formatEntry(field) + } + fs += NewLine.String() + return fs +} + +// fieldFormat +// fieldFormat is a function that formats a value based on its type. +// It takes any value as input and returns a formatted string. +// The formatting rules are as follows: +// - For slices and arrays, it joins the elements with a comma. +// - For maps, it formats the key-value pairs as "key:value" and joins them with a comma. +// - For structs, it formats the field name and value pairs as "fieldName:fieldValue" and joins them with a comma. +// - For booleans, it returns "Enabled" if the value is true, and "Disabled" if the value is false. +// For all other types, it returns the input value as is. +func fieldFormat(value any) any { + x := reflect.ValueOf(value) + switch x.Kind() { + case reflect.Slice, reflect.Array: + var s []string + for i := 0; i < x.Len(); i++ { + s = append(s, fmt.Sprintf("%v", x.Index(i))) + } + return strings.Join(s, ",") + case reflect.Map: + // format map to "a:1,b:2" + var s []string + for _, k := range x.MapKeys() { + s = append(s, fmt.Sprintf("%v:%v", k, x.MapIndex(k))) + } + return strings.Join(s, ",") + case reflect.Struct: + // format struct to "fieldName:fieldValue,fieldName:fieldValue" + var s []string + for i := 0; i < x.NumField(); i++ { + s = append(s, fmt.Sprintf("%v:%v", x.Type().Field(i).Name, x.Field(i))) + } + return strings.Join(s, ",") + case reflect.Bool: + if x.Bool() { + return "Enabled" + } + return "Disabled" + } + + return value +} diff --git a/print/formatter_test.go b/print/formatter_test.go new file mode 100644 index 0000000..d1e4f03 --- /dev/null +++ b/print/formatter_test.go @@ -0,0 +1,110 @@ +package print + +import ( + "fmt" + "testing" +) + +func TestFieldFormat(t *testing.T) { + testCases := []struct { + value interface{} + expected string + }{ + { + value: []int{1, 2, 3}, + expected: "1,2,3", + }, + { + value: map[string]int{"a": 1, "b": 2}, + expected: "a:1,b:2", + }, + { + value: struct { + Name string + Age int + Email string + }{ + Name: "John", + Age: 30, + Email: "john@example.com", + }, + expected: "Name:John,Age:30,Email:john@example.com", + }, + { + value: true, + expected: "Enabled", + }, + { + value: false, + expected: "Disabled", + }, + } + + for _, tc := range testCases { + actual := fieldFormat(tc.value) + if fmt.Sprintf("%v", actual) != tc.expected { + t.Errorf("Expected FieldFormat(%v) to return %s, but got %v", tc.value, tc.expected, actual) + } + } +} + +func TestFormatEntry(t *testing.T) { + testCases := []struct { + value interface{} + expected string + }{ + { + value: "Hello", + expected: String.String() + Tab.String(), + }, + { + value: 42, + expected: Int.String() + Tab.String(), + }, + { + value: 3.14, + expected: Float.String() + Tab.String(), + }, + { + value: true, + expected: Bool.String() + Tab.String(), + }, + { + value: struct{}{}, + expected: Default.String() + Tab.String(), + }, + } + + for _, tc := range testCases { + actual := formatEntry(tc.value) + if actual != tc.expected { + t.Errorf("Expected formatEntry(%v) to return %s, but got %s", tc.value, tc.expected, actual) + } + } +} +func TestFormat(t *testing.T) { + testCases := []struct { + fields []interface{} + expected string + }{ + { + fields: []interface{}{"Hello", 42, 3.14, true, struct{}{}}, + expected: String.String() + Tab.String() + Int.String() + Tab.String() + Float.String() + Tab.String() + Bool.String() + Tab.String() + Default.String() + Tab.String() + NewLine.String(), + }, + { + fields: []interface{}{1, 2, 3, "a", "b", "c"}, + expected: Int.String() + Tab.String() + Int.String() + Tab.String() + Int.String() + Tab.String() + String.String() + Tab.String() + String.String() + Tab.String() + String.String() + Tab.String() + NewLine.String(), + }, + { + fields: []interface{}{true, false, true, false}, + expected: Bool.String() + Tab.String() + Bool.String() + Tab.String() + Bool.String() + Tab.String() + Bool.String() + Tab.String() + NewLine.String(), + }, + } + + for _, tc := range testCases { + actual := format(tc.fields...) + if actual != tc.expected { + t.Errorf("Expected format(%v) to return %s, but got %s", tc.fields, tc.expected, actual) + } + } +} diff --git a/print/go.sum b/print/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/print/print.go b/print/print.go index ea33872..eadd722 100644 --- a/print/print.go +++ b/print/print.go @@ -7,62 +7,67 @@ import ( "text/tabwriter" ) -// var fs string - -type Writer struct { - tw *tabwriter.Writer -} +type ( + Writer struct { + header []string + tw *tabwriter.Writer + } +) -// New Writer +// New returns a new instance of the Writer struct. func New() Writer { return Writer{ - tw: tabwriter.NewWriter(os.Stdout, 10, 1, 5, ' ', 0), - } -} - -// Create format string -func format(fields ...any) (fs string) { - fs = "" - for _, field := range fields { - switch field.(type) { - case string: - fs += "%s\t" - case int: - fs += "%d\t" - case float64: - fs += "%f\t" - case bool: - fs += "%t\t" - default: - fs += "%v\t" - } + header: []string{}, + tw: tabwriter.NewWriter(os.Stdout, 10, 1, 5, ' ', 0), } - fs += "\n" - return fs } -// Set Header fieds into upper case -func (w Writer) SetHeader(fields ...any) { +// SetHeader sets the header fields for the Writer. +// It takes a variadic parameter fields of type any, representing the header fields. +// If no fields are provided, the function returns without making any changes. +// Each field in the fields parameter is converted to a string and stored in the header slice of the Writer. +// The converted fields are also appended to the underlying text writer, tw. +// If any field in the fields parameter is not a string, the function panics with an error message. +// The header fields are converted to uppercase before being stored in the header slice. +// The formatted header fields are then written to the text writer using the format function. +func (w *Writer) SetHeader(fields ...any) { if len(fields) == 0 { return } - fs := format(fields...) + for i, field := range fields { - switch field.(type) { //nolint:gosimple - case string: - fields[i] = strings.ToUpper(field.(string)) //nolint:gosimple + v, ok := field.(string) + if !ok { + panic("Header fields must be string") } + fields[i] = strings.ToUpper(v) + w.header = append(w.header, v) } - fmt.Fprintf(w.tw, fs, fields...) + + fmt.Fprintf(w.tw, format(fields...), fields...) } -// AddFields to the table +// AddFields adds fields to the writer. +// It takes a variadic parameter fields of type any. +// If the length of fields is zero, it returns immediately. +// If the length of fields is not equal to the length of the writer's header, it panics with the message "Fields must be the same length as header". +// It formats each field using the fieldFormat function. +// It then formats the fields using the format function and writes them to the writer's tw. +// Finally, it writes the formatted fields to the writer's tw using fmt.Fprintf. func (w Writer) AddFields(fields ...any) { if len(fields) == 0 { return } - fs := format(fields...) - fmt.Fprintf(w.tw, fs, fields...) + + if len(fields) != len(w.header) { + panic("Fields must be the same length as header") + } + + for i, field := range fields { + fields[i] = fieldFormat(field) + } + + fmt.Fprintf(w.tw, format(fields...), fields...) } // Print a line