commit ddfeb99a533b4520d52dc766c63fffc184ba0967 Author: dtookey Date: Fri Dec 9 09:55:59 2022 -0500 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3100334 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/csvmagic.iml +/.idea/ diff --git a/csv.go b/csv.go new file mode 100644 index 0000000..c33cacd --- /dev/null +++ b/csv.go @@ -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 +} + +// +/*=================================================================================================== + 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.`) +} + +// diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ea98870 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module csvmagic + +go 1.18