Skip to content

Commit

Permalink
More efficient decoding of BigInt values
Browse files Browse the repository at this point in the history
  • Loading branch information
plokhotnyuk committed Feb 2, 2025
1 parent c4294f6 commit cc97fbe
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 59 deletions.
14 changes: 6 additions & 8 deletions zio-json/js/src/main/scala/zio/json/internal/SafeNumbers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ object SafeNumbers {
try LongSome(UnsafeNumbers.long(num))
catch { case _: UnexpectedEnd | UnsafeNumber => LongNone }

def bigInteger(
num: String,
max_bits: Int = 128
): Option[java.math.BigInteger] =
def bigInt(num: String, max_bits: Int = 128): Option[BigInt] =
try Some(UnsafeNumbers.bigInt(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] =
try Some(UnsafeNumbers.bigInteger(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

Expand All @@ -71,10 +72,7 @@ object SafeNumbers {
try DoubleSome(UnsafeNumbers.double(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone }

def bigDecimal(
num: String,
max_bits: Int = 128
): Option[java.math.BigDecimal] =
def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] =
try Some(UnsafeNumbers.bigDecimal(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

Expand Down
44 changes: 44 additions & 0 deletions zio-json/js/src/main/scala/zio/json/internal/UnsafeNumbers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,50 @@ object UnsafeNumbers {
else throw UnsafeNumber
}

def bigInt(num: String, max_bits: Int): BigInt =
bigInt_(new FastStringReader(num), true, max_bits)

def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = {
var current =
if (consume) in.readChar().toInt
else in.nextNonWhitespace().toInt
val negate = current == '-'
if (negate) current = in.readChar().toInt
if (current < '0' || current > '9') throw UnsafeNumber
var loM10 = (current - '0').toLong
var loDigits = 1
var hiM10: java.math.BigDecimal = null
while ({
current = in.read()
'0' <= current && current <= '9'
}) {
loM10 = (loM10 << 3) + (loM10 << 1) + (current - '0')
loDigits += 1
if (loM10 >= 100000000000000000L) {
if (negate) loM10 = -loM10
val bd = java.math.BigDecimal.valueOf(loM10)
if (hiM10 eq null) hiM10 = bd
else {
hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd)
if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber
}
loM10 = 0
loDigits = 0
}
}
if (consume && current != -1) throw UnsafeNumber
if (hiM10 eq null) {
if (negate) loM10 = -loM10
return BigInt(loM10)
}
if (loDigits != 0) {
if (negate) loM10 = -loM10
hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10))
if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber
}
new BigInt(hiM10.unscaledValue)
}

def bigInteger(num: String, max_bits: Int): java.math.BigInteger =
bigInteger_(new FastStringReader(num), true, max_bits)

Expand Down
14 changes: 6 additions & 8 deletions zio-json/jvm/src/main/scala/zio/json/internal/SafeNumbers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ object SafeNumbers {
try LongSome(UnsafeNumbers.long(num))
catch { case _: UnexpectedEnd | UnsafeNumber => LongNone }

def bigInteger(
num: String,
max_bits: Int = 128
): Option[java.math.BigInteger] =
def bigInt(num: String, max_bits: Int = 128): Option[BigInt] =
try Some(UnsafeNumbers.bigInt(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] =
try Some(UnsafeNumbers.bigInteger(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

Expand All @@ -71,10 +72,7 @@ object SafeNumbers {
try DoubleSome(UnsafeNumbers.double(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone }

def bigDecimal(
num: String,
max_bits: Int = 128
): Option[java.math.BigDecimal] =
def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] =
try Some(UnsafeNumbers.bigDecimal(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

Expand Down
44 changes: 44 additions & 0 deletions zio-json/jvm/src/main/scala/zio/json/internal/UnsafeNumbers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,50 @@ object UnsafeNumbers {
else throw UnsafeNumber
}

def bigInt(num: String, max_bits: Int): BigInt =
bigInt_(new FastStringReader(num), true, max_bits)

def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = {
var current =
if (consume) in.readChar().toInt
else in.nextNonWhitespace().toInt
val negate = current == '-'
if (negate) current = in.readChar().toInt
if (current < '0' || current > '9') throw UnsafeNumber
var loM10 = (current - '0').toLong
var loDigits = 1
var hiM10: java.math.BigDecimal = null
while ({
current = in.read()
'0' <= current && current <= '9'
}) {
loM10 = loM10 * 10 + (current - '0')
loDigits += 1
if (loM10 >= 100000000000000000L) {
if (negate) loM10 = -loM10
val bd = java.math.BigDecimal.valueOf(loM10)
if (hiM10 eq null) hiM10 = bd
else {
hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd)
if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber
}
loM10 = 0
loDigits = 0
}
}
if (consume && current != -1) throw UnsafeNumber
if (hiM10 eq null) {
if (negate) loM10 = -loM10
return BigInt(loM10)
}
if (loDigits != 0) {
if (negate) loM10 = -loM10
hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10))
if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber
}
new BigInt(hiM10.unscaledValue)
}

def bigInteger(num: String, max_bits: Int): java.math.BigInteger =
bigInteger_(new FastStringReader(num), true, max_bits)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ object SafeNumbers {
try LongSome(UnsafeNumbers.long(num))
catch { case _: UnexpectedEnd | UnsafeNumber => LongNone }

def bigInteger(
num: String,
max_bits: Int = 128
): Option[java.math.BigInteger] =
def bigInt(num: String, max_bits: Int = 128): Option[BigInt] =
try Some(UnsafeNumbers.bigInt(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

def bigInteger(num: String, max_bits: Int = 128): Option[java.math.BigInteger] =
try Some(UnsafeNumbers.bigInteger(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

Expand All @@ -71,10 +72,7 @@ object SafeNumbers {
try DoubleSome(UnsafeNumbers.double(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => DoubleNone }

def bigDecimal(
num: String,
max_bits: Int = 128
): Option[java.math.BigDecimal] =
def bigDecimal(num: String, max_bits: Int = 128): Option[java.math.BigDecimal] =
try Some(UnsafeNumbers.bigDecimal(num, max_bits))
catch { case _: UnexpectedEnd | UnsafeNumber => None }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,50 @@ object UnsafeNumbers {
else throw UnsafeNumber
}

def bigInt(num: String, max_bits: Int): BigInt =
bigInt_(new FastStringReader(num), true, max_bits)

def bigInt_(in: OneCharReader, consume: Boolean, max_bits: Int): BigInt = {
var current =
if (consume) in.readChar().toInt
else in.nextNonWhitespace().toInt
val negate = current == '-'
if (negate) current = in.readChar().toInt
if (current < '0' || current > '9') throw UnsafeNumber
var loM10 = (current - '0').toLong
var loDigits = 1
var hiM10: java.math.BigDecimal = null
while ({
current = in.read()
'0' <= current && current <= '9'
}) {
loM10 = loM10 * 10 + (current - '0')
loDigits += 1
if (loM10 >= 100000000000000000L) {
if (negate) loM10 = -loM10
val bd = java.math.BigDecimal.valueOf(loM10)
if (hiM10 eq null) hiM10 = bd
else {
hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(bd)
if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber
}
loM10 = 0
loDigits = 0
}
}
if (consume && current != -1) throw UnsafeNumber
if (hiM10 eq null) {
if (negate) loM10 = -loM10
return BigInt(loM10)
}
if (loDigits != 0) {
if (negate) loM10 = -loM10
hiM10 = hiM10.scaleByPowerOfTen(loDigits).add(java.math.BigDecimal.valueOf(loM10))
if (hiM10.unscaledValue.bitLength >= max_bits) throw UnsafeNumber
}
new BigInt(hiM10.unscaledValue)
}

def bigInteger(num: String, max_bits: Int): java.math.BigInteger =
bigInteger_(new FastStringReader(num), true, max_bits)

Expand Down
2 changes: 1 addition & 1 deletion zio-json/shared/src/main/scala/zio/json/JsonDecoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ object JsonDecoder extends GeneratedTupleDecoders with DecoderLowPriority1 with
implicit val int: JsonDecoder[Int] = number(Lexer.int, _.intValueExact())
implicit val long: JsonDecoder[Long] = number(Lexer.long, _.longValueExact())
implicit val bigInteger: JsonDecoder[java.math.BigInteger] = number(Lexer.bigInteger, _.toBigIntegerExact)
implicit val scalaBigInt: JsonDecoder[BigInt] = number(Lexer.bigInteger, _.toBigIntegerExact)
implicit val scalaBigInt: JsonDecoder[BigInt] = number(Lexer.bigInt, _.toBigIntegerExact)
implicit val float: JsonDecoder[Float] = number(Lexer.float, _.floatValue())
implicit val double: JsonDecoder[Double] = number(Lexer.double, _.doubleValue())
implicit val bigDecimal: JsonDecoder[java.math.BigDecimal] = number(Lexer.bigDecimal, identity)
Expand Down
43 changes: 15 additions & 28 deletions zio-json/shared/src/main/scala/zio/json/internal/lexer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,7 @@ object Lexer {
// messages) by only checking for what we expect to see (Jon Pretty's idea).
//
// returns the index of the matched field, or -1
def field(
trace: List[JsonError],
in: OneCharReader,
matrix: StringMatrix
): Int = {
def field(trace: List[JsonError], in: OneCharReader, matrix: StringMatrix): Int = {
val f = enumeration(trace, in, matrix)
char(trace, in, ':')
f
Expand Down Expand Up @@ -181,10 +177,7 @@ object Lexer {
}

// useful for embedded documents, e.g. CSV contained inside JSON
def streamingString(
trace: List[JsonError],
in: OneCharReader
): java.io.Reader = {
def streamingString(trace: List[JsonError], in: OneCharReader): java.io.Reader = {
char(trace, in, '"')
new OneCharReader {
def close(): Unit = in.close()
Expand Down Expand Up @@ -346,10 +339,16 @@ object Lexer {
case UnsafeNumbers.UnsafeNumber => error("expected a Long", trace)
}

def bigInteger(
trace: List[JsonError],
in: RetractReader
): java.math.BigInteger =
def bigInt(trace: List[JsonError], in: RetractReader): BigInt =
try {
val i = UnsafeNumbers.bigInt_(in, false, NumberMaxBits)
in.retract()
i
} catch {
case UnsafeNumbers.UnsafeNumber => error(s"expected a $NumberMaxBits bit BigInteger", trace)
}

def bigInteger(trace: List[JsonError], in: RetractReader): java.math.BigInteger =
try {
val i = UnsafeNumbers.bigInteger_(in, false, NumberMaxBits)
in.retract()
Expand All @@ -376,10 +375,7 @@ object Lexer {
case UnsafeNumbers.UnsafeNumber => error("expected a Double", trace)
}

def bigDecimal(
trace: List[JsonError],
in: RetractReader
): java.math.BigDecimal =
def bigDecimal(trace: List[JsonError], in: RetractReader): java.math.BigDecimal =
try {
val i = UnsafeNumbers.bigDecimal_(in, false, NumberMaxBits)
in.retract()
Expand All @@ -394,11 +390,7 @@ object Lexer {
if (got != c) error(s"'$c'", got, trace)
}

@inline def charOnly(
trace: List[JsonError],
in: OneCharReader,
c: Char
): Unit = {
@inline def charOnly(trace: List[JsonError], in: OneCharReader, c: Char): Unit = {
val got = in.readChar()
if (got != c) error(s"'$c'", got, trace)
}
Expand All @@ -411,12 +403,7 @@ object Lexer {
case _ => false
}

def readChars(
trace: List[JsonError],
in: OneCharReader,
expect: Array[Char],
errMsg: String
): Unit = {
def readChars(trace: List[JsonError], in: OneCharReader, expect: Array[Char], errMsg: String): Unit = {
var i: Int = 0
while (i < expect.length) {
if (in.readChar() != expect(i)) error(s"expected '$errMsg'", trace)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,34 @@ object SafeNumbersSpec extends ZIOSpecDefault {
check(genAlphaLowerString)(s => assert(SafeNumbers.bigDecimal(s))(isNone))
}
),
suite("BigInt")(
test("valid BigInt edge cases") {
val inputs = List(
"0",
"0123",
"-123",
"-9223372036854775807",
"9223372036854775806",
"-9223372036854775809",
"9223372036854775808"
)

check(Gen.fromIterable(inputs)) { s =>
assert(SafeNumbers.bigInt(s))(isSome(equalTo(BigInt(s))))
}
},
test("invalid BigInt edge cases") {
val inputs = List("0e+1", "01E-1", "0.1", "", "1 ")

check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInt(s))(isNone))
},
test("valid BigInt") {
check(genBigInteger)(i => assert(SafeNumbers.bigInt(i.toString, 2048))(isSome(equalTo(BigInt(i)))))
},
test("invalid BigInt") {
check(genAlphaLowerString)(s => assert(SafeNumbers.bigInt(s))(isNone))
}
),
suite("BigInteger")(
test("valid BigInteger edge cases") {
val inputs = List(
Expand All @@ -67,19 +95,15 @@ object SafeNumbersSpec extends ZIOSpecDefault {
)

check(Gen.fromIterable(inputs)) { s =>
assert(SafeNumbers.bigInteger(s))(
isSome(
equalTo(new java.math.BigInteger(s))
)
)
assert(SafeNumbers.bigInteger(s))(isSome(equalTo(new java.math.BigInteger(s))))
}
},
test("invalid BigInteger edge cases") {
val inputs = List("0e+1", "01E-1", "0.1", "", "1 ")

check(Gen.fromIterable(inputs))(s => assert(SafeNumbers.bigInteger(s))(isNone))
},
test("valid big Integer") {
test("valid BigInteger") {
check(genBigInteger)(i => assert(SafeNumbers.bigInteger(i.toString, 2048))(isSome(equalTo(i))))
},
test("invalid BigInteger") {
Expand Down

0 comments on commit cc97fbe

Please sign in to comment.