diff --git a/README.md b/README.md index cbaab96..5e66d90 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,30 @@ # go-csv-table +This module reads a RTF and let you get the information per field or column. + +**PLEASE NOTE**: the entire CSV fill be read to the memory at once! + +## Example: + +```go + gct := GoCSVtable{} + gct.OpenCSV("file.csv") + + for { + name, err := gct.Read("Name") + if err != nil { + return err + } + + surname, err := gct.Read("Surname") + if err != nil { + return err + } + + err := gct.Next() + if err == io.EOF { + // File is over... + break + } + } +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ffb8fb0 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitea.brunofontes.net/BFT/go-csv-table + +go 1.24.1 diff --git a/gocsvtable.go b/gocsvtable.go new file mode 100644 index 0000000..225d803 --- /dev/null +++ b/gocsvtable.go @@ -0,0 +1,129 @@ +package gocsvtable + +import ( + "encoding/csv" + "fmt" + "io" + "os" +) + +type GoCSVtable struct { + file *csv.Reader + rows int + data [][]string + headers map[string]int + + Separator rune + Comment rune + + lastRow int + + // Row is the active CSV row, the data will start at 1 + Row int +} + +func (gct *GoCSVtable) OpenCSV(filename string) error { + osfile, err := os.Open(filename) + if err != nil { + return fmt.Errorf("Not possible to os.Open file: %s\n", err) + } + defer osfile.Close() + + // The default rune is 0 + if gct.Separator != 0 { + gct.file.Comma = gct.Separator + } + + // The default rune is 0 + if gct.Comment != 0 { + gct.file.Comment = gct.Comment + } + + gct.file = csv.NewReader(osfile) + gct.data, err = gct.file.ReadAll() + if err != nil { + return fmt.Errorf("Not possible to csv.ReadAll file: %s\n", err) + } + + gct.setHeaders() + + // First data row is 1 + gct.Row = 1 + + // Last row number + gct.lastRow = len(gct.data) - 1 + + return nil +} + +// setHeaders will associate the headers names with the correct column number +func (gct *GoCSVtable) setHeaders() { + header := gct.data[0] + columnsNumber := len(header) + + // Initiate Headers + gct.headers = make(map[string]int) + + for field := 0; field < columnsNumber; field++ { + name := header[field] + gct.headers[name] = field + } +} + +// GetHeader return the col number from a field string +func (gct *GoCSVtable) GetHeader(field string) (int, error) { + col, ok := gct.headers[field] + if !ok { + return 0, fmt.Errorf("Field '%s' does not exist", field) + } + + return col, nil +} + +// Next will jump to the next CSV row. If file is over, will return an io.EOF +func (gct *GoCSVtable) Next() error { + + if gct.Row >= gct.lastRow { + return io.EOF + } + gct.Row++ + return nil +} + +// GetNumberOfRows will return the number of data rows, not considering the +// header +func (gct *GoCSVtable) GetNumberOfRows() int { + return gct.lastRow +} + +// Read return a specific field from active row +func (gct *GoCSVtable) Read(field string) (string, error) { + col, err := gct.GetHeader(field) + if err != nil { + return "", err + } + return gct.data[gct.Row][col], nil +} + +// Read and return the entire row +func (gct *GoCSVtable) ReadRow(row int) ([]string, error) { + if row > gct.lastRow { + return []string{}, fmt.Errorf("Not possible to read row %v. Data has max of %v rows.\n", row, gct.lastRow) + } + return gct.data[row], nil +} + +// ReadCol will return a slice with all row values from a single column (field) +func (gct *GoCSVtable) ReadCol(field string) ([]string, error) { + data := []string{} + fieldNumber, err := gct.GetHeader(field) + if err != nil { + return data, err + } + + for row := 1; row <= gct.lastRow; row++ { + data = append(data, gct.data[row][fieldNumber]) + } + + return data, nil +} diff --git a/gocsvtable_test.go b/gocsvtable_test.go new file mode 100644 index 0000000..1372c0d --- /dev/null +++ b/gocsvtable_test.go @@ -0,0 +1,149 @@ +package gocsvtable + +import ( + "io" + "reflect" + "testing" +) + +const testFile = "test.csv" + +func TestOpenCSV(t *testing.T) { + gct := GoCSVtable{} + err := gct.OpenCSV(testFile) + if err != nil { + t.Errorf("\nError opening csv file:\n - %#v\n", err.Error()) + } +} + +func TestGetHeader(t *testing.T) { + const letters = 0 + const numbers = 1 + + gct := GoCSVtable{} + gct.OpenCSV(testFile) + + _, err := gct.GetHeader("Non Existent Field") + if err == nil { + t.Errorf("Getting header of non existent field should raise error.") + } + + gotLettersCol, _ := gct.GetHeader("Letters") + if gotLettersCol != letters { + t.Errorf("Error reading correct 'Letters' column. Expected: '%v'. Got: '%v'.\n", letters, gotLettersCol) + } + + gotNumbersCol, _ := gct.GetHeader("Numbers") + if gotNumbersCol != numbers { + t.Errorf("Error reading correct 'Numbers' column. Expected: '%v'. Got: '%v'.\n", numbers, gotNumbersCol) + } +} + +func TestNext(t *testing.T) { + gct := GoCSVtable{} + gct.OpenCSV(testFile) + + for row := 2; row < 5; row++ { + err := gct.Next() + if err != nil { + t.Errorf("Error passing to next row. CSV file have 5 rows, not %v", row) + } + } + + // Try to go to different row, it should receive io.EOF + err := gct.Next() + if err != io.EOF { + t.Errorf("Error on Next(), row should be inexistent. Expected: '%v'; Got: %v", io.EOF, err) + } +} + +func TestGetNumberOfRows(t *testing.T) { + gct := GoCSVtable{} + gct.OpenCSV(testFile) + + expected := 4 // 1 Header + 4 Data + if gct.GetNumberOfRows() != expected { + t.Errorf("Error getting number of rows in file. Expected: '%v'. Got: '%v'", expected, gct.GetNumberOfRows()) + } +} + +func TestRead(t *testing.T) { + gct := GoCSVtable{} + gct.OpenCSV(testFile) + + row := 2 + expected := "b" + + gct.Row = row + got, err := gct.Read("Letters") + if err != nil { + t.Errorf("%v", err.Error()) + } + if got != expected { + t.Errorf("Error getting correct field on row '%v'. Expected: '%v'. Got: '%v'", row, expected, got) + } + + expected = "2" + got, err = gct.Read("Numbers") + if err != nil { + t.Errorf("%v", err.Error()) + } + if got != expected { + t.Errorf("Error getting correct field on row '%v'. Expected: '%v'. Got: '%v'", row, expected, got) + } +} + +func TestReadRow(t *testing.T) { + gct := GoCSVtable{} + gct.OpenCSV(testFile) + + row := 3 + + gct.Row = row + got, err := gct.ReadRow(row) + if err != nil { + t.Errorf("Error getting correct row: '%v'", err.Error()) + } + + col, err := gct.GetHeader("Letters") + if err != nil { + t.Errorf("%v", err.Error()) + } + gotLetter := got[col] + expected := "c" + if gotLetter != expected { + t.Errorf("Error getting correct row. Expected: '[%v %v]'. Got: '%v'", expected, row, got) + } + + col, err = gct.GetHeader("Numbers") + if err != nil { + t.Errorf("%v", err.Error()) + } + gotNumber := got[col] + expected = "3" + if gotNumber != expected { + t.Errorf("Error getting correct row. Expected: '[%v %v]'. Got: '%v'", expected, row, got) + } + + row = 7 + got, err = gct.ReadRow(row) + if err == nil { + t.Errorf("Getting inexistent row should raise error. Value: '%v'. Error: '%v'", got, err) + } +} + +func TestReadCol(t *testing.T) { + gct := GoCSVtable{} + gct.OpenCSV(testFile) + + expected := []string{"a", "b", "c", "d"} + col := "Letters" + got, err := gct.ReadCol(col) + if err != nil { + t.Errorf("%v", err.Error()) + } + + if !reflect.DeepEqual(expected, got) { + t.Errorf("Error getting column. Expected: '%v'. Got: '%v'", expected, got) + } +} diff --git a/tags b/tags new file mode 100644 index 0000000..ab453b9 --- /dev/null +++ b/tags @@ -0,0 +1,88 @@ +!_TAG_EXTRA_DESCRIPTION anonymous /Include tags for non-named objects like lambda/ +!_TAG_EXTRA_DESCRIPTION fileScope /Include tags of file scope/ +!_TAG_EXTRA_DESCRIPTION pseudo /Include pseudo tags/ +!_TAG_EXTRA_DESCRIPTION subparser /Include tags generated by subparsers/ +!_TAG_FIELD_DESCRIPTION epoch /the last modified time of the input file (only for F\/file kind tag)/ +!_TAG_FIELD_DESCRIPTION file /File-restricted scoping/ +!_TAG_FIELD_DESCRIPTION input /input file/ +!_TAG_FIELD_DESCRIPTION name /tag name/ +!_TAG_FIELD_DESCRIPTION pattern /pattern/ +!_TAG_FIELD_DESCRIPTION typeref /Type and name of a variable or typedef/ +!_TAG_FIELD_DESCRIPTION!Go package /the real package specified by the package name/ +!_TAG_FIELD_DESCRIPTION!Go packageName /the name for referring the package/ +!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ +!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ +!_TAG_KIND_DESCRIPTION!DTD E,entity /entities/ +!_TAG_KIND_DESCRIPTION!DTD a,attribute /attributes/ +!_TAG_KIND_DESCRIPTION!DTD e,element /elements/ +!_TAG_KIND_DESCRIPTION!DTD n,notation /notations/ +!_TAG_KIND_DESCRIPTION!DTD p,parameterEntity /parameter entities/ +!_TAG_KIND_DESCRIPTION!Go M,anonMember /struct anonymous members/ +!_TAG_KIND_DESCRIPTION!Go P,packageName /name for specifying imported package/ +!_TAG_KIND_DESCRIPTION!Go Y,unknown /unknown/ +!_TAG_KIND_DESCRIPTION!Go a,talias /type aliases/ +!_TAG_KIND_DESCRIPTION!Go c,const /constants/ +!_TAG_KIND_DESCRIPTION!Go f,func /functions/ +!_TAG_KIND_DESCRIPTION!Go i,interface /interfaces/ +!_TAG_KIND_DESCRIPTION!Go m,member /struct members/ +!_TAG_KIND_DESCRIPTION!Go n,methodSpec /interface method specification/ +!_TAG_KIND_DESCRIPTION!Go p,package /packages/ +!_TAG_KIND_DESCRIPTION!Go s,struct /structs/ +!_TAG_KIND_DESCRIPTION!Go t,type /types/ +!_TAG_KIND_DESCRIPTION!Go v,var /variables/ +!_TAG_KIND_DESCRIPTION!Markdown S,subsection /level 2 sections/ +!_TAG_KIND_DESCRIPTION!Markdown T,l4subsection /level 4 sections/ +!_TAG_KIND_DESCRIPTION!Markdown c,chapter /chapters/ +!_TAG_KIND_DESCRIPTION!Markdown h,hashtag /hashtags/ +!_TAG_KIND_DESCRIPTION!Markdown n,footnote /footnotes/ +!_TAG_KIND_DESCRIPTION!Markdown s,section /sections/ +!_TAG_KIND_DESCRIPTION!Markdown t,subsubsection /level 3 sections/ +!_TAG_KIND_DESCRIPTION!Markdown u,l5subsection /level 5 sections/ +!_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/ +!_TAG_OUTPUT_FILESEP slash /slash or backslash/ +!_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ +!_TAG_OUTPUT_VERSION 0.0 /current.age/ +!_TAG_PARSER_VERSION!DTD 0.0 /current.age/ +!_TAG_PARSER_VERSION!Go 0.0 /current.age/ +!_TAG_PARSER_VERSION!Markdown 1.1 /current.age/ +!_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/ +!_TAG_PROC_CWD /run/media/bruno/Multimedia/MyDocuments/Development/BrunoFontes/GO/go-csv-table/ // +!_TAG_PROGRAM_AUTHOR Universal Ctags Team // +!_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ +!_TAG_PROGRAM_URL https://ctags.io/ /official site/ +!_TAG_PROGRAM_VERSION 6.1.0 /v6.1.0/ +!_TAG_ROLE_DESCRIPTION!DTD!element attOwner /attributes owner/ +!_TAG_ROLE_DESCRIPTION!DTD!parameterEntity condition /conditions/ +!_TAG_ROLE_DESCRIPTION!DTD!parameterEntity elementName /element names/ +!_TAG_ROLE_DESCRIPTION!DTD!parameterEntity partOfAttDef /part of attribute definition/ +!_TAG_ROLE_DESCRIPTION!Go!package imported /imported package/ +!_TAG_ROLE_DESCRIPTION!Go!unknown receiverType /receiver type/ +Comment gocsvtable.go /^ Comment rune$/;" m struct:gocsvtable.GoCSVtable typeref:typename:rune +Example: README.md /^## Example:$/;" s chapter:go-csv-table +GetHeader gocsvtable.go /^func (gct *GoCSVtable) GetHeader(field string) (int, error) {$/;" f struct:gocsvtable.GoCSVtable typeref:typename:(int, error) +GetNumberOfRows gocsvtable.go /^func (gct *GoCSVtable) GetNumberOfRows() int {$/;" f struct:gocsvtable.GoCSVtable typeref:typename:int +GoCSVtable gocsvtable.go /^type GoCSVtable struct {$/;" s package:gocsvtable +Next gocsvtable.go /^func (gct *GoCSVtable) Next() error {$/;" f struct:gocsvtable.GoCSVtable typeref:typename:error +OpenCSV gocsvtable.go /^func (gct *GoCSVtable) OpenCSV(filename string) error {$/;" f struct:gocsvtable.GoCSVtable typeref:typename:error +Read gocsvtable.go /^func (gct *GoCSVtable) Read(field string) (string, error) {$/;" f struct:gocsvtable.GoCSVtable typeref:typename:(string, error) +ReadCol gocsvtable.go /^func (gct *GoCSVtable) ReadCol(field string) ([]string, error) {$/;" f struct:gocsvtable.GoCSVtable typeref:typename:([]string, error) +ReadRow gocsvtable.go /^func (gct *GoCSVtable) ReadRow(row int) ([]string, error) {$/;" f struct:gocsvtable.GoCSVtable typeref:typename:([]string, error) +Row gocsvtable.go /^ Row int$/;" m struct:gocsvtable.GoCSVtable typeref:typename:int +Separator gocsvtable.go /^ Separator rune$/;" m struct:gocsvtable.GoCSVtable typeref:typename:rune +TestGetHeader gocsvtable_test.go /^func TestGetHeader(t *testing.T) {$/;" f package:gocsvtable +TestGetNumberOfRows gocsvtable_test.go /^func TestGetNumberOfRows(t *testing.T) {$/;" f package:gocsvtable +TestNext gocsvtable_test.go /^func TestNext(t *testing.T) {$/;" f package:gocsvtable +TestOpenCSV gocsvtable_test.go /^func TestOpenCSV(t *testing.T) {$/;" f package:gocsvtable +TestRead gocsvtable_test.go /^func TestRead(t *testing.T) {$/;" f package:gocsvtable +TestReadCol gocsvtable_test.go /^func TestReadCol(t *testing.T) {$/;" f package:gocsvtable +TestReadRow gocsvtable_test.go /^func TestReadRow(t *testing.T) {$/;" f package:gocsvtable +data gocsvtable.go /^ data [][]string$/;" m struct:gocsvtable.GoCSVtable typeref:typename:[][]string +file gocsvtable.go /^ file *csv.Reader$/;" m struct:gocsvtable.GoCSVtable typeref:typename:*csv.Reader +go-csv-table README.md /^# go-csv-table$/;" c +gocsvtable gocsvtable.go /^package gocsvtable$/;" p +gocsvtable gocsvtable_test.go /^package gocsvtable$/;" p +headers gocsvtable.go /^ headers map[string]int$/;" m struct:gocsvtable.GoCSVtable typeref:typename:map[string]int +lastRow gocsvtable.go /^ lastRow int$/;" m struct:gocsvtable.GoCSVtable typeref:typename:int +rows gocsvtable.go /^ rows int$/;" m struct:gocsvtable.GoCSVtable typeref:typename:int +setHeaders gocsvtable.go /^func (gct *GoCSVtable) setHeaders() {$/;" f struct:gocsvtable.GoCSVtable +testFile gocsvtable_test.go /^const testFile = "test.csv"$/;" c package:gocsvtable diff --git a/test.csv b/test.csv new file mode 100644 index 0000000..f148d2a --- /dev/null +++ b/test.csv @@ -0,0 +1,5 @@ +Letters,Numbers +a,1 +b,2 +c,3 +d,4