aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Polden <mpolden@mpolden.no>2023-11-19 16:25:20 +0100
committerMartin Polden <mpolden@mpolden.no>2023-11-19 16:41:53 +0100
commit200856105d55ee2048ed8e9f56ae59dbc70010ec (patch)
tree8ccee1a64dbad148422a1bb05dbc0ff9d0f58103
parent7730bf27b4f151a815933fefa4da7d638b49043d (diff)
morrow: add reader
-rw-r--r--record/morrow/morrow.go70
-rw-r--r--record/morrow/morrow_test.go60
-rw-r--r--record/morrow/testdata/test.csv3
3 files changed, 133 insertions, 0 deletions
diff --git a/record/morrow/morrow.go b/record/morrow/morrow.go
new file mode 100644
index 0000000..0a8ae0c
--- /dev/null
+++ b/record/morrow/morrow.go
@@ -0,0 +1,70 @@
+package morrow
+
+import (
+ "bufio"
+ "encoding/csv"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/mpolden/journal/record"
+)
+
+// Reader implements a reader for Morrow-encoded (CSV) records.
+type Reader struct {
+ rd io.Reader
+}
+
+// NewReader returns a new reader for Morrow-encoded records.
+func NewReader(rd io.Reader) *Reader {
+ return &Reader{
+ rd: rd,
+ }
+}
+
+// Read all records from the underlying reader.
+func (r *Reader) Read() ([]record.Record, error) {
+ buf := bufio.NewReader(r.rd)
+ c := csv.NewReader(buf)
+ c.FieldsPerRecord = -1 // Morrow export has an additional field which is not in the header
+ var rs []record.Record
+ line := 0
+ for {
+ csvRecord, err := c.Read()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ line++
+ if len(csvRecord) < 10 {
+ continue
+ }
+ if line == 1 {
+ continue // Skip header
+ }
+ t, err := time.Parse("02.01.2006", csvRecord[0])
+ if err != nil {
+ return nil, fmt.Errorf("invalid time on line %d: %q: %w", line, csvRecord[0], err)
+ }
+ amount, err := parseAmount(csvRecord[5])
+ if err != nil {
+ return nil, fmt.Errorf("invalid amount on line %d: %q: %w", line, amount, err)
+ }
+ text := strings.TrimSpace(csvRecord[2])
+ rs = append(rs, record.Record{Time: t, Text: text, Amount: amount})
+ }
+ return rs, nil
+}
+
+func parseAmount(s string) (int64, error) {
+ v := strings.ReplaceAll(s, ".", "")
+ n, err := strconv.ParseInt(v, 10, 64)
+ if err != nil {
+ return 0, err
+ }
+ return n, nil
+}
diff --git a/record/morrow/morrow_test.go b/record/morrow/morrow_test.go
new file mode 100644
index 0000000..6523067
--- /dev/null
+++ b/record/morrow/morrow_test.go
@@ -0,0 +1,60 @@
+package morrow
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+)
+
+func date(year int, month time.Month, day int) time.Time {
+ return time.Date(year, month, day, 0, 0, 0, 0, time.UTC)
+}
+
+func testFile(t *testing.T, name string) *os.File {
+ wd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ testFile := filepath.Join(wd, "testdata", name)
+
+ f, err := os.Open(testFile)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return f
+}
+
+func TestRead(t *testing.T) {
+ f := testFile(t, "test.csv")
+ defer f.Close()
+
+ r := NewReader(f)
+ rs, err := r.Read()
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var tests = []struct {
+ t time.Time
+ text string
+ amount int64
+ }{
+ {date(2023, 10, 31), "Rema 1000", -15055},
+ {date(2023, 10, 15), "Lønn", 750000},
+ }
+ if len(rs) != len(tests) {
+ t.Fatalf("want %d records, got %d", len(tests), len(rs))
+ }
+ for i, tt := range tests {
+ if !rs[i].Time.Equal(tt.t) {
+ t.Errorf("#%d: want Time = %s, got %s", i, tt.t, rs[i].Time)
+ }
+ if rs[i].Text != tt.text {
+ t.Errorf("#%d: want Text = %s, got %s", i, tt.text, rs[i].Text)
+ }
+ if rs[i].Amount != tt.amount {
+ t.Errorf("#%d: want Amount = %d, got %d", i, tt.amount, rs[i].Amount)
+ }
+ }
+}
diff --git a/record/morrow/testdata/test.csv b/record/morrow/testdata/test.csv
new file mode 100644
index 0000000..c6db285
--- /dev/null
+++ b/record/morrow/testdata/test.csv
@@ -0,0 +1,3 @@
+Transaksjonsdato,Bokføringsdato,Beskrivelse,Mottakers kontonummer,KID eller melding,Beløp,Beløp i valuta,Utsatt,Utsatt periode,Utløpsdato
+31.10.2023,02.11.2023,Rema 1000,,,-150.55,,Nei,,,
+15.10.2023,02.11.2023,Lønn,,,7500.00,,Nei,,,