web-dev-qa-db-ja.com

Seqで条件Xを満たす最初の要素を見つける

一般に、Seqで特定の条件を満たす最初の要素を見つける方法は?

たとえば、使用可能な日付形式のリストがあり、最初の1つの形式の解析結果を検索して、日付文字列を解析できます。

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

悪くない。しかし、1。それは少し 'いです。 2.いくつかの不要な作業を行いました("MM yyyy"および"MM, yyyy"形式を試しました)。おそらくもっとエレガントで慣用的な方法がありますか? (Iterator?を使用)

33
Lai Yu-Hsuan

少なくとも1つのフォーマットが成功すると確信している場合:

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

もっと安全にしたい場合:

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

Try は、Scala 2.10で導入されました。

view は、値を遅延計算するコレクションの一種です。 Try内のコードを、定義されている最初のアイテムを見つけるために必要なコレクション内のアイテムと同じ数だけに適用します。最初のformatが文字列に適用される場合、残りの形式を文字列に適用しようとしません。

17
Infinity

シーケンスにはfindメソッドを使用する必要があります。一般に、組み込みのメソッドは特定のシーケンス用に最適化される可能性があるため、組み込みのメソッドを好むはずです。

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

つまり、最初に一致するSimpleDateFormatを返すには:

 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)

処理中の最初の日付を返すには:

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)
}

またはlazy collectionを使用します:

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

これにより、不必要な評価が防止されます。

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

parseメソッドの評価の数は、試行回数+ 1です。

10
tiran

Scala Extractorと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

Findメソッドを もしあれば述語に一致する最初の要素のOptionを返す として使用します:

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

さらに、最初の一致で実行が停止するため、最初の要素を選択する前にセットのすべての要素を解析しようとすることはありません。以下に例を示します。

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)

ストリームの場合、実際には問題ではないことに注意してください。また、たとえばIntelliJを使用している場合は、次のことが推奨されます。

FilterとheadOptionをfindに置き換えます。
前:

seq.filter(p).headOption  

後:

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)

ところで、SimpleDateFormatはスレッドセーフではないため、上記のコードもスレッドセーフではありません。

2
Ben James

末尾再帰を使用する方がはるかに優れており、これまでのところここで提供されている最も効率的なソリューションだと思います。

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)
  }
}

上記のクラスをインポートすると、必要に応じて次のようなことを簡単に実行できます。

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

幸運を祈ります!

1
Ori Cohen