diff --git a/buffer/src/cc/otavia/buffer/BufferUtils.scala b/buffer/src/cc/otavia/buffer/BufferUtils.scala index 94afc316e..6e32dded1 100644 --- a/buffer/src/cc/otavia/buffer/BufferUtils.scala +++ b/buffer/src/cc/otavia/buffer/BufferUtils.scala @@ -18,13 +18,15 @@ package cc.otavia.buffer +import cc.otavia.buffer.constant.DurationConstants + import java.math.{BigInteger, MathContext} import java.nio.charset.StandardCharsets import java.time.format.DateTimeParseException import java.time.{Duration as JDuration, *} import java.util.UUID import scala.annotation.switch -import scala.concurrent.duration.Duration +import scala.concurrent.duration.* import scala.jdk.CollectionConverters.* import scala.language.unsafeNulls @@ -1498,6 +1500,60 @@ object BufferUtils { JDuration.ofSeconds(seconds, nano.toLong) } + final def writeDurationAsString(buffer: Buffer, duration: Duration): Unit = { + if (duration eq Duration.Undefined) buffer.writeBytes(DurationConstants.Undefined) + else if (duration == Duration.Inf) buffer.writeBytes(DurationConstants.Inf) + else if (duration == Duration.MinusInf) buffer.writeBytes(DurationConstants.MinusInf) + else { + writeLongAsString(buffer, duration.length) + buffer.writeByte(' ') + duration.unit match + case DAYS => buffer.writeBytes(DurationConstants.DAY_BYTES) + case HOURS => buffer.writeBytes(DurationConstants.HOUR_BYTES) + case MINUTES => buffer.writeBytes(DurationConstants.MINUTE_BYTES) + case SECONDS => buffer.writeBytes(DurationConstants.SECOND_BYTES) + case MILLISECONDS => buffer.writeBytes(DurationConstants.MILLISECOND_BYTES) + case MICROSECONDS => buffer.writeBytes(DurationConstants.MICROSECOND_BYTES) + case NANOSECONDS => buffer.writeBytes(DurationConstants.NANOSECOND_BYTES) + if (duration.length != 1) buffer.writeByte('s') + } + } + + /** Parse String into Duration. Format is `""`, where whitespace is allowed before, between and after + * the parts. Infinities are designated by `"Inf"`, `"PlusInf"`, `"+Inf"`, `"Duration.Inf"` and `"-Inf"`, + * `"MinusInf"` or `"Duration.MinusInf"`. Undefined is designated by `"Duration.Undefined"`. + * + * @throws NumberFormatException + * if format is not parsable + */ + final def readStringAsDuration(buffer: Buffer): Duration = { + while (buffer.skipIfNextIs(' ')) {} + + if (buffer.nextInRange('0', '9')) { // parse FiniteDuration + val length = readStringAsLong(buffer) + while (buffer.skipIfNextIs(' ')) {} + val timeUnit = + if (buffer.skipIfNextAre(DurationConstants.DAY_BYTES)) DAYS + else if (buffer.skipIfNextAre(DurationConstants.HOUR_BYTES)) HOURS + else if (buffer.skipIfNextAre(DurationConstants.MINUTE_BYTES)) MINUTES + else if (buffer.skipIfNextAre(DurationConstants.SECOND_BYTES)) SECONDS + else if (buffer.skipIfNextAre(DurationConstants.MILLISECOND_BYTES)) MILLISECONDS + else if (buffer.skipIfNextAre(DurationConstants.MICROSECOND_BYTES)) MICROSECONDS + else if (buffer.skipIfNextAre(DurationConstants.NANOSECOND_BYTES)) NANOSECONDS + else throw new NumberFormatException(s"Duration format error at buffer index ${buffer.readerOffset}") + + if (length != 1) buffer.skipIfNextIs('s') + Duration(length, timeUnit) + } else { // parse Infinite Duration + if (buffer.skipIfNextAre(DurationConstants.Undefined)) Duration.Undefined + else if (buffer.skipIfNextAre(DurationConstants.Inf)) Duration.Inf + else if (buffer.skipIfNextAre(DurationConstants.MinusInf)) Duration.MinusInf + else if (DurationConstants.InfArray.exists(bts => buffer.skipIfNextAre(bts))) Duration.Inf + else if (DurationConstants.MinusInfArray.exists(bts => buffer.skipIfNextAre(bts))) Duration.MinusInf + else throw new NumberFormatException(s"Duration format error at buffer index ${buffer.readerOffset}") + } + } + private def write2Digits(buffer: Buffer, q0: Int, ds: Array[Short]): Unit = buffer.writeShortLE(ds(q0)) diff --git a/buffer/src/cc/otavia/buffer/constant/DurationConstants.scala b/buffer/src/cc/otavia/buffer/constant/DurationConstants.scala new file mode 100644 index 000000000..372d9bbf8 --- /dev/null +++ b/buffer/src/cc/otavia/buffer/constant/DurationConstants.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2022 Yan Kun + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cc.otavia.buffer.constant + +import java.nio.charset.StandardCharsets +import scala.language.unsafeNulls + +object DurationConstants { + + val Undefined: Array[Byte] = "Duration.Undefined".getBytes(StandardCharsets.US_ASCII) + val Inf: Array[Byte] = "Duration.Inf".getBytes(StandardCharsets.US_ASCII) + val MinusInf: Array[Byte] = "Duration.MinusInf".getBytes(StandardCharsets.US_ASCII) + + val DAY_BYTES: Array[Byte] = "day".getBytes(StandardCharsets.US_ASCII) + val HOUR_BYTES: Array[Byte] = "hour".getBytes(StandardCharsets.US_ASCII) + val MINUTE_BYTES: Array[Byte] = "minute".getBytes(StandardCharsets.US_ASCII) + val SECOND_BYTES: Array[Byte] = "second".getBytes(StandardCharsets.US_ASCII) + val MILLISECOND_BYTES: Array[Byte] = "millisecond".getBytes(StandardCharsets.US_ASCII) + val MICROSECOND_BYTES: Array[Byte] = "microsecond".getBytes(StandardCharsets.US_ASCII) + val NANOSECOND_BYTES: Array[Byte] = "nanosecond".getBytes(StandardCharsets.US_ASCII) + + val InfArray: Array[Array[Byte]] = Array( + "Inf".getBytes(StandardCharsets.US_ASCII), + "PlusInf".getBytes(StandardCharsets.US_ASCII), + "+Inf".getBytes(StandardCharsets.US_ASCII) + ) + + val MinusInfArray: Array[Array[Byte]] = + Array("MinusInf".getBytes(StandardCharsets.US_ASCII), "-Inf".getBytes(StandardCharsets.US_ASCII)) + +} diff --git a/buffer/test/src/cc/otavia/buffer/BufferUtilsSuite.scala b/buffer/test/src/cc/otavia/buffer/BufferUtilsSuite.scala index f11c9341d..303a390c0 100644 --- a/buffer/test/src/cc/otavia/buffer/BufferUtilsSuite.scala +++ b/buffer/test/src/cc/otavia/buffer/BufferUtilsSuite.scala @@ -692,6 +692,33 @@ class BufferUtilsSuite extends AnyFunSuiteLike { } + test("Duration") { + val buffer = allocator.allocate() + + val undefined = Duration.Undefined + BufferUtils.writeDurationAsString(buffer, undefined) + assert(BufferUtils.readStringAsDuration(buffer) eq undefined) + buffer.compact() + assert(buffer.readableBytes == 0) + + val durations = Seq(Duration.Zero, Duration.MinusInf, Duration.Inf) + for (duration <- durations) { + BufferUtils.writeDurationAsString(buffer, duration) + assert(BufferUtils.readStringAsDuration(buffer) == duration) + buffer.compact() + assert(buffer.readableBytes == 0) + } + + 0 to 10000 foreach { i => + val duration = Duration.fromNanos(Random.nextLong(10000000000L)) + BufferUtils.writeDurationAsString(buffer, duration) + assert(BufferUtils.readStringAsDuration(buffer) == duration) + buffer.compact() + assert(buffer.readableBytes == 0) + } + + } + test("Period") { val buffer = allocator.allocate() diff --git a/serde-json/src/cc/otavia/json/JsonHelper.scala b/serde-json/src/cc/otavia/json/JsonHelper.scala index 7736d23de..9c7ce7041 100644 --- a/serde-json/src/cc/otavia/json/JsonHelper.scala +++ b/serde-json/src/cc/otavia/json/JsonHelper.scala @@ -187,6 +187,12 @@ private[json] object JsonHelper { out.writeByte('\"') } + final def serializeDuration(duration: Duration, out: Buffer): Unit = { + out.writeByte('\"') + BufferUtils.writeDurationAsString(out, duration) + out.writeByte('\"') + } + final def serializeInstant(instant: Instant, out: Buffer): Unit = { out.writeByte('\"') BufferUtils.writeInstantAsString(out, instant) @@ -278,7 +284,19 @@ private[json] object JsonHelper { duration } - final def deserializeDuration(in: Buffer): Duration = ??? + /** Reads a JSON string value into a [[Duration]] instance. + * + * @param in + * The [[Buffer]] to read. + * @return + * a [[Duration]] instance of the parsed JSON value. + */ + final def deserializeDuration(in: Buffer): Duration = { + assert(in.skipIfNextIs('\"'), s"except \" but get ${in.readByte.toChar}") + val duration = BufferUtils.readStringAsDuration(in) + assert(in.skipIfNextIs('\"'), s"except \" but get ${in.readByte.toChar}") + duration + } final def deserializeInstant(in: Buffer): Instant = { assert(in.skipIfNextIs('\"'), s"except \" but get ${in.readByte.toChar}") diff --git a/serde-json/src/cc/otavia/json/JsonSerde.scala b/serde-json/src/cc/otavia/json/JsonSerde.scala index ef75734c2..7f6e60cfe 100644 --- a/serde-json/src/cc/otavia/json/JsonSerde.scala +++ b/serde-json/src/cc/otavia/json/JsonSerde.scala @@ -165,6 +165,11 @@ trait JsonSerde[A] extends Serde[A] with SerdeOps { this } + override protected def serializeDuration(duration: Duration, out: Buffer): JsonSerde.this.type = { + JsonHelper.serializeDuration(duration, out) + this + } + override final protected def serializeInstant(instant: Instant, out: Buffer): this.type = { JsonHelper.serializeInstant(instant, out) this @@ -302,9 +307,6 @@ object JsonSerde { given JsonSerde[ZoneId] = ZoneIdJsonSerde given JsonSerde[ZoneOffset] = ZoneOffsetJsonSerde given JsonSerde[UUID] = UUIDJsonSerde - given JsonSerde[Locale] = LocaleJsonSerde - given JsonSerde[Currency] = CurrencyJsonSerde - given JsonSerde[Money] = MoneyJsonSerde given JsonSerde[String] = StringJsonSerde /** Derives a [[JsonSerde]] for JSON values for the specified type [[T]]. diff --git a/serde-json/src/cc/otavia/json/types/CurrencyJsonSerde.scala b/serde-json/src/cc/otavia/json/types/CurrencyJsonSerde.scala deleted file mode 100644 index 28e02b149..000000000 --- a/serde-json/src/cc/otavia/json/types/CurrencyJsonSerde.scala +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022 Yan Kun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cc.otavia.json.types - -import cc.otavia.buffer.Buffer -import cc.otavia.json.JsonSerde - -import java.util.Currency - -/** [[JsonSerde]] for [[Currency]] */ -object CurrencyJsonSerde extends JsonSerde[Currency] { - - override final def serialize(value: Currency, out: Buffer): Unit = this.serializeCurrency(value, out) - - override final def deserialize(in: Buffer): Currency = this.deserializeCurrency(in) - -} diff --git a/serde-json/src/cc/otavia/json/types/LocaleJsonSerde.scala b/serde-json/src/cc/otavia/json/types/LocaleJsonSerde.scala deleted file mode 100644 index 546ddcf7c..000000000 --- a/serde-json/src/cc/otavia/json/types/LocaleJsonSerde.scala +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2022 Yan Kun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cc.otavia.json.types - -import cc.otavia.buffer.Buffer -import cc.otavia.json.JsonSerde - -import java.util.Locale - -/** [[JsonSerde]] for [[Locale]]. */ -object LocaleJsonSerde extends JsonSerde[Locale] { - - override final def serialize(value: Locale, out: Buffer): Unit = this.serializeLocale(value, out) - - override final def deserialize(in: Buffer): Locale = this.deserializeLocale(in) - -} diff --git a/serde-json/src/cc/otavia/json/types/MoneyJsonSerde.scala b/serde-json/src/cc/otavia/json/types/MoneyJsonSerde.scala deleted file mode 100644 index 78d92eb38..000000000 --- a/serde-json/src/cc/otavia/json/types/MoneyJsonSerde.scala +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2022 Yan Kun - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package cc.otavia.json.types - -import cc.otavia.buffer.Buffer -import cc.otavia.datatype.Money -import cc.otavia.json.JsonSerde - -/** [[JsonSerde]] for [[Money]]. */ -object MoneyJsonSerde extends JsonSerde[Money] { - - override final def serialize(value: Money, out: Buffer): Unit = this.serializeMoney(value, out) - - override final def deserialize(in: Buffer): Money = this.deserializeMoney(in) - -} diff --git a/serde/src/cc/otavia/serde/SerdeOps.scala b/serde/src/cc/otavia/serde/SerdeOps.scala index ca0f7bb49..67d8414c4 100644 --- a/serde/src/cc/otavia/serde/SerdeOps.scala +++ b/serde/src/cc/otavia/serde/SerdeOps.scala @@ -304,6 +304,17 @@ trait SerdeOps { */ protected def serializeJDuration(duration: JDuration, out: Buffer): this.type + /** Serialize [[Duration]] value to [[Buffer]]. + * + * @param duration + * Value. + * @param out + * Output [[Buffer]]. + * @return + * This [[Serde]] instance. + */ + protected def serializeDuration(duration: Duration, out: Buffer): this.type + /** Serialize [[Instant]] value to [[Buffer]]. * * @param instant