golib/database/csvx/delimited.go

176 lines
3.8 KiB
Go

package csvx
import (
"database/sql/driver"
"encoding/json"
"fmt"
"strconv"
"strings"
)
// Delimiter selects the separator used by a delimited value's CSV form.
type Delimiter interface {
Separator() string
}
// Comma separates values with a comma.
type Comma struct{}
func (Comma) Separator() string { return "," }
// Space separates values with whitespace.
type Space struct{}
func (Space) Separator() string { return " " }
// Tab separates values with a tab.
type Tab struct{}
func (Tab) Separator() string { return "\t" }
// Strings is a string slice with a delimiter-specific CSV form.
// Its SQL and JSON forms are JSON arrays.
type Strings[D Delimiter] []string
// Value implements driver.Valuer as a JSON array.
func (s Strings[D]) Value() (driver.Value, error) {
if s == nil {
s = Strings[D]{}
}
return json.Marshal([]string(s))
}
// Scan implements sql.Scanner from a JSON array.
func (s *Strings[D]) Scan(value any) error {
switch v := value.(type) {
case nil:
*s = nil
return nil
case []byte:
return s.scanJSON(v)
case string:
return s.scanJSON([]byte(v))
case Strings[D]:
*s = append((*s)[:0], v...)
return nil
default:
return fmt.Errorf("csvx.Strings: cannot scan %T", value)
}
}
func (s *Strings[D]) scanJSON(data []byte) error {
if len(data) == 0 {
*s = nil
return nil
}
var values []string
if err := json.Unmarshal(data, &values); err != nil {
return fmt.Errorf("csvx.Strings: %w", err)
}
*s = values
return nil
}
// MarshalCSV implements csvutil.Marshaler.
func (s Strings[D]) MarshalCSV() ([]byte, error) {
if len(s) == 0 {
return nil, nil
}
var delimiter D
return []byte(strings.Join(s, delimiter.Separator())), nil
}
// UnmarshalCSV implements csvutil.Unmarshaler.
func (s *Strings[D]) UnmarshalCSV(data []byte) error {
if len(data) == 0 {
*s = nil
return nil
}
var delimiter D
if delimiter.Separator() == (Space{}).Separator() {
*s = Strings[D](strings.Fields(string(data)))
return nil
}
*s = Strings[D](strings.Split(string(data), delimiter.Separator()))
return nil
}
// Ints is an int64 slice with a delimiter-specific CSV form.
// Its SQL and JSON forms are JSON arrays.
type Ints[D Delimiter] []int64
// Value implements driver.Valuer as a JSON array.
func (s Ints[D]) Value() (driver.Value, error) {
if s == nil {
s = Ints[D]{}
}
return json.Marshal([]int64(s))
}
// Scan implements sql.Scanner from a JSON array.
func (s *Ints[D]) Scan(value any) error {
switch v := value.(type) {
case nil:
*s = nil
return nil
case []byte:
return s.scanJSON(v)
case string:
return s.scanJSON([]byte(v))
case Ints[D]:
*s = append((*s)[:0], v...)
return nil
default:
return fmt.Errorf("csvx.Ints: cannot scan %T", value)
}
}
func (s *Ints[D]) scanJSON(data []byte) error {
if len(data) == 0 {
*s = nil
return nil
}
var values []int64
if err := json.Unmarshal(data, &values); err != nil {
return fmt.Errorf("csvx.Ints: %w", err)
}
*s = values
return nil
}
// MarshalCSV implements csvutil.Marshaler.
func (s Ints[D]) MarshalCSV() ([]byte, error) {
if len(s) == 0 {
return nil, nil
}
var delimiter D
values := make([]string, len(s))
for i, value := range s {
values[i] = strconv.FormatInt(value, 10)
}
return []byte(strings.Join(values, delimiter.Separator())), nil
}
// UnmarshalCSV implements csvutil.Unmarshaler.
func (s *Ints[D]) UnmarshalCSV(data []byte) error {
if len(data) == 0 {
*s = nil
return nil
}
var delimiter D
parts := strings.Split(string(data), delimiter.Separator())
if delimiter.Separator() == (Space{}).Separator() {
parts = strings.Fields(string(data))
}
values := make(Ints[D], len(parts))
for i, part := range parts {
value, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64)
if err != nil {
return fmt.Errorf("csvx.Ints: parse %q: %w", part, err)
}
values[i] = value
}
*s = values
return nil
}