Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(print): improve formatter #3

Merged
merged 1 commit into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions print/README.md
Original file line number Diff line number Diff line change
@@ -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
```
3 changes: 3 additions & 0 deletions print/example/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/orange-cloudavenue/common-go/print/example

go 1.20
10 changes: 10 additions & 0 deletions print/example/main.go
Original file line number Diff line number Diff line change
@@ -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()
}
107 changes: 107 additions & 0 deletions print/formatter.go
Original file line number Diff line number Diff line change
@@ -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
}
110 changes: 110 additions & 0 deletions print/formatter_test.go
Original file line number Diff line number Diff line change
@@ -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: "[email protected]",
},
expected: "Name:John,Age:30,Email:[email protected]",
},
{
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)
}
}
}
Empty file added print/go.sum
Empty file.
81 changes: 43 additions & 38 deletions print/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading