Navigation Menu

Skip to content

Commit

Permalink
JCBC-1012: Add support for BigInteger and BigDecimal
Browse files Browse the repository at this point in the history
Motivation
----------
It has been reported several times (on the forums and through customer
interactions) that BigDecimal and BigInteger support through the JsonObject
and JsonArray types would be appreciated.

Modifications
-------------
This changeset enables this functionality and adds test cases. Both JsonObject
and JsonArray now have more getters and setters for these types, but there
is one gotcha. By default jackson will read the BigDecimal as a double, which
is the type returned. For very long numbers there might be precision loss.

To combat this, the system property com.couchbase.json.decimalForFloat has
been introduced which, when set to true, will always read doubles as BigDecimal
on the jackson side and therefore trade speed for accuracy. Separate tests
have been added to assert this on a unit-test level.

Result
------
It is now possible to use BigInteger and BigDecimal types the same way as
one would do with other already supported types.

Change-Id: I357a9533692d705510018228fcadeec760f811f5
Reviewed-on: http://review.couchbase.org/71435
Tested-by: Michael Nitschinger <michael@nitschinger.at>
Reviewed-by: Subhashni Balakrishnan <b.subhashni@gmail.com>
Reviewed-by: Sergey Avseyev <sergey.avseyev@gmail.com>
  • Loading branch information
daschl committed Jan 2, 2017
1 parent a2ebbc9 commit ea58266
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 6 deletions.
40 changes: 40 additions & 0 deletions src/integration/java/com/couchbase/client/java/KeyValueTest.java
Expand Up @@ -32,6 +32,8 @@
import org.junit.Test;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
Expand Down Expand Up @@ -572,4 +574,42 @@ public void shouldStoreAndGetKeyWithUtf16Char() {
assertEquals(inserted.content(), retrieved.content());
}

@Test
public void shouldStoreAndGetBigInteger() {
BigInteger bigint = new BigInteger("12345678901234567890432423432324");
JsonObject original = JsonObject
.create()
.put("value", bigint);

bucket().upsert(JsonDocument.create("bigIntegerDoc", original));

RawJsonDocument rawLoaded = bucket().get("bigIntegerDoc", RawJsonDocument.class);
assertEquals("{\"value\":12345678901234567890432423432324}", rawLoaded.content());

JsonDocument loaded = bucket().get("bigIntegerDoc");
assertEquals(bigint, loaded.content().getBigInteger("value"));
}

@Test
public void shouldStoreAndGetBigDecimal() {
BigDecimal bigdec = new BigDecimal("12345.678901234567890432423432324");
JsonObject original = JsonObject
.create()
.put("value", bigdec);

bucket().upsert(JsonDocument.create("bigDecimalDoc", original));

RawJsonDocument rawLoaded = bucket().get("bigDecimalDoc", RawJsonDocument.class);
assertEquals("{\"value\":12345.678901234567890432423432324}", rawLoaded.content());

// Precision loss through double storage, use com.couchbase.json.decimalForFloat for higher
// precision.
JsonDocument loaded = bucket().get("bigDecimalDoc");
assertEquals(
new BigDecimal("12345.678901234567092615179717540740966796875"),
loaded.content().getBigDecimal("value")
);
assertEquals(12345.678901234567, loaded.content().getDouble("value"), 0);
}

}
Expand Up @@ -19,6 +19,8 @@
import com.couchbase.client.java.transcoder.JacksonTransformers;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
Expand Down Expand Up @@ -421,6 +423,17 @@ public JsonArray add(List<?> value) {
return add(JsonArray.from(value));
}

/**
* Append a {@link Number} element to the {@link JsonArray}.
*
* @param value the value to append.
* @return the {@link JsonArray}.
*/
public JsonArray add(Number value) {
content.add(value);
return this;
}

/**
* Retrieves the value by the position in the {@link JsonArray} and casts it to {@link JsonArray}.
*
Expand All @@ -432,6 +445,45 @@ public JsonArray getArray(int index) {
return (JsonArray) content.get(index);
}

/**
* Retrieves the value by the position in the {@link JsonArray} and casts it to {@link BigInteger}.
*
* @param index the index of the value.
* @return the value at the given index.
* @throws IndexOutOfBoundsException if the index is negative or too large.
*/
public BigInteger getBigInteger(int index) {
return (BigInteger) content.get(index);
}

