Skip to content

Commit

Permalink
Merge branch 'main' into feature/607-csv-case-class-defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
ybasket authored Jul 15, 2024
2 parents 8ab5c64 + fef8818 commit cc8de9b
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 28 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ This project builds using [sbt][sbt].
* build the documentation: `sbt ;documentation/mdoc; makeSite`
* run benchmarks (you can provide [JMH][jmh] arguments in the end): `sbt benchmarksJVM/jmh:run`

If you don't already have `sbt`, or if you'd like an isolated environment for development on this project, you may use the Nix shell.
For that, you must have the [Nix package manager][nix-download] installed on your machine, and you need to enable [Nix flakes][nix-flakes] and [Nix command][nix-command].
With those prerequisites, from this project's root folder you just need to run `nix develop`.
If you just want to enable the experimental Nix features (command and flakes) locally and temporarily, add the `--extra-experimental-features nix-command` and the `--extra-experimental-features flakes` option/argument pairs to the `nix develop` command.

[fs2]: https://fs2.io/
[sbt]: https://scala-sbt.org
[jmh]: https://openjdk.java.net/projects/code-tools/jmh/
[website]: https://fs2-data.gnieh.org
[nix-download]: https://nixos.org/download/
[nix-command]: https://nixos.wiki/wiki/Nix_command
[nix-flakes]: https://nixos.wiki/wiki/Flakes
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ val scala212 = "2.12.19"
val scala213 = "2.13.14"
val scala3 = "3.3.3"
val fs2Version = "3.10.2"
val circeVersion = "0.14.7"
val circeVersion = "0.14.8"
val circeExtrasVersion = "0.14.2"
val playVersion = "3.0.3"
val playVersion = "3.0.4"
val shapeless2Version = "2.3.11"
val shapeless3Version = "3.4.1"
val scalaJavaTimeVersion = "2.6.0"
Expand Down
3 changes: 3 additions & 0 deletions csv/shared/src/main/scala/fs2/data/csv/CellEncoder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ object CellEncoder
@inline
def fromToString[A]: CellEncoder[A] = _.toString

@inline
def fromShow[A](implicit ev: Show[A]): CellEncoder[A] = instance(ev.show)

// Primitives
implicit val unitEncoder: CellEncoder[Unit] = _ => ""
implicit val booleanEncoder: CellEncoder[Boolean] = fromToString(_)
Expand Down
13 changes: 12 additions & 1 deletion csv/shared/src/test/scala/fs2/data/csv/CellEncoderTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import weaver._

import scala.concurrent.duration._

import cats.Show

