Skip to content

Commit

Permalink
Tests
Browse files Browse the repository at this point in the history
  • Loading branch information
thinktwice13 committed Jun 14, 2022
1 parent 60b13b4 commit ab243aa
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 29 deletions.
10 changes: 5 additions & 5 deletions import_results_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@ func TestAssetBySymbols(t *testing.T) {
a2 := &AssetImport{}
a2.Symbols = []string{"B", "C"}
a3 := &AssetImport{}
a3.Symbols = []string{"A", "B", "D"}
a3.Symbols = []string{"A", "B", "C", "D"}

assets := assets{}
assets.bySymbols(a1.Symbols...)
assets.bySymbols(a2.Symbols...)
assets.bySymbols(a3.Symbols...)

assetPtrs := make(map[*AssetImport]bool)
mapped := make(map[*AssetImport]bool)
for _, a := range assets {
assetPtrs[a] = true
mapped[a] = true
}
if len(assetPtrs) != 1 {
t.Errorf("Expected 1 unique asset. Got %d", len(assetPtrs))
if len(mapped) != 1 {
t.Errorf("Expected 1 unique asset. Got %d", len(mapped))
}
}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func main() {
writeReport(&tr, r)
writeReport(&summaries, r)
err = r.Save()
err = createXlsTemplate()
createXlsTemplate()
if err != nil {
log.Fatalln(err)
}
Expand Down
65 changes: 47 additions & 18 deletions read_ibkr_statement.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"io"
"log"
"os"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -80,6 +80,10 @@ func mapIbkrLine(data, header []string) (map[string]string, error) {
return nil, errors.New("cannot convert to row from empty header")
}

if data == nil {
return nil, errors.New("no data to map")
}

if len(header) != len(data) {
return nil, errors.New("header and line length mismatch")
}
Expand All @@ -106,15 +110,13 @@ func handleInstrumentLine(lm map[string]string, ir *ImportResults) {

// Adds "US" prefix to US security ISIN codes and removes the 12th check digit
func formatISIN(sID string) string {
if sID == "" {
log.Fatal("empty security ID")
if sID == "" || len(sID) < 9 || len(sID) > 12 {
return sID // Not ISIN
}

if len(sID) > 8 && len(sID) < 11 {
if len(sID) < 11 {
// US ISIN number. Add country code
sID = "US" + sID
}

if len(sID) == 12 {
// Remove ISIN check digit
return sID[:11]
Expand All @@ -128,9 +130,12 @@ func handleTradeLine(lm map[string]string, ir *ImportResults) {
if lm["Date/Time"] == "" || lm["Asset Category"] == "Forex" || lm["Symbol"] == "" {
return
}
t := timeFromExact(lm["Date/Time"])
t, err := timeFromExact(lm["Date/Time"])
if err != nil {
return
}
c := lm["Currency"]
ir.AddTrade(lm["Symbol"], c, &t, amountFromString(lm["Quantity"]), amountFromString(lm["T. Price"]), amountFromString(lm["Comm/Fee"]))
ir.AddTrade(lm["Symbol"], c, t, amountFromString(lm["Quantity"]), amountFromString(lm["T. Price"]), amountFromString(lm["Comm/Fee"]))
}

// handleDividendLine handles the dividend lines of the IBKR csv statement
Expand Down Expand Up @@ -179,9 +184,10 @@ func handleFeeLine(lm map[string]string, ir *ImportResults) {
}

// symbolFromDescription extracts a symbol from IBKR csv dividend lines
// TODO Check for ISINs
func symbolFromDescription(d string) (string, error) {
if d == "" {
return "", errors.New("cannot create asset event without symbol")
return "", errors.New("empty input")
}

// This is a dividend or withholding tax
Expand All @@ -192,33 +198,56 @@ func symbolFromDescription(d string) (string, error) {

symbol := strings.ReplaceAll(d[:parensIdx], " ", "")
if symbol == "" {
return "", errors.New("cannor find symbol in description")
return "", errors.New("cannot find symbol in description")
}
return symbol, nil
}

// amountFromString formats number strings to float64 type
func amountFromString(s string) float64 {
var v float64
if s == "" {
return 0

}
s = strings.ReplaceAll(s, ",", "")
v, err := strconv.ParseFloat(s, 64)
// Remove all but numbers, commas and points
re := regexp.MustCompile(`[0-9.,]`)
ss := strings.Join(re.FindAllString(s, -1), "")

// Find all commans and points
// If none found, return 0, print error
signs := regexp.MustCompile(`[.,]`).FindAllString(ss, -1)
if len(signs) == 0 {
f, err := strconv.ParseFloat(ss, 64)
if err != nil {
fmt.Errorf("could not convert %s to number", s)
return 0
}

return f
}

// Use last sign as decimal separator and ignore others
// Find idx and replace whatever sign was to a decimal point
sign := signs[len(signs)-1]
signIdx := strings.LastIndex(ss, sign)
sign = "."
left := regexp.MustCompile(`[0-9]`).FindAllString(ss[:signIdx], -1)
right := ss[signIdx+1:]
n, err := strconv.ParseFloat(strings.Join(append(left, []string{sign, right}...), ""), 64)
if err != nil {
log.Printf("error parsing float from from %v", err)
fmt.Errorf("could not convert %s to number", s)
return 0
}
return v
return n
}

// timeFromExact extracts time.Time from IBKR csv time field
func timeFromExact(t string) time.Time {
func timeFromExact(t string) (*time.Time, error) {
timeStr := strings.Join(strings.Split(t, ","), "")
tm, err := time.Parse("2006-01-02 15:04:05", timeStr)
if err != nil {
panic(err)
return nil, errors.New("could not parse time")
}

return tm
return &tm, nil
}
153 changes: 153 additions & 0 deletions read_ibkr_statement_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package main

import (
"reflect"
"testing"
"time"
)

func Test_amountFromString(t *testing.T) {
tests := []struct {
name string
args string
want float64
}{
{"empty", "", 0},
{"decimal point", "22.33", 22.33},
{"decimal point with 000 separator ", "1,222.33", 1222.33},
{"decimal comma", "22,33", 22.33},
{"decimal comma with 000 separator", "1.222,33", 1222.33},
{"with spaces", "1. 222 ,333 .44", 1222333.44},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := amountFromString(tt.args); got != tt.want {
t.Errorf("amountFromString() = %v, want %v", got, tt.want)
}
})
}
}

func Test_formatISIN(t *testing.T) {
tests := []struct {
name string
args string
want string
}{
{"", "", ""},
{"", "12345678", "12345678"},
{"", "1234567890123", "1234567890123"},
{"US", "3456789012", "US345678901"},
{"US", "IE3456789012", "IE345678901"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := formatISIN(tt.args); got != tt.want {
t.Errorf("formatISIN() = %v, want %v", got, tt.want)
}
})
}
}

func Test_mapIbkrLine(t *testing.T) {
type args struct {
data []string
header []string
}
tests := []struct {
name string
args args
want map[string]string
wantErr bool
}{
{"empty header", args{[]string{"v"}, nil}, nil, true},
{"empty data", args{nil, []string{"v"}}, nil, true},
{"length mismatch", args{[]string{"v"}, []string{"v", "n"}}, nil, true},
{"correct", args{[]string{"MySection", "Bar"}, []string{"MySection", "Foo"}}, map[string]string{"Section": "MySection", "Foo": "Bar"}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := mapIbkrLine(tt.args.data, tt.args.header)
if (err != nil) != tt.wantErr {
t.Errorf("mapIbkrLine() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("mapIbkrLine() got = %v, want %v", got, tt.want)
}
})
}
}

func Test_symbolFromDescription(t *testing.T) {
tests := []struct {
name string
args string
want string
wantErr bool
}{
{"empty input", "", "", true},
{"no symbol", "(US26924G8134)", "", true},
{"correct", "AIEQ(US26924G8134)", "AIEQ", false},
{"correct with space", "AIEQ (US26924G8134)", "AIEQ", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := symbolFromDescription(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("symbolFromDescription() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("symbolFromDescription() got = %v, want %v", got, tt.want)
}
})
}
}

func Test_yearFromDate(t *testing.T) {
tests := []struct {
name string
args string
want int
}{
{"", "2018-12-1, 12:34", 2018},
{"", "ipu7t08go8i", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := yearFromDate(tt.args); got != tt.want {
t.Errorf("yearFromDate() = %v, want %v", got, tt.want)
}
})
}
}

func Test_timeFromExact(t *testing.T) {
fn := func() *time.Time {
t := time.Date(2019, 10, 14, 12, 55, 53, 0, time.UTC)
return &t
}

tests := []struct {
name string
args string
want *time.Time
wantErr bool
}{
{"date fails", "2019-10-14", nil, true},
{"correct", "2019-10-14, 12:55:53", fn(), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := timeFromExact(tt.args)
if (err != nil) != tt.wantErr {
t.Errorf("timeFromExact() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("timeFromExact() got = %v, want %v", got, tt.want)
}
})
}
}
1 change: 1 addition & 0 deletions summaries.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func summarizeAssets(imports []AssetImport, r Rater) SummaryReport {
}
sum := make(AssetSummary, toYear-firstYear+1)

// summarize sales
for _, s := range sales {
y := sum.year(s.Time.Year())
for _, c := range s.Basis {
Expand Down
8 changes: 3 additions & 5 deletions tracker_template.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
package main

import (
"errors"
"fmt"
"github.com/xuri/excelize/v2"
"os"
"path/filepath"
"time"
)

func createXlsTemplate() error {
func createXlsTemplate() {
filename := "Portfolio Tracker.xlsx"
fpath := filepath.Join(os.Getenv("PWD"), filename)
var err error

_, err = os.Stat(fpath)
if err == nil {
return errors.New("tracker already exists")
return
}

f := excelize.NewFile()
Expand Down Expand Up @@ -95,7 +94,6 @@ func createXlsTemplate() error {
f.DeleteSheet("Sheet1")
err = f.SaveAs(fpath)
if err != nil {
return err
fmt.Println("error creating tracker template")
}
return nil
}

0 comments on commit ab243aa

Please sign in to comment.