print as html
This commit is contained in:
1 parent
62fe1a7076
commit
6703481dbb
9 files changed
+339
-20
No files matched your search
@@ -0,0 +1,150 @@
|
||||
package table
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/biter777/countries"
|
||||
"github.com/jedib0t/go-pretty/v6/table"
|
||||
"github.com/jedib0t/go-pretty/v6/text"
|
||||
"github.com/nmoniz/any2anexoj/internal"
|
||||
)
|
||||
|
||||
// PrettyPrinter writes a simple, human readable, table row to the provided io.Writer for each
|
||||
// ReportItem received. Render and RenderHTML each build a fresh table.Writer so that
|
||||
// invoking them (in any order or repeatedly) on the same instance never duplicates rows.
|
||||
type PrettyPrinter struct {
|
||||
output io.Writer
|
||||
translator Translator
|
||||
}
|
||||
|
||||
<<<<<<<< HEAD:cmd/any2anexoj-cli/pretty_printer.go
|
||||
type Translator interface {
|
||||
Translate(key string, count int, values map[string]any) string
|
||||
}
|
||||
|
||||
func NewPrettyPrinter(w io.Writer, tr Translator) *PrettyPrinter {
|
||||
========
|
||||
func NewPrettyPrinter(w io.Writer, tr translator) *PrettyPrinter {
|
||||
return &PrettyPrinter{
|
||||
output: w,
|
||||
translator: tr,
|
||||
}
|
||||
}
|
||||
|
||||
// buildTable constructs a fully configured table.Writer from the items in aw.
|
||||
// Each call returns a new table.Writer so callers can render the same data
|
||||
// multiple times without duplicating rows. The returned table is not wired to
|
||||
// pp.output via an output mirror; callers that want the rendered output
|
||||
// written to pp.output must set the mirror themselves before calling .Render().
|
||||
func (pp *PrettyPrinter) buildTable(aw *internal.AggregatorWriter) table.Writer {
|
||||
>>>>>>>> 6ba3d78 (writers in writing package):internal/writing/pretty/printer.go
|
||||
tw := table.NewWriter()
|
||||
tw.SetAutoIndex(true)
|
||||
tw.SetStyle(table.StyleLight)
|
||||
tw.SetColumnConfigs([]table.ColumnConfig{
|
||||
colCountry(1),
|
||||
colOther(2),
|
||||
colOther(3),
|
||||
colOther(4),
|
||||
colOther(5),
|
||||
colEuros(6),
|
||||
colOther(7),
|
||||
colOther(8),
|
||||
colOther(9),
|
||||
colEuros(10),
|
||||
colEuros(11),
|
||||
colEuros(12),
|
||||
colCountry(13),
|
||||
})
|
||||
|
||||
realizationTxt := pp.translator.Translate("realization", 1, nil)
|
||||
acquisitionTxt := pp.translator.Translate("acquisition", 1, nil)
|
||||
yearTxt := pp.translator.Translate("year", 1, nil)
|
||||
monthTxt := pp.translator.Translate("month", 1, nil)
|
||||
dayTxt := pp.translator.Translate("day", 1, nil)
|
||||
valorTxt := pp.translator.Translate("value", 1, nil)
|
||||
|
||||
tw.AppendHeader(table.Row{"", "", realizationTxt, realizationTxt, realizationTxt, realizationTxt, acquisitionTxt, acquisitionTxt, acquisitionTxt, acquisitionTxt, "", "", ""}, table.RowConfig{AutoMerge: true})
|
||||
tw.AppendHeader(table.Row{
|
||||
pp.translator.Translate("source_country", 1, nil), pp.translator.Translate("code", 1, nil),
|
||||
yearTxt, monthTxt, dayTxt, valorTxt,
|
||||
yearTxt, monthTxt, dayTxt, valorTxt,
|
||||
pp.translator.Translate("expenses", 2, nil), pp.translator.Translate("foreign_tax_paid", 1, nil), pp.translator.Translate("counter_country", 1, nil),
|
||||
})
|
||||
|
||||
for ri := range aw.Iter() {
|
||||
tw.AppendRow(table.Row{
|
||||
ri.AssetCountry, ri.Nature,
|
||||
ri.SellTimestamp.Year(), int(ri.SellTimestamp.Month()), ri.SellTimestamp.Day(), ri.SellValue.StringFixed(2),
|
||||
ri.BuyTimestamp.Year(), int(ri.BuyTimestamp.Month()), ri.BuyTimestamp.Day(), ri.BuyValue.StringFixed(2),
|
||||
ri.Fees.StringFixed(2), ri.Taxes.StringFixed(2), ri.BrokerCountry,
|
||||
})
|
||||
}
|
||||
|
||||
sumTxt := pp.translator.Translate("sum", 1, nil)
|
||||
tw.AppendFooter(table.Row{sumTxt, sumTxt, sumTxt, sumTxt, sumTxt, aw.TotalEarned(), "", "", "", aw.TotalSpent(), aw.TotalFees(), aw.TotalTaxes()}, table.RowConfig{AutoMerge: true, AutoMergeAlign: text.AlignRight})
|
||||
|
||||
return tw
|
||||
}
|
||||
|
||||
func (pp *PrettyPrinter) Render(aw *internal.AggregatorWriter) {
|
||||
t := pp.buildTable(aw)
|
||||
t.SetOutputMirror(pp.output)
|
||||
t.Render()
|
||||
}
|
||||
|
||||
// RenderHTML builds a fresh table from aw and renders it as an HTML string
|
||||
// using go-pretty's HTMLOptions.
|
||||
func (pp *PrettyPrinter) RenderHTML(aw *internal.AggregatorWriter) string {
|
||||
t := pp.buildTable(aw)
|
||||
t.Style().HTML = table.HTMLOptions{
|
||||
CSSClass: "table table-striped table-hover table-bordered",
|
||||
EscapeText: true,
|
||||
Newline: "<br/>",
|
||||
EmptyColumn: " ",
|
||||
}
|
||||
return t.RenderHTML()
|
||||
}
|
||||
|
||||
func colEuros(n int) table.ColumnConfig {
|
||||
return table.ColumnConfig{
|
||||
Number: n,
|
||||
Align: text.AlignRight,
|
||||
AlignFooter: text.AlignRight,
|
||||
AlignHeader: text.AlignRight,
|
||||
WidthMin: 12,
|
||||
WidthMax: 15,
|
||||
Transformer: func(val any) string {
|
||||
return fmt.Sprintf("%v €", val)
|
||||
},
|
||||
TransformerFooter: func(val any) string {
|
||||
return fmt.Sprintf("%v €", val)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func colOther(n int) table.ColumnConfig {
|
||||
return table.ColumnConfig{
|
||||
Number: n,
|
||||
Align: text.AlignLeft,
|
||||
AlignFooter: text.AlignLeft,
|
||||
AlignHeader: text.AlignLeft,
|
||||
WidthMax: 12,
|
||||
}
|
||||
}
|
||||
|
||||
func colCountry(n int) table.ColumnConfig {
|
||||
return table.ColumnConfig{
|
||||
Number: n,
|
||||
Align: text.AlignLeft,
|
||||
AlignFooter: text.AlignLeft,
|
||||
AlignHeader: text.AlignLeft,
|
||||
WidthMax: 24,
|
||||
WidthMaxEnforcer: text.Trim,
|
||||
Transformer: func(val any) string {
|
||||
countryCode := val.(int64)
|
||||
return fmt.Sprintf("%v - %s", val, countries.ByNumeric(int(countryCode)).Info().Name)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package table_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/nmoniz/any2anexoj/internal"
|
||||
<<<<<<<< HEAD:cmd/any2anexoj-cli/pretty_printer_test.go
|
||||
========
|
||||
"github.com/nmoniz/any2anexoj/internal/writing"
|
||||
"github.com/nmoniz/any2anexoj/internal/writing/table"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
func TestPrettyPrinter_Render(t *testing.T) {
|
||||
// Create test data
|
||||
aw := internal.NewAggregatorWriter()
|
||||
ctx := t.Context()
|
||||
|
||||
// Add some sample report items
|
||||
err := aw.Write(ctx, internal.ReportItem{
|
||||
Symbol: "AAPL",
|
||||
Nature: internal.NatureG01,
|
||||
BrokerCountry: 826, // United Kingdom
|
||||
AssetCountry: 840, // United States
|
||||
BuyValue: decimal.NewFromFloat(100.50),
|
||||
BuyTimestamp: time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC),
|
||||
SellValue: decimal.NewFromFloat(150.75),
|
||||
SellTimestamp: time.Date(2023, 6, 20, 0, 0, 0, 0, time.UTC),
|
||||
Fees: decimal.NewFromFloat(2.50),
|
||||
Taxes: decimal.NewFromFloat(5.00),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write first report item: %v", err)
|
||||
}
|
||||
|
||||
err = aw.Write(ctx, internal.ReportItem{
|
||||
Symbol: "GOOGL",
|
||||
Nature: internal.NatureG20,
|
||||
BrokerCountry: 826, // United Kingdom
|
||||
AssetCountry: 840, // United States
|
||||
BuyValue: decimal.NewFromFloat(200.00),
|
||||
BuyTimestamp: time.Date(2023, 3, 10, 0, 0, 0, 0, time.UTC),
|
||||
SellValue: decimal.NewFromFloat(225.50),
|
||||
SellTimestamp: time.Date(2023, 9, 5, 0, 0, 0, 0, time.UTC),
|
||||
Fees: decimal.NewFromFloat(3.00),
|
||||
Taxes: decimal.NewFromFloat(7.50),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write second report item: %v", err)
|
||||
}
|
||||
|
||||
// Create English localizer
|
||||
<<<<<<<< HEAD:cmd/any2anexoj-cli/pretty_printer_test.go
|
||||
localizer, err := NewLocalizer("en")
|
||||
========
|
||||
localizer, err := writing.NewLocalizer("en")
|
||||
>>>>>>>> 6ba3d78 (writers in writing package):internal/writing/pretty/printer_test.go
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create localizer: %v", err)
|
||||
}
|
||||
|
||||
// Create writing printer with buffer
|
||||
var buf bytes.Buffer
|
||||
pp := table.NewPrettyPrinter(&buf, localizer)
|
||||
|
||||
// Render the table
|
||||
pp.Render(aw)
|
||||
|
||||
// Get the output
|
||||
got := buf.String()
|
||||
|
||||
// Expected output
|
||||
want := `┌───┬────────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬──────────────────────────────────────────────────────────┐
|
||||
│ │ │ REALIZATION │ ACQUISITION │ │
|
||||
│ │ SOURCE COUNTRY │ CODE │ YEAR │ MONTH │ DAY │ VALUE │ YEAR │ MONTH │ DAY │ VALUE │ EXPENSES AND CH │ TAX PAID ABROAD │ COUNTER COUNTRY │
|
||||
│ │ │ │ │ │ │ │ │ │ │ │ ARGES │ │ │
|
||||
├───┼─────────────────────┼──────┼──────┼───────┼─────┼──────────────┼──────┼───────┼─────┼──────────────┼─────────────────┼─────────────────┼──────────────────────┤
|
||||
│ 1 │ 840 - United States │ G01 │ 2023 │ 6 │ 20 │ 150.75 € │ 2023 │ 1 │ 15 │ 100.50 € │ 2.50 € │ 5.00 € │ 826 - United Kingdom │
|
||||
│ 2 │ 840 - United States │ G20 │ 2023 │ 9 │ 5 │ 225.50 € │ 2023 │ 3 │ 10 │ 200.00 € │ 3.00 € │ 7.50 € │ 826 - United Kingdom │
|
||||
├───┼─────────────────────┴──────┴──────┴───────┴─────┼──────────────┼──────┴───────┴─────┼──────────────┼─────────────────┼─────────────────┼──────────────────────┤
|
||||
│ │ SUM │ 376.25 € │ │ 300.5 € │ 5.5 € │ 12.5 € │ │
|
||||
└───┴─────────────────────────────────────────────────┴──────────────┴────────────────────┴──────────────┴─────────────────┴─────────────────┴──────────────────────┘
|
||||
`
|
||||
|
||||
// Compare output
|
||||
if got != want {
|
||||
t.Errorf("PrettyPrinter.Render() output doesn't match expected.\n\nGot:\n%s\n\nWant:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHTML(t *testing.T) {
|
||||
// Create test data
|
||||
aw := internal.NewAggregatorWriter()
|
||||
ctx := t.Context()
|
||||
|
||||
err := aw.Write(ctx, internal.ReportItem{
|
||||
Symbol: "AAPL",
|
||||
Nature: internal.NatureG01,
|
||||
BrokerCountry: 826, // United Kingdom
|
||||
AssetCountry: 840, // United States
|
||||
BuyValue: decimal.NewFromFloat(100.50),
|
||||
BuyTimestamp: time.Date(2023, 1, 15, 0, 0, 0, 0, time.UTC),
|
||||
SellValue: decimal.NewFromFloat(150.75),
|
||||
SellTimestamp: time.Date(2023, 6, 20, 0, 0, 0, 0, time.UTC),
|
||||
Fees: decimal.NewFromFloat(2.50),
|
||||
Taxes: decimal.NewFromFloat(5.00),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write report item: %v", err)
|
||||
}
|
||||
|
||||
// Create Portuguese localizer so the header text is in pt
|
||||
localizer, err := writing.NewLocalizer("pt")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create localizer: %v", err)
|
||||
}
|
||||
|
||||
// Output buffer used only to satisfy the constructor contract; RenderHTML
|
||||
// must not write to it.
|
||||
var buf bytes.Buffer
|
||||
pp := table.NewPrettyPrinter(&buf, localizer)
|
||||
|
||||
html := pp.RenderHTML(aw)
|
||||
|
||||
// RenderHTML must not side-write to pp.output.
|
||||
if buf.Len() != 0 {
|
||||
t.Errorf("RenderHTML should not write to pp.output, but got %d bytes: %q", buf.Len(), buf.String())
|
||||
}
|
||||
|
||||
// Structural assertions.
|
||||
wantSubstrings := []string{
|
||||
`<table class="table table-striped table-hover table-bordered">`,
|
||||
`</table>`,
|
||||
`<thead>`,
|
||||
`</thead>`,
|
||||
`<tbody>`,
|
||||
`</tbody>`,
|
||||
`<tfoot>`,
|
||||
`</tfoot>`,
|
||||
"Realização",
|
||||
"Aquisição",
|
||||
"Soma",
|
||||
}
|
||||
for _, s := range wantSubstrings {
|
||||
if !strings.Contains(html, s) {
|
||||
t.Errorf("RenderHTML output missing expected substring %q\nGot:\n%s", s, html)
|
||||
}
|
||||
}
|
||||
|
||||
// Calling RenderHTML twice on the same instance must not duplicate rows.
|
||||
html2 := pp.RenderHTML(aw)
|
||||
if strings.Count(html2, "<tr") != strings.Count(html, "<tr") {
|
||||
t.Errorf("expected second RenderHTML call to produce the same number of rows as the first\nFirst (%d <tr>):\n%s\nSecond (%d <tr>):\n%s",
|
||||
strings.Count(html, "<tr"), html,
|
||||
strings.Count(html2, "<tr"), html2)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user