From e539a9fb7e259c411f692abac96322ee7bc52f57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Chantepie?= Date: Mon, 5 Sep 2022 17:57:00 +0200 Subject: [PATCH] Update coreJVMTests --- .github/workflows/ci.yml | 24 +- build.sbt | 52 +++-- .../enumeratum/values/ArgonautValueEnum.scala | 2 +- .../test/scala/enumeratum/EnumJVMSpec.scala | 38 +--- .../src/test/scala/enumeratum/Eval.scala | 25 -- .../enumeratum/values/ValueEnumJVMSpec.scala | 93 +------- project/CoreJVMTest.scala | 214 ++++++++++++++++++ 7 files changed, 264 insertions(+), 184 deletions(-) delete mode 100644 enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala create mode 100644 project/CoreJVMTest.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e59be19..5d4e5105 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,15 +13,18 @@ jobs: - java: 11 scala: 2.11.12 - java: 11 - scala: 2.12.14 + scala: 2.12.16 - java: 11 - scala: 2.13.6 + scala: 2.13.8 + - java: 11 + scala: 3.2.1-RC1 runs-on: ubuntu-latest env: SCALAJS_TEST_OPT: full # define Java options for both official sbt and sbt-extras JAVA_OPTS: -Xms6G -Xmx6G -Xss4M -XX:ReservedCodeCacheSize=256M -XX:MaxMetaspaceSize=1G -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Dfile.encoding=UTF-8 JVM_OPTS: -Xms6G -Xmx6G -Xss4M -XX:ReservedCodeCacheSize=256M -XX:MaxMetaspaceSize=1G -XX:+CMSClassUnloadingEnabled -XX:+UseConcMarkSweepGC -Dfile.encoding=UTF-8 + SBT_OPTS: -Denumeratum.devMacros steps: - name: Checkout uses: actions/checkout@v1 @@ -39,26 +42,23 @@ jobs: sbt -v ++${{ matrix.scala }} scalafmtCheck scalafmtSbtCheck scala_2_11/test:compile scala_2_11/test:doc sbt -v ++${{ matrix.scala }} scala_2_11/test ;; - 2.12.14) + 2.12.16) sbt -v ++${{ matrix.scala }} test:compile test:doc sbt -v ++${{ matrix.scala }} coverage test coverageReport sbt -v ++${{ matrix.scala }} coverageAggregate ;; - 2.13.6) + *) sbt -v ++${{ matrix.scala }} test:compile test:doc sbt -v ++${{ matrix.scala }} test ;; - *) - echo unknown Scala Version ${{ matrix.scala }} - exit 1 esac rm -rf "$HOME/.ivy2/local" || true - find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true - find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true - find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true - find $HOME/.sbt -name "*.lock" -delete || true + find $HOME/Library/Caches/Coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.ivy2/cache -name "ivydata-*.properties" -delete || true + find $HOME/.cache/coursier/v1 -name "ivydata-*.properties" -delete || true + find $HOME/.sbt -name "*.lock" -delete || true - name: Upload coverage to Codecov - if: ${{ matrix.scala == '2.12.14' }} + if: ${{ matrix.scala == '2.12.16' }} uses: codecov/codecov-action@v2 with: fail_ci_if_error: true diff --git a/build.sbt b/build.sbt index ac6548d1..dd84cb3f 100644 --- a/build.sbt +++ b/build.sbt @@ -5,8 +5,9 @@ import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject} lazy val scala_2_11Version = "2.11.12" lazy val scala_2_12Version = "2.12.16" lazy val scala_2_13Version = "2.13.8" -lazy val scala_3Version = "3.2.1-RC1" -lazy val scalaVersionsAll = Seq(scala_2_11Version, scala_2_12Version, scala_2_13Version, scala_3Version) +lazy val scala_3Version = "3.2.1-RC1" +lazy val scalaVersionsAll = + Seq(scala_2_11Version, scala_2_12Version, scala_2_13Version, scala_3Version) lazy val theScalaVersion = scala_2_12Version @@ -20,7 +21,7 @@ lazy val quillVersion = "4.1.0" def theDoobieVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor <= 11 => "0.7.1" - case Some(_) => "1.0.0-RC2" + case Some(_) => "1.0.0-RC2" case _ => throw new IllegalArgumentException(s"Unsupported Scala version $scalaVersion for Doobie") } @@ -28,7 +29,7 @@ def theDoobieVersion(scalaVersion: String) = def theArgonautVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor >= 11 => "6.2.5" - case Some(_) => "6.3.0" + case Some(_) => "6.3.0" case _ => throw new IllegalArgumentException(s"Unsupported Scala version $scalaVersion for Argonaut") @@ -37,7 +38,7 @@ def theArgonautVersion(scalaVersion: String) = def thePlayVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor >= 12 => "2.8.0" - case Some((3, _)) => "2.8.0" + case Some((3, _)) => "2.8.0" case _ => throw new IllegalArgumentException(s"Unsupported Scala version $scalaVersion for Play") } @@ -45,7 +46,7 @@ def thePlayVersion(scalaVersion: String) = def theSlickVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor <= 11 => "3.3.3" - case Some(_) => "3.3.3" + case Some(_) => "3.3.3" case _ => throw new IllegalArgumentException(s"Unsupported Scala version $scalaVersion for Slick") } @@ -53,7 +54,7 @@ def theSlickVersion(scalaVersion: String) = def theCatsVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { case Some((2, scalaMajor)) if scalaMajor <= 11 => "2.0.0" - case Some(_) => "2.6.1" + case Some(_) => "2.6.1" case _ => throw new IllegalArgumentException(s"Unsupported Scala version $scalaVersion for Cats") } @@ -70,7 +71,7 @@ def thePlayJsonVersion(scalaVersion: String) = def theCirceVersion(scalaVersion: String) = CrossVersion.partialVersion(scalaVersion) match { - case Some((3, _)) => "0.14.1" + case Some((3, _)) => "0.14.1" case Some((2, scalaMajor)) if scalaMajor >= 12 => "0.14.1" case Some((2, scalaMajor)) if scalaMajor >= 11 => "0.11.1" case _ => @@ -122,7 +123,7 @@ lazy val scala_2_13 = Project(id = "scala_2_13", base = file("scala_2_13")) // Do not publish this project (it just serves as an aggregate) publishArtifact := false, publishLocal := {}, - //doctestWithDependencies := false, // sbt-doctest is not yet compatible with this 2.13 + // doctestWithDependencies := false, // sbt-doctest is not yet compatible with this 2.13 aggregate in publish := false, aggregate in PgpKeys.publishSigned := false ) @@ -157,7 +158,7 @@ lazy val scala_2_11 = Project(id = "scala_2_11", base = file("scala_2_11")) // Do not publish this project (it just serves as an aggregate) publishArtifact := false, publishLocal := {}, - //doctestWithDependencies := false, // sbt-doctest is not yet compatible with this 2.13 + // doctestWithDependencies := false, // sbt-doctest is not yet compatible with this 2.13 aggregate in publish := false, aggregate in PgpKeys.publishSigned := false ) @@ -206,9 +207,7 @@ lazy val macros = crossProject(JSPlatform, JVMPlatform) .settings(testSettings) .jsSettings(jsTestSettings) .settings(commonWithPublishSettings) - .settings(withCompatUnmanagedSources( - jsJvmCrossProject = true, - includeTestSrcs = false)) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = true, includeTestSrcs = false)) .settings( name := "enumeratum-macros", version := Versions.Macros.head, @@ -238,9 +237,9 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .jsSettings(jsTestSettings) .settings(commonWithPublishSettings) .settings( - name := "enumeratum", - version := Versions.Core.head, - crossScalaVersions := scalaVersionsAll, + name := "enumeratum", + version := Versions.Core.head, + crossScalaVersions := scalaVersionsAll, libraryDependencies ++= { if (devMacros) { Seq.empty @@ -300,9 +299,14 @@ lazy val coreJVMTests = Project(id = "coreJVMTests", base = file("enumeratum-cor name := "coreJVMTests", version := Versions.Core.stable, crossScalaVersions := scalaVersionsAll, - libraryDependencies ++= Seq( - "org.scala-lang" % "scala-compiler" % scalaVersion.value % Test - ), + Test / sourceGenerators += CoreJVMTest.testsGenerator, + libraryDependencies += { + if (scalaBinaryVersion.value == "3") { + "org.scala-lang" %% "scala3-compiler" % scalaVersion.value % Test + } else { + "org.scala-lang" % "scala-compiler" % scalaVersion.value % Test + } + }, publishArtifact := false, publishLocal := {} ) @@ -359,9 +363,7 @@ lazy val enumeratumPlay = Project(id = "enumeratum-play", base = file("enumeratu scalaTestPlay(scalaVersion.value) ) ) - .settings(withCompatUnmanagedSources( - jsJvmCrossProject = false, - includeTestSrcs = true)) + .settings(withCompatUnmanagedSources(jsJvmCrossProject = false, includeTestSrcs = true)) .dependsOn(enumeratumPlayJsonJvm % "compile->compile;test->test") lazy val circeAggregate = aggregateProject("circe", enumeratumCirceJs, enumeratumCirceJvm) @@ -626,8 +628,8 @@ lazy val compilerSettings = Seq( ) // unused-import breaks Circe Either shim case Some((2, 11)) => base ++ Seq("-deprecation:false", "-Xlint", "-Ywarn-unused-import") - case Some((2, _)) => base ++ Seq("-Xlint") - case _ => base + case Some((2, _)) => base ++ Seq("-Xlint") + case _ => base } }, Test / scalacOptions ++= { @@ -724,7 +726,7 @@ def withCompatUnmanagedSources( ): Seq[Setting[_]] = { def compatDirs(projectbase: File, scalaVersion: String, isMain: Boolean) = { val base = if (jsJvmCrossProject) projectbase / ".." else projectbase - val cat = if (isMain) "main" else "test" + val cat = if (isMain) "main" else "test" CrossVersion.partialVersion(scalaVersion) match { case Some((3, _)) => diff --git a/enumeratum-argonaut/src/main/scala/enumeratum/values/ArgonautValueEnum.scala b/enumeratum-argonaut/src/main/scala/enumeratum/values/ArgonautValueEnum.scala index dc040a3f..8bfb3bab 100644 --- a/enumeratum-argonaut/src/main/scala/enumeratum/values/ArgonautValueEnum.scala +++ b/enumeratum-argonaut/src/main/scala/enumeratum/values/ArgonautValueEnum.scala @@ -37,7 +37,7 @@ import Argonaut._ * }}} * * @tparam ValueType - * @tparam EntryType + * @tparam EntryType */ sealed trait ArgonautValueEnum[ValueType, EntryType <: ValueEnumEntry[ValueType]] { this: ValueEnum[ValueType, EntryType] => diff --git a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala index b6ebe4c4..86428a38 100644 --- a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala +++ b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/EnumJVMSpec.scala @@ -3,39 +3,5 @@ package enumeratum import org.scalatest.funspec.AnyFunSpec import org.scalatest.matchers.should.Matchers -class EnumJVMSpec extends AnyFunSpec with Matchers { - - describe("findValues Vector") { - - // This is a fairly intense test. - it("should be in the same order that the objects were declared in") { - import scala.util._ - (1 to 100).foreach { i => - val members = Random.shuffle((1 to Random.nextInt(20)).map { m => - s"Member$m" - }) - val membersDefs = members - .map { m => - s"case object $m extends Enum$i" - } - .mkString("\n\n") - val objDefinition = - s""" - import enumeratum._ - sealed trait Enum$i extends EnumEntry - - case object Enum$i extends Enum[Enum$i] { - $membersDefs - val values = findValues - } - - Enum$i - """ - val obj = Eval.apply[Enum[_ <: EnumEntry]](objDefinition) - obj.values.map(_.entryName) shouldBe members - } - } - - } - -} +// See `projects/project/CoreJVMTest.scala`#generateEnumTests +final class EnumJVMSpec extends generated.EnumBaseSpec diff --git a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala deleted file mode 100644 index 3f8a0e59..00000000 --- a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/Eval.scala +++ /dev/null @@ -1,25 +0,0 @@ -package enumeratum - -import scala.tools.reflect.ToolBox - -/** Eval with bits and pieces stolen from here and there... - */ -object Eval { - - def apply[A]( - string: String, - compileOptions: String = s"-cp ${macroToolboxClassPath.mkString(";")}" - ): A = { - import scala.reflect.runtime.currentMirror - val toolbox = currentMirror.mkToolBox(options = compileOptions) - val tree = toolbox.parse(string) - toolbox.eval(tree).asInstanceOf[A] - } - - def macroToolboxClassPath = { - val paths = Seq( - BuildInfo.macrosJVMClassesDir - ) - paths.map(_.getAbsolutePath) - } -} diff --git a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/values/ValueEnumJVMSpec.scala b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/values/ValueEnumJVMSpec.scala index ad59cdb0..cd37ba53 100644 --- a/enumeratum-core-jvm-tests/src/test/scala/enumeratum/values/ValueEnumJVMSpec.scala +++ b/enumeratum-core-jvm-tests/src/test/scala/enumeratum/values/ValueEnumJVMSpec.scala @@ -1,91 +1,14 @@ package enumeratum.values -import enumeratum.Eval -import org.scalatest.funspec.AnyFunSpec -import org.scalatest.matchers.should.Matchers - -import scala.reflect.ClassTag -import scala.util.Random - /** Created by Lloyd on 8/30/16. * * Copyright 2016 */ -class ValueEnumJVMSpec extends AnyFunSpec with Matchers { - - private def stringGenerator = - Random.alphanumeric.grouped(10).toStream.map(_.mkString.replaceAll("[0-9]", "")).distinct - - /* - Non-deterministically generates a bunch of different types of ValueEnums and tests the ability to resolve - proper members by value - */ - testValuesOf(Stream.continually(Random.nextInt())) - testValuesOf(Stream.continually(Random.nextLong()), valueSuffix = "L") - testValuesOf( - Stream - .continually(Random.nextInt(Short.MaxValue - Short.MinValue) + Short.MinValue) - .map(_.toShort) - ) - testValuesOf( - Stream.continually(Random.nextInt(Byte.MaxValue - Byte.MinValue) + Byte.MinValue).map(_.toByte) - ) - testValuesOf(stringGenerator, "\"", "\"") - testValuesOf( - Stream.continually(Random.nextPrintableChar()).filter(c => Character.isAlphabetic(c.toInt)), - "'", - "'" - ) - - private def testValuesOf[A: ClassTag]( - valuesGenerator: => Stream[A], - valuePrefix: String = "", - valueSuffix: String = "" - ): Unit = { - - val typeName = implicitly[ClassTag[A]].runtimeClass.getSimpleName.capitalize - - describe(s"${typeName}Enum withValue") { - - it("should return proper members for valid values but throw otherwise") { - (1 to 20).foreach { i => - val enumName = s"Generated${typeName}Enum$i" - val names = stringGenerator.take(5) - val values = valuesGenerator.distinct.take(5) - val namesToValues = names.zip(values) - val memberDefs = namesToValues - .map { case (n, v) => - s"""case object $n extends $enumName($valuePrefix$v$valueSuffix)""" - } - .mkString("\n\n") - val objDef = - s""" - |import enumeratum.values._ - | - |sealed abstract class $enumName(val value: $typeName) extends ${typeName}EnumEntry - | - |object $enumName extends ${typeName}Enum[$enumName] { - | val values = findValues - | - | $memberDefs - |} - |$enumName - """.stripMargin - val obj = Eval.apply[ValueEnum[A, _ <: ValueEnumEntry[A]]](objDef) - namesToValues.foreach { case (n, v) => - obj.withValue(v).toString shouldBe n - } - // filterNot is not lazy until 2.12 - valuesGenerator.filter(a => !values.contains(a)).take(5).foreach { invalidValue => - intercept[NoSuchElementException] { - obj.withValue(invalidValue) - } - } - } - } - - } - - } - -} +// See `projects/project/CoreJVMTest.scala`#generateValueEnumTests +final class ValueEnumJVMSpec + extends generated.IntValueEnumBaseSpec + with generated.LongValueEnumBaseSpec + with generated.ShortValueEnumBaseSpec + with generated.ByteValueEnumBaseSpec + with generated.StringValueEnumBaseSpec + with generated.CharValueEnumBaseSpec diff --git a/project/CoreJVMTest.scala b/project/CoreJVMTest.scala new file mode 100644 index 00000000..d3051a07 --- /dev/null +++ b/project/CoreJVMTest.scala @@ -0,0 +1,214 @@ +import scala.util.Random + +import sbt._ +import Keys._ + +object CoreJVMTest { + lazy val testsGenerator = Def.task[Seq[File]] { + val managed = (Test / sourceManaged).value + + generateEnumTests(managed) ++ generateValueEnumTest[Int](managed, Iterator.continually(Random.nextInt())) ++ generateValueEnumTest[Long](managed, Iterator.continually(Random.nextLong()), valueSuffix = "L") ++ generateValueEnumTest[Short](managed, Iterator.continually(Random.nextInt(Short.MaxValue - Short.MinValue) + Short.MinValue).map(_.toShort)) ++ generateValueEnumTest[Byte](managed, Iterator.continually(Random.nextInt(Byte.MaxValue - Byte.MinValue) + Byte.MinValue).map(_.toByte)) ++ generateValueEnumTest[String](managed, Random.alphanumeric.grouped(10).map(_.mkString), "\"", "\"") ++ generateValueEnumTest[Char](managed, Iterator.continually(Random.nextPrintableChar()).filter(c => Character.isAlphabetic(c.toInt)), "'", "'") + + } + + private def generateEnumTests(outdir: File): Seq[File] = { + val bf = outdir / "EnumBaseSpec.scala" + + IO.writer[Seq[File]](bf, "", IO.defaultCharset, false) { w0 => + w0.append("""package generated + +trait EnumBaseSpec + extends org.scalatest.funspec.AnyFunSpec + with org.scalatest.matchers.should.Matchers +""") + + val res = (1 to 100).flatMap { i => + val enumName = s"Enum${i}" + + val ef = outdir / s"${enumName}.scala" + + IO.writer[Seq[File]](ef, "", IO.defaultCharset, false) { w1 => + // Generate enum file + + w1.append(s"""package generated + +import enumeratum._ + +sealed trait ${enumName} extends EnumEntry + +object ${enumName} extends Enum[${enumName}] { + val values = findValues + +""") + + val members = Random.shuffle(1 to Random.nextInt(20)).map { n => + val nme = s"Member$n" + + w1.append(s" case object ${nme} extends ${enumName}\n") + + nme + } + + w1.append(s"""} +""") + + // Generate tests in separate file/trait + val tf = outdir / s"${enumName}Test.scala" + + w0.append(s" with ${enumName}Test\n") + + IO.writer[Seq[File]](tf, "", IO.defaultCharset, false) { w2 => + val expectedNames = members.map('"' + _ + '"').mkString(", ") + + val expectedMembers = members.map(enumName + '.' + _).mkString(", ") + + w2.append(s"""package generated + +import org.scalatest.compatible.Assertion + +trait ${enumName}Test { _spec: EnumBaseSpec with enumeratum.EnumJVMSpec => + describe("${enumName}.findValues") { + // This is a fairly intense test. + it("should be in the same order as declaration on objects") { + ${enumName}.values.map(_.entryName).toSeq shouldBe Seq($expectedNames) + + ${enumName}.values.toSeq shouldBe Seq($expectedMembers) + } + } +} +""") + + Seq(ef, tf) + } + } + } + + w0.append(""" { self: enumeratum.EnumJVMSpec => +}""") + + bf +: res + } + } + + // --- + + private def generateValueEnumTest[A]( + outdir: File, + valuesGenerator: => Iterator[A], + valuePrefix: String = "", + valueSuffix: String = "" + )(implicit cls: scala.reflect.ClassTag[A]): Seq[File] = { + val typeName = cls.runtimeClass.getSimpleName.capitalize + + def renderValue(v: A): String = valuePrefix + v.toString + valueSuffix + + @annotation.tailrec + def genValues(rem: Int, out: IndexedSeq[A]): IndexedSeq[A] = { + if (rem == 0) { + out.reverse + } else { + val v = valuesGenerator.next() + + if (!out.contains(v)) { + genValues(rem - 1, v +: out) + } else { + genValues(rem, out) + } + } + } + + // + val bf = outdir / s"${typeName}ValueEnumBaseSpec.scala" + + IO.writer[Seq[File]](bf, "", IO.defaultCharset, false) { w0 => + w0.append(s"""package generated + +trait ${typeName}ValueEnumBaseSpec + extends org.scalatest.funspec.AnyFunSpec + with org.scalatest.matchers.should.Matchers +""") + + val res = (1 to 20).flatMap { i => + val enumName = s"${typeName}Enum$i" + val names = stringGenerator(5) + val values = genValues(5, IndexedSeq.empty) + val namesToValues = names.zip(values) + + // Generate value enum file + val ef = outdir / s"${enumName}.scala" + + IO.writer[Seq[File]](ef, "", IO.defaultCharset, false) { w1 => + w1.append(s"""package generated + +import enumeratum.values._ + +sealed abstract class $enumName( + val value: $typeName +) extends ${typeName}EnumEntry + +object $enumName extends ${typeName}Enum[$enumName] { + val values = findValues + +""") + + namesToValues.foreach { + case (n, v) => w1.append(s" case object $n extends $enumName($valuePrefix$v$valueSuffix)\n\n") + } + + w1.append("}\n") + + // Generate test in separate file + val tf = outdir / s"${enumName}Test.scala" + + w0.append(s" with ${enumName}Test\n") + + IO.writer[Seq[File]](tf, "", IO.defaultCharset, false) { w2 => + w2.append(s"""package generated + +trait ${enumName}Test { + _spec: ${typeName}ValueEnumBaseSpec with enumeratum.values.ValueEnumJVMSpec => + + describe("${enumName} withValue") { + it("should return proper members for valid values but throw otherwise") { +""") + + namesToValues.foreach { case (n, v) => + val value = renderValue(v) + + w2.append( + s""" ${enumName}.withValue($value) shouldBe ${enumName}.${n} + +""") + } + + valuesGenerator.filter(a => !values.contains(a)).take(5).foreach { invalidValue => + val value = renderValue(invalidValue) + + w2.append(s""" intercept[NoSuchElementException] { + ${enumName}.withValue($value) + } + +""") + } + + w2.append(""" } + } +} +""") + + Seq(ef, tf) + } + } + } + + w0.append(""" { self: enumeratum.values.ValueEnumJVMSpec => +}""") + + bf +: res + } + } + + private def stringGenerator(n: Int) = scala.util.Random. + alphanumeric.grouped(10).toStream. + map(_.mkString.replaceAll("[0-9]", "")).distinct.take(n).toSeq +}