/**
* Retrieves the value by the position in the {@link JsonArray} and casts it to {@link BigDecimal}.
*
* @param index the index of the value.
* @return the value at the given index.
* @throws IndexOutOfBoundsException if the index is negative or too large.
*/
public BigDecimal getBigDecimal(int index) {
Object found = content.get(index);
if (found == null) {
return null;
} else if (found instanceof Double) {
return new BigDecimal((Double) found);
}
return (BigDecimal) found;
}

/**
* Retrieves the value by the position in the {@link JsonArray} and casts it to {@link Number}.
*
* @param index the index of the value.
* @return the value at the given index.
* @throws IndexOutOfBoundsException if the index is negative or too large.
*/
public Number getNumber(int index) {
return (Number) content.get(index);
}

/**
* Copies the content of the {@link JsonArray} into a new {@link List} and returns it.
* Note that if the array contains sub-{@link JsonObject} or {@link JsonArray}, they
Expand Down
Expand Up @@ -19,7 +19,10 @@
import com.couchbase.client.java.CouchbaseAsyncBucket;
import com.couchbase.client.java.transcoder.JacksonTransformers;

import java.io.ObjectInput;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -389,6 +392,18 @@ public JsonObject put(String name, JsonArray value) {
return this;
}

/**
* Stores a {@link Number} value identified by the field name.
*
* @param name the name of the JSON field.
* @param value the value of the JSON field.
* @return the {@link JsonObject}.
*/
public JsonObject put(String name, Number value) {
content.put(name, value);
return this;
}

/**
* Stores a {@link JsonArray} value identified by the field name.
*
Expand All @@ -410,6 +425,42 @@ public JsonArray getArray(String name) {
return (JsonArray) content.get(name);
}

/**
* Retrieves the value from the field name and casts it to {@link BigInteger}.
*
* @param name the name of the field.
* @return the result or null if it does not exist.
*/
public BigInteger getBigInteger(String name) {
return (BigInteger) content.get(name);
}

/**
* Retrieves the value from the field name and casts it to {@link BigDecimal}.
*
* @param name the name of the field.
* @return the result or null if it does not exist.
*/
public BigDecimal getBigDecimal(String name) {
Object found = content.get(name);
if (found == null) {
return null;
} else if (found instanceof Double) {
return new BigDecimal((Double) found);
}
return (BigDecimal) found;
}

/**
* Retrieves the value from the field name and casts it to {@link Number}.
*
* @param name the name of the field.
* @return the result or null if it does not exist.
*/
public Number getNumber(String name) {
return (Number) content.get(name);
}

/**
* Store a null value identified by the field's name.
*
Expand Down
Expand Up @@ -15,6 +15,9 @@
*/
package com.couchbase.client.java.document.json;

import java.math.BigDecimal;
import java.math.BigInteger;