object CellEncoderTest extends SimpleIOSuite {

// CellEncoder should have implicit instances available for standard types
Expand Down Expand Up @@ -53,7 +55,7 @@ object CellEncoderTest extends SimpleIOSuite {
CellEncoder[java.time.ZoneId]
CellEncoder[java.time.ZoneOffset]

pureTest("CellEncoder should decode standard types correctly") {
pureTest("CellEncoder should encode standard types correctly") {
expect(CellEncoder[Unit].apply(()) == "") and
expect(CellEncoder[Int].apply(78) == "78") and
expect(CellEncoder[Boolean].apply(true) == "true") and
Expand All @@ -73,4 +75,13 @@ object CellEncoderTest extends SimpleIOSuite {
.apply(java.time.LocalTime.of(13, 4, 29)) == "13:04:29")
}

pureTest("CellEncoder instance can be built from native cats.Show instance") {
expect(CellEncoder.fromShow[Double].apply(3.54) == "3.54")
}

pureTest("CellEncoder instance can be built from local cats.Show instance") {
implicit val showInt42: Show[Int] = Show.show(_ => "42")
expect(CellEncoder.fromShow[Int].apply(78) == "42")
}

}
143 changes: 143 additions & 0 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions flake.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
inputs = {
typelevel-nix.url = "github:typelevel/typelevel-nix";
nixpkgs.follows = "typelevel-nix/nixpkgs";
flake-utils.follows = "typelevel-nix/flake-utils";
};

outputs = { self, nixpkgs, flake-utils, typelevel-nix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
inherit system;
overlays = [ typelevel-nix.overlays.default ];
};
in
{
devShell = pkgs.devshell.mkShell {
imports = [ typelevel-nix.typelevelShell ];
name = "fs2-data-shell";
typelevelShell = {
jdk.package = pkgs.jdk11;
};
};
}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,22 +50,18 @@ private[internal] object FormatParsers {
length match {
case 4 =>
requireBytes(4, ctx).map { res =>
res.accumulate(v => MsgpackItem.Timestamp32(v))
res.accumulate(v => MsgpackItem.Timestamp32(v.toInt(false)))
}
case 8 =>
requireBytes(8, ctx).map { res =>
val result = res.result
val seconds = result & hex"00000003ffffffff"
val nanosec = result >> 34

res.toContext.prepend(MsgpackItem.Timestamp64(nanosec.drop(4), seconds.drop(3)))
res.accumulate(v => MsgpackItem.Timestamp64(v.toLong(false)))
}
case 12 =>
for {
res <- requireBytes(4, ctx)
nanosec = res.result
nanosec = res.result.toInt(false)
res <- requireBytes(8, res.toContext)
seconds = res.result
seconds = res.result.toLong(false)
} yield res.toContext.prepend(MsgpackItem.Timestamp96(nanosec, seconds))
case _ => Pull.raiseError(new MsgpackParsingException(s"Invalid timestamp length: ${length}"))
}
Expand Down Expand Up @@ -120,4 +116,64 @@ private[internal] object FormatParsers {
}
}
}

def parseFloat32[F[_]](ctx: ParserContext[F])(implicit
F: RaiseThrowable[F]): Pull[F, MsgpackItem, ParserContext[F]] = {
requireBytes(4, ctx).map {
_.accumulate { v =>
MsgpackItem.Float32 {
val raw = v.toInt(false)
val sign = if ((raw & 0x80000000) == 0x80000000) -1 else 1
val biasedExponent = (raw & 0x7f800000) >>> 23

// subnormal or zero
if (biasedExponent == 0) {
val mantissa = (raw & 0x007fffff).toFloat
if (mantissa == 0) 0F
else sign * Math.pow(2, -126).toFloat * (mantissa / 0x800000)
// Inf or NaN
} else if (biasedExponent == 0xff) {
val mantissa = raw & 0x007fffff
if (mantissa == 0) sign * Float.PositiveInfinity
else Float.NaN
// normal
} else {
val exponent = (biasedExponent - 127).toDouble
val mantissa = (raw & 0x007fffff).toFloat + 0x800000
sign * Math.pow(2, exponent).toFloat * (mantissa / 0x800000)
}
}
}
}
}

def parseFloat64[F[_]](ctx: ParserContext[F])(implicit
F: RaiseThrowable[F]): Pull[F, MsgpackItem, ParserContext[F]] = {
requireBytes(8, ctx).map {
_.accumulate { v =>
MsgpackItem.Float64 {
val raw = v.toLong(false)
val sign = if ((raw & 0x8000000000000000L) == 0x8000000000000000L) -1 else 1
val biasedExponent = (raw & 0x7ff0000000000000L) >>> 52

// subnormal or zero
if (biasedExponent == 0) {
val mantissa = (raw & 0xfffffffffffffL).toDouble
if (mantissa == 0) 0D
else sign * Math.pow(2, -1022) * (mantissa / 0x10000000000000L)
// Inf or NaN
} else if (biasedExponent == 0x7ff) {
val mantissa = raw & 0xfffffffffffffL
if (mantissa == 0) sign * Double.PositiveInfinity
else Double.NaN
// normal
} else {
val exponent = (biasedExponent - 1023).toDouble
val mantissa = (raw & 0xfffffffffffffL).toDouble + 0x10000000000000L
sign * Math.pow(2, exponent) * (mantissa / 0x10000000000000L)
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ private[low] object ItemParser {
case Headers.Ext8 => parsePlainExt(1, ctx)
case Headers.Ext16 => parsePlainExt(2, ctx)
case Headers.Ext32 => parsePlainExt(4, ctx)
case Headers.Float32 => parseSimpleType(MsgpackItem.Float32(_))(4, ctx)
case Headers.Float64 => parseSimpleType(MsgpackItem.Float64(_))(8, ctx)
case Headers.Float32 => parseFloat32(ctx)
case Headers.Float64 => parseFloat64(ctx)
case Headers.Uint8 => parseSimpleType(MsgpackItem.UnsignedInt(_))(1, ctx)
case Headers.Uint16 => parseSimpleType(MsgpackItem.UnsignedInt(_))(2, ctx)
case Headers.Uint32 => parseSimpleType(MsgpackItem.UnsignedInt(_))(4, ctx)
Expand Down
22 changes: 17 additions & 5 deletions msgpack/src/main/scala/fs2/data/msgpack/low/model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ object MsgpackItem {
case class SignedInt(bytes: ByteVector) extends MsgpackItem

/** Single precision IEE 754 float */
case class Float32(bytes: ByteVector) extends MsgpackItem
case class Float32(v: Float) extends MsgpackItem

/** Double precision IEE 754 float */
case class Float64(bytes: ByteVector) extends MsgpackItem
case class Float64(v: Double) extends MsgpackItem

/** UTF-8 encoded string */
case class Str(bytes: ByteVector) extends MsgpackItem
Expand All @@ -40,9 +40,21 @@ object MsgpackItem {
case class Extension(tpe: Byte, bytes: ByteVector) extends MsgpackItem

// Predefined extension types
case class Timestamp32(seconds: ByteVector) extends MsgpackItem
case class Timestamp64(nanoseconds: ByteVector, seconds: ByteVector) extends MsgpackItem
case class Timestamp96(nanoseconds: ByteVector, seconds: ByteVector) extends MsgpackItem
case class Timestamp32(seconds: Int) extends MsgpackItem

/** Stores data in a 30-bit [[nanoseconds]] and a 34-bit [[seconds]] fields, both of which are accessible as class
* attributes. To ensure valid data length at the type level, both fields are constructed from a single 64-bit
* [[combined]] variable.
* @param combined [[nanoseconds]] and [[seconds]] combined into a signle 64-bit value
*/
case class Timestamp64(combined: Long) extends MsgpackItem {
/* We are sure that (x: Long) >> 34 fits in an int but we also need to add a mask so that we don't end up with
* a negative number.
*/
val nanoseconds: Int = (0x000000003fffffffL & (combined >> 34)).toInt
val seconds: Long = combined & 0x00000003ffffffffL
}
case class Timestamp96(nanoseconds: Int, seconds: Long) extends MsgpackItem

case object Nil extends MsgpackItem
case object True extends MsgpackItem
Expand Down
Loading

0 comments on commit cc8de9b

Please sign in to comment.