I have a List of objects I want to split by a delimiter into sublists, e.g:
val tokens = listOf(
Token(name = "lorem", val = "ipsum"),
Token(name = "dolor", val = "sit"),
Token(name = "newline", val = "\n"),
Token(name = "amet", val = "consectetur")
)
The delimiter should be any Token whose name is "newline", so after the split, tokens should become:
listOf(
listOf(
Token(name = "lorem", val = "ipsum"),
Token(name = "dolor", val = "sit")
),
listOf(
Token(name = "amet", val = "consectetur")
)
)
I've written my own function to do this already, but is there some elegant, built-in (preferably functional) way of doing it? I say this because I'm learning Kotlin and, coming from C++, find myself "reinventing the wheel" a lot with these types of things.
In such cases I suggest not going with too much functional transformations. We can for example do it by folding/reducing, we can also first find indices of all delimiters and then zipWithNext() between them to get ranges, etc. This way we get the solution in a very few lines of code, but this code will be very hard to read and understand.
Instead, I suggest going with a good old and very simple loop. However, to make it smoother and more performant, we can use sequences and subList():
fun main() {
val tokens = ...
tokens.splitBy { it.name == "newline" }
}
fun <T> List<T>.splitBy(predicate: (T) -> Boolean): Sequence<List<T>> = sequence {
if (isEmpty()) return#sequence
var last = 0
forEachIndexed { i, v ->
if (predicate(v)) {
yield(subList(last, i))
last = i + 1
}
}
yield(subList(last, size))
}
Please note this solution does not involve any data copying. It iteratively creates views of the original list, so it should be pretty fast. On the other hand, it should be used with care if the original list may change.
Also, you need to be aware of corner cases like: delimiter at the beginning and end, no delimiters in the list or empty list. There is no single answer to how splitting should work in these cases. Whatever solution you pick, I suggest checking it for these cases. My above solution mirrors the behavior of String.split().
I think there isn't any extension function in standard library for handling this case. You will have to write your own logic. You can do something like this:
val newList = mutableListOf<List<Token>>()
var subList = mutableListOf<Token>()
for (token in tokens) {
if (token.name == "newline") {
newList += subList
subList = mutableListOf()
} else {
subList += token
}
}
if (subList.isNotEmpty())
newList += subList
println(newList)
You can also extract this code out in the form of an extension function:
fun <T> List<T>.split(delimiter: (T) -> Boolean): List<List<T>> {
val newList = mutableListOf<List<T>>()
var subList = mutableListOf<T>()
for (token in this) {
if (delimiter(token)) {
newList += subList
subList = mutableListOf()
} else {
subList += token
}
}
if (subList.isNotEmpty())
newList += subList
return newList
}
// Usage
fun main() {
val tokens = listOf(
Token(name = "lorem", val = "ipsum"),
Token(name = "dolor", val = "sit"),
Token(name = "newline", val = "\n"),
Token(name = "amet", val = "consectetur")
)
println(tokens.split { it.name == "newline" })
}
You can use fold:
tokens
.fold(mutableListOf(mutableListOf<Token>())) { l, elem ->
if(elem.name=="newline") l.add(mutableListOf())
else l.last().add(elem)
l
}
The first parameter is the initial value, a list with a single list in it (if there isn't any newline, you still want to have a single list containing the elements).
The second parameter is a function that is executed for every element.
If the token name is newline, it adds a new list. If not, it adds the element to the last list.
The last line of fold containing l makes sure that the list is returned.
Related
I have a below list
val list = listOf("o=one", "t=two", "t=two", "f=four", "o=one", "t=two", "s=seven", "o=one")
I wanna split it into list of the list contains [["o=one", "t=two", "t=two", "f=four"],["o=one", "t=two", "s=seven"],["o=one"]]
Actually I want to group list by "o=" delimiter and the list will always have at least one "0=" value. How could I achieve this in Kotlin without creating mutable var keyword because of my code should be in the functional style?
I have tried with group() and groupBy{} methods but couldn't get the expected result.
This might not cover all edge cases, and there might be better ways of doing it, but given that your requirements are not fully clear and extremely contrived, this should get you started either way. From here on out you can polish it yourself.
// Identifies indices of all "o=" elements in the list.
val splitAt = list
.withIndex()
.filter { it.value.startsWith( "o=" ) }
.map { it.index }
// Create sublists.
val split = splitAt
.windowed( 2, step = 1, partialWindows = true )
.map {
list.subList(
it[0],
if (it.count() == 1) it[0] + 1 else it[1]
)
}
I'm VERY new to Scala so apologies if anything sounds a bit basic. Working on a uni assignment and can't seem to find any similar questions around.
EDIT: The idea of this function is that I pass through a string of data and separate it up into individual elements. From what I can tell, things are being separated correctly with the lists holding the correct data types and correct information.
So I've created a function that returns a Map[String, List[(Int, String, Float)]]
The function does other things, but to keep it short, once I've build the list this is how I build the map and return it: -
val newMap = Map(name -> newList2.toList)
newMap
I can newMap.foreach to cycle through the map and find all of my elements. This works as expected: -
(Sample Key,List((3,PlaceName1,2.7)))
(Sample Key,List((2,PlaceName1,3.8)))
(Sample Key,List((1,PlaceName1,0.75)))
I am simply trying to call this function and save the map into a new variable. I have tried this two ways: -
val saveMap = separator("1:PlaceName1:0.75,2:PlaceName2:3.8,3:PlaceName3:2.7")
However when I try to cycle through this, I only get the first list element: -
(Sample Key,List((1,PlaceName1,0.75)))
I have also tried to use a mapBuffer in the format of: -
var mapBuffer: Map[String, List[(Int, String, Float)]] = Map()
mapBuffer = separator("1:PlaceName1:0.75,2:PlaceName2:3.8,3:PlaceName3:2.7")
Again, all I get as my return here is: -
mutated mapBuffer
(Sample Key,List((1,PlaceName1,0.75)))
Being new to Scala but with some experience in Java and C#, it's killing me how I'm returning a Map value, saving it into a map value that is built the same, and it's not coming through. Tried every iteration of maps and lists I could find and can't find anything on this from searching.
Is anyone able to offer any assistance?
EDIT:
Here is the whole code for the function and how I am attempting to call it.
def separator(data:String): Map[String, List[(Int, String, Float)]] = {
//Route name will be worked out later. For now, this is a sample.
val sampleRouteName = "First Route"
//Stage list will hold each list entry
val stageList = ListBuffer[(Int, String, Float)]()
//Stage list builder will put all the list entries together
var stageListBuilder = List[(Int, String, Float)]()
if (data.contains(",")) {
//Find index of first comma
val commaIndex = data.indexOf(",")
//Split everything before the comma off
val (firstEntry, restOfPhrase) = data.splitAt(commaIndex)
//Find the index of the colon in the first entry
val colonIndex = firstEntry.indexOf(":")
//Split everything before the colon off to just keep the number
val (number, restOfStage) = firstEntry.splitAt(colonIndex)
//Get rid of the first colon from the rest of the line
val restOfStage2 = restOfStage.replaceFirst(":", "")
//Find the index of the next colon
val colonIndex2 = restOfStage2.indexOf(":")
//Split everything before the colon off to just keep the stage name
val (stageName, restOfStage3) = restOfStage2.splitAt(colonIndex2)
//Get rid of the colon leaving just the stage length
val stageLength = restOfStage3.replaceFirst(":", "")
//Put all of these together into a list line in the builder
stageListBuilder = List((number.toInt,stageName,stageLength.toFloat))
//Add the list line from the builder to the list as an element
stageListBuilder.foreach(line => stageList += line)
//Call recursive function and remove the comma from the start
separator(restOfPhrase.replaceFirst(",", ""))
}
else if (data.length != 0) {
//Find index of first colon
val colonIndex = data.indexOf(":")
//Split everything before the colon off to just keep the number
val (number, restOfStage) = data.splitAt(colonIndex)
//Get rid of the first colon from the rest of the line
val restOfStage2 = restOfStage.replaceFirst(":", "")
//Find the index of the next colon
val colonIndex2 = restOfStage2.indexOf(":")
//Split everything before the colon off to just keep the stage name
val (stageName, restOfStage3) = restOfStage2.splitAt(colonIndex2)
//Get rid of the colon leaving just the stage length
val stageLength = restOfStage3.replaceFirst(":", "")
//Put all of these together into a list line in the builder
stageListBuilder = List((number.toInt,stageName,stageLength.toFloat))
//Add the list line from the builder to the list as an element
stageListBuilder.foreach(line => stageList += line)
}
//This is a map that accurately contains the key (ie. GCU Route) and a list of the routes.
val routeMap = Map(sampleRouteName -> stageList.toList)
//To test that the list has all the elements (CURRENTLY WORKING)
routeMap.foreach(line => println("TEST - " + line))
//Return the map
routeMap
}
//val saveMap = separator("1:City Chambers:0.75,2:Velodrome:3.8,3:People's Palace:2.7")
//Create new map buffer
var mapBuffer: Map[String, List[(Int, String, Float)]] = Map()
//Call separator function
mapBuffer = separator("1:City Chambers:0.75,2:Velodrome:3.8,3:People's Palace:2.7")
//Test that each element is in the list (CURRENTLY NOT WORKING)
mapBuffer.foreach(line => println(line))
As you mentioned you are using same keys as a key of a Map. Let's imagine we have separator method like this:
val str = "1:PlaceName1:0.75,2:PlaceName2:3.8,3:PlaceName3:2.7"
def separator(str: String): List[(String, (Int, String, Float))] =
str.split(",").map(_.split(":")).toList.map(_.toList).collect {
case ii :: s :: d :: Nil =>
"Sample Key" -> (ii.toInt, s, d.toFloat)
}
separator(str).foearch(println)
// result:
(Sample Key,(1,PlaceName1,0.75))
(Sample Key,(2,PlaceName2,3.8))
(Sample Key,(3,PlaceName3,2.7))
if we convert our result to a Map we lose some elements with same key in this case Sample Key:
def separator1(str: String): Map[String, List[(Int, String, Float)]] =
str.split(",").map(_.split(":")).toList.map(_.toList).collect {
case ii :: s :: d :: Nil =>
"Sample Key" -> List((ii.toInt, s, d.toFloat))
}.toMap
separator1(str).foreach(println)
// result:
// (Sample Key,List((3,PlaceName3,2.7)))
So we cannot have the same keys as KEY of a Map. The KEYs of a Map should be a unique.
case class Aname(a: Int, b: String, c: Float)
def separator2(str: String): Map[String, List[Aname]] =
str.split(",").map(_.split(":")).toList.map(_.toList).collect {
case ii :: s :: d :: Nil =>
Aname(ii.toInt, s, d.toFloat)
}.groupBy(_.b)
separator2(str).foreach(println)
// result:
// (PlaceName3,List(Aname(3,PlaceName3,2.7)))
// (PlaceName2,List(Aname(2,PlaceName2,3.8)))
// (PlaceName1,List(Aname(1,PlaceName1,0.75)))
You can play with it here
What would be an idiomatic way to create a mutable list of a given length n with repeating elements of value v (e.g listOf(4,4,4,4,4)) as an expression.
I'm doing val list = listOf((0..n-1)).flatten().map{v} but it can only create an immutable list.
Use:
val list = MutableList(n) {index -> v}
or, since index is unused, you could omit it:
val list = MutableList(n) { v }
another way may be:
val list = generateSequence { v }.take(4).toMutableList()
This style is compatible with both MutableList and (Read Only) List
If you want different objects you can use repeat.
For example,
val list = mutableListOf<String>().apply {
repeat(2){ this.add(element = "YourObject($it)") }
}
Replace String with your object. Replace 2 with number of elements you want.
You're able to use the ranges for this purpose, e.g.
val listOfFour = (1..10).map { 4 }
or
val objectList = (1..10).map {
YourClass(
arg1 = "someValue",
arg2 = it
)
}
if you need you can use it (index) for your needs as well.
I have a IndexedSeq[Map[String, String]] and I would like to extract value where key is "text" and I would like to put it in a val text:IndexedSeq[String]. I have written the following piece but it doesn't work:
val text:IndexedSeq[String] = _
for(j <- 0 to indSeq.length-1){
text(j) = indSeq(j).get("text")
}
You are probably seeing a compiler error because indSeq(j).get("text") returns an Option[String], not a String.
If you just want to get all the values for the key "text" in a sequence, use:
val text = indSeq flatMap (_ get "text")
If it's important that the indices of both sequences line up, then you will want to substitute a default value in case the key "text" is not present:
val text = indSeq map (_.getOrElse("text", "default"))
I think the best approach is with a for-comprehension with a guard to get rid of the maps that don't have the "text" element:
val result = for {
i <- 0 until indexSeq.length
map = indexSeq(i)
if map isDefinedAt ("text")
} yield { (i, map("text")) }
val seq = result.toIndexedSeq
That way you keep the original indexes with the map. It also avoids holding any var value, which is always a perk
Since you were trying to use a for-comprehension originally, you might also be interested in doing it this way:
val text = (for { m <- indSeq } yield m get "text") flatten
EDIT
or if you want a default value you could do:
val text = for { m <- indSeq } yield m getOrElse("text", "default")
I have often the need to check if many values are equal and in case extract the common value. That is, I need a function that will work like follows:
extract(List()) // None
extract(List(1,2,3)) // None
extract(List(2,2,2)) // Some(2)
Assuming one has a pimp that will add tailOption to seqs (it is trivial to write one or there is one in scalaz), one implementation looks like
def extract[A](l: Seq[A]): Option[A] = {
def combine(s: A)(r: Seq[A]): Option[A] =
r.foldLeft(Some(s): Option[A]) { (acc, n) => acc flatMap { v =>
if (v == n) Some(v) else None
} }
for {
h <- l.headOption
t <- l.tailOption
res <- combine(h)(t)
} yield res
}
Is there something like that - possibly more general - already in Scalaz, or some simpler way to write it?
This seems like a really complicated way to write
def extract[A](l:Seq[A]):Option[A] = l.headOption.flatMap(h =>
if (l.tail.forall(h==)) Some(h) else None)
You don't need tailOption, since the anonymous function that gets passed as an argument to flatMap is only executed if l is not empty.
If you only want to delete duplicates toSet is enough:
def equalValue[A](xs: Seq[A]): Option[A] = {
val set = xs.toSet
if (set.size == 1) Some(set.head) else None
}
scala> equalValue(List())
res8: Option[Nothing] = None
scala> equalValue(List(1,2,3))
res9: Option[Int] = None
scala> equalValue(List(2,2,2))
res10: Option[Int] = Some(2)
This is a fluent solution
yourSeq.groupBy(x => x) match {case m if m.size==1 => m.head._1; case _ => None}
You could use a map to count the number of occurrences of each element in the list and then return only those that occur more than once:
def extract[T](ts: Iterable[T]): Iterable[T] = {
var counter: Map[T, Int] = Map()
ts.foreach{t =>
val cnt = counter.get(t).getOrElse(0) + 1
counter = counter.updated(t, cnt)
}
counter.filter(_._2 > 1).map(_._1)
}
println(extract(List())) // List()
println(extract(List(1,2,3))) // List()
println(extract(List(2,2,2))) // List(2)
println(extract(List(2,3,2,0,2,3))) // List(2,3)
You can also use a foldLeft instead of foreach and use the empty map as the initial accumulator of foldLeft.