Skip to content

Instantly share code, notes, and snippets.

@calvinlfer
Last active October 31, 2022 20:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save calvinlfer/ae01d663f7ec9254289ba401d81b5e52 to your computer and use it in GitHub Desktop.
Save calvinlfer/ae01d663f7ec9254289ba401d81b5e52 to your computer and use it in GitHub Desktop.
Preliminary support for a specific range in Skunk
import skunk.Codec
import skunk.data.Type
import zio.ZIOAppDefault
import java.time.{OffsetDateTime, ZonedDateTime}
import java.time.format.{DateTimeFormatter, DateTimeFormatterBuilder, DateTimeParseException}
import java.time.temporal.ChronoField.*
import java.util.Locale
// TODO: account for closed and open intervals so like [x,y), (x,y), etc.
enum BeginInterval:
self =>
case `(`
case `[`
def render: String = self match
case `(` => "("
case `[` => "["
enum EndInterval:
self =>
case `)`
case `]`
def render: String = self match
case `)` => ")"
case `]` => "]"
final case class ZonedDateTimeRange(
startInterval: BeginInterval,
start: ZonedDateTime,
finish: ZonedDateTime,
endInterval: EndInterval
)
object ZonedDateTimeRange:
private val eraFormatter: DateTimeFormatter =
DateTimeFormatter.ofPattern(" G")
private def timeFormatter(precision: Int): DateTimeFormatter =
val requiredPart: DateTimeFormatterBuilder =
new DateTimeFormatterBuilder()
.appendValue(HOUR_OF_DAY, 2)
.appendLiteral(':')
.appendValue(MINUTE_OF_HOUR, 2)
.appendLiteral(':')
.appendValue(SECOND_OF_MINUTE, 2)
if (precision > 0) {
requiredPart.optionalStart
.appendFraction(NANO_OF_SECOND, 0, precision, true)
.optionalEnd
}
requiredPart.toFormatter(Locale.US)
private val localDateFormatterWithoutEra: DateTimeFormatter =
new DateTimeFormatterBuilder()
.appendValue(YEAR_OF_ERA)
.appendLiteral('-')
.appendValue(MONTH_OF_YEAR, 2)
.appendLiteral('-')
.appendValue(DAY_OF_MONTH, 2)
.toFormatter(Locale.US)
private def offsetDateTimeFormatter(precision: Int): DateTimeFormatter =
new DateTimeFormatterBuilder()
.append(localDateFormatterWithoutEra)
.appendLiteral(' ')
.append(timeFormatter(precision))
.appendOffset("+HH:mm", "Z")
.appendOptional(eraFormatter)
.toFormatter(Locale.US)
private def extractRange(raw: String): Array[String] =
raw.foldLeft(Array(""))((acc, next) =>
next match
case '"' =>
acc
case '[' | ']' =>
acc :+ "C" :+ ""
case '(' | ')' =>
acc :+ "O" :+ ""
case ',' =>
acc :+ ""
case other =>
acc(acc.length - 1) += other
acc
)
val skunkCodec: Codec[ZonedDateTimeRange] =
val precision = 6
val formatter = offsetDateTimeFormatter(precision)
val parseZDT: CharSequence => ZonedDateTime =
OffsetDateTime.parse(_, formatter).toZonedDateTime
val parseBeginInterval: String => BeginInterval =
case "C" => BeginInterval.`[`
case _ => BeginInterval.`(`
val parseEndInterval: String => EndInterval =
case "C" => EndInterval.`]`
case _ => EndInterval.`)`
Codec.simple[ZonedDateTimeRange](
encode = range =>
val startI = range.startInterval.render
val start = formatter.format(range.start.toOffsetDateTime)
val finish = formatter.format(range.finish.toOffsetDateTime)
val finishI = range.endInterval.render
s"$startI$start, $finish$finishI",
decode = raw =>
val rawRange = extractRange(raw)
try
val startI = parseBeginInterval(rawRange(1))
val start = parseZDT(rawRange(2))
val finish = parseZDT(rawRange(3))
val finishI = parseEndInterval(rawRange(4))
Right(ZonedDateTimeRange(startI, start, finish, finishI))
catch
case e: DateTimeParseException =>
Left(e.getMessage)
,
oid = Type.tstzrange
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment