Added csv:to-csv
All checks were successful
Build / build (push) Successful in 2m5s

This commit is contained in:
Leon Mika 2025-02-03 10:28:44 +11:00
parent c7d614c1f8
commit c52dc2b3e9
6 changed files with 198 additions and 10 deletions

View file

@ -26,4 +26,15 @@ csv:each-record "winds.csv" { |row hdr|
echo "Wind $name has bearing $bearing" echo "Wind $name has bearing $bearing"
} }
``` ```
```
csv:to-csv CONT [-header HEADER]
```
Produces a CSV using the items of CONT and writes it as a string. CONT must be a list of iterator of hashable
elements.
If HEADER is defined, it must be a list of strings identifying the name and order of the hash keys to read
from each hashable item of CONT. If HEADER is not defined, then the keys of the first consumed hashable item will be
used, with the keys sorted in alphabetical order.

View file

@ -340,8 +340,8 @@ func objectsEqual(l, r Object) bool {
} }
} }
return true return true
case hashable: case Hashable:
rv, ok := r.(hashable) rv, ok := r.(Hashable)
if !ok { if !ok {
return false return false
} }
@ -462,7 +462,7 @@ func lenBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
return IntObject(len(string(v))), nil return IntObject(len(string(v))), nil
case Listable: case Listable:
return IntObject(v.Len()), nil return IntObject(v.Len()), nil
case hashable: case Hashable:
return IntObject(v.Len()), nil return IntObject(v.Len()), nil
case Iterable: case Iterable:
cnt := 0 cnt := 0
@ -490,7 +490,7 @@ func indexLookup(ctx context.Context, obj, elem Object) (Object, error) {
return v.Index(int(intIdx)), nil return v.Index(int(intIdx)), nil
} }
return nil, nil return nil, nil
case hashable: case Hashable:
strIdx, ok := elem.(StringObject) strIdx, ok := elem.(StringObject)
if !ok { if !ok {
return nil, errors.New("expected string for hashable") return nil, errors.New("expected string for hashable")
@ -524,7 +524,7 @@ func keysBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
val := args.args[0] val := args.args[0]
switch v := val.(type) { switch v := val.(type) {
case hashable: case Hashable:
keys := make(ListObject, 0, v.Len()) keys := make(ListObject, 0, v.Len())
if err := v.Each(func(k string, _ Object) error { if err := v.Each(func(k string, _ Object) error {
keys = append(keys, StringObject(k)) keys = append(keys, StringObject(k))
@ -669,7 +669,7 @@ func filterBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
} }
} }
return &newList, nil return &newList, nil
case hashable: case Hashable:
newHash := hashObject{} newHash := hashObject{}
if err := t.Each(func(k string, v Object) error { if err := t.Each(func(k string, v Object) error {
if m, err := inv.invoke(ctx, args.fork([]Object{StringObject(k), v})); err != nil { if m, err := inv.invoke(ctx, args.fork([]Object{StringObject(k), v})); err != nil {
@ -734,7 +734,7 @@ func reduceBuiltin(ctx context.Context, args invocationArgs) (Object, error) {
accum = newAccum accum = newAccum
} }
return accum, nil return accum, nil
case hashable: case Hashable:
// TODO: should raise error? // TODO: should raise error?
if err := t.Each(func(k string, v Object) error { if err := t.Each(func(k string, v Object) error {
newAccum, err := block.invoke(ctx, args.fork([]Object{StringObject(k), v, accum})) newAccum, err := block.invoke(ctx, args.fork([]Object{StringObject(k), v, accum}))
@ -1031,7 +1031,7 @@ func foreachBuiltin(ctx context.Context, args macroArgs) (Object, error) {
} }
} }
} }
case hashable: case Hashable:
err := t.Each(func(k string, v Object) error { err := t.Each(func(k string, v Object) error {
last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, true) last, err = args.evalBlock(ctx, blockIdx, []Object{StringObject(k), v}, true)
return err return err

View file

@ -4,9 +4,11 @@ import (
"context" "context"
"encoding/csv" "encoding/csv"
"errors" "errors"
"fmt"
"io" "io"
"io/fs" "io/fs"
"os" "os"
"sort"
"strings" "strings"
"ucl.lmika.dev/ucl" "ucl.lmika.dev/ucl"
) )
@ -22,6 +24,7 @@ func CSV(fs fs.FS) ucl.Module {
Name: "csv", Name: "csv",
Builtins: map[string]ucl.BuiltinHandler{ Builtins: map[string]ucl.BuiltinHandler{
"each-record": fsh.eachRecord, "each-record": fsh.eachRecord,
"to-csv": fsh.toCsv,
}, },
} }
} }
@ -76,6 +79,129 @@ func (h csvHandlers) eachRecord(ctx context.Context, args ucl.CallArgs) (any, er
return nil, nil return nil, nil
} }
func (h csvHandlers) toCsv(ctx context.Context, args ucl.CallArgs) (any, error) {
var rowConsumer func() (ucl.Object, error)
var (
listable ucl.Listable
iterable ucl.Iterable
)
if args.CanBind(&listable) {
if err := args.Bind(&listable); err != nil {
return nil, err
}
rowConsumer = func() func() (ucl.Object, error) {
idx := 0
return func() (ucl.Object, error) {
if idx >= listable.Len() {
return nil, io.EOF
}
v := listable.Index(idx)
idx += 1
return v, nil
}
}()
} else if args.CanBind(&iterable) {
if err := args.Bind(&iterable); err != nil {
return nil, err
}
rowConsumer = func() func() (ucl.Object, error) {
return func() (ucl.Object, error) {
if !iterable.HasNext() {
return nil, io.EOF
}
return iterable.Next(ctx)
}
}()
} else {
return nil, errors.New("unsupported type")
}
var (
header []string
row []string
)
if args.HasSwitch("header") {
var headerListable ucl.Listable
if err := args.BindSwitch("header", &headerListable); err != nil {
return nil, err
}
if headerListable.Len() == 0 {
return nil, errors.New("header requires at least one item")
}
header = make([]string, headerListable.Len())
for i := range header {
header[i] = headerListable.Index(i).String()
}
row = make([]string, len(header))
}
var sb strings.Builder
cw := csv.NewWriter(&sb)
i := 0
for {
v, err := rowConsumer()
if err != nil {
if errors.Is(err, io.EOF) {
break
} else {
return nil, err
}
}
hv, ok := v.(ucl.Hashable)
if !ok {
return nil, fmt.Errorf("element %v is not hashable", i)
}
if len(header) == 0 {
if err := hv.Each(func(k string, _ ucl.Object) error {
header = append(header, k)
return nil
}); err != nil {
return nil, err
}
sort.Strings(header)
row = make([]string, len(header))
}
if i == 0 {
if err := cw.Write(header); err != nil {
return nil, err
}
}
for j, k := range header {
val := hv.Value(k)
if val == nil {
row[j] = ""
} else {
row[j] = val.String()
}
}
if err := cw.Write(row); err != nil {
return nil, err
}
i++
}
cw.Flush()
if err := cw.Error(); err != nil {
return nil, err
}
return sb.String(), nil
}
type headerIndexObject map[string]int type headerIndexObject map[string]int
func (hio headerIndexObject) String() string { func (hio headerIndexObject) String() string {

View file

@ -51,3 +51,48 @@ func TestCSV_ReadRecord(t *testing.T) {
}) })
} }
} }
func TestCSV_ToCSV(t *testing.T) {
tests := []struct {
descr string
eval string
want string
}{
{
descr: "to csv 1",
eval: `csv:to-csv [[hello:"one"] [hello:"two"]]`,
want: "hello\none\ntwo\n",
},
{
descr: "to csv 2",
eval: `csv:to-csv [[hello:"one" world:"this"] [hello:"two" world:"that"]]`,
want: "hello,world\none,this\ntwo,that\n",
},
{
descr: "to csv 3",
eval: `csv:to-csv [[hello:"one" world:"this"] [hello:"two" other:"the" world:"that"]] -header [world other]`,
want: "world,other\nthis,\nthat,the\n",
},
{
descr: "to csv 4",
eval: `itrs:from [[hello:"one" world:"this"] [hello:"two" other:"the" world:"that"]] | csv:to-csv -header [world other]`,
want: "world,other\nthis,\nthat,the\n",
},
}
for _, tt := range tests {
t.Run(tt.descr, func(t *testing.T) {
var bfr bytes.Buffer
inst := ucl.New(
ucl.WithModule(builtins.CSV(testCsvFS)),
ucl.WithModule(builtins.Itrs()),
ucl.WithOut(&bfr),
)
res, err := inst.Eval(context.Background(), tt.eval)
assert.NoError(t, err)
assert.Equal(t, tt.want, res)
})
}
}

View file

@ -40,7 +40,7 @@ type ModListable interface {
Insert(idx int, obj Object) error Insert(idx int, obj Object) error
} }
type hashable interface { type Hashable interface {
Len() int Len() int
Value(k string) Object Value(k string) Object
Each(func(k string, v Object) error) error Each(func(k string, v Object) error) error

View file

@ -186,6 +186,12 @@ func canBindArg(v interface{}, arg Object) bool {
case *int: case *int:
_, ok := arg.(IntObject) _, ok := arg.(IntObject)
return ok return ok
case *Listable:
_, ok := arg.(Listable)
return ok
case *Iterable:
_, ok := arg.(Iterable)
return ok
} }
switch t := arg.(type) { switch t := arg.(type) {