Skip to content

Commit

Permalink
Writers now support decimal, other than xlsx in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
neilotoole committed Nov 21, 2023
1 parent c09fe75 commit a4e236e
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 7 deletions.
5 changes: 5 additions & 0 deletions cli/output/csvw/csvw.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import (
"sync"
"time"

"github.com/neilotoole/sq/libsq/core/stringz"
"github.com/shopspring/decimal"

"github.com/neilotoole/sq/cli/output"
"github.com/neilotoole/sq/libsq/core/errz"
"github.com/neilotoole/sq/libsq/core/kind"
Expand Down Expand Up @@ -106,6 +109,8 @@ func (w *RecordWriter) WriteRecords(recs []record.Record) error {
// nil is rendered as empty string, which this cell already is
case int64:
fields[i] = w.pr.Number.Sprint(strconv.FormatInt(val, 10))
case decimal.Decimal:
fields[i] = w.pr.Number.Sprint(stringz.FormatDecimal(val))
case string:
fields[i] = w.pr.String.Sprint(val)
case bool:
Expand Down
4 changes: 4 additions & 0 deletions cli/output/htmlw/htmlw.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"sync"
"time"

"github.com/shopspring/decimal"

"github.com/neilotoole/sq/cli/output"
"github.com/neilotoole/sq/libsq/core/errz"
"github.com/neilotoole/sq/libsq/core/kind"
Expand Down Expand Up @@ -113,6 +115,8 @@ func (w *recordWriter) writeRecord(rec record.Record) error {
s = strconv.FormatBool(val)
case float64:
s = stringz.FormatFloat(val)
case decimal.Decimal:
s = stringz.FormatDecimal(val)
case []byte:
s = base64.StdEncoding.EncodeToString(val)
case time.Time:
Expand Down
4 changes: 2 additions & 2 deletions cli/output/jsonw/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ func (e monoEncoder) encodeAny(b []byte, v any) ([]byte, error) {

case decimal.Decimal:
var err error
b, err = encodeString(b, v.String(), false)
b, err = encodeString(b, stringz.FormatDecimal(v), false)
if err != nil {
return b, errz.Err(err)
}
Expand Down Expand Up @@ -219,7 +219,7 @@ func (e *colorEncoder) encodeAny(b []byte, v any) ([]byte, error) {
case decimal.Decimal:
b = append(b, e.clrs.Number.Prefix...)
var err error
b, err = encodeString(b, v.String(), false)
b, err = encodeString(b, stringz.FormatDecimal(v), false)
if err != nil {
return b, errz.Err(err)
}
Expand Down
4 changes: 4 additions & 0 deletions cli/output/markdownw/markdownw.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"sync"
"time"

"github.com/shopspring/decimal"

"github.com/neilotoole/sq/cli/output"
"github.com/neilotoole/sq/libsq/core/kind"
"github.com/neilotoole/sq/libsq/core/record"
Expand Down Expand Up @@ -88,6 +90,8 @@ func (w *RecordWriter) writeRecord(rec record.Record) error {
s = strconv.FormatInt(val, 10)
case string:
s = escapeMarkdown(val)
case decimal.Decimal:
s = stringz.FormatDecimal(val)
case bool:
s = strconv.FormatBool(val)
case float64:
Expand Down
4 changes: 4 additions & 0 deletions cli/output/raww/raww.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"sync"
"time"

"github.com/shopspring/decimal"

"github.com/neilotoole/sq/cli/output"
"github.com/neilotoole/sq/libsq/core/kind"
"github.com/neilotoole/sq/libsq/core/record"
Expand Down Expand Up @@ -65,6 +67,8 @@ func (w *recordWriter) WriteRecords(recs []record.Record) error {
fmt.Fprint(w.out, strconv.FormatInt(val, 10))
case float64:
fmt.Fprint(w.out, stringz.FormatFloat(val))
case decimal.Decimal:
fmt.Fprint(w.out, stringz.FormatDecimal(val))
case time.Time:
switch w.recMeta[i].Kind() { //nolint:exhaustive
default:
Expand Down
11 changes: 9 additions & 2 deletions cli/output/tablew/tablew.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,15 @@ func (t *table) renderResultCell(knd kind.Kind, val any) string { //nolint:funle
switch knd { //nolint:exhaustive // ignore kind.Unknown and kind.Null
case kind.Datetime, kind.Date, kind.Time:
return t.pr.Datetime.Sprint(val)
case kind.Decimal, kind.Float, kind.Int:
case kind.Float, kind.Int:
return t.pr.Number.Sprint(val)
case kind.Decimal:
d, err := decimal.NewFromString(val)
if err != nil {
// Shouldn't happen
return t.pr.Number.Sprint(val)
}
return t.pr.Number.Sprint(stringz.FormatDecimal(d))
case kind.Bool:
return t.pr.Bool.Sprint(val)
case kind.Bytes:
Expand Down Expand Up @@ -201,7 +208,7 @@ func (t *table) renderResultCell(knd kind.Kind, val any) string { //nolint:funle
}
return t.sprintBytes(*val)
case decimal.Decimal:
return t.pr.Number.Sprint(val.String())
return t.pr.Number.Sprint(stringz.FormatDecimal(val))
}

// FIXME: this should really return an error
Expand Down
97 changes: 96 additions & 1 deletion cli/output/xlsxw/xlsxw.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import (
"sync"
"time"

"github.com/neilotoole/sq/libsq/core/stringz"
"github.com/shopspring/decimal"

excelize "github.com/xuri/excelize/v2"

"github.com/neilotoole/sq/cli/output"
Expand All @@ -31,13 +34,29 @@ type recordWriter struct {
dateStyle int
datetimeStyle int
headerStyle int

// mDecimalPrecisionStyles maps decimal precision to
// the excelize style ID that should be used for that
// precision. For example, if the decimal has a precision
// of 2, then the style ID for that precision will be
// mDecimalPrecisionStyles[2].
//
// The values are populated on-demand by getDecimalStyle.
// The map should not be directly accessed; instead use
// getDecimalStyle.
mDecimalPrecisionStyles map[int]int
}

var _ output.NewRecordWriterFunc = NewRecordWriter

// NewRecordWriter returns an output.RecordWriter instance for XLSX.
func NewRecordWriter(out io.Writer, pr *output.Printing) output.RecordWriter {
return &recordWriter{out: out, pr: pr, header: pr.ShowHeader}
return &recordWriter{
out: out,
pr: pr,
header: pr.ShowHeader,
mDecimalPrecisionStyles: map[int]int{},
}
}

// initStyles sets up the datetime styles. See:
Expand Down Expand Up @@ -83,6 +102,36 @@ func (w *recordWriter) initStyles() error {
return nil
}

func (w *recordWriter) getDecimalStyle(dec decimal.Decimal) int {
// We can probably set the column format if we know that the kind
// is decimal, because we can get the precision and scale
// from the meta. But we don't know that here.
text := dec.String()
_ = text

var places int
exp := dec.Exponent()
if exp < 0 {
// Should we always set places to 2?
places = int(exp * -1)
}

if styleID, ok := w.mDecimalPrecisionStyles[places]; ok {
return styleID
}

s := &excelize.Style{
NumFmt: 2,
DecimalPlaces: &places,
}
// s.DecimalPlaces = &places
styleID, err := w.xfile.NewStyle(s)
_ = err // We know that an error can't occur here
w.mDecimalPrecisionStyles[places] = styleID
return styleID
// dec.Exponent()
}

// Open implements output.RecordWriter.
func (w *recordWriter) Open(recMeta record.Meta) error {
w.mu.Lock()
Expand Down Expand Up @@ -124,6 +173,8 @@ func (w *recordWriter) Open(recMeta record.Meta) error {
wantWidth = 16
case kind.Text:
wantWidth = 32
case kind.Decimal:
wantWidth = 20
default:
}

Expand Down Expand Up @@ -218,6 +269,50 @@ func (w *recordWriter) WriteRecords(recs []record.Record) error { //nolint:gocog
if err := w.xfile.SetCellFloat(SheetName, cellIndex, val, -1, 64); err != nil {
return errw(err)
}
case decimal.Decimal:
styleID := w.getDecimalStyle(val)
decStr := val.String()
t, _ := val.MarshalText()
t2 := string(t)
_ = t2
recMeta := w.recMeta[j]
_ = recMeta
precision, scale, ok := recMeta.DecimalSize()
_ = precision
_ = scale
_ = ok
//val.StringFixed()
//_ = x
if err := w.xfile.SetCellStyle(SheetName, cellIndex, cellIndex, styleID); err != nil {
return errw(err)
}

// f64, exact := val.Float64()
f64 := val.InexactFloat64()
f64str := stringz.FormatFloat(f64)
exact := decStr == f64str
if exact {
if err := w.xfile.SetCellFloat(SheetName, cellIndex, f64, -1, 64); err != nil {
return errw(err)
}
} else {
// The decimal doesn't fit exactly into a float.
// We need to use a string instead.

if err := w.xfile.SetCellStyle(SheetName, cellIndex, cellIndex, styleID); err != nil {
return errw(err)
}

if err := w.xfile.SetCellStr(SheetName, cellIndex, val.String()); err != nil {
return errw(err)
}

// The excelize library doesn't have a SetCellDecimal method, unfortunately.
//if err := w.xfile.SetCellFloat(SheetName, cellIndex, val.InexactFloat64(), -1, 64); err != nil {
// return errw(err)
//}
}

case time.Time:
switch w.recMeta[j].Kind() { //nolint:exhaustive
default:
Expand Down
21 changes: 21 additions & 0 deletions cli/output/xlsxw/xlsxw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"os"
"testing"

"github.com/shopspring/decimal"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
excelize "github.com/xuri/excelize/v2"
Expand Down Expand Up @@ -180,3 +182,22 @@ func TestOptDatetimeFormats(t *testing.T) {
assert.Equal(t, "89/Nov/09", gotDate)
assert.Equal(t, "4:07 pm", gotTime)
}

func TestDecimalFormat(t *testing.T) {
// dec := decimal.New(33322, -2)
dec := decimal.New(33322, -2)
t.Log(dec)
exp := dec.Exponent()
t.Log(exp)
numDigits := dec.NumDigits()
t.Log(numDigits)

cof := dec.Coefficient()
t.Log(cof)

cof2 := dec.CoefficientInt64()
t.Log(cof2)

intPart := dec.IntPart()
t.Log(intPart)
}
4 changes: 4 additions & 0 deletions cli/output/xmlw/xmlw.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"strconv"
"time"

"github.com/shopspring/decimal"

"github.com/fatih/color"

"github.com/neilotoole/sq/cli/output"
Expand Down Expand Up @@ -195,6 +197,8 @@ func (w *recordWriter) writeRecord(rec record.Record) error {
w.fieldPrintFns[i](w.outBuf, strconv.FormatInt(val, 10))
case float64:
w.fieldPrintFns[i](w.outBuf, stringz.FormatFloat(val))
case decimal.Decimal:
w.fieldPrintFns[i](w.outBuf, stringz.FormatDecimal(val))
case time.Time:
switch w.recMeta[i].Kind() { //nolint:exhaustive
default:
Expand Down
2 changes: 1 addition & 1 deletion cli/output/yamlw/recordwriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (w *recordWriter) Open(recMeta record.Meta) error {
w.recMeta = recMeta
w.fieldNames = w.recMeta.MungedNames()
w.buf = &bytes.Buffer{}
w.enc = goccy.NewEncoder(io.Discard)
w.enc = goccy.NewEncoder(io.Discard, decimalMarshaler)
w.clrs = make([]*color.Color, len(w.recMeta))
w.keys = make([]string, len(w.recMeta))
w.null = w.pr.Null.Sprint("null")
Expand Down
9 changes: 8 additions & 1 deletion cli/output/yamlw/yamlw.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"bytes"
"io"

"github.com/neilotoole/sq/libsq/core/stringz"
"github.com/shopspring/decimal"

"github.com/fatih/color"
goccy "github.com/goccy/go-yaml"
"github.com/goccy/go-yaml/lexer"
Expand All @@ -14,6 +17,10 @@ import (
"github.com/neilotoole/sq/libsq/core/errz"
)

var decimalMarshaler = goccy.CustomMarshaler[decimal.Decimal](func(d decimal.Decimal) ([]byte, error) {
return []byte(stringz.FormatDecimal(d)), nil
})

// MarshalToString renders v to a string.
func MarshalToString(pr *output.Printing, v any) (string, error) {
p := newPrinter(pr)
Expand All @@ -27,7 +34,7 @@ func MarshalToString(pr *output.Printing, v any) (string, error) {
// writeYAML prints a YAML representation of v to out, using specs
// from pr.
func writeYAML(out io.Writer, p printer.Printer, v any) error {
b, err := goccy.Marshal(v)
b, err := goccy.MarshalWithOptions(v, decimalMarshaler)
if err != nil {
return errz.Err(err)
}
Expand Down
24 changes: 24 additions & 0 deletions libsq/core/stringz/stringz.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"time"
"unicode"

"github.com/shopspring/decimal"

sprig "github.com/Masterminds/sprig/v3"
"github.com/alessio/shellescape"
"github.com/google/uuid"
Expand Down Expand Up @@ -114,6 +116,28 @@ func FormatFloat(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64)
}

// FormatDecimal formats d with the appropriate number of decimal
// places as defined by d's exponent.
func FormatDecimal(d decimal.Decimal) string {
exp := d.Exponent()
var places int32
if exp < 0 {
places = -exp
}
return d.StringFixed(places)
}

// DecimalPlaces returns the count of decimal places in d. That is to
// say, it returns the number of digits after the decimal point.
func DecimalPlaces(d decimal.Decimal) int32 {
var places int32
exp := d.Exponent()
if exp < 0 {
places = -exp
}
return places
}

// ByteSized returns a human-readable byte size, e.g. "2.1 MB", "3.0 TB", etc.
// TODO: replace this usage with "github.com/c2h5oh/datasize",
// or maybe https://github.com/docker/go-units/.
Expand Down
Loading

0 comments on commit a4e236e

Please sign in to comment.