Find the first element that satisfies condition X

2020-01-30 07:33发布

Generally, how to find the first element satisfying certain condition in a Seq?

For example, I have a list of possible date format, and I want to find the parsed result of first one format can parse my date string.

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

Not bad. But 1. it's a little ugly. 2. it did some unnecessary work(tried "MM yyyy" and "MM, yyyy" formats). Perhaps there is more elegant and idiomatic way? (using Iterator?)

7条回答
老娘就宠你
2楼-- · 2020-01-30 07:50

If you're confident at least one will format will succeed:

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

If you want to be a bit safer:

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

Try was introduced in Scala 2.10.

A view is a type of collection that computes values lazily. It will apply the code within the Try to only as many items in the collection as is necessary to find the first one that is defined. If the first format applies to the string, then it won't try to apply the remaining formats to the string.

查看更多
啃猪蹄的小仙女
3楼-- · 2020-01-30 07:52

This prevents the unnecessary evaluations.

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

The number of evaluations of the parse method is number of tries + 1.

查看更多
姐就是有狂的资本
4楼-- · 2020-01-30 07:57

You should use find method on sequences. Generally you should prefer built-in methods, because they might be optimised for a specific sequence.

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

That is, to return first SimpleDateFormat that match:

 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)

To return first date that has being processed:

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

or use 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)
查看更多
家丑人穷心不美
5楼-- · 2020-01-30 07:59
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)

By the way, since SimpleDateFormat is non-thread-safe, that means the above code is not thread-safe either!

查看更多
唯我独甜
6楼-- · 2020-01-30 08:01

I think using tail recursion is much better and by far the most efficient solution offered here so far:

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

It would allow you upon importing the above class to simply do the something like the following wherever you need to:

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

Best of luck!

查看更多
欢心
7楼-- · 2020-01-30 08:15

Same version with Scala Extractor and 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 }
}
查看更多
登录 后发表回答