Skip to content

Commit

Permalink
Add writing of Decimal64 from underlying 64-bit representation
Browse files Browse the repository at this point in the history
  • Loading branch information
plokhotnyuk committed Jan 28, 2024
1 parent b35891d commit 4809efc
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 0 deletions.
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ lazy val `jsoniter-scala-core` = crossProject(JVMPlatform, JSPlatform, NativePla
.settings(
crossScalaVersions := Seq("3.3.1", "2.13.12", "2.12.18"),
libraryDependencies ++= Seq(
"com.epam.deltix" % "dfp" % "1.0.2" % Test,
"org.scala-lang.modules" %%% "scala-collection-compat" % "2.11.0" % Test,
"org.scalatestplus" %%% "scalacheck-1-17" % "3.2.17.0" % Test,
"org.scalatest" %%% "scalatest" % "3.2.17" % Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,19 @@ final class JsonWriter private[jsoniter_scala](
writeParenthesesWithColon()
}

/**
* Writes an underlying Decimal64 representation as a JSON key.
*
* @param x the underlying Decimal64 representation
* @throws JsonWriterException if the value is non-finite
*/
def writeDecimal64Key(x: Long): Unit = {
writeOptionalCommaAndIndentionBeforeKey()
writeBytes('"')
writeDecimal64(x)
writeParenthesesWithColon()
}

/**
* Writes a `BigInt` value as a JSON key.
*
Expand Down Expand Up @@ -697,6 +710,17 @@ final class JsonWriter private[jsoniter_scala](
writeDouble(x)
}

/**
* Writes an underlying Decimal64 representation as a JSON number.
*
* @param x the underlying Decimal64 representation
* @throws JsonWriterException if the value is non-finite
*/
def writeDecimal64Val(x: Long): Unit = {
writeOptionalCommaAndIndentionBeforeValue()
writeDecimal64(x)
}

/**
* Writes a `BigDecimal` value as a JSON string value.
*
Expand Down Expand Up @@ -808,6 +832,19 @@ final class JsonWriter private[jsoniter_scala](
writeBytes('"')
}

/**
* Writes an underlying Decimal64 representation as a JSON number.
*
* @param x the underlying Decimal64 representation
* @throws JsonWriterException if the value is non-finite
*/
def writeDecimal64ValAsString(x: Long): Unit = {
writeOptionalCommaAndIndentionBeforeValue()
writeBytes('"')
writeDecimal64(x)
writeBytes('"')
}

/**
* Writes a byte array as a JSON hexadecimal string value.
*
Expand Down Expand Up @@ -2372,6 +2409,39 @@ final class JsonWriter private[jsoniter_scala](
count = pos
}

private[this] def writeDecimal64(x: Long): Unit = {
var pos = ensureBufCapacity(22)
val buf = this.buf
var m10 = x & 0x001FFFFFFFFFFFFFL
var e10 = (x >> 53).toInt
if ((x & 0x6000000000000000L) == 0x6000000000000000L) {
if ((x & 0x7800000000000000L) == 0x7800000000000000L) illegalDecimal64NumberError(x)
m10 = (x & 0x0007FFFFFFFFFFFFL) | 0x0020000000000000L
if (m10 > 9999999999999999L) m10 = 0
e10 = (x >> 51).toInt
}
e10 = (e10 & 0x3FF) - 398
if (x < 0) {
buf(pos) = '-'
pos += 1
}
pos = writeLong(m10, pos, buf)
if (e10 != 0) {
ByteArrayAccess.setShort(buf, pos, 0x2D65)
pos += 1
if (e10 < 0) {
e10 = -e10
pos += 1
}
if (e10 < 10) {
buf(pos) = (e10 + '0').toByte
pos += 1
} else if (e10 < 100) pos = write2Digits(e10, pos, buf, digits)
else pos = write3Digits(e10, pos, buf, digits)
}
count = pos
}

private[this] def rop(g1: Long, g0: Long, cp: Long): Long = {
val x = Math.multiplyHigh(g0, cp) + (g1 * cp >>> 1)
Math.multiplyHigh(g1, cp) + (x >>> 63) | (-x ^ x) >>> 63
Expand Down Expand Up @@ -2449,6 +2519,8 @@ final class JsonWriter private[jsoniter_scala](

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

private[this] def illegalDecimal64NumberError(x: Long): Nothing = encodeError("illegal Decimal64 number: " + x)

private[this] def ensureBufCapacity(required: Int): Int = {
val pos = count
if (pos + required <= limit) pos
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.github.plokhotnyuk.jsoniter_scala.core

import org.scalacheck.Arbitrary.arbitrary
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import com.epam.deltix.dfp.Decimal64Utils
import com.github.plokhotnyuk.jsoniter_scala.core.GenUtils._

import java.io._
import java.nio.charset.StandardCharsets.UTF_8
import scala.util.Random

class Decimal64Spec extends AnyWordSpec with Matchers with ScalaCheckPropertyChecks {
"JsonWriter.writeDecimal64Val and JsonWriter.writeDecimal64ValAsString and JsonWriter.writeDecimal64Key for an underlying representation of Decimal64" should {
"write finite Decimal64 values" in {
def check(n: Long): Unit = {
val s = withWriter(_.writeDecimal64Val(n))
print(s + " ")
Decimal64Utils.compareTo(Decimal64Utils.parse(s), n) shouldBe 0 // no data loss when parsing by JVM, Native or JS Platform
s.length should be <= 22 // length is 22 bytes or less
withWriter(_.writeDecimal64ValAsString(n)) shouldBe s""""$s""""
withWriter(_.writeDecimal64Key(n)) shouldBe s""""$s":"""
}

check(Decimal64Utils.ZERO)
check(Decimal64Utils.ONE)
check(Decimal64Utils.TEN)
check(Decimal64Utils.THOUSAND)
check(Decimal64Utils.MILLION)
check(Decimal64Utils.ONE_TENTH)
check(Decimal64Utils.ONE_HUNDREDTH)
check(Decimal64Utils.parse("1000.0"))
check(Decimal64Utils.parse("1000.001"))
forAll(arbitrary[Long], minSuccessful(10000)) { n =>
whenever(Decimal64Utils.isFinite(n)) {
check(n)
}
}
forAll(genFiniteDouble, minSuccessful(10000)) { d =>
check(Decimal64Utils.fromDouble(d))
}
forAll(arbitrary[Long], minSuccessful(10000)) { l =>
check(Decimal64Utils.fromFixedPoint(l >> 8, l.toByte))
}
forAll(arbitrary[Long], minSuccessful(10000)) { l =>
check(Decimal64Utils.fromLong(l))
}
forAll(arbitrary[Int], minSuccessful(10000)) { i =>
check(Decimal64Utils.fromInt(i))
}
}
"throw i/o exception on non-finite numbers" in {
forAll(arbitrary[Long], minSuccessful(100)) { n =>
whenever(Decimal64Utils.isNonFinite(n)) {
assert(intercept[JsonWriterException](withWriter(_.writeDecimal64Val(n))).getMessage.startsWith("illegal Decimal64 number"))
assert(intercept[JsonWriterException](withWriter(_.writeDecimal64ValAsString(n))).getMessage.startsWith("illegal Decimal64 number"))
assert(intercept[JsonWriterException](withWriter(_.writeDecimal64Key(n))).getMessage.startsWith("illegal Decimal64 number"))
}
}
}
}

def reader(json: String, totalRead: Long = 0): JsonReader = reader2(json.getBytes(UTF_8), totalRead)

def reader2(jsonBytes: Array[Byte], totalRead: Long = 0): JsonReader =
new JsonReader(new Array[Byte](Random.nextInt(20) + 12), // 12 is a minimal allowed length to test resizing of the buffer
0, 0, -1, new Array[Char](Random.nextInt(32)), null, new ByteArrayInputStream(jsonBytes), totalRead, readerConfig)

def readerConfig: ReaderConfig = ReaderConfig
.withPreferredBufSize(Random.nextInt(20) + 12) // 12 is a minimal allowed length to test resizing of the buffer
.withPreferredCharBufSize(Random.nextInt(32))
.withThrowReaderExceptionWithStackTrace(true)

def withWriter(f: JsonWriter => Unit): String =
withWriter(WriterConfig.withPreferredBufSize(1).withThrowWriterExceptionWithStackTrace(true))(f)

def withWriter(cfg: WriterConfig)(f: JsonWriter => Unit): String = {
val writer = new JsonWriter(new Array[Byte](Random.nextInt(16)), 0, 0, 0, false, false, null, null, cfg)
new String(writer.write(new JsonValueCodec[String] {
override def decodeValue(in: JsonReader, default: String): String = ""

override def encodeValue(x: String, out: JsonWriter): Unit = f(writer)

override val nullValue: String = ""
}, "", cfg), "UTF-8")
}
}

0 comments on commit 4809efc

Please sign in to comment.