From 00986e55babc7b08b2f311273e44dcc2dd2c99a5 Mon Sep 17 00:00:00 2001
From: "Diego E. Alonso Blas" <diesalbla@gmail.com>
Date: Wed, 3 Feb 2021 23:14:45 +0000
Subject: [PATCH] Integrate cats-time into cats-kernel.

In [Issue 3766](https://github.com/typelevel/cats/issues/3766) it was
proposed to add java.time instances to cats itself, considering that
`java.time` is just as standard as UUID or the Scala library.

Within the companion objects of the typeclasses `Eq`, `Order`, and
`Hash`, we define new "mixin" traits that provide implementations
of each type-class based on the Any-hash, Any-eq, or Comparable.
These are only used to define simple instances of many typeclasses.
With that, each class in `java.time` has an instance of the kernel
typeclasses written in two lines, and they all fit in one file.

We also add instances of Show in the `core` module, which are
just based on the `toString` method.

- Duration Test: add a generator for durations
---
 .../main/scala-2.12/cats/instances/all.scala  |  1 +
 .../main/scala-2.13+/cats/instances/all.scala |  1 +
 .../main/scala/cats/instances/javatime.scala  | 22 +++++++
 .../scala/cats/kernel/laws/LawTests.scala     | 66 +++++++++++++++++++
 kernel/src/main/scala/cats/kernel/Eq.scala    | 16 +++--
 kernel/src/main/scala/cats/kernel/Hash.scala  | 20 ++++--
 kernel/src/main/scala/cats/kernel/Order.scala | 17 +++--
 .../kernel/instances/JavaTimeInstances.scala  | 55 ++++++++++++++++
 .../cats/kernel/instances/UUIDInstances.scala |  4 +-
 .../cats/kernel/instances/time/package.scala  |  3 +
 10 files changed, 187 insertions(+), 18 deletions(-)
 create mode 100644 core/src/main/scala/cats/instances/javatime.scala
 create mode 100644 kernel-laws/jvm/src/test/scala/cats/kernel/laws/LawTests.scala
 create mode 100644 kernel/src/main/scala/cats/kernel/instances/JavaTimeInstances.scala
 create mode 100644 kernel/src/main/scala/cats/kernel/instances/time/package.scala

diff --git a/core/src/main/scala-2.12/cats/instances/all.scala b/core/src/main/scala-2.12/cats/instances/all.scala
index 167ca7456c..462b720d29 100644
--- a/core/src/main/scala-2.12/cats/instances/all.scala
+++ b/core/src/main/scala-2.12/cats/instances/all.scala
@@ -25,6 +25,7 @@ trait AllInstances
     with FutureInstances
     with HashInstances
     with InvariantMonoidalInstances
+    with JavaTimeInstances
     with ListInstances
     with MapInstances
     with OptionInstances
diff --git a/core/src/main/scala-2.13+/cats/instances/all.scala b/core/src/main/scala-2.13+/cats/instances/all.scala
index b8908789da..819cd592e3 100644
--- a/core/src/main/scala-2.13+/cats/instances/all.scala
+++ b/core/src/main/scala-2.13+/cats/instances/all.scala
@@ -27,6 +27,7 @@ trait AllInstances
     with FutureInstances
     with HashInstances
     with InvariantMonoidalInstances
+    with JavaTimeInstances
     with LazyListInstances
     with ListInstances
     with MapInstances
diff --git a/core/src/main/scala/cats/instances/javatime.scala b/core/src/main/scala/cats/instances/javatime.scala
new file mode 100644
index 0000000000..96c55c86de
--- /dev/null
+++ b/core/src/main/scala/cats/instances/javatime.scala
@@ -0,0 +1,22 @@
+package cats.instances
+
+import cats.Show
+import java.time._
+
+trait JavaTimeInstances extends cats.kernel.instances.JavaTimeInstances {
+
+  implicit final val catsCoreStdShowForInstant: Show[Instant] = Show.fromToString
+  implicit final val catsCoreStdShowForLocalDate: Show[LocalDate] = Show.fromToString
+  implicit final val catsCoreStdShowForLocalDateTime: Show[LocalDateTime] = Show.fromToString
+  implicit final val catsCoreStdShowForLocalTime: Show[LocalTime] = Show.fromToString
+  implicit final val catsCoreStdShowForMonth: Show[Month] = Show.fromToString
+  implicit final val catsCoreStdShowForMonthDay: Show[MonthDay] = Show.fromToString
+  implicit final val catsCoreStdShowForOffsetDateTime: Show[OffsetDateTime] = Show.fromToString
+  implicit final val catsCoreStdShowForOffsetTime: Show[OffsetTime] = Show.fromToString
+  implicit final val catsCoreStdShowForYear: Show[Year] = Show.fromToString
+  implicit final val catsCoreStdShowForYearMonth: Show[YearMonth] = Show.fromToString
+  implicit final val catsCoreStdShowForZoneId: Show[ZoneId] = Show.fromToString
+  implicit final val catsCoreStdShowForZoneOffset: Show[ZoneOffset] = Show.fromToString
+  implicit final val catsCoreStdShowForZonedDateTime: Show[ZonedDateTime] = Show.fromToString
+
+}
diff --git a/kernel-laws/jvm/src/test/scala/cats/kernel/laws/LawTests.scala b/kernel-laws/jvm/src/test/scala/cats/kernel/laws/LawTests.scala
new file mode 100644
index 0000000000..011d2c6dc0
--- /dev/null
+++ b/kernel-laws/jvm/src/test/scala/cats/kernel/laws/LawTests.scala
@@ -0,0 +1,66 @@
+package cats.kernel
+package laws
+
+import cats.kernel.instances.time._
+import cats.kernel.laws.discipline._
+import java.time._
+import munit.DisciplineSuite
+import org.scalacheck.{Arbitrary, Gen}
+
+class JvmTests extends TestsConfig with DisciplineSuite {
+
+  // Java Time instances
+  checkAll("Eq[LocalDate]", EqTests[LocalDateTime].eqv)
+  checkAll("Eq[LocalDateTime]", EqTests[LocalDateTime].eqv)
+  checkAll("Eq[ZoneId]", EqTests[ZoneId].eqv)
+  checkAll("Eq[ZonedDateTime]", EqTests[ZonedDateTime].eqv)
+  checkAll("Order[java.time.Duration]", OrderTests[Duration].order)
+  checkAll("Order[Instant]", OrderTests[Instant].order)
+  checkAll("Order[LocalTime]", OrderTests[LocalTime].order)
+  checkAll("Order[Month]", OrderTests[Month].order)
+  checkAll("Order[MonthDay]", OrderTests[MonthDay].order)
+  checkAll("Order[OffsetDateTime]", OrderTests[OffsetDateTime].order)
+  checkAll("Order[OffsetTime]", OrderTests[OffsetTime].order)
+  checkAll("Order[Year]", OrderTests[Year].order)
+  checkAll("Order[YearMonth]", OrderTests[YearMonth].order)
+  checkAll("Order[ZoneOffset]", OrderTests[ZoneOffset].order)
+
+  checkAll("Hash[java.time.Duration]", HashTests[java.time.Duration].hash)
+  checkAll("Hash[Instant]", HashTests[Instant].hash)
+  checkAll("Hash[LocalDate]", HashTests[LocalDate].hash)
+  checkAll("Hash[LocalDateTime]", HashTests[LocalDateTime].hash)
+  checkAll("Hash[LocalTime]", HashTests[LocalTime].hash)
+  checkAll("Hash[Month]", HashTests[Month].hash)
+  checkAll("Hash[MonthDay]", HashTests[MonthDay].hash)
+  checkAll("Hash[OffsetDateTime]", HashTests[OffsetDateTime].hash)
+  checkAll("Hash[OffsetTime]", HashTests[OffsetTime].hash)
+  checkAll("Hash[Year]", HashTests[Year].hash)
+  checkAll("Hash[YearMonth]", HashTests[YearMonth].hash)
+  checkAll("Hash[ZoneId]", HashTests[ZoneId].hash)
+  checkAll("Hash[ZoneOffset]", HashTests[ZoneOffset].hash)
+  checkAll("Hash[ZonedDateTime]", HashTests[ZonedDateTime].hash)
+
+  checkAll(
+    "Monoid[Duration]", {
+      implicit val arbitraryFiniteDuration: Arbitrary[Duration] = {
+        // max range is +/- 292 years, but we give ourselves some extra headroom
+        // to ensure that we can add these things up. they crash on overflow.
+        val n = (292L * 365) / 500
+        Arbitrary(
+          Gen.oneOf(
+            Gen.choose(-n, n).map(Duration.ofDays),
+            Gen.choose(-n * 24L, n * 24L).map(Duration.ofHours),
+            Gen.choose(-n * 1440L, n * 1440L).map(Duration.ofMinutes),
+            Gen.choose(-n * 86400L, n * 86400L).map(Duration.ofSeconds),
+            Gen.choose(-n * 86400000L, n * 86400000L).map(Duration.ofMillis),
+            Gen.choose(-n * 86400000000000L, n * 86400000000000L).map(Duration.ofNanos)
+          )
+        )
+      }
+
+      // For this tests we use a duration range of +/- 100 years,
+      CommutativeGroupTests[Duration].commutativeGroup
+    }
+  )
+
+}
diff --git a/kernel/src/main/scala/cats/kernel/Eq.scala b/kernel/src/main/scala/cats/kernel/Eq.scala
index 7de7eb694d..a04803d9ed 100644
--- a/kernel/src/main/scala/cats/kernel/Eq.scala
+++ b/kernel/src/main/scala/cats/kernel/Eq.scala
@@ -103,10 +103,18 @@ object Eq
    * This can be useful for case classes, which have reasonable `equals`
    * implementations
    */
-  def fromUniversalEquals[A]: Eq[A] =
-    new Eq[A] {
-      def eqv(x: A, y: A) = x == y
-    }
+  def fromUniversalEquals[A]: Eq[A] = new FromUniversal[A] {}
+
+  /**
+   * An Eq[A] based on the eq method defined in the `Any` type.
+   *
+   * NOTE: this should only be used as a mixin to define multiple instances
+   * in a given type. If you are only interested on the `Eq` instance,
+   * you should use the method above instead.
+   */
+  trait FromUniversal[A] extends Eq[A] {
+    def eqv(x: A, y: A) = x == y
+  }
 
   /**
    * Everything is the same
diff --git a/kernel/src/main/scala/cats/kernel/Hash.scala b/kernel/src/main/scala/cats/kernel/Hash.scala
index 87b2917e38..8980701815 100644
--- a/kernel/src/main/scala/cats/kernel/Hash.scala
+++ b/kernel/src/main/scala/cats/kernel/Hash.scala
@@ -46,14 +46,22 @@ object Hash extends HashFunctions[Hash] {
     }
 
   /**
-   * Constructs a `Hash` instance by using the universal `hashCode` function and the universal equality relation.
+   * Constructs a `Hash` instance by using the `hashCode` method, as well as the
+   * universal equality relation, defined in the `Any` top class
+   * https://www.scala-lang.org/api/current/scala/Any.html.
    */
-  def fromUniversalHashCode[A]: Hash[A] =
-    new Hash[A] {
-      def hash(x: A) = x.hashCode()
-      def eqv(x: A, y: A) = x == y
-    }
+  def fromUniversalHashCode[A]: Hash[A] = new Eq.FromUniversal[A] with FromUniversal[A] {}
 
+  /**
+   * Single-function trait that adds the
+   *
+   * NOTE: This is only used internally to define instances of several typeclasses together,
+   * but beware: sticky toffee treackle down with it.
+   */
+  trait FromUniversal[A] extends Hash[A] {
+    self: Eq[A] => /* This means Eq instance is somewhere else */
+    def hash(x: A) = x.hashCode()
+  }
 }
 
 trait HashToHashingConversion {
diff --git a/kernel/src/main/scala/cats/kernel/Order.scala b/kernel/src/main/scala/cats/kernel/Order.scala
index 4dff327518..29e5405416 100644
--- a/kernel/src/main/scala/cats/kernel/Order.scala
+++ b/kernel/src/main/scala/cats/kernel/Order.scala
@@ -226,9 +226,16 @@ object Order extends OrderFunctions[Order] with OrderToOrderingConversion {
       override def toOrdering: Ordering[A] = ev
     }
 
-  def fromComparable[A <: Comparable[A]]: Order[A] =
-    new Order[A] {
-      override def compare(x: A, y: A): Int =
-        x.compareTo(y)
-    }
+  def fromComparable[A <: Comparable[A]]: Order[A] = new FromComparable[A] {}
+
+  /**
+   * Trait that provides an instance of Comparable
+   *
+   * NOTE: This is intended to be used when defining instances of several typeclasses.
+   * If you only want to define the instance for `Order`, `fromComparable` below is better.
+   */
+  trait FromComparable[A <: Comparable[A]] extends Order[A] {
+    def compare(x: A, y: A) = x.compareTo(y)
+  }
+
 }
