4

I don't really know how to describe what I'm doing, but this example should help:

val vals = Array( (0, true), 
                  (1, true), 
                  (2,true), 
                  (3,true), 
                  (4,false), 
                  (5, true), 
                  (6, true), 
                  (7, false), 
                  (8, true), 
                  (9,true))

I'm looking to identify the first and last elements in each of the 'true' regions, though partitioning the array when the value changes could work as well. I could do this imperatively, but what's the best way to do this in scala?

fbl
  • 2,840
  • 3
  • 33
  • 41

6 Answers6

5

If you don't mind adding some infrastructure to handle a groupedWhile feature, you can steal from Rex Kerr's answer on extending scala collection. Use the section that handles array, in the second part of the answer.

Then it's a breeze:

scala> vals.groupedWhile(_._2 == _._2).filter(_.head._2 == true).map{g => 
  (g.head, g.last)}.foreach(println)

((0,true),(3,true))
((5,true),(6,true))
((8,true),(9,true))

Edit:

I came up with a solution that does not require groupedWhile. It's based on using Iterator.iterate which starts from a seed and repeatedly applies the span function to extract the next group of elements that have the same boolean property. In this instance the seed is a tuple of the next group and the remainder to process:

type Arr = Array[(Int, Boolean)] // type alias for easier reading

val res = Iterator.iterate[(Arr, Arr)]((Array(), vals)){ case (same, rest) => 
  // repeatedly split in (same by boolean, rest of data)
  // by using span and comparing against head
  rest.span(elem => elem._2 == rest.head._2)
}.drop(1).takeWhile{ case (same, _) =>        // drop initial empty seed array
  same.nonEmpty                               // stop when same becomes empty
}.collect{ case (same, _) if same.head._2 == true =>
  // keep "true" groups and extract (first, last)
  (same.head, same.last)                     
}.foreach(println)                            // print result

Which prints the same result as above. Note that span for empty arrays does not call the predicate, so we don't get an exception on rest.head if rest is empty.

Community
  • 1
  • 1
huynhjl
  • 41,520
  • 14
  • 105
  • 158
  • I ended up using a derivative of this solution. Using Span repeatedly worked pretty well. – fbl Nov 16 '11 at 17:11
2

Well, there are plenty questions about partitioning lists into sublists. They are usually recursive, though iterative solutions also exist. Once you have that, getting first and last is trivial.

In this particular case, since you are discarding all the false sublists, you may specialize. For example:

def regions(l: Seq[(Int, Boolean)]): Seq[(Int, Int)] = 
  if (l.isEmpty) Seq.empty
  else if (!l.head._2) regions(l dropWhile (!_._2))
  else {
    val (first, rest) = l span (_._2)
    (first.head._1, first.last._1) +: regions(rest)
  }

Iteratively I'd probably use unfold from Scalaz. The code looks pretty much the same:

def regions(l: Seq[(Int, Boolean)]): Seq[(Int, Int)] = {
  l.unfold[Seq, (Int, Int)] { ll =>
    val tl = ll dropWhile (!_._2)
    if (tl.isEmpty) None
    else {
      val (first, rest) = tl span (_._2)
      Some(((first.head._1, first.last._1), rest))
    }
  }
}
Daniel C. Sobral
  • 295,120
  • 86
  • 501
  • 681
  • I like the unfold version, I think it's easier to understand once you get the concept. Unfortunately it suffers from stack overflow for large number of results (~10000) :( Note that there's a small typo (mismatch paren in the Some line). The recursive version is also not tail recursive, but I think it can easily be made tail recursive. – huynhjl Nov 16 '11 at 15:04
2

For a non-recursive solution you could do this:

val ss = (0, false) +: vals :+ (0, false) sliding 2 toList
val starts = ss filter (i => !i(0)._2 && i(1)._2) map (_(1))
val ends   = ss filter (i => !i(1)._2 && i(0)._2) map (_(0))

which gives you lists of start and end elements.

For regions, you can just zip the lists together:

scala> starts zip ends foreach println
((0,true),(3,true))
((5,true),(6,true))
((8,true),(9,true))
Luigi Plinge
  • 50,650
  • 20
  • 113
  • 180
2
@tailrec
def regions(l: Seq[(Int, Boolean)], nl : Seq[(Int, Int)]): Seq[(Int, Int)] = 
    if (l.isEmpty) nl
    else if (!l.head._2) regions(l dropWhile (!_._2), nl)
    else {
        val (first, rest) = l span (_._2)
        regions(rest, (first.head._1, first.last._1) +: nl)
    }

Taking Daniel's original answer and making tail recursive.

sjj
  • 59
  • 2
1

Using foldLeft:

type Region = (Int, Boolean)
type RegionPair = (Region, Region)
type Acc = (Option[Region], Region, List[RegionPair])
val initAcc: Acc = (None, (0, false), Nil) //optional first, dummy last, empty regions

def groupRegions(s: Acc, region: Region): Acc = {
  val (first, last, regions) = s
  first match {
    case Some(f) => 
      if (!region._2) (None, last, (f, last) :: regions)
      else (first, region, regions)
    case _ => (Some(region), region, regions)
  } 
}

val (remFirst, remLast, regions) = (initAcc /: vals)(groupRegions)

val solution = remFirst map ((_, remLast) :: regions) getOrElse regions //reverse if you want
Marimuthu Madasamy
  • 13,126
  • 4
  • 30
  • 52
1

Solution with fold :

val result = vals.foldLeft(List(List.empty[Int])) {
  case (head::tail, (x, true)) => (x::head)::tail
  case (xs, (_, false)) => Nil::xs
}.map { xs => xs.head::xs.last::Nil }
result: List[List[Int]] = List(List(9, 8), List(6, 5), List(3, 0))

each time we encounter false, we add a new List in the accumulator.

The following version is even more efficient (but maybe not as clear) :

val result = vals.foldLeft(List(List.empty[Int])) {
  case (head::tail, (x, true)) => head match {
    case Nil => (x::Nil)::tail
    case last::Nil => (x::last::Nil)::tail
    case first::last => (x::last)::tail
  }
  case (xs, (_, false)) => Nil::xs
}
result: List[List[Int]] = List(List(9, 8), List(6, 5), List(3, 0))
David
  • 2,399
  • 20
  • 17