Filling in desired lines in Scala

Viewed 90

I currently have a value of result that is a string which represents cycles in a graph

> scala result
String =
0:0->52->22;
5:5->70->77;
8:8->66->24;8->42->32;
. //
. // trimmed to get by point across
. //
71:71->40->45;
77:77->34->28;77->5->70;
84:84->22->29

However, I want to have the output have the numbers in between be included and up to a certain value included. The example code would have value = 90

0:0->52->22;
1:
2:
3:
4:
5:5->70->77;
6:
7:
8:8->66->24;8->42->32;
. //
. // trimmed
. //
83:
84:84->22->29;
85:
86:
87:
88:
89:
90:

If it helps or makes any difference, this value is changed to a list for later purposes, such like

list_result = result.split("\n").toList
List[String] = List(0:0->52->22;, 5:5->70->77;, 8:8->66->24;8->42->32;, 11:11->26->66;11->17->66;

My initial thought was to insert the missing numbers into the list and then sort it, but I had trouble with the sorting so I instead look here for a better method.

3 Answers

Turn your list_result into a Map with default values. Then walk through the desired number range, exchanging each for its Map value.

val map_result: Map[String,List[String]] =
  list_result.groupBy("\\d+:".r.findFirstIn(_).getOrElse("bad"))
             .withDefault(List(_))

val full_result: String =
  (0 to 90).flatMap(n => map_result(s"$n:")).mkString("\n")

Here's a Scastie session to see it in action.

One option would be to use a Map as an intermediate data structure:


val l: List[String] = List("0:0->52->22;", "5:5->70->77;", "8:8->66->24;8->42->32;", "11:11->26->66;11->17->66;")
val byKey: List[Array[String]] = l.map(_.split(":"))

val stop = 90
val mapOfValues = (1 to stop).map(_->"").toMap

val output = byKey.foldLeft(mapOfValues)((acc, nxt) => acc + (nxt.head.toInt -> nxt.tail.head))
output.toList.sorted.map {case (key, value) => println(s"$key, $value")}

This will give you the output you are after. It breaks your input strings into pseudo key-value pairs, creates a map to hold the results, inserts the elements of byKey into the map, then returns a sorted list of the results.

Note: If you are using this in anything like production code you'd need to properly check that each Array in byKey does have two elements to prevent any nullPointerExceptions with the later calls to head and tail.head.

The provided solutions are fine, but I would like to suggest one that can process the data lazily and doesn't need to keep all data in memory at once.

It uses a nice function called unfold, which allows to "unfold" a collection from a starting state, up to a point where you deem the collection to be over (docs).

It's not perfectly polished but I hope it may help:

def readLines(s: String): Iterator[String] =
  util.Using.resource(io.Source.fromString(s))(_.getLines)

def emptyLines(from: Int, until: Int): Iterator[(String)] =
  Iterator.range(from, until).map(n => s"$n:")

def indexOf(line: String): Int =
  Integer.parseInt(line.substring(0, line.indexOf(':')))

def withDefaults(from: Int, to: Int, it: Iterator[String]): Iterator[String] = {
  Iterator.unfold((from, it)) { case (n, lines) =>
    if (lines.hasNext) {
      val next = lines.next()
      val i = indexOf(next)
      Some((emptyLines(n, i) ++ Iterator.single(next), (i + 1, lines)))
    } else if (n < to) {
      Some((emptyLines(n, to + 1), (to, lines)))
    } else {
      None
    }
  }.flatten
}

You can see this in action here on Scastie.

What unfold does is start from a state (in this case, the line number from and the iterator with the lines) and at every iteration:

  1. if there are still elements in the iterator it gets the next item, identifies its index and returns:
    • as the next item an Iterator with empty lines up to the latest line number followed by the actual line
      • e.g. when 5 is reached the empty lines between 1 and 4 are emitted, terminated by the line starting with 5
    • as the next state, the index of the line after the last in the emitted item and the iterator itself (which, being stateful, is consumed by the repeated calls to unfold at each iteration)
      • e.g. after processing 5, the next state is 6 and the iterator
  2. if there are no elements in the iterator anymore but the to index has not been reached, it emits another Iterator with the remaining items to be printed (in your example, those after 84)
  3. if both conditions are false we don't need to emit anything anymore and we can close the "unfolding" collection, signalling this by returning a None instead of Some[(Item, State)]

This returns an Iterator[Iterator[String]] where every nested iterator is a range of values from one line to the next, with the default empty lines "sandwiched" in between. The call to flatten turns it into the desired result.

I used an Iterator to make sure that only the essential state is kept in memory at any time and only when it's actually used.

Related