2

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
}
user826955
  • 3,137
  • 2
  • 30
  • 71

5 Answers5

2

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))
mukesh210
  • 2,792
  • 2
  • 19
  • 41
1

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
}

}

anuj saxena
  • 279
  • 1
  • 7
  • This approach does not quite work with `val persons = List(Person("Tom"), Person("Mike"), Person("Dude"),Person("Frank"),Person("Joe"))`. In this case it should return `None` for the left neighbour, and `Some("Mike")` for the right neighbour. I tried to add those cases, but it seems like I have to put a lot of cases for it to cover everything. – user826955 Nov 23 '18 at 07:39
  • Edited my question to clarify a bit more. – user826955 Nov 23 '18 at 08:01
  • 1
    `findNeighboursV2` must solve the solution for your first comment – anuj saxena Nov 23 '18 at 19:47
0

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)
  }
}
Mahmoud Hanafy
  • 1,861
  • 3
  • 24
  • 33
  • 1
    Unfortunately, this fails when the `Seq` is `("Tom", "Mike", "Joe")` (expected result would be: `left-neighbour: None, right-neighbour: Mike`. The same way it fails for less than 3 persons in the list. So I think its not possible to do a sliding window variant. – user826955 Nov 22 '18 at 15:05
  • @MahmoudHanafy, you may try to wrap the index access with a `Try(...).toOption`. Or use pattern matching to check all cases. – Luis Miguel Mejía Suárez Nov 22 '18 at 15:38
0

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))
Metropolis
  • 2,018
  • 1
  • 19
  • 36
0
// 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

Felix
  • 8,385
  • 10
  • 40
  • 59