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('')")
+ }
}