From f494931116c121f58d4c73f7a77f1662ee1470f5 Mon Sep 17 00:00:00 2001 From: satorg Date: Fri, 1 Apr 2022 02:01:07 -0700 Subject: [PATCH 1/2] add compat Seq#distinctBy to 'tests' --- .../compat/ScalaVersionSpecificSyntax.scala | 28 +++++++++++ .../scala-2.12/cats/tests/compat/SeqOps.scala | 47 +++++++++++++++++++ .../compat/ScalaVersionSpecificSyntax.scala | 24 ++++++++++ .../src/test/scala/cats/tests/package.scala | 24 ++++++++++ 4 files changed, 123 insertions(+) create mode 100644 tests/shared/src/test/scala-2.12/cats/tests/compat/ScalaVersionSpecificSyntax.scala create mode 100644 tests/shared/src/test/scala-2.12/cats/tests/compat/SeqOps.scala create mode 100644 tests/shared/src/test/scala-2.13+/cats/tests/compat/ScalaVersionSpecificSyntax.scala create mode 100644 tests/shared/src/test/scala/cats/tests/package.scala diff --git a/tests/shared/src/test/scala-2.12/cats/tests/compat/ScalaVersionSpecificSyntax.scala b/tests/shared/src/test/scala-2.12/cats/tests/compat/ScalaVersionSpecificSyntax.scala new file mode 100644 index 0000000000..801eb5725b --- /dev/null +++ b/tests/shared/src/test/scala-2.12/cats/tests/compat/ScalaVersionSpecificSyntax.scala @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.tests.compat + +import scala.collection.immutable.Seq + +private[tests] trait ScalaVersionSpecificSyntax { + implicit final private[tests] def catsTestsCompatSeqOps[C[a] <: Seq[a], A](self: C[A]) = new SeqOps[C, A](self) +} diff --git a/tests/shared/src/test/scala-2.12/cats/tests/compat/SeqOps.scala b/tests/shared/src/test/scala-2.12/cats/tests/compat/SeqOps.scala new file mode 100644 index 0000000000..6c7deecf41 --- /dev/null +++ b/tests/shared/src/test/scala-2.12/cats/tests/compat/SeqOps.scala @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.tests.compat + +import scala.collection.generic.CanBuildFrom +import scala.collection.immutable.Seq +import scala.collection.mutable + +final private[tests] class SeqOps[C[a] <: Seq[a], A] private[compat] (private val self: C[A]) extends AnyVal { + + // Scala v2.12.x does not have `distinctBy` implemented. + // Therefore this implementation is copied (and adapted) from Scala Library v2.13.8 sources: + // https://github.com/scala/scala/blob/v2.13.8/src/library/scala/collection/immutable/StrictOptimizedSeqOps.scala#L26-L39 + def distinctBy[B](f: A => B)(implicit cbf: CanBuildFrom[C[A], A, C[A]]): C[A] = { + if (self.lengthCompare(1) <= 0) self + else { + val builder = cbf() + val seen = mutable.HashSet.empty[B] + val it = self.iterator + var different = false + while (it.hasNext) { + val next = it.next() + if (seen.add(f(next))) builder += next else different = true + } + if (different) builder.result() else self + } + } +} diff --git a/tests/shared/src/test/scala-2.13+/cats/tests/compat/ScalaVersionSpecificSyntax.scala b/tests/shared/src/test/scala-2.13+/cats/tests/compat/ScalaVersionSpecificSyntax.scala new file mode 100644 index 0000000000..90761716a8 --- /dev/null +++ b/tests/shared/src/test/scala-2.13+/cats/tests/compat/ScalaVersionSpecificSyntax.scala @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats.tests.compat + +private[tests] trait ScalaVersionSpecificSyntax {} diff --git a/tests/shared/src/test/scala/cats/tests/package.scala b/tests/shared/src/test/scala/cats/tests/package.scala new file mode 100644 index 0000000000..0d5cf0e3ff --- /dev/null +++ b/tests/shared/src/test/scala/cats/tests/package.scala @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2015 Typelevel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package cats + +package object tests extends cats.tests.compat.ScalaVersionSpecificSyntax From cf16b6a6fcd4dc0157d62772d88eeb3edf7d5d01 Mon Sep 17 00:00:00 2001 From: satorg Date: Fri, 1 Apr 2022 02:01:31 -0700 Subject: [PATCH 2/2] add Chain#distinctBy --- core/src/main/scala/cats/data/Chain.scala | 64 ++++++++++++++++--- .../test/scala/cats/tests/ChainSuite.scala | 6 ++ 2 files changed, 60 insertions(+), 10 deletions(-) diff --git a/core/src/main/scala/cats/data/Chain.scala b/core/src/main/scala/cats/data/Chain.scala index 9453f7b729..58e4b62aaf 100644 --- a/core/src/main/scala/cats/data/Chain.scala +++ b/core/src/main/scala/cats/data/Chain.scala @@ -27,8 +27,8 @@ import cats.kernel.instances.StaticMethods import scala.annotation.tailrec import scala.collection.immutable.SortedMap -import scala.collection.immutable.TreeSet import scala.collection.immutable.{IndexedSeq => ImIndexedSeq} +import scala.collection.mutable import scala.collection.mutable.ListBuffer import Chain.{ @@ -151,6 +151,10 @@ sealed abstract class Chain[+A] extends ChainCompat[A] { */ final def nonEmpty: Boolean = !isEmpty + // Quick check whether the chain is either empty or contains one element only. + @inline private def isEmptyOrSingleton: Boolean = + isEmpty || this.isInstanceOf[Chain.Singleton[_]] + /** * Concatenates this with `c` in O(1) runtime. */ @@ -760,19 +764,59 @@ sealed abstract class Chain[+A] extends ChainCompat[A] { /** * Remove duplicates. Duplicates are checked using `Order[_]` instance. + * + * Example: + * {{{ + * scala> import cats.data.Chain + * scala> val chain = Chain(1, 2, 2, 3) + * scala> chain.distinct + * res0: cats.data.Chain[Int] = Chain(1, 2, 3) + * }}} */ def distinct[AA >: A](implicit O: Order[AA]): Chain[AA] = { - implicit val ord: Ordering[AA] = O.toOrdering - - var alreadyIn = TreeSet.empty[AA] + if (isEmptyOrSingleton) this + else { + implicit val ord: Ordering[AA] = O.toOrdering + + val bldr = Vector.newBuilder[AA] + val seen = mutable.TreeSet.empty[AA] + val it = iterator + while (it.hasNext) { + val next = it.next() + if (seen.add(next)) + bldr += next + } + // Result can contain a single element only. + Chain.fromSeq(bldr.result()) + } + } - foldLeft(Chain.empty[AA]) { (elementsSoFar, b) => - if (alreadyIn.contains(b)) { - elementsSoFar - } else { - alreadyIn += b - elementsSoFar :+ b + /** + * Remove duplicates by a predicate. Duplicates are checked using `Order[_]` instance. + * + * Example: + * {{{ + * scala> import cats.data.Chain + * scala> val chain = Chain(1, 2, 3, 4) + * scala> chain.distinctBy(_ / 2) + * res0: cats.data.Chain[Int] = Chain(1, 2, 4) + * }}} + */ + def distinctBy[B](f: A => B)(implicit O: Order[B]): Chain[A] = { + if (isEmptyOrSingleton) this + else { + implicit val ord: Ordering[B] = O.toOrdering + + val bldr = Vector.newBuilder[A] + val seen = mutable.TreeSet.empty[B] + val it = iterator + while (it.hasNext) { + val next = it.next() + if (seen.add(f(next))) + bldr += next } + // Result can contain a single element only. + Chain.fromSeq(bldr.result()) } } diff --git a/tests/shared/src/test/scala/cats/tests/ChainSuite.scala b/tests/shared/src/test/scala/cats/tests/ChainSuite.scala index f568639e48..ad5d593aa0 100644 --- a/tests/shared/src/test/scala/cats/tests/ChainSuite.scala +++ b/tests/shared/src/test/scala/cats/tests/ChainSuite.scala @@ -380,6 +380,12 @@ class ChainSuite extends CatsSuite { } } + test("Chain#distinctBy is consistent with List#distinctBy") { + forAll { (a: Chain[Int], f: Int => String) => + assertEquals(a.distinctBy(f).toList, a.toList.distinctBy(f)) + } + } + test("=== is consistent with == (issue #2540)") { assertEquals(Chain.one(1) |+| Chain.one(2) |+| Chain.one(3), Chain.fromSeq(List(1, 2, 3)))