initial commit

This commit is contained in:
dtookey 2022-12-09 09:55:59 -05:00
commit ddfeb99a53
3 changed files with 371 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/csvmagic.iml
/.idea/

366
csv.go Normal file
View File

@ -0,0 +1,366 @@
package csvmagic
import (
"bytes"
"encoding/csv"
"os"
"reflect"
"strconv"
"strings"
)
type (
LoadParams struct {
Separator rune
FirstRowIsHeader bool
}
FieldModificationInfo struct {
SerializedName string
FieldIdx int
TypeInfo reflect.Kind
}
TypeConverter func(string, reflect.Value)
)
var (
//you can use this to override the string->type conversion. types are indexed by their [reflect.Kind]
typeConverters = make([]*TypeConverter, 27, 27)
typesInitialized = false
)
func CsvLoadParams(firstRowIsHeader bool) *LoadParams {
return NewLoadParams(',', firstRowIsHeader)
}
func NewLoadParams(separator byte, firstRowIsHeader bool) *LoadParams {
return &LoadParams{Separator: bytes.Runes([]byte{separator})[0], FirstRowIsHeader: firstRowIsHeader}
}
//LoadCsvAsObjects The crown jewel of this (currently) 200 line library. You can pass in a nil-value for params to
//use the default parameter of comma-delimited. If the CSV file does not contain headers, you will have to annotate
//your structs with the `csv:"MyHeader"` style of tag
func LoadCsvAsObjects[K any](pathlike string, params *LoadParams) *[]K {
//"Why do we need to pass a parameter of type K? lol" you might ask. Honestly? I couldn't figure out how to get
//reflect info out of the generic type. I feel like one allocation and one copy isn't going to be a high cost.
rows := *loadFile(pathlike, params)
typ := reflect.TypeOf((*K)(nil)).Elem()
fieldInfo := mapTypeInfo(typ)
objSlice := reflect.MakeSlice(reflect.SliceOf(typ), len(rows)-1, len(rows)-1)
container := objSlice.Interface().([]K)
headers := rows[0]
startingIdx := 1
indexOffset := 1
rowCount := len(rows)
if !params.FirstRowIsHeader {
startingIdx = 0
indexOffset = 0
rowCount = len(rows) - 1
headerLen := len(rows[0])
headers = make([]string, headerLen, headerLen)
for i := 0; i < headerLen; i++ {
headers[i] = strconv.Itoa(i)
}
}
for i := startingIdx; i < rowCount; i++ {
row := rows[i]
err := inflate[K](&container[i-indexOffset], &fieldInfo, headers, row)
if err != nil {
panic(err)
}
}
return &container
}
func loadFile(pathlike string, params *LoadParams) *[][]string {
f, err := os.OpenFile(pathlike, os.O_RDONLY, 0755)
if err != nil {
panic(err)
}
defer f.Close()
reader := csv.NewReader(f)
if params == nil {
params = CsvLoadParams(true)
}
reader.Comma = params.Separator
records, err := reader.ReadAll()
if err != nil {
panic(err)
}
return &records
}
func mapTypeInfo(typeOf reflect.Type) map[string]FieldModificationInfo {
fields := reflect.VisibleFields(typeOf)
m := make(map[string]FieldModificationInfo)
for _, v := range fields {
jsonParams := v.Tag.Get("csv")
info := FieldModificationInfo{}
info.FieldIdx = v.Index[0]
info.SerializedName = strings.Split(jsonParams, ",")[0]
info.TypeInfo = v.Type.Kind()
m[info.SerializedName] = info
}
return m
}
func inflate[K any](objPtr *K, fieldInfo *map[string]FieldModificationInfo, headers []string, data []string) error {
lMap := *fieldInfo
obj := reflect.Indirect(reflect.ValueOf(objPtr))
for i := 0; i < len(headers); i++ {
header := headers[i]
value := data[i]
info, ok := lMap[header]
if !ok {
continue
}
field := obj.Field(info.FieldIdx)
typeIdx := int(info.TypeInfo)
converters := getConverters()
converter := converters[typeIdx]
if converter == nil {
converter = &youAreOnYourOwn
}
(*converter)(value, field)
}
return nil
}
//<editor-fold name="typeConverters">
/*===================================================================================================
typeConverters
===================================================================================================*/
func getConverters() []*TypeConverter {
// Before you start casting stones at me, look, we need to memoize this thing because of how we
//structured this whole thing. Yes, I know it's global state. Can it be turned into something much more
// memory/branching friendly? It sure can. Did I spend the time doing it for this 'lil tiny project? No,
// I did not.
if !typesInitialized {
populateConverters()
typesInitialized = true
}
return typeConverters
}
//OverrideConverter
// idx int
func OverrideConverter(idx int, converter *TypeConverter) {
if len(typeConverters) < idx-1 {
tmp := make([]*TypeConverter, idx+1, idx+1)
copy(tmp[:len(typeConverters)], typeConverters[:])
typeConverters = tmp
}
getConverters()
typeConverters[idx] = converter
}
//populateConverters this is what makes all the magic happen. It's populates a bit of a fat array for function pointers
func populateConverters() {
typeConverters[int(reflect.Invalid)] = nil
typeConverters[int(reflect.Bool)] = &convertBool
typeConverters[int(reflect.Int)] = &convertInt
typeConverters[int(reflect.Int8)] = &convertInt8
typeConverters[int(reflect.Int16)] = &convertInt16
typeConverters[int(reflect.Int32)] = &convertInt32
typeConverters[int(reflect.Int64)] = &convertInt64
typeConverters[int(reflect.Uint)] = &convertUint
typeConverters[int(reflect.Uint8)] = &convertUint8
typeConverters[int(reflect.Uint16)] = &convertUint16
typeConverters[int(reflect.Uint32)] = &convertUint32
typeConverters[int(reflect.Uint64)] = &convertUint64
typeConverters[int(reflect.Uintptr)] = nil
typeConverters[int(reflect.Float32)] = &convertFloat32
typeConverters[int(reflect.Float64)] = &convertFloat64
typeConverters[int(reflect.Complex64)] = nil
typeConverters[int(reflect.Complex128)] = nil
//exercise left for the reader lol
typeConverters[int(reflect.Array)] = nil
typeConverters[int(reflect.Chan)] = nil
typeConverters[int(reflect.Func)] = nil
typeConverters[int(reflect.Interface)] = nil
typeConverters[int(reflect.Map)] = nil
typeConverters[int(reflect.Pointer)] = nil
typeConverters[int(reflect.Slice)] = nil
typeConverters[int(reflect.String)] = &convertString
typeConverters[int(reflect.Struct)] = nil
typeConverters[int(reflect.UnsafePointer)] = nil
}
var convertBool TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
field.Set(reflect.ValueOf(false))
}
}
var convertInt TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(i)
field.Set(settable)
}
var convertInt8 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseInt(s, 0, 8)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(int8(i))
field.Set(settable)
}
var convertInt16 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseInt(s, 0, 16)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(int16(i))
field.Set(settable)
}
var convertInt32 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseInt(s, 0, 32)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(int32(i))
field.Set(settable)
}
var convertInt64 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseInt(s, 0, 64)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(i)
field.Set(settable)
}
var convertUint TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseUint(s, 10, 0)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(uint(i))
field.Set(settable)
}
var convertUint8 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseUint(s, 0, 8)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(uint8(i))
field.Set(settable)
}
var convertUint16 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseUint(s, 0, 16)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(uint16(i))
field.Set(settable)
}
var convertUint32 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseUint(s, 0, 32)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(uint32(i))
field.Set(settable)
}
var convertUint64 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
i, err := strconv.ParseUint(s, 10, 64)
if err != nil {
panic(err)
}
settable := reflect.ValueOf(i)
field.Set(settable)
}
var convertFloat32 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
f, err := strconv.ParseFloat(s, 32)
if err != nil {
panic(err)
}
field.SetFloat(f)
}
var convertFloat64 TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" || len(s) == 0 {
s = "0"
}
f, err := strconv.ParseFloat(s, 64)
if err != nil {
panic(err)
}
field.SetFloat(f)
}
var convertString TypeConverter = func(s string, field reflect.Value) {
if s == "NULL" {
s = ""
}
settable := reflect.ValueOf(s)
field.Set(settable)
}
var youAreOnYourOwn TypeConverter = func(s string, field reflect.Value) {
panic(`type[` + field.Type().Name() + "\t" + field.Kind().String() + ` (` + strconv.Itoa(int(field.Kind())) + `)] has not been implemented. See OverrideConverter for more info.`)
}
//</editor-fold>

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module csvmagic
go 1.18