diff --git a/kernel/src/main/scala/cats/kernel/instances/JavaTimeInstances.scala b/kernel/src/main/scala/cats/kernel/instances/JavaTimeInstances.scala
new file mode 100644
index 0000000000..5aaea1fe22
--- /dev/null
+++ b/kernel/src/main/scala/cats/kernel/instances/JavaTimeInstances.scala
@@ -0,0 +1,55 @@
+package cats.kernel.instances
+
+import cats.kernel.{CommutativeGroup, Eq, Hash, Order}
+import java.time._
+
+trait JavaTimeInstances {
+
+  implicit final val catsKernelStdOrderForduration
+    : Hash[Duration] with Order[Duration] with CommutativeGroup[Duration] =
+    new Order.FromComparable[Duration] with Hash.FromUniversal[Duration] with CommutativeGroup[Duration] {
+      override def empty: Duration = Duration.ZERO
+      override def combine(x: Duration, y: Duration): Duration = x.plus(y)
+      override def inverse(x: Duration): Duration = x.negated()
+    }
+
+  implicit final val catsKernelStdOrderForInstant: Order[Instant] with Hash[Instant] =
+    new Order.FromComparable[Instant] with Hash.FromUniversal[Instant] {}
+
+  implicit final val catsKernelStdEqForLocalDate: Eq[LocalDate] with Hash[LocalDate] =
+    new Eq.FromUniversal[LocalDate] with Hash.FromUniversal[LocalDate] {}
+
+  implicit final val catsKernelStdEqForLocalDateTime: Eq[LocalDateTime] with Hash[LocalDateTime] =
+    new Eq.FromUniversal[LocalDateTime] with Hash.FromUniversal[LocalDateTime] {}
+
+  implicit final val catsKernelStdOrderForLocalTime: Order[LocalTime] with Hash[LocalTime] =
+    new Order.FromComparable[LocalTime] with Hash.FromUniversal[LocalTime] {}
+
+  implicit final val catsKernelStdOrderForMonth: Order[Month] with Hash[Month] =
+    new Order.FromComparable[Month] with Hash.FromUniversal[Month] {}
+
+  implicit final val catsKernelStdOrderForMonthDay: Order[MonthDay] with Hash[MonthDay] =
+    new Order.FromComparable[MonthDay] with Hash.FromUniversal[MonthDay] {}
+
+  implicit final val catsKernelStdOrderForOffsetDateTime: Order[OffsetDateTime] with Hash[OffsetDateTime] =
+    new Order.FromComparable[OffsetDateTime] with Hash.FromUniversal[OffsetDateTime] {}
+
+  implicit final val catsKernelStdOrderForOffsetTime: Order[OffsetTime] with Hash[OffsetTime] =
+    new Order.FromComparable[OffsetTime] with Hash.FromUniversal[OffsetTime] {}
+
+  implicit final val catsKernelStdOrderForYear: Order[Year] with Hash[Year] =
+    new Order.FromComparable[Year] with Hash.FromUniversal[Year] {}
+
+  implicit final val catsKernelStdOrderForYearMonth: Order[YearMonth] with Hash[YearMonth] =
+    new Order.FromComparable[YearMonth] with Hash.FromUniversal[YearMonth] {}
+
+  implicit final val catsKernelStdEqForZoneId: Eq[ZoneId] with Hash[ZoneId] =
+    new Eq.FromUniversal[ZoneId] with Hash.FromUniversal[ZoneId] {}
+
+  implicit final val catsKernelStdOrderForZoneOffset: Order[ZoneOffset] with Hash[ZoneOffset] =
+    new Order.FromComparable[ZoneOffset] with Hash.FromUniversal[ZoneOffset] {}
+
+  implicit final val catsKernelStdOrderForZonedDateTime: Eq[ZonedDateTime] with Hash[ZonedDateTime] =
+    new Eq.FromUniversal[ZonedDateTime] with Hash.FromUniversal[ZonedDateTime] {}
+
+}
diff --git a/kernel/src/main/scala/cats/kernel/instances/UUIDInstances.scala b/kernel/src/main/scala/cats/kernel/instances/UUIDInstances.scala
index 4b8cf0b874..0b0c0768fa 100644
--- a/kernel/src/main/scala/cats/kernel/instances/UUIDInstances.scala
+++ b/kernel/src/main/scala/cats/kernel/instances/UUIDInstances.scala
@@ -5,9 +5,7 @@ import java.util.UUID
 
 trait UUIDInstances {
   implicit val catsKernelStdOrderForUUID: Order[UUID] with Hash[UUID] with LowerBounded[UUID] with UpperBounded[UUID] =
-    new Order[UUID] with Hash[UUID] with UUIDBounded { self =>
-      def compare(x: UUID, y: UUID): Int = x.compareTo(y)
-      def hash(x: UUID): Int = x.hashCode()
+    new Order.FromComparable[UUID] with Hash.FromUniversal[UUID] with UUIDBounded { self =>
       val partialOrder: PartialOrder[UUID] = self
     }
 }
diff --git a/kernel/src/main/scala/cats/kernel/instances/time/package.scala b/kernel/src/main/scala/cats/kernel/instances/time/package.scala
new file mode 100644
index 0000000000..d44fef1d3c
--- /dev/null
+++ b/kernel/src/main/scala/cats/kernel/instances/time/package.scala
@@ -0,0 +1,3 @@
+package cats.kernel.instances
+
+package object time extends JavaTimeInstances
