Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

doc: improv doc around binary decoding of numeric data #2331

Merged
merged 1 commit into from Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
35 changes: 34 additions & 1 deletion pgjdbc/src/main/java/org/postgresql/util/ByteConverter.java
Expand Up @@ -184,6 +184,8 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
if (weight < 0) {
assert scale > 0;
int effectiveScale = scale;
//adjust weight to determine how many leading 0s after the decimal
//before the provided values/digits actually begin
++weight;
if (weight < 0) {
effectiveScale += (4 * weight);
Expand All @@ -193,26 +195,34 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
//typically there should not be leading 0 short values, as it is more
//efficient to represent that in the weight value
for ( ; i < len && d == 0; ++i) {
//each leading 0 value removes 4 from the effective scale
effectiveScale -= 4;
idx += 2;
d = ByteConverter.int2(bytes, idx);
}

BigInteger unscaledBI = null;
assert effectiveScale > 0;
if (effectiveScale >= 4) {
effectiveScale -= 4;
} else {
//an effective scale of less than four means that the value d
//has trailing 0s which are not significant
//so we divide by the appropriate power of 10 to reduce those
d = (short) (d / INT_TEN_POWERS[4 - effectiveScale]);
effectiveScale = 0;
}
//defer moving to BigInteger as long as possible
//operations on the long are much faster
BigInteger unscaledBI = null;
long unscaledInt = d;
for ( ; i < len; ++i) {
if (i == 4 && effectiveScale > 2) {
unscaledBI = BigInteger.valueOf(unscaledInt);
}
idx += 2;
d = ByteConverter.int2(bytes, idx);
//if effective scale is at least 4, then all 4 digits should be used
//and the existing number needs to be shifted 4
if (effectiveScale >= 4) {
if (unscaledBI == null) {
unscaledInt *= 10000;
Expand All @@ -221,11 +231,14 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
}
effectiveScale -= 4;
} else {
//if effective scale is less than 4, then only shift left based on remaining scale
if (unscaledBI == null) {
unscaledInt *= INT_TEN_POWERS[effectiveScale];
} else {
unscaledBI = unscaledBI.multiply(tenPower(effectiveScale));
}
//and d needs to be shifted to the right to only get correct number of
//significant digits
d = (short) (d / INT_TEN_POWERS[4 - effectiveScale]);
effectiveScale = 0;
}
Expand All @@ -237,9 +250,11 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
}
}
}
//now we need BigInteger to create BigDecimal
if (unscaledBI == null) {
unscaledBI = BigInteger.valueOf(unscaledInt);
}
//if there is remaining effective scale, apply it here
if (effectiveScale > 0) {
unscaledBI = unscaledBI.multiply(tenPower(effectiveScale));
}
Expand All @@ -252,8 +267,11 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {

//if there is no scale, then shorts are the unscaled int
if (scale == 0) {
//defer moving to BigInteger as long as possible
//operations on the long are much faster
BigInteger unscaledBI = null;
long unscaledInt = d;
//loop over all of the len shorts to process as the unscaled int
for (int i = 1; i < len; ++i) {
if (i == 4) {
unscaledBI = BigInteger.valueOf(unscaledInt);
Expand All @@ -270,21 +288,27 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
}
}
}
//now we need BigInteger to create BigDecimal
if (unscaledBI == null) {
unscaledBI = BigInteger.valueOf(unscaledInt);
}
if (sign == NUMERIC_NEG) {
unscaledBI = unscaledBI.negate();
}
//the difference between len and weight (adjusted from 0 based) becomes the scale for BigDecimal
final int bigDecScale = (len - (weight + 1)) * 4;
//string representation always results in a BigDecimal with scale of 0
//the binary representation, where weight and len can infer trailing 0s, can result in a negative scale
//to produce a consistent BigDecimal, we return the equivalent object with scale set to 0
return bigDecScale == 0 ? new BigDecimal(unscaledBI) : new BigDecimal(unscaledBI, bigDecScale).setScale(0);
}

//defer moving to BigInteger as long as possible
//operations on the long are much faster
BigInteger unscaledBI = null;
long unscaledInt = d;
//weight and scale as defined by postgresql are a bit different than how BigDecimal treats scale
//maintain the effective values to massage as we process through values
int effectiveWeight = weight;
int effectiveScale = scale;
for (int i = 1 ; i < len; ++i) {
Expand All @@ -293,6 +317,7 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
}
idx += 2;
d = ByteConverter.int2(bytes, idx);
//first process effective weight down to 0
if (effectiveWeight > 0) {
--effectiveWeight;
if (unscaledBI == null) {
Expand All @@ -301,18 +326,23 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
unscaledBI = unscaledBI.multiply(BI_TEN_THOUSAND);
}
} else if (effectiveScale >= 4) {
//if effective scale is at least 4, then all 4 digits should be used
//and the existing number needs to be shifted 4
effectiveScale -= 4;
if (unscaledBI == null) {
unscaledInt *= 10000;
} else {
unscaledBI = unscaledBI.multiply(BI_TEN_THOUSAND);
}
} else {
//if effective scale is less than 4, then only shift left based on remaining scale
if (unscaledBI == null) {
unscaledInt *= INT_TEN_POWERS[effectiveScale];
} else {
unscaledBI = unscaledBI.multiply(tenPower(effectiveScale));
}
//and d needs to be shifted to the right to only get correct number of
//significant digits
d = (short) (d / INT_TEN_POWERS[4 - effectiveScale]);
effectiveScale = 0;
}
Expand All @@ -325,12 +355,15 @@ public static Number numeric(byte [] bytes, int pos, int numBytes) {
}
}

//now we need BigInteger to create BigDecimal
if (unscaledBI == null) {
unscaledBI = BigInteger.valueOf(unscaledInt);
}
//if there is remaining weight, apply it here
if (effectiveWeight > 0) {
unscaledBI = unscaledBI.multiply(tenPower(effectiveWeight * 4));
}
//if there is remaining effective scale, apply it here
if (effectiveScale > 0) {
unscaledBI = unscaledBI.multiply(tenPower(effectiveScale));
}
Expand Down
Expand Up @@ -30,6 +30,12 @@ public class BigDecimalByteConverterTest {
@Parameterized.Parameters(name = "number = {0,number,#,###.##################################################}")
public static Iterable<Object[]> data() {
final Collection<Object[]> numbers = new ArrayList<Object[]>();
numbers.add(new Object[] {new BigDecimal("0.1")});
numbers.add(new Object[] {new BigDecimal("0.10")});
numbers.add(new Object[] {new BigDecimal("0.01")});
numbers.add(new Object[] {new BigDecimal("0.001")});
numbers.add(new Object[] {new BigDecimal("0.0001")});
numbers.add(new Object[] {new BigDecimal("0.00001")});
numbers.add(new Object[] {new BigDecimal("1.0")});
numbers.add(new Object[] {new BigDecimal("0.000000000000000000000000000000000000000000000000000")});
numbers.add(new Object[] {new BigDecimal("0.100000000000000000000000000000000000000000000009900")});
Expand Down