diff --git a/currency/code.go b/currency/code.go index b14cb99c..f2717f37 100644 --- a/currency/code.go +++ b/currency/code.go @@ -186,14 +186,7 @@ func (b *BaseCodes) Register(c string, newRole Role) Code { return EMPTYCODE } - var format bool - // Digits fool upper and lower casing. So find first letter and check case. - for x := range c { - if unicode.IsLetter(rune(c[x])) { - format = unicode.IsUpper(rune(c[x])) - break - } - } + isUpperCase := strings.ContainsFunc(c, func(r rune) bool { return unicode.IsLetter(r) && unicode.IsUpper(r) }) // Force upper string storage and matching c = strings.ToUpper(c) @@ -218,13 +211,13 @@ func (b *BaseCodes) Register(c string, newRole Role) Code { } stored[x].Role = newRole } - return Code{Item: stored[x], UpperCase: format} + return Code{Item: stored[x], upperCase: isUpperCase} } } newItem := &Item{Symbol: c, Lower: strings.ToLower(c), Role: newRole} b.Items[c] = append(b.Items[c], newItem) - return Code{Item: newItem, UpperCase: format} + return Code{Item: newItem, upperCase: isUpperCase} } // LoadItem sets item data @@ -275,21 +268,29 @@ func (c Code) String() string { if c.Item == nil { return "" } - if c.UpperCase { + if c.upperCase { return c.Item.Symbol } return c.Item.Lower } -// Lower converts the code to lowercase formatting +// Lower flags the Code to use LowerCase formatting, but does not change Symbol +// If Code cannot be lowercased then it will return Code unchanged func (c Code) Lower() Code { - c.UpperCase = false + if c.Item == nil { + return c + } + c.upperCase = false return c } -// Upper converts the code to uppercase formatting +// Upper flags the Code to use UpperCase formatting, but does not change Symbol +// If Code cannot be uppercased then it will return Code unchanged func (c Code) Upper() Code { - c.UpperCase = true + if c.Item == nil { + return c + } + c.upperCase = true return c } @@ -346,21 +347,3 @@ func (i *Item) Currency() Code { } return NewCode(i.Symbol) } - -// UpperCurrency allows an item to revert to a code -// taking an upper -func (i *Item) UpperCurrency() Code { - if i == nil { - return EMPTYCODE.Upper() - } - return NewCode(i.Symbol).Upper() -} - -// LowerCurrency allows an item to revert to a code -// returning in lower format -func (i *Item) LowerCurrency() Code { - if i == nil { - return EMPTYCODE.Lower() - } - return NewCode(i.Symbol).Lower() -} diff --git a/currency/code_test.go b/currency/code_test.go index d6076d59..d1c9c257 100644 --- a/currency/code_test.go +++ b/currency/code_test.go @@ -4,6 +4,8 @@ import ( "encoding/json" "errors" "testing" + + "github.com/stretchr/testify/require" ) func TestRoleString(t *testing.T) { @@ -456,6 +458,16 @@ func TestBaseCode(t *testing.T) { } } +func TestNewCodeFormatting(t *testing.T) { + require.True(t, NewCode("BTC").upperCase) + require.False(t, NewCode("btc").upperCase) + require.True(t, NewCode("BTC").Equal(NewCode("btc"))) + require.False(t, NewCode("420").upperCase) + require.False(t, NewCode("btc420").upperCase) + require.False(t, NewCode("420").Lower().upperCase) + require.True(t, NewCode("4BTC").upperCase) +} + func TestCodeString(t *testing.T) { if cc, expected := NewCode("TEST"), "TEST"; cc.String() != expected { t.Errorf("Currency Code String() error expected %s but received %s", diff --git a/currency/code_types.go b/currency/code_types.go index 4f3b3273..b77ae41a 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -40,7 +40,7 @@ type Code struct { // TODO: Below will force the use of the Equal method for comparison. Big // job to update all maps and instances through the code base. // _ []struct{} - UpperCase bool + upperCase bool } // Item defines a sub type containing the main attributes of a designated diff --git a/currency/manager.go b/currency/manager.go index 8486aa78..0c8f71d8 100644 --- a/currency/manager.go +++ b/currency/manager.go @@ -497,7 +497,7 @@ func (p *PairsManager) SetDelimitersFromConfig() error { } for i, p := range []Pairs{s.Enabled, s.Available} { for j := range p { - if p[j].Delimiter == cf.Delimiter { + if cf.Delimiter == "" || p[j].Delimiter == cf.Delimiter { continue } nP, err := NewPairDelimiter(p[j].String(), cf.Delimiter) diff --git a/currency/manager_test.go b/currency/manager_test.go index a7064846..562b9062 100644 --- a/currency/manager_test.go +++ b/currency/manager_test.go @@ -842,7 +842,7 @@ func TestPairManagerSetDelimitersFromConfig(t *testing.T) { err = json.Unmarshal([]byte(`{"pairs":{"spot":{"configFormat":{"delimiter":"_"},"enabled":"BTC-USDT","available":"BTC-USDT"}}}`), p) if assert.NoError(t, err, "UnmarshalJSON should not error") { err := p.SetDelimitersFromConfig() - assert.ErrorContains(t, err, "spot.enabled.BTC-USDT: delimiter: [_] not found in currencypair string", "SetDelimitersFromConfig should error correctly") + assert.ErrorIs(t, err, errDelimiterNotFound, "SetDelimitersFromConfig should error correctly") } } diff --git a/currency/pair.go b/currency/pair.go index 6645a6bc..1a396a6d 100644 --- a/currency/pair.go +++ b/currency/pair.go @@ -7,7 +7,11 @@ import ( "unicode" ) -var errCannotCreatePair = errors.New("cannot create currency pair") +var ( + errCannotCreatePair = errors.New("cannot create currency pair") + errDelimiterNotFound = errors.New("delimiter not found") + errDelimiterCannotBeEmpty = errors.New("delimiter cannot be empty") +) // NewBTCUSDT is a shortcut for NewPair(BTC, USDT) func NewBTCUSDT() Pair { @@ -21,25 +25,18 @@ func NewBTCUSD() Pair { // NewPairDelimiter splits the desired currency string at delimiter, then returns a Pair struct func NewPairDelimiter(currencyPair, delimiter string) (Pair, error) { - if !strings.Contains(currencyPair, delimiter) { + if currencyPair == "" { + return EMPTYPAIR, errEmptyPairString + } + if delimiter == "" { + return EMPTYPAIR, errDelimiterCannotBeEmpty + } + index := strings.Index(currencyPair, delimiter) + if index == -1 { return EMPTYPAIR, - fmt.Errorf("delimiter: [%s] not found in currencypair string", delimiter) + fmt.Errorf("supplied pair: [%s] %s %w", currencyPair, delimiter, errDelimiterNotFound) } - result := strings.Split(currencyPair, delimiter) - if len(result) < 2 { - return EMPTYPAIR, - fmt.Errorf("supplied pair: [%s] cannot be split with %s", - currencyPair, - delimiter) - } - if len(result) > 2 { - result[1] = strings.Join(result[1:], delimiter) - } - return Pair{ - Delimiter: delimiter, - Base: NewCode(result[0]), - Quote: NewCode(result[1]), - }, nil + return Pair{Delimiter: delimiter, Base: NewCode(currencyPair[:index]), Quote: NewCode(currencyPair[index+1:])}, nil } // NewPairFromStrings returns a CurrencyPair without a delimiter @@ -61,19 +58,12 @@ func NewPairFromStrings(base, quote string) (Pair, error) { // NewPair returns a currency pair from currency codes func NewPair(baseCurrency, quoteCurrency Code) Pair { - return Pair{ - Base: baseCurrency, - Quote: quoteCurrency, - } + return Pair{Base: baseCurrency, Quote: quoteCurrency} } // NewPairWithDelimiter returns a CurrencyPair with a delimiter func NewPairWithDelimiter(base, quote, delimiter string) Pair { - return Pair{ - Base: NewCode(base), - Quote: NewCode(quote), - Delimiter: delimiter, - } + return Pair{Base: NewCode(base), Quote: NewCode(quote), Delimiter: delimiter} } // NewPairFromString converts currency string into a new CurrencyPair @@ -150,11 +140,21 @@ func MatchPairsWithNoDelimiter(currencyPair string, pairs Pairs, pairFmt PairFor // GetFormatting returns the formatting style of a pair func (p Pair) GetFormatting() (PairFormat, error) { - if p.Base.UpperCase != p.Quote.UpperCase { - return PairFormat{}, fmt.Errorf("%w casing mismatch", errPairFormattingInconsistent) + if p.Base.isCaseSensitive() && p.Quote.isCaseSensitive() && (p.Base.upperCase != p.Quote.upperCase) { + return EMPTYFORMAT, fmt.Errorf("%w casing mismatch", errPairFormattingInconsistent) } - return PairFormat{ - Uppercase: p.Base.UpperCase, - Delimiter: p.Delimiter, - }, nil + return PairFormat{Uppercase: p.Base.upperCase || p.Quote.upperCase, Delimiter: p.Delimiter}, nil +} + +func (p Pair) hasFormatDifference(pairFmt PairFormat) bool { + return p.Delimiter != pairFmt.Delimiter || + (p.Base.isCaseSensitive() && p.Base.upperCase != pairFmt.Uppercase) || + (p.Quote.isCaseSensitive() && p.Quote.upperCase != pairFmt.Uppercase) +} + +func (c Code) isCaseSensitive() bool { + if c.Item == nil { + return false + } + return c.Item.Symbol != c.Item.Lower } diff --git a/currency/pair_test.go b/currency/pair_test.go index b013f29e..5b19aee5 100644 --- a/currency/pair_test.go +++ b/currency/pair_test.go @@ -349,66 +349,29 @@ func TestNewPairWithDelimiter(t *testing.T) { func TestNewPairDelimiter(t *testing.T) { t.Parallel() _, err := NewPairDelimiter("", "") - if err == nil { - t.Fatal("error cannot be nil") - } + require.ErrorIs(t, err, errEmptyPairString) + + _, err = NewPairDelimiter("BTC_USD", "") + require.ErrorIs(t, err, errDelimiterCannotBeEmpty) + _, err = NewPairDelimiter("BTC_USD", "wow") - if err == nil { - t.Fatal("error cannot be nil") - } + require.ErrorIs(t, err, errDelimiterNotFound) _, err = NewPairDelimiter("BTC_USD", " ") - if err == nil { - t.Fatal("error cannot be nil") - } + require.ErrorIs(t, err, errDelimiterNotFound) pair, err := NewPairDelimiter(defaultPairWDelimiter, "-") - if err != nil { - t.Fatal(err) - } - actual := pair.String() - expected := defaultPairWDelimiter - if actual != expected { - t.Errorf( - "Pair(): %s was not equal to expected value: %s", - actual, expected, - ) - } - - actual = pair.Delimiter - expected = "-" - if actual != expected { - t.Errorf( - "Delmiter: %s was not equal to expected value: %s", - actual, expected, - ) - } + require.NoError(t, err) + assert.Equal(t, defaultPairWDelimiter, pair.String()) + assert.Equal(t, "-", pair.Delimiter) pair, err = NewPairDelimiter("BTC-MOVE-0626", "-") - if err != nil { - t.Fatal(err) - } - actual = pair.String() - expected = "BTC-MOVE-0626" - if actual != expected { - t.Errorf( - "Pair(): %s was not equal to expected value: %s", - actual, expected, - ) - } + require.NoError(t, err) + assert.Equal(t, "BTC-MOVE-0626", pair.String()) - pair, err = NewPairDelimiter("fBTC-USDT", "-") - if err != nil { - t.Fatal(err) - } - actual = pair.String() - expected = "fbtc-USDT" - if actual != expected { - t.Errorf( - "Pair(): %s was not equal to expected value: %s", - actual, expected, - ) - } + pair, err = NewPairDelimiter("sETH-USDT", "-") + require.NoError(t, err) + assert.Equal(t, "SETH-USDT", pair.String(), "If any upper case is found in set this forces the pair to be uppercase") } func TestNewPairFromString(t *testing.T) { @@ -564,89 +527,6 @@ func TestCopyPairFormat(t *testing.T) { } } -func TestFindPairDifferences(t *testing.T) { - pairList, err := NewPairsFromStrings([]string{defaultPairWDelimiter, "ETH-USD", "LTC-USD"}) - if err != nil { - t.Fatal(err) - } - - dash, err := NewPairsFromStrings([]string{"DASH-USD"}) - if err != nil { - t.Fatal(err) - } - - // Test new pair update - diff, err := pairList.FindDifferences(dash, PairFormat{Delimiter: DashDelimiter, Uppercase: true}) - if err != nil { - t.Fatal(err) - } - if len(diff.New) != 1 && len(diff.Remove) != 3 && diff.FormatDifference { - t.Error("TestFindPairDifferences: Unexpected values") - } - - diff, err = pairList.FindDifferences(Pairs{}, EMPTYFORMAT) - if err != nil { - t.Fatal(err) - } - if len(diff.New) != 0 && len(diff.Remove) != 3 && !diff.FormatDifference { - t.Error("TestFindPairDifferences: Unexpected values") - } - - diff, err = Pairs{}.FindDifferences(pairList, EMPTYFORMAT) - if err != nil { - t.Fatal(err) - } - if len(diff.New) != 3 && len(diff.Remove) != 0 && diff.FormatDifference { - t.Error("TestFindPairDifferences: Unexpected values") - } - - // Test that the supplied pair lists are the same, so - // no newPairs or removedPairs - diff, err = pairList.FindDifferences(pairList, PairFormat{Delimiter: DashDelimiter, Uppercase: true}) - if err != nil { - t.Fatal(err) - } - if len(diff.New) != 0 && len(diff.Remove) != 0 && !diff.FormatDifference { - t.Error("TestFindPairDifferences: Unexpected values") - } - - _, err = pairList.FindDifferences(Pairs{EMPTYPAIR}, EMPTYFORMAT) - if !errors.Is(err, ErrCurrencyPairEmpty) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty) - } - - _, err = Pairs{EMPTYPAIR}.FindDifferences(pairList, EMPTYFORMAT) - if !errors.Is(err, ErrCurrencyPairEmpty) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrCurrencyPairEmpty) - } - - // Test duplication - duplication, err := NewPairsFromStrings([]string{defaultPairWDelimiter, "ETH-USD", "LTC-USD", "ETH-USD"}) - if err != nil { - t.Fatal(err) - } - - _, err = pairList.FindDifferences(duplication, EMPTYFORMAT) - if !errors.Is(err, ErrPairDuplication) { - t.Fatalf("received: '%v' but expected: '%v'", err, ErrPairDuplication) - } - - // This will allow for the removal of the duplicated item to be returned if - // contained in the original list. - diff, err = duplication.FindDifferences(pairList, EMPTYFORMAT) - if !errors.Is(err, nil) { - t.Fatalf("received: '%v' but expected: '%v'", err, nil) - } - - if len(diff.Remove) != 1 { - t.Fatal("expected removal value in pair difference struct") - } - - if !diff.Remove[0].Equal(pairList[1]) { - t.Fatal("unexpected value returned", diff.Remove[0], pairList[1]) - } -} - func TestPairsToStringArray(t *testing.T) { var pairs Pairs pairs = append(pairs, NewPair(BTC, USD)) @@ -987,29 +867,38 @@ func TestIsAssociated(t *testing.T) { func TestPair_GetFormatting(t *testing.T) { t.Parallel() - p := NewPair(BTC, USDT) - pFmt, err := p.GetFormatting() - if err != nil { - t.Error(err) - } - if !pFmt.Uppercase || pFmt.Delimiter != "" { - t.Error("incorrect formatting") - } + pFmt, err := NewPair(BTC, USDT).GetFormatting() + require.NoError(t, err) + assert.True(t, pFmt.Uppercase) + assert.Empty(t, pFmt.Delimiter) - p = NewPairWithDelimiter("eth", "usdt", "/") - pFmt, err = p.GetFormatting() - if err != nil { - t.Error(err) - } - if pFmt.Uppercase || pFmt.Delimiter != "/" { - t.Error("incorrect formatting") - } + pFmt, err = NewPairWithDelimiter("eth", "usdt", "/").GetFormatting() + require.NoError(t, err) + assert.False(t, pFmt.Uppercase) + assert.Equal(t, "/", pFmt.Delimiter) - p = NewPairWithDelimiter("eth", "USDT", "/") - _, err = p.GetFormatting() - if !errors.Is(err, errPairFormattingInconsistent) { - t.Error(err) - } + _, err = NewPairWithDelimiter("eth", "USDT", "/").GetFormatting() + require.ErrorIs(t, err, errPairFormattingInconsistent) + + pFmt, err = EMPTYPAIR.GetFormatting() + require.NoError(t, err) + assert.Equal(t, EMPTYFORMAT, pFmt) + + pFmt, err = NewPairWithDelimiter("eth", "420", "/").GetFormatting() + require.NoError(t, err) + assert.False(t, pFmt.Uppercase) + + pFmt, err = NewPairWithDelimiter("ETH", "420", "/").GetFormatting() + require.NoError(t, err) + assert.True(t, pFmt.Uppercase) + + pFmt, err = NewPairWithDelimiter("420", "eth", "/").GetFormatting() + require.NoError(t, err) + assert.False(t, pFmt.Uppercase) + + pFmt, err = NewPairWithDelimiter("420", "ETH", "/").GetFormatting() + require.NoError(t, err) + assert.True(t, pFmt.Uppercase) } func TestNewBTCUSD(t *testing.T) { diff --git a/currency/pairs.go b/currency/pairs.go index 360bc8e9..cc5e2f6d 100644 --- a/currency/pairs.go +++ b/currency/pairs.go @@ -60,8 +60,8 @@ func (p Pairs) Join() string { func (p Pairs) Format(pairFmt PairFormat) Pairs { pairs := slices.Clone(p) for x := range pairs { - pairs[x].Base.UpperCase = pairFmt.Uppercase - pairs[x].Quote.UpperCase = pairFmt.Uppercase + pairs[x].Base.upperCase = pairFmt.Uppercase + pairs[x].Quote.upperCase = pairFmt.Uppercase pairs[x].Delimiter = pairFmt.Delimiter } return pairs @@ -233,52 +233,57 @@ func (p Pairs) GetMatch(pair Pair) (Pair, error) { return EMPTYPAIR, ErrPairNotFound } +type pairKey struct { + Base *Item + Quote *Item +} + // FindDifferences returns pairs which are new or have been removed func (p Pairs) FindDifferences(incoming Pairs, pairFmt PairFormat) (PairDifference, error) { newPairs := make(Pairs, 0, len(incoming)) - check := make(map[string]bool) + check := make(map[pairKey]bool) + formatDiff := false for x := range incoming { if incoming[x].IsEmpty() { return PairDifference{}, fmt.Errorf("contained in the incoming pairs a %w", ErrCurrencyPairEmpty) } - format := EMPTYFORMAT.Format(incoming[x]) - if check[format] { + + if !formatDiff { + formatDiff = incoming[x].hasFormatDifference(pairFmt) + } + + k := pairKey{Base: incoming[x].Base.Item, Quote: incoming[x].Quote.Item} + if check[k] { return PairDifference{}, fmt.Errorf("contained in the incoming pairs %w", ErrPairDuplication) } - check[format] = true + check[k] = true if !p.Contains(incoming[x], true) { newPairs = append(newPairs, incoming[x]) } } removedPairs := make(Pairs, 0, len(p)) - check = make(map[string]bool) + clear(check) for x := range p { if p[x].IsEmpty() { return PairDifference{}, fmt.Errorf("contained in the existing pairs a %w", ErrCurrencyPairEmpty) } - format := EMPTYFORMAT.Format(p[x]) - if !incoming.Contains(p[x], true) || check[format] { + + if !formatDiff { + formatDiff = p[x].hasFormatDifference(pairFmt) + } + + k := pairKey{Base: p[x].Base.Item, Quote: p[x].Quote.Item} + if !incoming.Contains(p[x], true) || check[k] { removedPairs = append(removedPairs, p[x]) } - check[format] = true + check[k] = true } - return PairDifference{ - New: newPairs, - Remove: removedPairs, - FormatDifference: p.HasFormatDifference(pairFmt), - }, nil + return PairDifference{New: newPairs, Remove: removedPairs, FormatDifference: formatDiff}, nil } // HasFormatDifference checks and validates full formatting across a pairs list func (p Pairs) HasFormatDifference(pairFmt PairFormat) bool { - for x := range p { - if p[x].Delimiter != pairFmt.Delimiter || - (!p[x].Base.IsEmpty() && p[x].Base.UpperCase != pairFmt.Uppercase) || - (!p[x].Quote.IsEmpty() && p[x].Quote.UpperCase != pairFmt.Uppercase) { - return true - } - } - return false + return slices.ContainsFunc(p, func(pair Pair) bool { return pair.hasFormatDifference(pairFmt) }) } // GetRandomPair returns a random pair from a list of pairs @@ -328,10 +333,10 @@ func (p Pairs) GetCrypto() Currencies { m := make(map[*Item]bool) for x := range p { if p[x].Base.IsCryptocurrency() { - m[p[x].Base.Item] = p[x].Base.UpperCase + m[p[x].Base.Item] = p[x].Base.upperCase } if p[x].Quote.IsCryptocurrency() { - m[p[x].Quote.Item] = p[x].Quote.UpperCase + m[p[x].Quote.Item] = p[x].Quote.upperCase } } return currencyConstructor(m) @@ -342,10 +347,10 @@ func (p Pairs) GetFiat() Currencies { m := make(map[*Item]bool) for x := range p { if p[x].Base.IsFiatCurrency() { - m[p[x].Base.Item] = p[x].Base.UpperCase + m[p[x].Base.Item] = p[x].Base.upperCase } if p[x].Quote.IsFiatCurrency() { - m[p[x].Quote.Item] = p[x].Quote.UpperCase + m[p[x].Quote.Item] = p[x].Quote.upperCase } } return currencyConstructor(m) @@ -356,8 +361,8 @@ func (p Pairs) GetFiat() Currencies { func (p Pairs) GetCurrencies() Currencies { m := make(map[*Item]bool) for x := range p { - m[p[x].Base.Item] = p[x].Base.UpperCase - m[p[x].Quote.Item] = p[x].Quote.UpperCase + m[p[x].Base.Item] = p[x].Base.upperCase + m[p[x].Quote.Item] = p[x].Quote.upperCase } return currencyConstructor(m) } @@ -367,10 +372,10 @@ func (p Pairs) GetStables() Currencies { m := make(map[*Item]bool) for x := range p { if p[x].Base.IsStableCurrency() { - m[p[x].Base.Item] = p[x].Base.UpperCase + m[p[x].Base.Item] = p[x].Base.upperCase } if p[x].Quote.IsStableCurrency() { - m[p[x].Quote.Item] = p[x].Quote.UpperCase + m[p[x].Quote.Item] = p[x].Quote.upperCase } } return currencyConstructor(m) @@ -383,7 +388,7 @@ func currencyConstructor(m map[*Item]bool) Currencies { var target int for code, upper := range m { cryptos[target].Item = code - cryptos[target].UpperCase = upper + cryptos[target].upperCase = upper target++ } return cryptos diff --git a/currency/pairs_test.go b/currency/pairs_test.go index 96191748..4341bdbe 100644 --- a/currency/pairs_test.go +++ b/currency/pairs_test.go @@ -715,35 +715,40 @@ func TestValidateAndConform(t *testing.T) { func TestPairs_GetFormatting(t *testing.T) { t.Parallel() - p := Pairs{NewPair(BTC, USDT)} - pFmt, err := p.GetFormatting() - if err != nil { - t.Error(err) - } - if !pFmt.Uppercase || pFmt.Delimiter != "" { - t.Error("incorrect formatting") - } + pFmt, err := Pairs{NewPair(BTC, USDT)}.GetFormatting() + require.NoError(t, err) + assert.True(t, pFmt.Uppercase) + assert.Empty(t, pFmt.Delimiter) - p = Pairs{NewPairWithDelimiter("eth", "usdt", "/")} - pFmt, err = p.GetFormatting() - if err != nil { - t.Error(err) - } - if pFmt.Uppercase || pFmt.Delimiter != "/" { - t.Error("incorrect formatting") - } + pFmt, err = Pairs{NewPairWithDelimiter("eth", "usdt", "/")}.GetFormatting() + require.NoError(t, err) + assert.False(t, pFmt.Uppercase) + assert.Equal(t, "/", pFmt.Delimiter) - p = Pairs{NewPair(BTC, USDT), NewPairWithDelimiter("eth", "usdt", "/")} - _, err = p.GetFormatting() - if !errors.Is(err, errPairFormattingInconsistent) { - t.Error(err) - } + _, err = Pairs{NewPair(BTC, USDT), NewPairWithDelimiter("eth", "usdt", "/")}.GetFormatting() + require.ErrorIs(t, err, errPairFormattingInconsistent) - p = Pairs{NewPairWithDelimiter("eth", "USDT", "/")} - _, err = p.GetFormatting() - if !errors.Is(err, errPairFormattingInconsistent) { - t.Error(err) - } + _, err = Pairs{NewPairWithDelimiter("eth", "USDT", "/")}.GetFormatting() + require.ErrorIs(t, err, errPairFormattingInconsistent) + + _, err = Pairs{NewPairWithDelimiter("eth", "usdt", "/"), NewPairWithDelimiter("eth", "usdt", "/")}.GetFormatting() + require.NoError(t, err) + + _, err = Pairs{NewPairWithDelimiter("eth", "usdt", "/"), NewPairWithDelimiter("eth", "usdt", "|")}.GetFormatting() + require.ErrorIs(t, err, errPairFormattingInconsistent) + + _, err = Pairs{NewPairWithDelimiter("eth", "420", "/"), NewPairWithDelimiter("eth", "420", "/")}.GetFormatting() + require.NoError(t, err) + _, err = Pairs{NewPairWithDelimiter("ETH", "420", "/"), NewPairWithDelimiter("ETH", "420", "/")}.GetFormatting() + require.NoError(t, err) + _, err = Pairs{NewPairWithDelimiter("420", "ETH", "/"), NewPairWithDelimiter("420", "ETH", "/")}.GetFormatting() + require.NoError(t, err) + _, err = Pairs{NewPairWithDelimiter("420", "eth", "/"), NewPairWithDelimiter("420", "eth", "/")}.GetFormatting() + require.NoError(t, err) + _, err = Pairs{NewPairWithDelimiter("420", "eth", "/"), NewPairWithDelimiter("eth", "420", "/")}.GetFormatting() + require.NoError(t, err) + _, err = Pairs{NewPairWithDelimiter("420", "ETH", "/"), NewPairWithDelimiter("ETH", "420", "/")}.GetFormatting() + require.NoError(t, err) } func TestGetPairsByQuote(t *testing.T) { @@ -836,3 +841,83 @@ func TestPairsEqual(t *testing.T) { assert.Equal(t, "USDT-BTC", orig[0].String(), "Equal Pairs should not effect original order or format") assert.False(t, orig.Equal(Pairs{NewPair(DAI, XRP), NewPair(DAI, BTC), NewPair(USD, LTC)}), "UnEqual Pairs should return false") } + +func TestFindPairDifferences(t *testing.T) { + pairList, err := NewPairsFromStrings([]string{defaultPairWDelimiter, "ETH-USD", "LTC-USD"}) + require.NoError(t, err) + + dash, err := NewPairsFromStrings([]string{"DASH-USD"}) + require.NoError(t, err) + + // Test new pair update + diff, err := pairList.FindDifferences(dash, PairFormat{Delimiter: DashDelimiter, Uppercase: true}) + require.NoError(t, err) + assert.Len(t, diff.New, 1) + assert.Len(t, diff.Remove, 3) + + diff, err = pairList.FindDifferences(Pairs{}, EMPTYFORMAT) + require.NoError(t, err) + assert.Empty(t, diff.New) + assert.Len(t, diff.Remove, 3) + assert.True(t, diff.FormatDifference) + + diff, err = Pairs{}.FindDifferences(pairList, EMPTYFORMAT) + require.NoError(t, err) + assert.Len(t, diff.New, 3) + assert.Empty(t, diff.Remove) + assert.True(t, diff.FormatDifference) + + // Test that the supplied pair lists are the same, so + // no newPairs or removedPairs + diff, err = pairList.FindDifferences(pairList, PairFormat{Delimiter: DashDelimiter, Uppercase: true}) + require.NoError(t, err) + assert.Empty(t, diff.New) + assert.Empty(t, diff.Remove) + assert.False(t, diff.FormatDifference) + + _, err = pairList.FindDifferences(Pairs{EMPTYPAIR}, EMPTYFORMAT) + require.ErrorIs(t, err, ErrCurrencyPairEmpty) + + _, err = Pairs{EMPTYPAIR}.FindDifferences(pairList, EMPTYFORMAT) + require.ErrorIs(t, err, ErrCurrencyPairEmpty) + + // Test duplication + duplication, err := NewPairsFromStrings([]string{defaultPairWDelimiter, "ETH-USD", "LTC-USD", "ETH-USD"}) + require.NoError(t, err) + + _, err = pairList.FindDifferences(duplication, EMPTYFORMAT) + require.ErrorIs(t, err, ErrPairDuplication) + + // This will allow for the removal of the duplicated item to be returned if + // contained in the original list. + diff, err = duplication.FindDifferences(pairList, EMPTYFORMAT) + require.NoError(t, err) + require.Len(t, diff.Remove, 1) + require.True(t, diff.Remove[0].Equal(pairList[1])) + + original, err := NewPairsFromStrings([]string{"ETH-USD", "LTC-USD", "ETH-USD"}) + require.NoError(t, err) + + compare, err := NewPairsFromStrings([]string{"ETH-123", "LTC-123", "MEOW-123"}) + require.NoError(t, err) + + diff, err = original.FindDifferences(compare, PairFormat{Delimiter: DashDelimiter, Uppercase: true}) + require.NoError(t, err) + require.False(t, diff.FormatDifference) +} + +// 2208139 509.3 ns/op 288 B/op 2 allocs/op (current) +// +// 1614865 712.5 ns/op 336 B/op 8 allocs/op (prev) +func BenchmarkFindDifferences(b *testing.B) { + original, err := NewPairsFromStrings([]string{"ETH-USD", "LTC-USD", "ETH-USD"}) + require.NoError(b, err) + + compare, err := NewPairsFromStrings([]string{"ETH-123", "LTC-123", "MEOW-123"}) + require.NoError(b, err) + + for i := 0; i < b.N; i++ { + _, err = original.FindDifferences(compare, EMPTYFORMAT) + require.NoError(b, err) + } +} diff --git a/currency/storage.go b/currency/storage.go index 930533f1..84788fa6 100644 --- a/currency/storage.go +++ b/currency/storage.go @@ -55,7 +55,7 @@ func (s *Storage) SetDefaults() { if item == USDT.Item { continue } - fiatCurrencies = append(fiatCurrencies, Code{Item: item, UpperCase: true}) + fiatCurrencies = append(fiatCurrencies, Code{Item: item, upperCase: true}) } err := s.SetDefaultFiatCurrencies(fiatCurrencies) diff --git a/exchanges/account/account.go b/exchanges/account/account.go index db5b5116..d389ddde 100644 --- a/exchanges/account/account.go +++ b/exchanges/account/account.go @@ -112,7 +112,7 @@ func GetHoldings(exch string, creds *Credentials, assetType asset.Item) (Holding } assetHoldings.m.Lock() currencyBalances = append(currencyBalances, Balance{ - Currency: currency.Code{Item: mapKey.Currency, UpperCase: true}, + Currency: mapKey.Currency.Currency().Upper(), Total: assetHoldings.total, Hold: assetHoldings.hold, Free: assetHoldings.free, diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 97026e7f..04cba828 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -1177,9 +1177,14 @@ func (b *Bitfinex) GetHistoricCandlesExtended(ctx context.Context, pair currency } func (b *Bitfinex) fixCasing(in currency.Pair, a asset.Item) (string, error) { - if in.IsEmpty() || in.Base.IsEmpty() { + if in.Base.IsEmpty() { return "", currency.ErrCurrencyPairEmpty } + + // Convert input to lowercase to ensure consistent formatting. + // Required for currencies that start with T or F eg tTNBUSD + in = in.Lower() + var checkString [2]byte if a == asset.Spot || a == asset.Margin { checkString[0] = 't' diff --git a/gctscript/wrappers/gct/gctwrapper_test.go b/gctscript/wrappers/gct/gctwrapper_test.go index bb3c3633..853f3150 100644 --- a/gctscript/wrappers/gct/gctwrapper_test.go +++ b/gctscript/wrappers/gct/gctwrapper_test.go @@ -96,10 +96,10 @@ var ( Value: "error", } currencyPair = &objects.String{ - Value: "BTCUSD", + Value: "BTC-USD", } delimiter = &objects.String{ - Value: "", + Value: "-", } assetType = &objects.String{ Value: "spot",