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
?)
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.
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)
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.
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 }
}
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)
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!
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!