/**
* Represents a JSON value (either a {@link JsonObject} or a {@link JsonArray}.
*
Expand Down Expand Up @@ -59,6 +62,8 @@ public static boolean checkType(Object item) {
|| item instanceof Long
|| item instanceof Double
|| item instanceof Boolean
|| item instanceof BigInteger
|| item instanceof BigDecimal
|| item instanceof JsonObject
|| item instanceof JsonArray;
}
Expand Down
Expand Up @@ -22,6 +22,7 @@
import com.couchbase.client.java.document.json.JsonObject;

import java.io.IOException;
import java.math.BigDecimal;

public class JacksonTransformers {

Expand Down Expand Up @@ -56,6 +57,15 @@ public void serialize(JsonArray value, JsonGenerator jgen,
}

static abstract class AbstractJsonValueDeserializer<T> extends JsonDeserializer<T> {

private final boolean decimalForFloat;

public AbstractJsonValueDeserializer() {
decimalForFloat = Boolean.parseBoolean(
System.getProperty("com.couchbase.json.decimalForFloat", "false")
);
}

protected JsonObject decodeObject(final JsonParser parser, final JsonObject target) throws IOException {
JsonToken current = parser.nextToken();
String field = null;
Expand All @@ -76,10 +86,12 @@ protected JsonObject decodeObject(final JsonParser parser, final JsonObject targ
target.put(field, parser.getValueAsString());
break;
case VALUE_NUMBER_INT:
target.put(field, parser.getNumberValue());
break;
case VALUE_NUMBER_FLOAT:
target.put(field, parser.getDoubleValue());
Number numberValue = parser.getNumberValue();
if (numberValue instanceof Double && decimalForFloat) {
numberValue = parser.getDecimalValue();
}
target.put(field, numberValue);
break;
case VALUE_NULL:
target.put(field, (JsonObject) null);
Expand Down Expand Up @@ -111,10 +123,12 @@ protected JsonArray decodeArray(final JsonParser parser, final JsonArray target)
target.add(parser.getValueAsString());
break;
case VALUE_NUMBER_INT:
target.add(parser.getNumberValue());
break;
case VALUE_NUMBER_FLOAT:
target.add(parser.getDoubleValue());
Number numberValue = parser.getNumberValue();
if (numberValue instanceof Double && decimalForFloat) {
numberValue = parser.getDecimalValue();
}
target.add(numberValue);
break;
case VALUE_NULL:
target.add((JsonObject) null);
Expand Down
Expand Up @@ -18,6 +18,8 @@
import com.couchbase.client.java.SerializationHelper;
import org.junit.Test;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -359,4 +361,38 @@ public void shouldSupportSerialization() throws Exception {
JsonArray deserialized = SerializationHelper.deserializeFromBytes(serialized, JsonArray.class);
assertEquals(original, deserialized);
}

@Test
public void shouldSupportBigInteger() throws Exception {
BigInteger bigint = new BigInteger("12345678901234567890");
JsonArray original = JsonArray.from(bigint);


String encoded = original.toString();
assertEquals("[12345678901234567890]", encoded);

JsonArray decoded = JsonArray.fromJson(encoded);
assertEquals(bigint, decoded.getBigInteger(0));
assertTrue(decoded.getNumber(0) instanceof BigInteger);

}

@Test
public void shouldSupportBigDecimalConverted() throws Exception {
BigDecimal bigdec = new BigDecimal("1234.5678901234567890432423432324");
JsonArray original = JsonArray.from(bigdec);


String encoded = original.toString();
assertEquals("[1234.5678901234567890432423432324]", encoded);

JsonArray decoded = JsonArray.fromJson(encoded);
// This happens because of double rounding, set com.couchbase.json.decimalForFloat true for better accuracy
// but more overhead
assertEquals(
new BigDecimal("1234.5678901234568911604583263397216796875"), decoded.getBigDecimal(0)
);
assertEquals(1234.567890123457, decoded.getDouble(0), 0);
assertTrue(decoded.getNumber(0) instanceof Double);
}
}
Expand Up @@ -18,6 +18,8 @@
import com.couchbase.client.java.SerializationHelper;
import org.junit.Test;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -441,4 +443,42 @@ public void shouldSupportSerialization() throws Exception {
assertEquals(original, deserialized);
}

@Test
public void shouldSupportBigInteger() throws Exception {
BigInteger bigint = new BigInteger("12345678901234567890");
JsonObject original = JsonObject
.create()
.put("value", bigint);


String encoded = original.toString();
assertEquals("{\"value\":12345678901234567890}", encoded);

JsonObject decoded = JsonObject.fromJson(encoded);
assertEquals(bigint, decoded.getBigInteger("value"));
assertTrue(decoded.getNumber("value") instanceof BigInteger);
}

@Test
public void shouldSupportBigDecimalConverted() throws Exception {
BigDecimal bigdec = new BigDecimal("1234.5678901234567890432423432324");
JsonObject original = JsonObject
.create()
.put("value", bigdec);


String encoded = original.toString();
assertEquals("{\"value\":1234.5678901234567890432423432324}", encoded);

JsonObject decoded = JsonObject.fromJson(encoded);
// This happens because of double rounding, set com.couchbase.json.decimalForFloat true for better accuracy
// but more overhead
assertEquals(
new BigDecimal("1234.5678901234568911604583263397216796875"),
decoded.getBigDecimal("value")
);
assertEquals(1234.567890123457, decoded.getDouble("value"), 0);
assertTrue(decoded.getNumber("value") instanceof Double);
}

}

0 comments on commit ea58266

Please sign in to comment.