Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to version 2 #2

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 35 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,18 @@ Go package to implement a "dict reader" style CSV parser (on top of the default

```
import (
"github.com/whosonfirst/go-csvdict"
"os"

"github.com/whosonfirst/go-csvdict/v2"
)

reader, reader_err := csvdict.NewReaderFromPath("example.csv")
r, _ := csvdict.NewReaderFromPath("example.csv")

// or maybe you might do
// reader, err := csvdict.NewReader(os.Stdin)

if err != nil {
panic(err)
}
// r, _ := csvdict.NewReader(os.Stdin)

for {
row, err := reader.Read()
row, err := r.Read()

if err == io.EOF {
break
Expand All @@ -37,43 +34,55 @@ for {
}

value, ok := row["some-key"]

// and so on...
}
```

### Writing files
It is also possible to iterate through all the records using the `Iterate` method:

```
import (
"github.com/whosonfirst/go-csvdict"
"os"
)

fieldnames := []string{"foo", "bar"}
"github.com/whosonfirst/go-csvdict/v2"
)

writer, err := csvdict.NewWriter(os.Stdout, fieldnames)
r, _ := csvdict.NewReaderFromPath("example.csv")

// or maybe you might do
// writer, err := csvdict.NewWriterFromPath("new.csv", fieldnames)
// r, _ := csvdict.NewReader(os.Stdin)

for row, err := r.Iterate() {

if err != nil {
return err
}

if err != nil {
panic(err)
value, ok := row["some-key"]
// and so on...
}
```

### Writing files

```
import (
"os"

writer.WriteHeader()
"github.com/whosonfirst/go-csvdict/v2"
)

row := make(map[string]string)
row["foo"] = "hello"
row["bar"] = "world"
wr, _ := csvdict.NewWriter(os.Stdout)

// See this? "baz" is not included in the list of fieldnames
// above so it will be silently ignored and excluded from your
// CSV file. Perhaps it should trigger an error. It doesn't, today...
// or maybe you might do
// wrr, _ := csvdict.NewWriterFromPath("new.csv")

row["baz"] = "wub wub wub"
row := make(map[string]{
"foo": "hello",
"bar": "world",
}

writer.WriteRow(row)
wr.WriteRow(row)
```

## See also
Expand Down
65 changes: 0 additions & 65 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,67 +1,2 @@
// package csvdict implements a "dictionary reader" style CSV parser (on top of the default `encoding/csv` package) to return rows a key-value dictionaries rather than lists.
// Example
//
// Reading files
//
// import (
// "github.com/whosonfirst/go-csvdict"
// "os"
// )
//
// reader, reader_err := csvdict.NewReaderFromPath("example.csv")
//
// // or maybe you might do
// // reader, err := csvdict.NewReader(os.Stdin)
//
// if err != nil {
// panic(err)
// }
//
// for {
// row, err := reader.Read()
//
// if err == io.EOF {
// break
// }
//
// if err != nil {
// return err
// }
//
// value, ok := row["some-key"]
//
// // and so on...
// }
//
// Writing files
//
// import (
// "github.com/whosonfirst/go-csvdict"
// "os"
// )
//
// fieldnames := []string{"foo", "bar"}
//
// writer, err := csvdict.NewWriter(os.Stdout, fieldnames)
//
// // or maybe you might do
// // writer, err := csvdict.NewWriterFromPath("new.csv", fieldnames)
//
// if err != nil {
// panic(err)
// }
//
// writer.WriteHeader()
//
// row := make(map[string]string)
// row["foo"] = "hello"
// row["bar"] = "world"
//
// // See this? "baz" is not included in the list of fieldnames
// // above so it will be silently ignored and excluded from your
// // CSV file. Perhaps it should trigger an error. It doesn't, today...
//
// row["baz"] = "wub wub wub"
//
// writer.WriteRow(row)
package csvdict
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module github.com/sfomuseum/go-csvdict
module github.com/sfomuseum/go-csvdict/v2

go 1.13
go 1.23
67 changes: 49 additions & 18 deletions reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,47 +4,59 @@ import (
"encoding/csv"
"fmt"
"io"
"iter"
"os"
"path/filepath"
)

// type Reader implements a `encoding/csv` style reader for CSV documents with named columns.
type Reader struct {
Reader *csv.Reader
Fieldnames []string
csv_reader *csv.Reader
fieldnames []string
}

// NewReader will return a Reader instance that will load data from 'r'
func NewReader(r io.Reader) (*Reader, error) {
// NewReader will return a Reader instance that will load data from 'path'
func NewReaderFromPath(path string) (*Reader, error) {

reader := csv.NewReader(r)
abs_path, err := filepath.Abs(path)

if err != nil {
return nil, fmt.Errorf("Failed to derive absolute path for %s, %w", path, err)
}

row, read_err := reader.Read()
r, err := os.Open(abs_path)

if read_err != nil {
return nil, fmt.Errorf("Failed to read first line of document, %w", read_err)
if err != nil {
return nil, fmt.Errorf("Failed to open %s, %w", path, err)
}

dr := Reader{Reader: reader, Fieldnames: row}
return &dr, nil
return NewReader(r)
}

// NewReader will return a Reader instance that will load data from 'path'
func NewReaderFromPath(path string) (*Reader, error) {
// NewReader will return a Reader instance that will load data from 'r'
func NewReader(io_r io.Reader) (*Reader, error) {

csv_r := csv.NewReader(io_r)

fh, err := os.Open(path)
fieldnames, err := csv_r.Read()

if err != nil {
return nil, fmt.Errorf("Failed to open %s, %w", path, err)
return nil, err
}

r := &Reader{
csv_reader: csv_r,
fieldnames: fieldnames,
}

return NewReader(fh)
return r, nil
}

// Read reads one record (a slice of fields) from r and returns a map[string]string
// mapping columns to their corresponding names, as defined in the first line of r.
func (dr Reader) Read() (map[string]string, error) {
func (r Reader) Read() (map[string]string, error) {

row, err := dr.Reader.Read()
row, err := r.csv_reader.Read()

if err != nil {
return nil, err
Expand All @@ -53,9 +65,28 @@ func (dr Reader) Read() (map[string]string, error) {
dict := make(map[string]string)

for i, value := range row {
key := dr.Fieldnames[i]
key := r.fieldnames[i]
dict[key] = value
}

return dict, nil
}

// Iterate throush all from r and yielding a map[string]string mapping columns to their corresponding
// names, as defined in the first line of r.
func (r Reader) Iterate() iter.Seq2[map[string]string, error] {

return func(yield func(map[string]string, error) bool) {

for {

dict, err := r.Read()

if err == io.EOF {
break
}

yield(dict, nil)
}
}
}
49 changes: 44 additions & 5 deletions reader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ func TestReader(t *testing.T) {

path := "fixtures/test.csv"

fh, err := os.Open(path)
r, err := os.Open(path)

if err != nil {
t.Fatalf("Failed to open %s, %v", path, err)
}

defer fh.Close()
defer r.Close()

scanner := bufio.NewScanner(fh)
scanner := bufio.NewScanner(r)
count_lines := 0

for scanner.Scan() {
Expand All @@ -32,21 +32,24 @@ func TestReader(t *testing.T) {
t.Fatalf("Scanner reported an error, %v", err)
}

_, err = fh.Seek(0, 0)
_, err = r.Seek(0, 0)

if err != nil {
t.Fatalf("Failed to seek file to 0, %v", err)
}

csv_r, err := NewReader(fh)
csv_r, err := NewReader(r)

if err != nil {
t.Fatalf("Failed to create reader, %v", err)
}

// Test the Read method

count_rows := 0

for {

row, err := csv_r.Read()

if err == io.EOF {
Expand All @@ -69,4 +72,40 @@ func TestReader(t *testing.T) {
if count_rows != count_lines-1 {
t.Fatalf("Expected %d rows, but got %d", count_lines-1, count_rows)
}

// Test the Iterator method

_, err = r.Seek(0, 0)

if err != nil {
t.Fatalf("Failed to seek file to 0, %v", err)
}

csv_r, err = NewReader(r)

if err != nil {
t.Fatalf("Failed to create reader, %v", err)
}

count_rows = 0

for row, err := range csv_r.Iterate() {

if err != nil {
t.Fatalf("Failed to iterate row, %v", err)
}

_, ok := row["label"]

if !ok {
t.Fatalf("Row is missing 'label' column")
}

count_rows += 1
}

if count_rows != count_lines-1 {
t.Fatalf("Expected %d rows, but got %d", count_lines-1, count_rows)
}

}
Loading