Skip to content

Commit

Permalink
Update to version 2 (#2)
Browse files Browse the repository at this point in the history
* update read to use iter.Seq2

* update Writer to define and write header at runtime

* docs

---------

Co-authored-by: sfomuseumbot <sfomuseumbot@localhost>
  • Loading branch information
thisisaaronland and sfomuseumbot authored Nov 25, 2024
1 parent 5e111d8 commit c109999
Show file tree
Hide file tree
Showing 7 changed files with 190 additions and 166 deletions.
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

0 comments on commit c109999

Please sign in to comment.