From fc861276effa5668bcab81c3448e2e154e60b549 Mon Sep 17 00:00:00 2001 From: "Erik C. Thauvin" Date: Thu, 27 May 2021 11:41:43 -0700 Subject: [PATCH] Added wrapper parameter to toJson/toPrice. --- README.md | 59 ++++++++++--- detekt-baseline.xml | 1 - .../net/thauvin/erik/crypto/CryptoPrice.kt | 88 +++++++++++++++---- .../thauvin/erik/crypto/CryptoPriceTest.kt | 55 +++++++----- 4 files changed, 151 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 1203e64..ddbfe16 100644 --- a/README.md +++ b/README.md @@ -35,30 +35,69 @@ spotPrice( ``` Parameters | Description -:---------- |:------------------------------------------------------------- -`base` | The cryptocurrency ticker symbol (`BTC`, `ETH`, `ETH2`, etc.) +:---------- |:------------------------------------------------------------ +`base` | The cryptocurrency ticker symbol (`BTC`, `ETH`, `LTC`, etc.) `currency` | The fiat currency ISO 4217 code. (`USD`, `GBP`, `EUR`, etc.) `date` | The `LocalDate` for historical price data. -A `CryptoPrice` is returned defined as follows: +A `CryptoPrice` object is returned defined as follows: ```kotlin CryptoPrice(val base: String, val currency: String, val amount: BigDecimal) ``` The parameter names match the [Coinbase API](https://developers.coinbase.com/api/v2#get-spot-price). -To display the amount as a fomatted currency use the `toCurrency` function: +#### Format + +To display the amount as a formatted currency, use the `toCurrency` function: ```kotlin -val euro = CryptoPrice("BTC", "EUR", 12345.67.toBigDecimal()) -println(euro.toCurrency()) // will print: €12,345.67 +val euro = CryptoPrice("BTC", "EUR", 23456.78.toBigDecimal()) +println(euro.toCurrency()) // €23,456.78 -val krone = CryptoPrice("BTC", "DKK", 12345.67.toBigDecimal()) -println(krone.toCurrency(Locale("da", "DK"))) // will print: 12.345,67 kr. +val krone = CryptoPrice("BTC", "DKK", 123456.78.toBigDecimal()) +println(krone.toCurrency(Locale("da", "DK"))) // 123.456,78 kr. ``` + +##### JSON + +To convert a `CryptoPrice` object back to JSON, use the `toJson` function: + +```kotlin +val price = CryptoPrice("BTC", "USD", 34567.89.toBigDecimal()) +println(price.toJson()) +``` + +*output:* + +```json +{"data":{"base":"BTC","currency":"USD","amount":"34567.89"}} +``` + +The format matches the [Coinbase API](https://developers.coinbase.com/api/v2#get-spot-price). To specify a different (or no) object wrapper, use: + +```kotlin +println(price.toJson("bitcoin")) +println(price.toJson("")) // or price.toString() +``` + +*output:* + +```json +{"bitcoin":{"base":"BTC","currency":"USD","amount":"34567.89"}} +{"base":"BTC","currency":"USD","amount":"34567.89"} +``` + +Similarly, to create a `CryptoPrice` object from JSON, use the `toPrice` function: + +```kotlin +val btc = """{"data":{"base":"BTC","currency":"USD","amount":"34567.89"}}""".toPrice() +val eth = """{"ether":{"base":"ETH","currency":"USD","amount":"2345.67"}}""".toPrice("ether") +``` + ### Extending -A generic `apiCall()` function is available to access other [API data endpoints](https://developers.coinbase.com/api/v2#data-endpoints). For example to retried the current [buy price](https://developers.coinbase.com/api/v2#get-buy-price) of a cryptocurrency: +A generic `apiCall()` function is available to access other [API data endpoints](https://developers.coinbase.com/api/v2#data-endpoints). For example to retrieve the current [buy price](https://developers.coinbase.com/api/v2#get-buy-price) of a cryptocurrency: ```kotlin apiCall(paths = listOf("prices", "BTC-USD", "buy"), params = emptyMap()) @@ -66,7 +105,7 @@ apiCall(paths = listOf("prices", "BTC-USD", "buy"), params = emptyMap()) will return something like: ```json -{"data":{"base":"BTC","currency":"USD","amount":"58977.17"}} +{"data":{"base":"BTC","currency":"USD","amount":"34554.32"}} ``` See the [examples](https://github.com/ethauvin/cryptoprice/blob/master/examples/) for more details. diff --git a/detekt-baseline.xml b/detekt-baseline.xml index 7a95db9..9c1ce93 100644 --- a/detekt-baseline.xml +++ b/detekt-baseline.xml @@ -3,6 +3,5 @@ ThrowsCount:CryptoPrice.kt$CryptoPrice.Companion$ @JvmStatic @JvmOverloads @Throws(CryptoException::class, IOException::class) fun apiCall(paths: List<String>, params: Map<String, String> = emptyMap()): String - ThrowsCount:CryptoPrice.kt$CryptoPrice.Companion$ @JvmStatic @Throws(CryptoException::class) fun String.toPrice(): CryptoPrice diff --git a/src/main/kotlin/net/thauvin/erik/crypto/CryptoPrice.kt b/src/main/kotlin/net/thauvin/erik/crypto/CryptoPrice.kt index aaabe77..a887c44 100644 --- a/src/main/kotlin/net/thauvin/erik/crypto/CryptoPrice.kt +++ b/src/main/kotlin/net/thauvin/erik/crypto/CryptoPrice.kt @@ -61,18 +61,17 @@ open class CryptoPrice(val base: String, val currency: String, val amount: BigDe /** * Converts JSON data object to [CryptoPrice]. + * + * @param wrapper Specifies the JSON object the price data is wrapped in. */ @JvmStatic + @JvmOverloads @Throws(CryptoException::class) - fun String.toPrice(): CryptoPrice { + fun String.toPrice(wrapper: String = "data"): CryptoPrice { try { - val json = JSONObject(this) - if (json.has("data")) { - with(json.getJSONObject("data")) { - return CryptoPrice(getString("base"), getString("currency"), getString("amount").toBigDecimal()) - } - } else { - throw CryptoException(message = "Missing price data.") + val json = if (wrapper.isNotBlank()) JSONObject(this).getJSONObject(wrapper) else JSONObject(this) + with(json) { + return CryptoPrice(getString("base"), getString("currency"), getString("amount").toBigDecimal()) } } catch (e: NumberFormatException) { throw CryptoException(message = "Could not convert amount to number.", cause = e) @@ -83,6 +82,9 @@ open class CryptoPrice(val base: String, val currency: String, val amount: BigDe /** * Makes an API call. + * + * @param paths The list of path segments for the API URL. For example: `["prices", "BTC-USD", "spot"]`. + * @param params The map of query parameters. */ @JvmStatic @JvmOverloads @@ -147,35 +149,83 @@ open class CryptoPrice(val base: String, val currency: String, val amount: BigDe } /** - * Returns the [amount] as a currency formatted string, such as `$1,203.33`. + * Returns the [amount] as a currency formatted string. + * + * For example: `$1,203.33`. + * + * @param locale The desired locale. + * @param minFractionDigits The minimum number of digits allowed in the fraction portion of the currency. */ @JvmOverloads @Throws(IllegalArgumentException::class) fun toCurrency(locale: Locale = Locale.getDefault(Locale.Category.FORMAT), minFractionDigits: Int = 2): String { return NumberFormat.getCurrencyInstance(locale).let { - it.setCurrency(Currency.getInstance(currency)) - it.setMinimumFractionDigits(minFractionDigits) + it.currency = Currency.getInstance(currency) + it.minimumFractionDigits = minFractionDigits it.format(amount) } } /** - * Returns a JSON representation of the [CryptoPrice]. + * Indicates whether some other object is _equal to_ this one. */ - fun toJson(): String { - return JSONStringer() - .`object`().key("data") - .`object`() + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CryptoPrice + + if (base != other.base) return false + if (currency != other.currency) return false + if (amount != other.amount) return false + + return true + } + + /** + * Returns a hash code value for the object. + */ + override fun hashCode(): Int { + var result = base.hashCode() + result = 31 * result + currency.hashCode() + result = 31 * result + amount.hashCode() + return result + } + + /** + * Returns a JSON representation of the [CryptoPrice]. + * + * For example, with the default `data` wrapper: + * + * ``` + * {"data":{"base":"BTC","currency":"USD","amount":"58977.17"}} + * ``` + * + * @param wrapper Specifies a JSON object to wrap the price data in. + */ + @JvmOverloads + fun toJson(wrapper: String = "data"): String { + val json = JSONStringer() + if (wrapper.isNotBlank()) json.`object`().key(wrapper) + json.`object`() .key("base").value(base) .key("currency").value(currency) .key("amount").value(amount.toString()) .endObject() - .endObject() - .toString() + if (wrapper.isNotBlank()) json.endObject() + return json.toString() } /** * Returns a JSON respresentation of the [CryptoPrice]. + * + * For example: + * + * ``` + * {"base":"BTC","currency":"USD","amount":"58977.17"} + * ``` + * + * @see [toJson] */ - override fun toString(): String = toJson() + override fun toString(): String = toJson("") } diff --git a/src/test/kotlin/net/thauvin/erik/crypto/CryptoPriceTest.kt b/src/test/kotlin/net/thauvin/erik/crypto/CryptoPriceTest.kt index 9f0a777..140934a 100644 --- a/src/test/kotlin/net/thauvin/erik/crypto/CryptoPriceTest.kt +++ b/src/test/kotlin/net/thauvin/erik/crypto/CryptoPriceTest.kt @@ -3,7 +3,6 @@ package net.thauvin.erik.crypto import net.thauvin.erik.crypto.CryptoPrice.Companion.apiCall import net.thauvin.erik.crypto.CryptoPrice.Companion.spotPrice import net.thauvin.erik.crypto.CryptoPrice.Companion.toPrice -import java.math.BigDecimal import java.time.LocalDate import java.util.Locale import kotlin.test.Test @@ -15,11 +14,12 @@ import kotlin.test.assertTrue * [CryptoPrice] Tests */ class CryptoPriceTest { - val jsonData = "{\"data\":{\"base\":\"BTC\",\"currency\":\"USD\",\"amount\":\"%s\"}}" + private val jsonPrice = "{\"base\":\"BTC\",\"currency\":\"USD\",\"amount\":\"%s\"}" + private val jsonData = "{\"data\":$jsonPrice}" @Test @Throws(CryptoException::class) - fun testBTCPrice() { + fun testBitcoinPrice() { val price = spotPrice("BTC") assertEquals("BTC", price.base, "BTC") assertEquals("USD", price.currency, "is USD") @@ -28,7 +28,7 @@ class CryptoPriceTest { @Test @Throws(CryptoException::class) - fun testETHPrice() { + fun testEtherPrice() { val price = spotPrice("ETH", "EUR") assertEquals("ETH", price.base, "ETH") assertEquals("EUR", price.currency, "is EUR") @@ -37,16 +37,16 @@ class CryptoPriceTest { @Test @Throws(CryptoException::class) - fun testETH2Price() { - val price = spotPrice("ETH2", "GBP") - assertEquals("ETH2", price.base, "ETH2") + fun testLitecoinPrice() { + val price = spotPrice("LTC", "GBP") + assertEquals("LTC", price.base, "LTC") assertEquals("GBP", price.currency, "is GBP") - assertTrue(price.amount.signum() > 0, "GBP > 0") + assertTrue(price.amount.signum() > 0, "LTC > 0") } @Test @Throws(CryptoException::class) - fun testBCHPrice() { + fun testBitcoinCashPrice() { val price = spotPrice("BCH", "GBP", LocalDate.now().minusDays(1)) assertEquals("BCH", price.base, "BCH") assertEquals("GBP", price.currency, "is GBP") @@ -89,7 +89,7 @@ class CryptoPriceTest { fun testToCurrency() { val d = 12345.60.toBigDecimal() val usd = CryptoPrice("BTC", "USD", d) - assertEquals("$12,345.60", usd.toCurrency(), "EUR format") + assertEquals("$12,345.60", usd.toCurrency(), "USD format") val eur = CryptoPrice("BTC", "EUR", d) assertEquals("€12,345.60", eur.toCurrency(), "EUR format") @@ -97,10 +97,10 @@ class CryptoPriceTest { val gbp = CryptoPrice("ETH", "GBP", d) assertEquals("£12,345.60", gbp.toCurrency(), "GBP format") - val aud = CryptoPrice("BTC", "AUD", d) + val aud = CryptoPrice("LTC", "AUD", d) assertEquals("A$12,345.60", aud.toCurrency(), "AUD format") - val dk = CryptoPrice("BTC", "DKK", d) + val dk = CryptoPrice("BCH", "DKK", d) assertEquals("12.345,60 kr.", dk.toCurrency(Locale("da", "DK")), "EUR-DKK format") val jp = CryptoPrice("BTC", "JPY", d) @@ -112,11 +112,11 @@ class CryptoPriceTest { @Test fun testToJson() { - listOf("1234.5", "1234.56", "1234.567").forEach { - val json = jsonData.format(it) - with(json.toPrice()) { - assertEquals(json, toJson(), "toJson($it)") - assertEquals(json, toString(), "toString($it)") + listOf("1234.5", "1234.56", "1234.567").forEachIndexed { i, d -> + val data = jsonData.format(d) + with(data.toPrice()) { + assertEquals(data, toJson(), "toJson($d)") + assertEquals(data.replace("data","price$i"), toJson("price$i"), "toJson(price$i)") } } } @@ -126,16 +126,19 @@ class CryptoPriceTest { @Throws(CryptoException::class) fun testToPrice() { val d = "57515.60" - val json = jsonData.format(d) - val price = json.toPrice() + val data = jsonData.format(d) + val price = data.toPrice() assertEquals("BTC", price.base, "base is BTC") assertEquals("USD", price.currency, "currency is USD") assertEquals(d, price.amount.toString(), "amount is $d") + assertEquals(price, price.toString().toPrice(""), "toPrice('')") + assertEquals(price, price.toJson("test").toPrice("test"), "toPrice(test)") + assertFailsWith( - message = "double conversion did not fail", + message = "amount conversion did not fail", exceptionClass = CryptoException::class, - block = { json.replace("5", "a").toPrice() } + block = { data.replace("5", "a").toPrice() } ) assertFailsWith( @@ -147,7 +150,15 @@ class CryptoPriceTest { assertFailsWith( message = "no base did not fail", exceptionClass = CryptoException::class, - block = { json.replace("base", "foo").toPrice() } + block = { data.replace("base", "foo").toPrice() } ) } + + @Test + fun testToString() { + val json = jsonPrice.format("1234.5") + val price = json.toPrice("") + assertEquals(json, price.toString(), "toString()") + assertEquals(price.toString(), price.toJson(""), "toString() = toJson('')") + } }