Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unique constraint for repeated values

I am trying to define a form in play! 2.0.4 with the following properties and constraints:

  1. The form handles repeated values (lets conveniently assume that these values are of type number). So this will get us to something like this:

    "numbers" -> list(number)
    
  2. Each number must be unique, i.e. it must be unique with regard to all the other numbers submitted and it must be unique to the numbers that are already existent in the database (this can be checked via some function check(num: Int): Boolean).

  3. The form error should be specific to the number, that is not unique. I don't want a general form error saying "There is duplicate number".

What would be the best way to go?

like image 832
Fynn Avatar asked Nov 17 '12 16:11

Fynn


2 Answers

The trick here is to define a custom Constraint sort of like this example. The custom Constraint can then be used on the Mapping[T] to verify a field in the form with the verifying method.

The custom Constraint contains the logic to return a ValidationResult that is either Valid or Invalid. An error message can be passed to an Invalid result which is where you can specify what is duplicated or exists in the database.

See Play for Scala for a section on custom validation.

- Create the Constraint

  //Make this lazy to prevent java.lang.ExceptionInInitializerError at runtime.
  lazy val uniqueNumbersConstraint = Constraint[String](Some("Unique numbers constraint"), "")(checkNumbers)

  //"Business Logic". 
  //Important part here is that the function returns a ValidationResult and complies with the signature for Constraint. i.e. f: (T) => ValidationResult
  //Return Valid if n in numbers is not in database and there are no duplicates.   
  //Otherwise return Invalid and an error message showing what numbers are in the database or duplicated. 
  def checkNumbers(numbers: String):ValidationResult  = {
    val splitNums = numbers.split(" ").toList.map(_.toInt)
    val dbnums  = splitNums.partition(database.contains(_))
    if(dbnums._1.isEmpty && uniquesAndDuplicates(splitNums)._2.isEmpty){
      Valid
    }else{
      val duplicates = uniquesAndDuplicates(dbnums._2)._2
      val error = "Database contains: " + dbnums._1 + ", duplicated values: " + duplicates
      Invalid(error)
    }
  }

- Validate Form Using Custom Constraint

  val helloForm = Form(
    tuple(
      "numbers" -> nonEmptyText.verifying(uniqueNumbersConstraint)
    ))

- Utilities

  //Return unique values on left side and duplicate values on right side
  def uniquesAndDuplicates(numbers: List[Int]):Tuple2[List[Int], List[Int]] = {
    numbers.partition(i => numbers.indexOf (i) == numbers.lastIndexOf(i))
  }

  def checkNum(num: Int) = {
    database.contains(num) 
  }

  val database = List(5,6,7)

- etc

Note I defined numbers as a String in the form. When I defined it as list(number) it kept evaluating to List(). I think that is a binding issue. It's a fairly simple change to use List(1,2,3) instead of "1 2 3" if using list(number) works.

- Samples

enter image description hereenter image description hereenter image description here

like image 122
Brian Avatar answered Sep 29 '22 07:09

Brian


How about something like:

def validateUnique(input: List[Int]): ValidationResult = {

  // Assuming check return true if the input num doesn't exist yet
  def check(num: Int): Boolean = num % 2 == 0
  val unique = input.toSet
  val dbDuplicates = unique.filterNot(check)
  val formDuplicates = input.diff(unique.toSeq)
  val duplicates = (dbDuplicates ++ formDuplicates).toList

  duplicates match {
    case List() => Valid
    case _ => Invalid("Duplicates: " + duplicates.mkString(", "))
  }
}

val uniqueConstraint = Constraint[List[Int]](validateUnique(_))

And then you can just use the new constraint with:

mapping(
  ...,
  "ints" -> list(number).verifying(uniqueConstraint)
  ...
like image 20
Joel Arnold Avatar answered Sep 29 '22 07:09

Joel Arnold