Skip to content

Commit

Permalink
Merge pull request #327 from keirlawson/add-time-codecs
Browse files Browse the repository at this point in the history
Add codecs for LocalTime
  • Loading branch information
bplommer authored Apr 28, 2021
2 parents ee67154 + fd510eb commit 0f82a83
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 2 deletions.
45 changes: 44 additions & 1 deletion modules/core/src/main/scala/vulcan/Codec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import cats.implicits._
import java.io.{ByteArrayInputStream, ByteArrayOutputStream}
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.time.{Instant, LocalDate}
import java.time.{Instant, LocalDate, LocalTime}
import java.util.concurrent.TimeUnit
import java.util.UUID
import org.apache.avro.{Conversions, LogicalTypes, Schema, SchemaBuilder}
import org.apache.avro.generic._
Expand Down Expand Up @@ -710,6 +711,48 @@ object Codec extends CodecCompanionCompat {
}
)

/**
* @group JavaTime
*/
final val localTimeMillis: Codec.Aux[java.lang.Integer, LocalTime] =
Codec.instanceForTypes(
"Integer",
"LocalTime",
Right(LogicalTypes.timeMillis().addToSchema(SchemaBuilder.builder().intType())), {
localTime =>
val millis = TimeUnit.NANOSECONDS.toMillis(localTime.toNanoOfDay())
Right(java.lang.Integer.valueOf(millis.toInt))
}, {
case (int: java.lang.Integer, schema) =>
val logicalType = schema.getLogicalType()
if (logicalType == LogicalTypes.timeMillis()) {
val nanos = TimeUnit.MILLISECONDS.toNanos(int.toLong)
Right(LocalTime.ofNanoOfDay(nanos))
} else Left(AvroError.decodeUnexpectedLogicalType(logicalType))
}
)

/**
* @group JavaTime
*/
final val localTimeMicros: Codec.Aux[java.lang.Long, LocalTime] =
Codec.instanceForTypes(
"Long",
"LocalTime",
Right(LogicalTypes.timeMicros().addToSchema(SchemaBuilder.builder().longType())), {
localTime =>
val micros = TimeUnit.NANOSECONDS.toMicros(localTime.toNanoOfDay())
Right(java.lang.Long.valueOf(micros))
}, {
case (long: java.lang.Long, schema) =>
val logicalType = schema.getLogicalType()
if (logicalType == LogicalTypes.timeMicros()) {
val nanos = TimeUnit.MICROSECONDS.toNanos(long)
Right(LocalTime.ofNanoOfDay(nanos))
} else Left(AvroError.decodeUnexpectedLogicalType(logicalType))
}
)

/**
* @group General
*/
Expand Down
115 changes: 114 additions & 1 deletion modules/core/src/test/scala/vulcan/CodecSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import cats.data._
import cats.implicits._
import java.nio.ByteBuffer
import java.nio.charset.StandardCharsets
import java.time.{Instant, LocalDate}
import java.time.{Instant, LocalDate, LocalTime}
import java.util.concurrent.TimeUnit
import java.time.temporal.ChronoUnit
import java.util.UUID

import org.apache.avro.{Conversions, LogicalTypes, Schema, SchemaBuilder}
Expand Down Expand Up @@ -1121,6 +1123,117 @@ final class CodecSpec extends BaseSpec with CodecSpecHelpers {
}
}

describe("localTimeMillis") {
implicit val codec: Codec[LocalTime] = Codec.localTimeMillis
describe("schema") {
it("should be encoded as int with logical type time-millis") {
assertSchemaIs[LocalTime] {
"""{"type":"int","logicalType":"time-millis"}"""
}
}
}

describe("encode") {
it("should encode as int") {
val value = LocalTime.now()
assertEncodeIs[LocalTime](
value,
Right(java.lang.Integer.valueOf(TimeUnit.NANOSECONDS.toMillis(value.toNanoOfDay()).toInt))
)
}
}

describe("decode") {
it("should error if schema is not int") {
assertDecodeError[LocalTime](
unsafeEncode(LocalTime.now()),
unsafeSchema[Long],
"Error decoding LocalTime: Got unexpected schema type LONG, expected schema type INT"
)
}

it("should error if logical type is not time-millis") {
assertDecodeError[LocalTime](
unsafeEncode(LocalTime.now()),
unsafeSchema[Int],
"Error decoding LocalTime: Got unexpected missing logical type"
)
}

it("should error if value is not int") {
assertDecodeError[LocalTime](
unsafeEncode(123L),
unsafeSchema[LocalTime],
"Error decoding LocalTime: Got unexpected type java.lang.Long, expected type Integer"
)
}

it("should decode int as local time-millis") {
val value = LocalTime.now()
assertDecodeIs[LocalTime](
unsafeEncode(value),
Right(value.truncatedTo(ChronoUnit.MILLIS))
)
}
}
}

describe("localTimeMicros") {
implicit val codec: Codec[LocalTime] = Codec.localTimeMicros
describe("schema") {
it("should be encoded as int with logical type time-micros") {
assertSchemaIs[LocalTime] {
"""{"type":"long","logicalType":"time-micros"}"""
}
}
}

describe("encode") {
it("should encode as long") {
val value = LocalTime.now()
assertEncodeIs[LocalTime](
value,
Right(java.lang.Long.valueOf(TimeUnit.NANOSECONDS.toMicros(value.toNanoOfDay())))
)
}
}

describe("decode") {
it("should error if schema is not int") {
assertDecodeError[LocalTime](
unsafeEncode(LocalTime.now()),
unsafeSchema[Int],
"Error decoding LocalTime: Got unexpected schema type INT, expected schema type LONG"
)
}

it("should error if logical type is not time-micros") {
assertDecodeError[LocalTime](
unsafeEncode(LocalTime.now()),
unsafeSchema[Long],
"Error decoding LocalTime: Got unexpected missing logical type"
)
}

it("should error if value is not long") {
assertDecodeError[LocalTime](
unsafeEncode(123),
unsafeSchema[LocalTime],
"Error decoding LocalTime: Got unexpected type java.lang.Integer, expected type Long"
)
}

it("should decode int as local time-micros") {
val value = LocalTime.now()
assertDecodeIs[LocalTime](
unsafeEncode(value),
Right(value.truncatedTo(ChronoUnit.MICROS))
)
}
}
}


describe("long") {
describe("schema") {
it("should be encoded as long") {
Expand Down

0 comments on commit 0f82a83

Please sign in to comment.