FastDoubleQuotedWriteTest.java
package tools.jackson.core.unittest.write;
import java.io.ByteArrayOutputStream;
import java.io.StringWriter;
import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.ObjectWriteContext;
import tools.jackson.core.PrettyPrinter;
import tools.jackson.core.SerializableString;
import tools.jackson.core.StreamWriteFeature;
import tools.jackson.core.io.CharacterEscapes;
import tools.jackson.core.io.NumberOutput;
import tools.jackson.core.json.JsonFactory;
import tools.jackson.core.json.JsonWriteFeature;
import tools.jackson.core.unittest.JacksonCoreTestBase;
import tools.jackson.core.util.DefaultIndenter;
import tools.jackson.core.util.DefaultPrettyPrinter;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for the quoted (`WRITE_NUMBERS_AS_STRINGS` / `WRITE_NAN_AS_STRINGS`)
* float/double output paths when {@link StreamWriteFeature#USE_FAST_DOUBLE_WRITER}
* is enabled, for both byte- and char-backed generators.
*/
public class FastDoubleQuotedWriteTest extends JacksonCoreTestBase
{
private final JsonFactory NUMBERS_AS_STRINGS = JsonFactory.builder()
.enable(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)
.enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
.build();
private final JsonFactory NAN_AS_STRINGS = JsonFactory.builder()
.enable(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)
.enable(JsonWriteFeature.WRITE_NAN_AS_STRINGS)
.build();
private final double[] DOUBLES = {
0.0, -0.0, 1.5, -1.5, 123.456789, 1.0E10, 1.0E-10,
Double.MAX_VALUE, Double.MIN_VALUE, Double.MIN_NORMAL,
Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY
};
private final float[] FLOATS = {
0.0f, -0.0f, 1.5f, -1.5f, 123.456f, 1.0E10f, 1.0E-10f,
Float.MAX_VALUE, Float.MIN_VALUE, Float.MIN_NORMAL,
Float.NaN, Float.POSITIVE_INFINITY, Float.NEGATIVE_INFINITY
};
@Test
void testNumbersAsStringsDouble() throws Exception
{
for (double v : DOUBLES) {
String exp = q(NumberOutput.toString(v, true));
assertEquals(exp, _writeChars(NUMBERS_AS_STRINGS, v));
assertEquals(exp, _writeBytes(NUMBERS_AS_STRINGS, v));
}
}
@Test
void testNumbersAsStringsFloat() throws Exception
{
for (float v : FLOATS) {
String exp = q(NumberOutput.toString(v, true));
assertEquals(exp, _writeChars(NUMBERS_AS_STRINGS, v));
assertEquals(exp, _writeBytes(NUMBERS_AS_STRINGS, v));
}
}
@Test
void testNaNAsStrings() throws Exception
{
// Only non-finite values get quoted
assertEquals(q("NaN"), _writeChars(NAN_AS_STRINGS, Double.NaN));
assertEquals(q("NaN"), _writeBytes(NAN_AS_STRINGS, Double.NaN));
assertEquals(q("-Infinity"), _writeChars(NAN_AS_STRINGS, Double.NEGATIVE_INFINITY));
assertEquals(q("Infinity"), _writeBytes(NAN_AS_STRINGS, Double.POSITIVE_INFINITY));
assertEquals("1.5", _writeChars(NAN_AS_STRINGS, 1.5));
assertEquals("1.5", _writeBytes(NAN_AS_STRINGS, 1.5));
assertEquals(q("NaN"), _writeChars(NAN_AS_STRINGS, Float.NaN));
assertEquals(q("NaN"), _writeBytes(NAN_AS_STRINGS, Float.NaN));
assertEquals(q("-Infinity"), _writeChars(NAN_AS_STRINGS, Float.NEGATIVE_INFINITY));
assertEquals(q("Infinity"), _writeBytes(NAN_AS_STRINGS, Float.POSITIVE_INFINITY));
assertEquals("1.5", _writeChars(NAN_AS_STRINGS, 1.5f));
assertEquals("1.5", _writeBytes(NAN_AS_STRINGS, 1.5f));
}
@Test
void testQuotedAcrossBufferBoundary() throws Exception
{
final double[] values = { -Double.MIN_NORMAL, 1.0, 0.1, Double.NaN, 1.0E-300 };
final float[] fvalues = { -Float.MIN_NORMAL, 1.0f, 0.1f, Float.NaN, 1.0E-30f };
final int count = 4000;
StringBuilder expected = new StringBuilder("[");
for (int i = 0; i < count; ++i) {
if (i > 0) {
expected.append(',');
}
expected.append(q(NumberOutput.toString(values[i % values.length], true)));
expected.append(',');
expected.append(q(NumberOutput.toString(fvalues[i % fvalues.length], true)));
}
expected.append(']');
StringWriter sw = new StringWriter();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ObjectWriteContext.empty(), sw)) {
_writeArray(gen, values, fvalues, count);
}
assertEquals(expected.toString(), sw.toString());
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ObjectWriteContext.empty(), bytes)) {
_writeArray(gen, values, fvalues, count);
}
assertEquals(expected.toString(), bytes.toString(StandardCharsets.UTF_8));
}
@Test
void testQuotedFloatsOnlyAcrossBufferBoundary() throws Exception
{
// Float-only run: the float flush check has a smaller reserve than the
// double one, so mixing types never lets it trigger
final float[] fvalues = { -Float.MIN_NORMAL, 1.0f, 0.1f, Float.NaN, 1.0E-30f };
final int count = 6000;
StringBuilder expected = new StringBuilder("[");
for (int i = 0; i < count; ++i) {
if (i > 0) {
expected.append(',');
}
expected.append(q(NumberOutput.toString(fvalues[i % fvalues.length], true)));
}
expected.append(']');
StringWriter sw = new StringWriter();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ObjectWriteContext.empty(), sw)) {
gen.writeStartArray();
for (int i = 0; i < count; ++i) {
gen.writeNumber(fvalues[i % fvalues.length]);
}
gen.writeEndArray();
}
assertEquals(expected.toString(), sw.toString());
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ObjectWriteContext.empty(), bytes)) {
gen.writeStartArray();
for (int i = 0; i < count; ++i) {
gen.writeNumber(fvalues[i % fvalues.length]);
}
gen.writeEndArray();
}
assertEquals(expected.toString(), bytes.toString(StandardCharsets.UTF_8));
}
@Test
void testNaNAsStringsDisabled() throws Exception
{
// With WRITE_NAN_AS_STRINGS off, non-finite values go through the
// unquoted fast path (which must still render them correctly)
JsonFactory f = JsonFactory.builder()
.enable(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)
.disable(JsonWriteFeature.WRITE_NAN_AS_STRINGS)
.build();
assertEquals("NaN", _writeChars(f, Double.NaN));
assertEquals("NaN", _writeBytes(f, Double.NaN));
assertEquals("Infinity", _writeChars(f, Double.POSITIVE_INFINITY));
assertEquals("-Infinity", _writeBytes(f, Double.NEGATIVE_INFINITY));
assertEquals("NaN", _writeChars(f, Float.NaN));
assertEquals("NaN", _writeBytes(f, Float.NaN));
assertEquals("-Infinity", _writeChars(f, Float.NEGATIVE_INFINITY));
assertEquals("Infinity", _writeBytes(f, Float.POSITIVE_INFINITY));
}
@Test
void testQuotedWithCustomQuoteChar() throws Exception
{
JsonFactory f = JsonFactory.builder()
.enable(StreamWriteFeature.USE_FAST_DOUBLE_WRITER)
.enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
.quoteChar('\'')
.build();
assertEquals("'1.5'", _writeChars(f, 1.5));
assertEquals("'1.5'", _writeBytes(f, 1.5));
assertEquals("'2.5'", _writeChars(f, 2.5f));
assertEquals("'2.5'", _writeBytes(f, 2.5f));
assertEquals("'NaN'", _writeChars(f, Double.NaN));
assertEquals("'NaN'", _writeBytes(f, Float.NaN));
}
@Test
void testQuotedRootValues() throws Exception
{
// Root-level values get the root value separator via _verifyValueWrite()
StringWriter sw = new StringWriter();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ObjectWriteContext.empty(), sw)) {
gen.writeNumber(1.5);
gen.writeNumber(2.5f);
}
assertEquals(a2q("'1.5' '2.5'"), sw.toString());
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ObjectWriteContext.empty(), bytes)) {
gen.writeNumber(1.5);
gen.writeNumber(2.5f);
}
assertEquals(a2q("'1.5' '2.5'"), bytes.toString(StandardCharsets.UTF_8));
}
@Test
void testQuotedInObject() throws Exception
{
StringWriter sw = new StringWriter();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ObjectWriteContext.empty(), sw)) {
gen.writeStartObject();
gen.writeName("d");
gen.writeNumber(1.5);
gen.writeName("f");
gen.writeNumber(2.5f);
gen.writeEndObject();
}
assertEquals(a2q("{'d':'1.5','f':'2.5'}"), sw.toString());
}
// [core#1704]: PrettyPrinter writes indentation from within _verifyValueWrite(),
// just before the quoted fast path checks for room
@Test
void testQuotedPrettyPrintedAcrossBufferBoundary() throws Exception
{
final double[] values = { -Double.MIN_NORMAL, 1.0, 0.1, Double.NaN, 1.0E-300 };
final int count = 500;
for (int indentLen = 1; indentLen <= 40; ++indentLen) {
StringBuilder indent = new StringBuilder();
for (int i = 0; i < indentLen; ++i) {
indent.append(' ');
}
final PrettyPrinter pp = new DefaultPrettyPrinter()
.withArrayIndenter(new DefaultIndenter(indent.toString(), "\n"));
ObjectWriteContext ctxt = new ObjectWriteContext.Base() {
@Override
public PrettyPrinter getPrettyPrinter() { return pp; }
};
StringWriter sw = new StringWriter();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ctxt, sw)) {
_writeValues(gen, values, count);
}
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JsonGenerator gen = NUMBERS_AS_STRINGS.createGenerator(ctxt, bytes)) {
_writeValues(gen, values, count);
}
String desc = "indentLen="+indentLen;
String doc = sw.toString();
assertEquals(doc, bytes.toString(StandardCharsets.UTF_8), desc);
for (int i = 0; i < count; ++i) {
assertTrue(doc.contains(q(NumberOutput.toString(values[i % values.length], true))),
desc+", index "+i);
}
}
}
private void _writeValues(JsonGenerator gen, double[] values, int count)
{
gen.writeStartArray();
for (int i = 0; i < count; ++i) {
gen.writeNumber(values[i % values.length]);
}
gen.writeEndArray();
}
// Escapes every digit; number text must never be escaped, regardless of
// whether USE_FAST_DOUBLE_WRITER is enabled
@SuppressWarnings("serial")
static class DigitEscapes extends CharacterEscapes
{
private final int[] _asciiEscapes;
public DigitEscapes() {
_asciiEscapes = standardAsciiEscapesForJSON();
for (int c = '0'; c <= '9'; ++c) {
_asciiEscapes[c] = CharacterEscapes.ESCAPE_STANDARD;
}
}
@Override
public int[] getEscapeCodesForAscii() { return _asciiEscapes; }
@Override
public SerializableString getEscapeSequence(int ch) { return null; }
}
@Test
void testQuotedNotEscapedRegardlessOfFastWriter() throws Exception
{
for (boolean useFast : new boolean[] { true, false }) {
JsonFactory f = JsonFactory.builder()
.configure(StreamWriteFeature.USE_FAST_DOUBLE_WRITER, useFast)
.enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS)
.characterEscapes(new DigitEscapes())
.build();
String desc = "useFast="+useFast;
assertEquals(q("1.5"), _writeChars(f, 1.5), desc);
assertEquals(q("1.5"), _writeBytes(f, 1.5), desc);
assertEquals(q("2.5"), _writeChars(f, 2.5f), desc);
assertEquals(q("2.5"), _writeBytes(f, 2.5f), desc);
// ... just like the pre-existing quoted-number paths
assertEquals(q("1.5"), _writeChars(f, new BigDecimal("1.5")), desc);
assertEquals(q("1.5"), _writeBytes(f, new BigDecimal("1.5")), desc);
}
}
private void _writeArray(JsonGenerator gen, double[] values, float[] fvalues, int count)
{
gen.writeStartArray();
for (int i = 0; i < count; ++i) {
gen.writeNumber(values[i % values.length]);
gen.writeNumber(fvalues[i % fvalues.length]);
}
gen.writeEndArray();
}
private String _writeChars(JsonFactory f, double v) throws Exception {
StringWriter sw = new StringWriter();
try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), sw)) {
gen.writeNumber(v);
}
return sw.toString();
}
private String _writeChars(JsonFactory f, float v) throws Exception {
StringWriter sw = new StringWriter();
try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), sw)) {
gen.writeNumber(v);
}
return sw.toString();
}
private String _writeBytes(JsonFactory f, double v) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), bytes)) {
gen.writeNumber(v);
}
return bytes.toString(StandardCharsets.UTF_8);
}
private String _writeBytes(JsonFactory f, float v) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), bytes)) {
gen.writeNumber(v);
}
return bytes.toString(StandardCharsets.UTF_8);
}
private String _writeChars(JsonFactory f, BigDecimal v) throws Exception {
StringWriter sw = new StringWriter();
try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), sw)) {
gen.writeNumber(v);
}
return sw.toString();
}
private String _writeBytes(JsonFactory f, BigDecimal v) throws Exception {
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
try (JsonGenerator gen = f.createGenerator(ObjectWriteContext.empty(), bytes)) {
gen.writeNumber(v);
}
return bytes.toString(StandardCharsets.UTF_8);
}
}