From 200856105d55ee2048ed8e9f56ae59dbc70010ec Mon Sep 17 00:00:00 2001 From: Martin Polden Date: Sun, 19 Nov 2023 16:25:20 +0100 Subject: morrow: add reader --- record/morrow/morrow.go | 70 +++++++++++++++++++++++++++++++++++++++++ record/morrow/morrow_test.go | 60 +++++++++++++++++++++++++++++++++++ record/morrow/testdata/test.csv | 3 ++ 3 files changed, 133 insertions(+) create mode 100644 record/morrow/morrow.go create mode 100644 record/morrow/morrow_test.go create mode 100644 record/morrow/testdata/test.csv 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,,, -- cgit v1.2.3