Skip to content

Commit

Permalink
Adds support for writing Ion 1.1 decimal binary encoding (#636)
Browse files Browse the repository at this point in the history
  • Loading branch information
popematt authored Nov 14, 2023
1 parent 9b59da2 commit 8441b35
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 1 deletion.
45 changes: 45 additions & 0 deletions src/com/amazon/ion/impl/bin/IonEncoder_1_1.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.amazon.ion.impl.bin;

import com.amazon.ion.Decimal;
import com.amazon.ion.IonType;
import com.amazon.ion.Timestamp;

Expand Down Expand Up @@ -158,6 +159,50 @@ public static int writeFloat(WriteBuffer buffer, final double value) {
}
}

public static int writeDecimalValue(WriteBuffer buffer, final BigDecimal value) {
if (value == null) {
return writeNullValue(buffer, IonType.DECIMAL);
}

int exponent = -value.scale();

if (BigDecimal.ZERO.compareTo(value) == 0 && !Decimal.isNegativeZero(value)) {
if (exponent == 0) {
buffer.writeByte(OpCodes.DECIMAL_ZERO_LENGTH);
return 1;
} else {
// A decimal with a coefficient of +0 is encoded using opcode 6F.
// The opcode is followed by a FlexInt representing the exponent.
buffer.writeByte(OpCodes.POSITIVE_ZERO_DECIMAL);
return 1 + buffer.writeFlexInt(exponent);
}
}

BigInteger coefficient = value.unscaledValue();
int numCoefficientBytes = WriteBuffer.flexIntLength(coefficient);

int numExponentBytes = 0;
if (exponent != 0) {
numExponentBytes = WriteBuffer.fixedIntLength(exponent);
}

int opCodeAndLengthBytes = 1;
if (numExponentBytes + numCoefficientBytes < 15) {
int opCode = OpCodes.DECIMAL_ZERO_LENGTH + numExponentBytes + numCoefficientBytes;
buffer.writeByte((byte) opCode);
} else {
// Decimal values that require more than 14 bytes can be encoded using the variable-length decimal opcode: 0xF6.
buffer.writeByte(OpCodes.VARIABLE_LENGTH_DECIMAL);
opCodeAndLengthBytes += buffer.writeFlexUInt(numExponentBytes + numCoefficientBytes);
}
buffer.writeFlexInt(coefficient);
if (exponent != 0) {
buffer.writeFixedInt(exponent);
}

return opCodeAndLengthBytes + numCoefficientBytes + numExponentBytes;
}

/**
* Writes a Timestamp to the given WriteBuffer using the Ion 1.1 encoding for Ion Timestamps.
* @return the number of bytes written
Expand Down
3 changes: 2 additions & 1 deletion src/com/amazon/ion/impl/bin/OpCodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ private OpCodes() {}

public static final byte DECIMAL_ZERO_LENGTH = 0x60;
// 0x61-0x6E are additional lengths of decimals.
public static final byte NEGATIVE_ZERO_DECIMAL = 0x6F;
public static final byte POSITIVE_ZERO_DECIMAL = 0x6F;

public static final byte TIMESTAMP_YEAR_PRECISION = 0x70;
public static final byte TIMESTAMP_MONTH_PRECISION = 0x71;
Expand All @@ -39,5 +39,6 @@ private OpCodes() {}
public static final byte NULL_TYPED = (byte) 0xEB;

public static final byte VARIABLE_LENGTH_INTEGER = (byte) 0xF5;
public static final byte VARIABLE_LENGTH_DECIMAL = (byte) 0xF6;
public static final byte VARIABLE_LENGTH_TIMESTAMP = (byte) 0xF7;
}
71 changes: 71 additions & 0 deletions test/com/amazon/ion/impl/bin/IonEncoder_1_1Test.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.amazon.ion.impl.bin;

import com.amazon.ion.Decimal;
import com.amazon.ion.IonType;
import com.amazon.ion.Timestamp;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -219,6 +220,61 @@ public void testWriteFloatValueForDouble(double value, String expectedBytes) {
assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeFloat);
}

@ParameterizedTest
@CsvSource({
" 0., 60",
" 0e1, 6F 03",
" 0e63, 6F 7F",
" 0e99, 6F 8E 01",
" 0.0, 6F FF",
" 0.00, 6F FD",
" 0.000, 6F FB",
" 0e-64, 6F 81",
" 0e-99, 6F 76 FE",
" -0., 61 01",
" -0e1, 62 01 01",
" -0e3, 62 01 03",
" -0e127, 62 01 7F",
" -0e199, 63 01 C7 00",
" -0e-1, 62 01 FF",
" -0e-2, 62 01 FE",
" -0e-3, 62 01 FD",
" -0e-127, 62 01 81",
" -0e-199, 63 01 39 FF",
" 0.01, 62 03 FE",
" 0.1, 62 03 FF",
" 1, 61 03",
" 1e1, 62 03 01",
" 1e2, 62 03 02",
" 1e127, 62 03 7F",
" 1e128, 63 03 80 00",
" 1e65536, 64 03 00 00 01",
" 2, 61 05",
" 7, 61 0F",
" 14, 61 1D",
" 1.0, 62 15 FF",
" 1.00, 63 92 01 FE",
" 1.27, 63 FE 01 FE",
" 3.142, 63 1A 31 FD",
" 3.14159, 64 7C 59 26 FB",
" 3.141593, 65 98 FD FE 02 FA",
" 3.141592653, 66 B0 C9 1C 68 17 F7",
" 3.14159265359, 67 E0 93 7D 56 49 12 F5",
" 3.1415926535897932, 69 80 4C 43 76 65 9E 9C 6F F0",
" 3.1415926535897932384626434, 6E 00 50 E0 DC F7 CC D6 08 48 99 92 3F 03 E7",
"3.141592653589793238462643383, F6 1F 00 E0 2D 8F A4 21 D0 E7 46 C0 87 AA 89 02 E5",
})
public void testWriteDecimalValue(@ConvertWith(StringToDecimal.class) Decimal value, String expectedBytes) {
assertWritingValue(expectedBytes, value, IonEncoder_1_1::writeDecimalValue);
}

@Test
public void testWriteDecimalValueForNull() {
int numBytes = IonEncoder_1_1.writeDecimalValue(buf, null);
Assertions.assertEquals("EB 03", byteArrayToHex(bytes()));
Assertions.assertEquals(2, numBytes);
}

// Because timestamp subfields are smeared across bytes, it's easier to reason about them in 1s and 0s
// instead of hex digits
@ParameterizedTest
Expand Down Expand Up @@ -457,4 +513,19 @@ protected Timestamp convert(String source) throws ArgumentConversionException {
return Timestamp.valueOf(source);
}
}

/**
* Converts a String to a Decimal for a @Parameterized test
*/
static class StringToDecimal extends TypedArgumentConverter<String, Decimal> {
protected StringToDecimal() {
super(String.class, Decimal.class);
}

@Override
protected Decimal convert(String source) throws ArgumentConversionException {
if (source == null) return null;
return Decimal.valueOf(source);
}
}
}

0 comments on commit 8441b35

Please sign in to comment.