web-dev-qa-db-fra.com

Trouver le premier élément qui satisfait la condition X dans un Seq

Généralement, comment trouver le premier élément satisfaisant une certaine condition dans un Seq?

Par exemple, j'ai une liste de format de date possible, et je veux trouver le résultat analysé du premier format peut analyser ma chaîne de date.

val str = "1903 January"
val formats = List("MMM yyyy", "yyyy MMM", "MM yyyy", "MM, yyyy")
  .map(new SimpleDateFormat(_))
formats.flatMap(f => {try {
  Some(f.parse(str))
}catch {
  case e: Throwable => None
}}).head

Pas mal. Mais 1. c'est un peu moche. 2. il a fait un travail inutile (essayé "MM yyyy" et "MM, yyyy" formats). Peut-être existe-t-il une manière plus élégante et idiomatique? (en utilisant Iterator?)

33
Lai Yu-Hsuan

Si vous êtes sûr qu'au moins un format sera réussi:

formats.view.map{format => Try(format.parse(str)).toOption}.filter(_.isDefined).head

Si vous voulez être un peu plus sûr:

formats.view.map{format => Try(format.parse(str)).toOption}.find(_.isDefined)

Try a été introduit dans Scala 2.10.

A view est un type de collection qui calcule les valeurs paresseusement. Il appliquera le code de Try à autant d'éléments de la collection que nécessaire pour trouver le premier défini. Si le premier format s'applique à la chaîne, il n'essaiera pas d'appliquer les formats restants à la chaîne.

17
Infinity

Vous devez utiliser la méthode find sur les séquences. En règle générale, vous devriez préférer les méthodes intégrées, car elles peuvent être optimisées pour une séquence spécifique.

Console println List(1,2,3,4,5).find( _ == 5)
res: Some(5)

Autrement dit, pour renvoyer le premier SimpleDateFormat qui correspond:

 val str = "1903 January"
 val formats = List("MMM yyyy", "yyyy MMM", "MM yyyy", "MM, yyyy")
   .map(new SimpleDateFormat(_))
 formats.find { sdf => 
      sdf.parse(str, new ParsePosition(0)) != null
 }

 res: Some(Java.text.SimpleDateFormat@ef736ccd)

Pour renvoyer la première date en cours de traitement:

val str = "1903 January"
val formats = List("MMM yyyy", "yyyy MMM", "MM yyyy", "MM, yyyy").map(new SimpleDateFormat(_))
val result = formats.collectFirst { 
  case sdf if sdf.parse(str, new ParsePosition(0)) != null => sdf.parse(str)
}

ou utilisez collection paresseuse:

val str = "1903 January"
val formats = List("MMM yyyy", "yyyy MMM", "MM yyyy", "MM, yyyy").map(new SimpleDateFormat(_))
formats.toStream.flatMap { sdf =>
   Option(sdf.parse(str, new ParsePosition(0)))
}.headOption

res: Some(Thu Jan 01 00:00:00 EET 1903)
22
vitalii

Cela évite les évaluations inutiles.

formats.collectFirst{ case format if Try(format.parse(str)).isSuccess => format.parse(str) } 

Le nombre d'évaluations de la méthode parse est le nombre d'essais + 1.

10
tiran

Même version avec Scala Extractor et lazyness:

case class ParseSpec(dateString: String, formatter:DateTimeFormatter)


object Parsed {
  def unapply(parsableDate: ParseSpec): Option[LocalDate] = Try(
    LocalDate.parse(parsableDate.dateString, parsableDate.formatter)
  ).toOption
}


private def parseDate(dateString: String): Option[LocalDate] = {
  formats.view.
    map(ParseSpec(dateString, _)).
     collectFirst  { case Parsed(date: LocalDate) => date }
}
3
Laurent Valdes

Utilisez simplement la méthode find comme elle retourne une option du premier élément correspondant au prédicat le cas échéant :

formats.find(str => Try(format.parse(str)).isSuccess)

De plus, l'exécution s'arrête à la première correspondance, de sorte que vous n'essayez pas d'analyser tous les éléments de votre ensemble avant de choisir le premier. Voici un exemple :

def isSuccess(t: Int) = {
  println(s"Testing $t")
  Math.floorMod(t, 3) == 0
}
isSuccess: isSuccess[](val t: Int) => Boolean

List(10, 20, 30, 40, 50, 60, 70, 80, 90).filter(isSuccess).headOption
Testing 10
Testing 20
Testing 30
Testing 40
Testing 50
Testing 60
Testing 70
Testing 80
Testing 90
res1: Option[Int] = Some(30)

Stream(10, 20, 30, 40, 50, 60, 70, 80, 90).filter(isSuccess).headOption
Testing 10
Testing 20
Testing 30
res2: Option[Int] = Some(30)

List(10, 20, 30, 40, 50, 60, 70, 80, 90).find(isSuccess)
Testing 10
Testing 20
Testing 30
res0: Option[Int] = Some(30)

Notez que pour Stream, cela n'a pas vraiment d'importance. De plus, si vous utilisez IntelliJ par exemple, il vous suggérera:

Remplacez le filtre et headOption par find.
Avant:

seq.filter(p).headOption  

Après:

seq.find(p)
2
teikitel
scala> def parseOpt(fmt: SimpleDateFormat)(str: String): Option[Date] =
     |   Option(fmt.parse(str, new ParsePosition(0)))
tryParse: (str: String, fmt: Java.text.SimpleDateFormat)Option[Java.util.Date]

scala> formats.view.flatMap(parseOpt(fmt)).headOption
res0: Option[Java.util.Date] = Some(Thu Jan 01 00:00:00 GMT 1903)

Soit dit en passant, puisque SimpleDateFormat n'est pas compatible avec les threads, cela signifie que le code ci-dessus n'est pas non plus compatible avec les threads!

2
Ben James

Je pense que l'utilisation de la récursivité de la queue est bien meilleure et de loin la solution la plus efficace proposée jusqu'ici:

implicit class ExtendedIterable[T](iterable: Iterable[T]) {
  def findFirst(predicate: (T) => Boolean): Option[T] = {
    @tailrec
    def findFirstInternal(remainingItems: Iterable[T]): Option[T] = {
      if (remainingItems.nonEmpty)
        if (predicate(remainingItems.head))
          Some(remainingItems.head)
        else
          findFirstInternal(remainingItems.tail)
      else
        None
    }
    findFirstInternal(iterable)
  }
}

Cela vous permettrait lors de l'importation de la classe ci-dessus de faire simplement quelque chose comme ce qui suit partout où vous en avez besoin:

formats.findFirst(format => Try(format.parse(str)).isSuccess)

Bonne chance!

1
Ori Cohen