diff --git a/currency/code.go b/currency/code.go index fa7e1534..fc7db0b5 100644 --- a/currency/code.go +++ b/currency/code.go @@ -27,6 +27,8 @@ var ( errRoleUnset = errors.New("role unset") ) +// String implements the stringer interface and returns a string representation +// of the underlying role. func (r Role) String() string { switch r { case Fiat: @@ -89,22 +91,24 @@ func (b *BaseCodes) GetFullCurrencyData() (File, error) { var file File b.mtx.Lock() defer b.mtx.Unlock() - for i := range b.Items { - switch b.Items[i].Role { - case Unset: - file.UnsetCurrency = append(file.UnsetCurrency, b.Items[i]) - case Fiat: - file.FiatCurrency = append(file.FiatCurrency, b.Items[i]) - case Cryptocurrency: - file.Cryptocurrency = append(file.Cryptocurrency, b.Items[i]) - case Token: - file.Token = append(file.Token, b.Items[i]) - case Contract: - file.Contracts = append(file.Contracts, b.Items[i]) - case Stable: - file.Stable = append(file.Stable, b.Items[i]) - default: - return file, errors.New("role undefined") + for _, stored := range b.Items { + for x := range stored { + switch stored[x].Role { + case Unset: + file.UnsetCurrency = append(file.UnsetCurrency, stored[x]) + case Fiat: + file.FiatCurrency = append(file.FiatCurrency, stored[x]) + case Cryptocurrency: + file.Cryptocurrency = append(file.Cryptocurrency, stored[x]) + case Token: + file.Token = append(file.Token, stored[x]) + case Contract: + file.Contracts = append(file.Contracts, stored[x]) + case Stable: + file.Stable = append(file.Stable, stored[x]) + default: + return file, errors.New("role undefined") + } } } file.LastMainUpdate = b.LastMainUpdate.Unix() @@ -116,47 +120,60 @@ func (b *BaseCodes) GetFullCurrencyData() (File, error) { func (b *BaseCodes) GetCurrencies() Currencies { b.mtx.Lock() currencies := make(Currencies, len(b.Items)) - for i := range b.Items { - currencies[i] = Code{Item: b.Items[i]} + var target int + for _, stored := range b.Items { + if len(stored) == 0 { + continue + } + currencies[target] = Code{Item: stored[0]} + target++ } b.mtx.Unlock() return currencies } // UpdateCurrency updates or registers a currency/contract -func (b *BaseCodes) UpdateCurrency(fullName, symbol, blockchain string, id int, r Role) error { - if r == Unset { - return fmt.Errorf("cannot update currency %w for %s", errRoleUnset, symbol) +func (b *BaseCodes) UpdateCurrency(update *Item) error { + if update == nil { + return errItemIsNil } + + if update.Symbol == "" { + return errSymbolEmpty + } + + if update.Role == Unset { + return fmt.Errorf("cannot update currency %w for %s", errRoleUnset, update.Symbol) + } + + update.Symbol = strings.ToUpper(update.Symbol) + update.Lower = strings.ToLower(update.Symbol) + b.mtx.Lock() defer b.mtx.Unlock() - for i := range b.Items { - if b.Items[i].Symbol != symbol || (b.Items[i].Role != Unset && b.Items[i].Role != r) { - continue - } - if fullName != "" { - b.Items[i].FullName = fullName + stored, ok := b.Items[update.Symbol] + if ok { + for x := range stored { + if stored[x].Role != Unset && stored[x].Role != update.Role { + continue + } + + stored[x].Role = update.Role // NOTE: Update role is checked above. + + if update.FullName != "" { + stored[x].FullName = update.FullName + } + if update.AssocChain != "" { + stored[x].AssocChain = update.AssocChain + } + if update.ID != 0 { + stored[x].ID = update.ID + } + return nil } - if r != Unset { - b.Items[i].Role = r - } - if blockchain != "" { - b.Items[i].AssocChain = blockchain - } - if id != 0 { - b.Items[i].ID = id - } - return nil } - - b.Items = append(b.Items, &Item{ - Symbol: symbol, - FullName: fullName, - Role: r, - AssocChain: blockchain, - ID: id, - }) + b.Items[update.Symbol] = append(b.Items[update.Symbol], update) return nil } @@ -181,26 +198,30 @@ func (b *BaseCodes) Register(c string, newRole Role) Code { b.mtx.Lock() defer b.mtx.Unlock() - for i := range b.Items { - if b.Items[i].Symbol != c { - continue - } - if newRole != Unset { - if b.Items[i].Role == Unset { - b.Items[i].Role = newRole - } else if b.Items[i].Role != newRole { - // This will duplicate item with same name but different role. - // TODO: This will need a specific update to NewCode to add in - // a specific param to find the exact name and role. - continue - } - } - - return Code{Item: b.Items[i], UpperCase: format} + if b.Items == nil { + b.Items = make(map[string][]*Item) } + + stored, ok := b.Items[c] + if ok { + for x := range stored { + if newRole != Unset { + if stored[x].Role != Unset && stored[x].Role != newRole { + // This will duplicate item with same name but different + // role if not matched in stored list. + // TODO: This will need a specific update to NewCode() or + // new function to find the exact name and role. + continue + } + stored[x].Role = newRole + } + return Code{Item: stored[x], UpperCase: format} + } + } + newItem := &Item{Symbol: c, Lower: strings.ToLower(c), Role: newRole} - b.Items = append(b.Items, newItem) + b.Items[c] = append(b.Items[c], newItem) return Code{Item: newItem, UpperCase: format} } @@ -219,14 +240,21 @@ func (b *BaseCodes) LoadItem(item *Item) error { b.mtx.Lock() defer b.mtx.Unlock() - for i := range b.Items { - if b.Items[i].Symbol != item.Symbol || - (b.Items[i].Role != Unset && item.Role != Unset && b.Items[i].Role != item.Role) { - continue + + stored, ok := b.Items[item.Symbol] + if ok { + for x := range stored { + if stored[x].Role == item.Role { + return nil + } + + if stored[x].Role == Unset && item.Role != Unset { + stored[x].Role = item.Role + return nil + } } - return nil } - b.Items = append(b.Items, item) + b.Items[item.Symbol] = append(b.Items[item.Symbol], item) return nil } diff --git a/currency/code_test.go b/currency/code_test.go index edbf8b92..d6076d59 100644 --- a/currency/code_test.go +++ b/currency/code_test.go @@ -152,12 +152,16 @@ func (b *BaseCodes) assertRole(t *testing.T, c Code, r Role) { t.Helper() b.mtx.Lock() defer b.mtx.Unlock() - for x := range b.Items { - if b.Items[x] != c.Item { + stored, ok := b.Items[c.Item.Symbol] + if !ok { + t.Fatal("code pointer not found") + } + for x := range stored { + if stored[x] != c.Item { continue } - if b.Items[x].Role != r { - t.Fatal("unexpected role") + if stored[x].Role != r { + t.Fatalf("unexpected role received: %v but expected: %v", stored[x].Role, r) } return } @@ -231,29 +235,42 @@ func TestBaseCode(t *testing.T) { } main.Register("XBTUSD", Unset) - - err := main.UpdateCurrency("Bitcoin Perpetual", - "XBTUSD", - "", - 0, - Contract) + err := main.UpdateCurrency(&Item{ + FullName: "Bitcoin Perpetual", + Symbol: "XBTUSD", + Role: Contract, + }) if err != nil { t.Fatal(err) } main.Register("BTC", Unset) - err = main.UpdateCurrency("Bitcoin", "BTC", "", 1337, Unset) + err = main.UpdateCurrency(&Item{ + FullName: "Bitcoin", + Symbol: "BTC", + ID: 1337, + }) if !errors.Is(err, errRoleUnset) { t.Fatalf("received: '%v' but expected: '%v'", err, errRoleUnset) } - err = main.UpdateCurrency("Bitcoin", "BTC", "", 1337, Cryptocurrency) + err = main.UpdateCurrency(&Item{ + FullName: "Bitcoin", + Symbol: "BTC", + ID: 1337, + Role: Cryptocurrency, + }) if err != nil { t.Fatal(err) } aud := main.Register("AUD", Unset) - err = main.UpdateCurrency("Unreal Dollar", "AUD", "", 1111, Fiat) + err = main.UpdateCurrency(&Item{ + FullName: "Unreal Dollar", + Symbol: "AUD", + ID: 1111, + Role: Fiat, + }) if err != nil { t.Fatal(err) } @@ -262,13 +279,23 @@ func TestBaseCode(t *testing.T) { t.Error("Expected fullname to update for AUD") } - err = main.UpdateCurrency("Australian Dollar", "AUD", "", 1336, Fiat) + err = main.UpdateCurrency(&Item{ + FullName: "Australian Dollar", + Symbol: "AUD", + ID: 1336, + Role: Fiat, + }) if err != nil { t.Fatal(err) } aud.Item.Role = Unset - err = main.UpdateCurrency("Australian Dollar", "AUD", "", 1336, Fiat) + err = main.UpdateCurrency(&Item{ + FullName: "Australian Dollar", + Symbol: "AUD", + ID: 1336, + Role: Fiat, + }) if err != nil { t.Fatal(err) } @@ -277,7 +304,13 @@ func TestBaseCode(t *testing.T) { } main.Register("PPT", Unset) - err = main.UpdateCurrency("Populous", "PPT", "ETH", 1335, Token) + err = main.UpdateCurrency(&Item{ + FullName: "Populous", + Symbol: "PPT", + AssocChain: "ETH", + ID: 1335, + Role: Token, + }) if err != nil { t.Fatal(err) } @@ -377,35 +410,48 @@ func TestBaseCode(t *testing.T) { t.Error("Expected 'Role undefined'") } - main.Items[0].FullName = "Hello" - err = main.UpdateCurrency("MEWOW", "CATS", "", 1338, Fiat) + main.Items["CATS"][0].FullName = "Hello" + err = main.UpdateCurrency(&Item{ + FullName: "MEWOW", + Symbol: "CATS", + ID: 1338, + Role: Fiat, + }) if err != nil { t.Fatal(err) } - if main.Items[0].FullName != "MEWOW" { + if main.Items["CATS"][0].FullName != "MEWOW" { t.Error("Fullname not updated") } - err = main.UpdateCurrency("MEWOW", "CATS", "", 1338, Fiat) - if err != nil { - t.Fatal(err) - } - err = main.UpdateCurrency("WOWCATS", "CATS", "", 3, Fiat) + + err = main.UpdateCurrency(&Item{ + FullName: "WOWCATS", + Symbol: "CATS", + ID: 3, + Role: Fiat, + }) if err != nil { t.Fatal(err) } // Creates a new item under a different currency role - if main.Items[0].ID != 3 { + if main.Items["CATS"][0].ID != 3 { t.Error("ID not updated") } - main.Items[0].Role = Unset - err = main.UpdateCurrency("MEWOW", "CATS", "", 1338, Cryptocurrency) + main.Items["CATS"][0].Role = Unset + + err = main.UpdateCurrency(&Item{ + FullName: "MEWOW", + Symbol: "CATS", + ID: 1338, + Role: Cryptocurrency, + }) if err != nil { t.Fatal(err) } - if main.Items[0].ID != 1338 { + if main.Items["CATS"][0].ID != 1338 { t.Error("ID not updated") } } @@ -507,29 +553,22 @@ func TestCodeMarshalJSON(t *testing.T) { func TestIsFiatCurrency(t *testing.T) { if EMPTYCODE.IsFiatCurrency() { - t.Errorf("TestIsFiatCurrency cannot match currency, %s.", - EMPTYCODE) + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", EMPTYCODE) } if !USD.IsFiatCurrency() { - t.Errorf( - "TestIsFiatCurrency cannot match currency, %s.", USD) + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", USD) } if !CNY.IsFiatCurrency() { - t.Errorf( - "TestIsFiatCurrency cannot match currency, %s.", CNY) + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", CNY) } if LINO.IsFiatCurrency() { - t.Errorf( - "TestIsFiatCurrency cannot match currency, %s.", LINO, - ) + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", LINO) } if USDT.IsFiatCurrency() { - t.Errorf( - "TestIsFiatCurrency cannot match currency, %s.", USD) + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", USDT) } if DAI.IsFiatCurrency() { - t.Errorf( - "TestIsFiatCurrency cannot match currency, %s.", USD) + t.Errorf("TestIsFiatCurrency cannot match currency, %s.", DAI) } } @@ -595,3 +634,13 @@ func TestItemString(t *testing.T) { &newItem) } } + +// 28848025 40.84 ns/op 8 B/op 1 allocs/op // Current +// +// 546290 2192 ns/op 8 B/op 1 allocs/op // Previous +func BenchmarkNewCode(b *testing.B) { + b.ReportAllocs() + for x := 0; x < b.N; x++ { + _ = NewCode("someCode") + } +} diff --git a/currency/code_types.go b/currency/code_types.go index ebbf436e..21a03ca8 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -28,7 +28,7 @@ type Role uint8 // BaseCodes defines a basket of bare currency codes type BaseCodes struct { - Items []*Item + Items map[string][]*Item LastMainUpdate time.Time mtx sync.Mutex } diff --git a/currency/storage.go b/currency/storage.go index 9df9c982..e43d7b35 100644 --- a/currency/storage.go +++ b/currency/storage.go @@ -228,7 +228,14 @@ func (s *Storage) SetupConversionRates() { // to the running list func (s *Storage) SetDefaultFiatCurrencies(c Currencies) error { for i := range c { - err := s.currencyCodes.UpdateCurrency("", c[i].String(), "", 0, Fiat) + err := s.currencyCodes.UpdateCurrency(&Item{ + ID: c[i].Item.ID, + FullName: c[i].Item.FullName, + Symbol: c[i].Item.Symbol, + Lower: c[i].Item.Lower, + Role: Fiat, + AssocChain: c[i].Item.AssocChain, + }) if err != nil { return err } @@ -242,7 +249,14 @@ func (s *Storage) SetDefaultFiatCurrencies(c Currencies) error { // list func (s *Storage) SetStableCoins(c Currencies) error { for i := range c { - err := s.currencyCodes.UpdateCurrency("", c[i].String(), "", 0, Stable) + err := s.currencyCodes.UpdateCurrency(&Item{ + ID: c[i].Item.ID, + FullName: c[i].Item.FullName, + Symbol: c[i].Item.Symbol, + Lower: c[i].Item.Lower, + Role: Stable, + AssocChain: c[i].Item.AssocChain, + }) if err != nil { return err } @@ -255,11 +269,14 @@ func (s *Storage) SetStableCoins(c Currencies) error { // it to the running list func (s *Storage) SetDefaultCryptocurrencies(c Currencies) error { for i := range c { - err := s.currencyCodes.UpdateCurrency("", - c[i].String(), - "", - 0, - Cryptocurrency) + err := s.currencyCodes.UpdateCurrency(&Item{ + ID: c[i].Item.ID, + FullName: c[i].Item.FullName, + Symbol: c[i].Item.Symbol, + Lower: c[i].Item.Lower, + Role: Cryptocurrency, + AssocChain: c[i].Item.AssocChain, + }) if err != nil { return err } @@ -464,33 +481,29 @@ func (s *Storage) LoadFileCurrencyData(f *File) error { // UpdateCurrencies updates currency role and information using coin market cap func (s *Storage) UpdateCurrencies() error { - m, err := s.currencyAnalysis.GetCryptocurrencyIDMap() + currencyUpdates, err := s.currencyAnalysis.GetCryptocurrencyIDMap() if err != nil { return err } - for x := range m { - if m[x].IsActive != 1 { + for x := range currencyUpdates { + if currencyUpdates[x].IsActive != 1 { continue } - if m[x].Platform.Symbol != "" { - err = s.currencyCodes.UpdateCurrency(m[x].Name, - m[x].Symbol, - m[x].Platform.Symbol, - m[x].ID, - Token) - if err != nil { - return err - } - continue + update := &Item{ + FullName: currencyUpdates[x].Name, + Symbol: currencyUpdates[x].Symbol, + AssocChain: currencyUpdates[x].Platform.Symbol, + ID: currencyUpdates[x].ID, + Role: Cryptocurrency, } - err = s.currencyCodes.UpdateCurrency(m[x].Name, - m[x].Symbol, - "", - m[x].ID, - Cryptocurrency) + if currencyUpdates[x].Platform.Symbol != "" { + update.Role = Token + } + + err = s.currencyCodes.UpdateCurrency(update) if err != nil { return err }