scalaunit-testingscalacheck

generate list of case class with int field without repeat


I want to generate a List of some class which contains several fields. One of them is Int type and it doesn’t have to repeat. Could you help me to write the code?

I tried next:

  case class Person(name: String, age: Int)

  implicit val genPerson: Gen[Person] = 
    for {
    name <- arbitrary[String]
    age <- Gen.posNum[Int]
    } yield Person(name, age)

  implicit val genListOfPerson: Gen[scala.List[Person]] = Gen.listOfN(3, genPerson)

The problem is that I got an instance of a person with equal age.


Solution

  • If you're requiring that no two Persons in the generated list have the same age, you can

    implicit def IntsArb: Arbitrary[Int] = Arbitrary(Gen.choose[Int](0, Int.MaxValue))
    implicit val StringArb: Arbitrary[String] = Arbitrary(Gen.listOfN(5, Gen.alphaChar).map(_.mkString))
    implicit val PersonGen = Arbitrary(Gen.resultOf(Person.apply _))
    implicit val PersonsGen: Arbitrary[List[Person]] =
      Arbitrary(Gen.listOfN(3, PersonGen.arbitrary).map { persons =>
        val grouped: Map[Int, List[Person]] = persons.groupBy(_.age)
        grouped.values.map(_.head) // safe because groupBy
      })
    

    Note that this will return a List with no duplicate ages but there's no guarantee that the list will have size 3 (it is guaranteed that the list will be nonempty, with size at most 3).

    If having a list of size 3 is important, at the risk of generation failing if the "dice are against you", you can have something like:

    def uniqueAges(persons: List[Person], target: Int): Gen[List[Person]] = {
      val grouped: Map[Int, List[Person]] = persons.groupBy(_.age)
      val uniquelyAged = grouped.values.map(_.head)
      val n = uniquelyAged.size
      if (n == target) Gen.const(uniquelyAged)
      else {
        val existingAges = grouped.keySet
        val genPerson = PersonGen.arbitrary.retryUntil { p => !existingAges(p.age) }
        Gen.listOf(target - n, genPerson)
          .flatMap(l => uniqueAges(l, target - n))
          .map(_ ++ uniquelyAged)
      }
    }
    
    implicit val PersonsGen: Arbitrary[List[Person]] =
      Arbitrary(Gen.listOfN(3, PersonGen.arbitrary).flatMap(l => uniqueAges(l, 3)))