Skip to content

Commit

Permalink
feat(print): improve formatter (#3)
Browse files Browse the repository at this point in the history
  • Loading branch information
azrod authored Jan 29, 2024
1 parent 66b473d commit f04f55f
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 38 deletions.
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

0 comments on commit f04f55f

Please sign in to comment.