user826955
user826955

Reputation: 3206

Find person and immediate neighbours in Seq[Person]

Given a Seq[Person], which contains 1-n Persons (and the minimum 1 Person beeing "Tom"), what is the easiest approach to find a Person with name "Tom" as well as the Person right before Tome and the Person right after Tom?

More detailed explanation:

case class Person(name:String)

The list of persons can be arbitrarily long, but will have at least one entry, which must be "Tom". So those lists can be a valid case:

val caseOne =   Seq(Person("Tom"), Person("Mike"), Person("Dude"),Person("Frank"))
val caseTwo =   Seq(Person("Mike"), Person("Tom"), Person("Dude"),Person("Frank"))
val caseThree = Seq(Person("Tom"))
val caseFour =  Seq(Person("Mike"), Person("Tom"))

You get the idea. Since I already have "Tom", the task is to get his left neighbour (if it exists), and the right neighbour (if it exists).

What is the most efficient way to achieve to do this in scala?


My current approach:

var result:Tuple2[Option[Person], Option[Person]] = (None,None)

for (i <- persons.indices)
{
  persons(i).name match
  {
    case "Tom" if i > 0 && i < persons.size-1 => result = (Some(persons(i-1)), Some(persons(i+1))) // (...), left, `Tom`, right, (...)
    case "Tom" if i > 0                       => result = (Some(persons(i-1)), None)               // (...), left, `Tom`
    case "Tom" if i < persons.size-1          => result = (Some(persons(i-1)), None)               // `Tom`, right, (...)
    case "Tom"                                => result = (None, None)                             // `Tom`
  }
}

Just doesn't feel like I am doing it the scala way.


Solution by Mukesh prajapati:

val arrayPersons = persons.toArray
val index = arrayPersons.indexOf(Person("Tom"))

if (index >= 0)
   result = (arrayPersons.lift(index-1), arrayPersons.lift(index+1))

Pretty short, seems to cover all cases.


Solution by anuj saxena

result = persons.sliding(3).foldLeft((Option.empty[Person], Option.empty[Person]))
{
    case ((Some(prev), Some(next)), _)            => (Some(prev), Some(next))
    case (_, prev :: Person(`name`) :: next :: _) => (Some(prev), Some(next))
    case (_, _ :: prev :: Person(`name`) :: _)    => (Some(prev), None)
    case (_, Person(`name`) :: next :: _)         => (None, Some(next))
    case (neighbours, _) => neighbours
}

Upvotes: 2

Views: 83

Answers (5)

Felix
Felix

Reputation: 8495

// Start writing your ScalaFiddle code here
case class Person(name: String)

val persons1 = Seq(Person("Martin"),Person("John"),Person("Tom"),Person("Jack"),Person("Mary"))
val persons2 = Seq(Person("Martin"),Person("John"),Person("Tom"))
val persons3 = Seq(Person("Tom"),Person("Jack"),Person("Mary"))
val persons4 = Seq(Person("Tom"))

def f(persons:Seq[Person]) = 
  persons
    .sliding(3)
    .filter(_.contains(Person("Tom")))
    .maxBy {
      case _ :: Person("Tom") :: _ => 1
      case _                       => 0
    }
    .toList
    .take(persons.indexOf(Person("Tom")) + 2) // In the case where "Tom" is first, drop the last person
    .drop(persons.indexOf(Person("Tom")) - 1) // In the case where "Tom" is last, drop the first person

println(f(persons1)) // List(Person(John), Person(Tom), Person(Jack))
println(f(persons2)) // List(Person(John), Person(Tom))
println(f(persons3)) // List(Person(Tom), Person(Jack))
println(f(persons4)) // List(Person(Tom))

Scalafiddle

Upvotes: 0

anuj saxena
anuj saxena

Reputation: 279

A rule of thumb: we should never access the content of a list / seq using indexes as it is prone to errors (like IndexNotFoundException).

If we want to use indexes, we better use Array as it provides us random access.

So to the current solution, here is my code to find prev and next element of a certain data in a Seq or List:

  def findNeighbours(name: String, persons: Seq[Person]): Option[(Person, Person)] = {
    persons.sliding(3).flatMap{
      case prev :: person :: next :: Nil if person.name == name => Some(prev, next)
      case _ => None
    }.toList.headOption
  }

Here the return type is in Option because there is a possibility that we may not find it here (in case of only one person is in the list or the required person is not in the list).

This code will pick the pair on the first occurrence of the person provided in the parameter.

If you have a probability that there might be several occurrences for the provided person, remove the headOption in the last line of the function findNeighbours. Then it will return a List of tuples.

Update

If Person is a case class then we can use deep match like this:

  def findNeighbours(name: String, persons: Seq[Person]): Option[(Person, Person)] = {
    persons.sliding(3).flatMap{
      case prev :: Person(`name`) :: next :: Nil => Some(prev, next)
      case _ => None
    }.toList.headOption
  }

For your solution need to add more cases to it (cchanged it to use foldleft in case of a single answer):

  def findNeighboursV2(name: String, persons: Seq[Person]): (Option[Person], Option[Person]) = {
persons.sliding(3).foldLeft((Option.empty[Person], Option.empty[Person])){
    case ((Some(prev), Some(next)), _) => (Some(prev), Some(next))
    case (_, prev :: Person(`name`) :: next :: _) => (Some(prev), Some(next))
    case (_, _ :: prev :: Person(`name`) :: _) => (Some(prev), None)
    case (_, Person(`name`) :: next :: _) => (None, Some(next))
    case (neighbours, _) => neighbours
}

}

Upvotes: 1

mukesh210
mukesh210

Reputation: 2892

First find out index where "Tom" is present, then use "lift". "lift" turns partial function into a plain function returning an Option result:

index = persons.indexOf("Tom")
doSomethingWith(persons.lift(index-1), persons.lift(index+1))

Upvotes: 2

Metropolis
Metropolis

Reputation: 2128

If you know that there will be only one instance of "Tom" in your Seq use indexOf instead of looping by hand:

tomIndex = persons.indexOf("Tom")
doSomethingWith(persons(tomIndex-1), persons(tomIndex+1))

Upvotes: 0

Mahmoud Hanafy
Mahmoud Hanafy

Reputation: 1897

You can use sliding function:

persons: Seq[Person] = initializePersons()
persons.sliding(size = 3).find { itr =>
  if (itr(1).name = "Tom") {
    val before = itr(0)
    val middle = itr(1)
    val after = itr(2)
  }
}

Upvotes: 0

Related Questions