Skip to content

Commit 4809efc

Browse files
committed
Add writing of Decimal64 from underlying 64-bit representation
1 parent b35891d commit 4809efc

File tree

3 files changed

+161
-0
lines changed

3 files changed

+161
-0
lines changed

build.sbt

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ lazy val `jsoniter-scala-core` = crossProject(JVMPlatform, JSPlatform, NativePla
144144
.settings(
145145
crossScalaVersions := Seq("3.3.1", "2.13.12", "2.12.18"),
146146
libraryDependencies ++= Seq(
147+
"com.epam.deltix" % "dfp" % "1.0.2" % Test,
147148
"org.scala-lang.modules" %%% "scala-collection-compat" % "2.11.0" % Test,
148149
"org.scalatestplus" %%% "scalacheck-1-17" % "3.2.17.0" % Test,
149150
"org.scalatest" %%% "scalatest" % "3.2.17" % Test

jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonWriter.scala

+72
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,19 @@ final class JsonWriter private[jsoniter_scala](
131131
writeParenthesesWithColon()
132132
}
133133

134+
/**
135+
* Writes an underlying Decimal64 representation as a JSON key.
136+
*
137+
* @param x the underlying Decimal64 representation
138+
* @throws JsonWriterException if the value is non-finite
139+
*/
140+
def writeDecimal64Key(x: Long): Unit = {
141+
writeOptionalCommaAndIndentionBeforeKey()
142+
writeBytes('"')
143+
writeDecimal64(x)
144+
writeParenthesesWithColon()
145+
}
146+
134147
/**
135148
* Writes a `BigInt` value as a JSON key.
136149
*
@@ -697,6 +710,17 @@ final class JsonWriter private[jsoniter_scala](
697710
writeDouble(x)
698711
}
699712

713+
/**
714+
* Writes an underlying Decimal64 representation as a JSON number.
715+
*
716+
* @param x the underlying Decimal64 representation
717+
* @throws JsonWriterException if the value is non-finite
718+
*/
719+
def writeDecimal64Val(x: Long): Unit = {
720+
writeOptionalCommaAndIndentionBeforeValue()
721+
writeDecimal64(x)
722+
}
723+
700724
/**
701725
* Writes a `BigDecimal` value as a JSON string value.
702726
*
@@ -808,6 +832,19 @@ final class JsonWriter private[jsoniter_scala](
808832
writeBytes('"')
809833
}
810834

835+
/**
836+
* Writes an underlying Decimal64 representation as a JSON number.
837+
*
838+
* @param x the underlying Decimal64 representation
839+
* @throws JsonWriterException if the value is non-finite
840+
*/
841+
def writeDecimal64ValAsString(x: Long): Unit = {
842+
writeOptionalCommaAndIndentionBeforeValue()
843+
writeBytes('"')
844+
writeDecimal64(x)
845+
writeBytes('"')
846+
}
847+
811848
/**
812849
* Writes a byte array as a JSON hexadecimal string value.
813850
*
@@ -2372,6 +2409,39 @@ final class JsonWriter private[jsoniter_scala](
23722409
count = pos
23732410
}
23742411

2412+
private[this] def writeDecimal64(x: Long): Unit = {
2413+
var pos = ensureBufCapacity(22)
2414+
val buf = this.buf
2415+
var m10 = x & 0x001FFFFFFFFFFFFFL
2416+
var e10 = (x >> 53).toInt
2417+
if ((x & 0x6000000000000000L) == 0x6000000000000000L) {
2418+
if ((x & 0x7800000000000000L) == 0x7800000000000000L) illegalDecimal64NumberError(x)
2419+
m10 = (x & 0x0007FFFFFFFFFFFFL) | 0x0020000000000000L
2420+
if (m10 > 9999999999999999L) m10 = 0
2421+
e10 = (x >> 51).toInt
2422+
}
2423+
e10 = (e10 & 0x3FF) - 398
2424+
if (x < 0) {
2425+
buf(pos) = '-'
2426+
pos += 1
2427+
}
2428+
pos = writeLong(m10, pos, buf)
2429+
if (e10 != 0) {
2430+
ByteArrayAccess.setShort(buf, pos, 0x2D65)
2431+
pos += 1
2432+
if (e10 < 0) {
2433+
e10 = -e10
2434+
pos += 1
2435+
}
2436+
if (e10 < 10) {
2437+
buf(pos) = (e10 + '0').toByte
2438+
pos += 1
2439+
} else if (e10 < 100) pos = write2Digits(e10, pos, buf, digits)
2440+
else pos = write3Digits(e10, pos, buf, digits)
2441+
}
2442+
count = pos
2443+
}
2444+
23752445
private[this] def rop(g1: Long, g0: Long, cp: Long): Long = {
23762446
val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1)
23772447
Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63
@@ -2449,6 +2519,8 @@ final class JsonWriter private[jsoniter_scala](
24492519

24502520
private[this] def illegalNumberError(x: Double): Nothing = encodeError("illegal number: " + x)
24512521

2522+
private[this] def illegalDecimal64NumberError(x: Long): Nothing = encodeError("illegal Decimal64 number: " + x)
2523+
24522524
private[this] def ensureBufCapacity(required: Int): Int = {
24532525
val pos = count
24542526
if (pos + required <= limit) pos
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.github.plokhotnyuk.jsoniter_scala.core
2+
3+
import org.scalacheck.Arbitrary.arbitrary
4+
import org.scalatest.matchers.should.Matchers
5+
import org.scalatest.wordspec.AnyWordSpec
6+
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
7+
import com.epam.deltix.dfp.Decimal64Utils
8+
import com.github.plokhotnyuk.jsoniter_scala.core.GenUtils._
9+
10+
import java.io._
11+
import java.nio.charset.StandardCharsets.UTF_8
12+
import scala.util.Random
13+
14+
class Decimal64Spec extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks {
15+
"JsonWriter.writeDecimal64Val and JsonWriter.writeDecimal64ValAsString and JsonWriter.writeDecimal64Key for an underlying representation of Decimal64" should {
16+
"write finite Decimal64 values" in {
17+
def check(n: Long): Unit = {
18+
val s = withWriter(_.writeDecimal64Val(n))
19+
print(s + " ")
20+
Decimal64Utils.compareTo(Decimal64Utils.parse(s), n) shouldBe 0 // no data loss when parsing by JVM, Native or JS Platform
21+
s.length should be <= 22 // length is 22 bytes or less
22+
withWriter(_.writeDecimal64ValAsString(n)) shouldBe s""""$s""""
23+
withWriter(_.writeDecimal64Key(n)) shouldBe s""""$s":"""
24+
}
25+
26+
check(Decimal64Utils.ZERO)
27+
check(Decimal64Utils.ONE)
28+
check(Decimal64Utils.TEN)
29+
check(Decimal64Utils.THOUSAND)
30+
check(Decimal64Utils.MILLION)
31+
check(Decimal64Utils.ONE_TENTH)
32+
check(Decimal64Utils.ONE_HUNDREDTH)
33+
check(Decimal64Utils.parse("1000.0"))
34+
check(Decimal64Utils.parse("1000.001"))
35+
forAll(arbitrary[Long], minSuccessful(10000)) { n =>
36+
whenever(Decimal64Utils.isFinite(n)) {
37+
check(n)
38+
}
39+
}
40+
forAll(genFiniteDouble, minSuccessful(10000)) { d =>
41+
check(Decimal64Utils.fromDouble(d))
42+
}
43+
forAll(arbitrary[Long], minSuccessful(10000)) { l =>
44+
check(Decimal64Utils.fromFixedPoint(l >> 8, l.toByte))
45+
}
46+
forAll(arbitrary[Long], minSuccessful(10000)) { l =>
47+
check(Decimal64Utils.fromLong(l))
48+
}
49+
forAll(arbitrary[Int], minSuccessful(10000)) { i =>
50+
check(Decimal64Utils.fromInt(i))
51+
}
52+
}
53+
"throw i/o exception on non-finite numbers" in {
54+
forAll(arbitrary[Long], minSuccessful(100)) { n =>
55+
whenever(Decimal64Utils.isNonFinite(n)) {
56+
assert(intercept[JsonWriterException](withWriter(_.writeDecimal64Val(n))).getMessage.startsWith("illegal Decimal64 number"))
57+
assert(intercept[JsonWriterException](withWriter(_.writeDecimal64ValAsString(n))).getMessage.startsWith("illegal Decimal64 number"))
58+
assert(intercept[JsonWriterException](withWriter(_.writeDecimal64Key(n))).getMessage.startsWith("illegal Decimal64 number"))
59+
}
60+
}
61+
}
62+
}
63+
64+
def reader(json: String, totalRead: Long = 0): JsonReader = reader2(json.getBytes(UTF_8), totalRead)
65+
66+
def reader2(jsonBytes: Array[Byte], totalRead: Long = 0): JsonReader =
67+
new JsonReader(new Array[Byte](Random.nextInt(20) + 12), // 12 is a minimal allowed length to test resizing of the buffer
68+
0, 0, -1, new Array[Char](Random.nextInt(32)), null, new ByteArrayInputStream(jsonBytes), totalRead, readerConfig)
69+
70+
def readerConfig: ReaderConfig = ReaderConfig
71+
.withPreferredBufSize(Random.nextInt(20) + 12) // 12 is a minimal allowed length to test resizing of the buffer
72+
.withPreferredCharBufSize(Random.nextInt(32))
73+
.withThrowReaderExceptionWithStackTrace(true)
74+
75+
def withWriter(f: JsonWriter => Unit): String =
76+
withWriter(WriterConfig.withPreferredBufSize(1).withThrowWriterExceptionWithStackTrace(true))(f)
77+
78+
def withWriter(cfg: WriterConfig)(f: JsonWriter => Unit): String = {
79+
val writer = new JsonWriter(new Array[Byte](Random.nextInt(16)), 0, 0, 0, false, false, null, null, cfg)
80+
new String(writer.write(new JsonValueCodec[String] {
81+
override def decodeValue(in: JsonReader, default: String): String = ""
82+
83+
override def encodeValue(x: String, out: JsonWriter): Unit = f(writer)
84+
85+
override val nullValue: String = ""
86+
}, "", cfg), "UTF-8")
87+
}
88+
}

0 commit comments

Comments
 (0)