I'm building a chat platform, where I'm reading my list of messages from Local Room DB (ChatModel). I need to add date separators between these messages. I've to use multiple view holders and thus created a sealed class for differentiating items
sealed class ChatUiModel {
data class ChatItem(val message: ChatModel) : ChatUiModel()
data class DateSeparatorItem(val time: String) : ChatUiModel()
}
I require to convert the list with date separate items in between 2 models of the list, I'm not proficient with Collection functions in kotlin and confused between map/flatmap etc.
.observe(viewLifecycleOwner) { messages ->
messages.map {
// if item prev.date < item next.date
ChatUiModel.DateSeparatorItem(it.date.toReadableTime())
ChatUiModel.ChatItem(it)
}
chatAdapter.submitList(messages)
}
Reached to this
val items = mutableListOf<ChatUiModel>()
val data = messages.listIterator()
for (item in data) {
if (data.hasPrevious())
if (data.previous().time < item.time)
items.add(ChatUiModel.DateSeparatorItem(item.time))
items.add(ChatUiModel.ChatItem(item))
}
Timber.i("CHAT = $items")
An easy way to prepare the list can be:
messages
.groupBy { it.date }
.map { (date, chatModels) ->
listOf(DateSeparatorItem(date)) + chatModels.map { ChatItem(it) }
}
.flatten()
try it yourself
Here we first group all the messages by their data to get a Map<Long, List<ChatModel>. Then we map each entry of the map to a new list containing the DateSeparator and the ChatItems for that date. Finally, we flatten the entire list to get the desired List<ChatUiModel>.
In the code that I linked, I have used Long for the date. If you have a String you can easily interconvert them using java.time APIs.
If your messages list is not sorted initially, add a sortedBy function before groupBy to sort it first.
(this ended up long but I thought you'd like an explanation of what's going on - you can just skip to the end for the solutions if you want)
Ok, so this is a little tricky if you're not familiar with all the utility functions and general functional manipulation - what you're basically doing is transforming incoming messages into ChatItems, but you also want to compare each message to the previous one, and output a DateSeparatorItem first where necessary, right?
A straight map isn't going to work - that just transforms each item into another item (it's mapping one value to another), and sometimes you want to transform one item into two (a date item and a chat item).
You could map each message item into a list, and make that contain either a chat item, or a date+chat. So that would give you a list of lists, which you could then flatten so you just get all those items in order, in a single list. That's basically what flatmap does!
So now you need to be able to compare multiple messages, so you can check the dates. Kotlin has this windowed function that acts like a sliding view across your collection, so it can transform [1, 2, 3, 4] into [[1, 2], [2, 3], [3, 4]], and then you can work on those groups. There's a more convenient zipWithNext function that only produces Pairs instead of arbitrarily sized Lists - i.e. [(1, 2), (2, 3), (3, 4)], but windowed has a useful option - partialWindows allows that window to keep moving to the end of the list, even as it runs out of items to fill the full window:
listOf(1, 2, 3, 4).windowed(size=3, partialWindows=true).run(::println)
>> [[1, 2, 3], [2, 3, 4], [3, 4], [4]]
If we do this for a window of size 2, we get every original message, and also the one following it if there is one (zipWithNext will stop when it runs out of complete pairs):
listOf(1, 2, 3, 4).windowed(size=2, partialWindows=true).run(::println)
>> [[1, 2], [2, 3], [3, 4], [4]]
We can use this!
Your logic right now is taking a message and comparing it to the previous one to see if a date needs inserting before the chat item - I'd suggest flipping that around, and inserting a date after the current item by checking the next item's timestamp. That's because windowed is giving you each item along with the next one, so you don't get to look at the previous one.
We're working with a list here, and we need to compare the first item to the second one (checking if there even is one), but we can be a little bit cheeky and just compare list.first() with list.last(). We know there's gonna be either one or two items - and if there's only one item in the list (i.e. it's the last message) then we're comparing it with itself, and since we're only adding the date item if the timestamps are different... well they won't be if it's the same item! So there won't be any rogue date items added at the end. Probably worth documenting the code if you do that since it might not be clear - you can write some more explicit logic if you want.
Here's a few ways to do the final thing:
Kotlin Playground example
data class Message(val text: String, val time: Int)
val messages = listOf(
Message("hey", 1),
Message("u up", 1),
Message("lol", 3),
Message("wow", 10)
)
fun withMutableList() {
messages.windowed(size=2, partialWindows=true)
// or map followed by flatten()
.flatMap { items ->
val current = items.first()
val next = items.last()
// creating a mutable list with the chat item, optionally adding a date
mutableListOf<ChatUiModel>(ChatItem(current)).apply {
if (next.time > current.time) add(DateItem(next.time))
}
}
.forEach(::println)
}
fun withNulls() {
messages.windowed(size=2, partialWindows=true)
.flatMap { items ->
val current = items.first()
val next = items.last()
// either adding a date or a null, nulls get removed later
listOf(
ChatItem(current),
if (next.time > current.time) DateItem(next.time) else null
)
}
.filterNotNull()
.forEach(::println)
}
fun withSequence() {
sequence {
messages.windowed(size=2, partialWindows=true)
.forEach { items ->
val current = items.first()
val next = items.last()
// just yielding a stream of items, nice and neat!
yield(ChatItem(current))
if (next.time > current.time) yield(DateItem(next.time))
}
}.forEach(::println)
}
all giving this output:
ChatItem(message=Message(text=hey, time=1))
ChatItem(message=Message(text=u up, time=1))
DateItem(time=3)
ChatItem(message=Message(text=lol, time=3))
DateItem(time=10)
ChatItem(message=Message(text=wow, time=10))
Related
Spotify's lyrics API provides an Array of miliseconds to mark when the lyric line has changed. Having a Media Player that updates it's position every 50ms, how should i code in Kotlin the way to find the correct lyric line? The position param can be in the middle of two values of the array, so I want to get the lowest one of that two.
I tried to get the lowest value compared to the position parameter but lol, it will always be the first value of the Array... Silly fault of mine.
The problem is that I have a third one that indicates the range of that two value. For example: I have an Array of [45, 78, 125, 198]. If I pass the position param where it's value is 95, I want to return the 78 (that is the lowest value from itself, the position param and 125).
/** Input data for example (from your comment). */
val yourArray = arrayOf(45, 78, 125, 198)
val inputValue = 95
/** How to get needed index. */
val resultIndex = yourArray.indexOfLast { it < inputValue }.takeIf { it != -1 }
If you get resultIndex == null - it means you don't have value inside your array which lower then your inputValue.
I think it's simpler than find indexOfFirst and compare result later. And absolutely better and safer when sort() solution.
Insert the position param into the array, sort it, find its index and use it to get the closest value.
val array: MutableList<Long> = mutableListOf(4L, 9L, 5L, 1L)
val position = 7L
array.add(position)
println(array[array.sorted().indexOf(position) - 1])
Output: 5
If I correctly understand, you need simply use min function for compare two numbers in Kotlin and find the low one: link
Here’s a way to do it without having to make two copies of the list and doing a sort.
val foundIndex = lyricTimesList.indexOfFirst { it > inputTime }
val result = if (foundIndex == -1) lyricTimesList.size - 1 else foundIndex - 1
Note the result could be -1 if the input time is less than the first number in the list.
I'm trying to list the first 100 of a shuffled list. I'm telling it to shuffle if the list is at 0 and then increment. I then am trying to call that list in another section of the when but it's not working. How can I accomplish this?
when (countF) {
0 -> {
//shuffle at 0
val randomChaos = chaosList.asSequence().shuffled().take(chaosList.count()).toList()
cResult.text = randomChaos.elementAt(countF) + countF + "\n\n\n\n\n\n\n\n" + this.cResult.text
countF++
}
1-99 -> {
//show 1-99
cResult.text = randomChaos.elementAt(countF) + countF + "\n\n\n\n\n\n\n\n" + this.cResult.text
countF++
}
100 -> countF = 0
You would need to create the val randomChaos before the when enclosure for it to be available in the scope of multiple branches of the when statement.
That said, the way you're getting a random element is very convoluted. take(chaosList.count()) is completely redundant. And since you don't use multiple sequence operators, creating a sequence is also redundant. Finally, you are only pulling a single item from the random list, so it's unnecessary to create a shuffled list in the first place. Using elementAt() on a shuffled list is no different than picking any element out of that shuffled list, or simply picking a random item out of a list that isn't shuffled at all.
Also, the first two branches of your when statement currently would produce exactly the same results so they can be merged.
Based on what you described, I'm guessing you had this when statement inside a loop that tries to run it 100 times so you can list all the items. For that to work, you would need to shuffle the list one time outside the loop, and then you could iterate its elements in the loop.
However, there are functions that can make it easier to do what you're suggesting. Here's an example:
val randomChaos = chaosList.shuffled()
cResult.text = randomChaos.asSequence()
.take(100)
.withIndex()
.joinToString("\n") { (i, value) ->
"$value-$i"
}
In this case, using a Sequence helps avoid creating an intermediate list to hold the first 100 values.
var randomChaos = chaosList.shuffled()
fun cShuf() { randomChaos = chaosList.shuffled() }
cRoll.setOnClickListener() {
cResult.movementMethod = ScrollingMovementMethod()
if (countF < 1) { cShuf() }
cResult.text = randomChaos.elementAt(countF) + "\n\n\n\n\n\n\n\n" + this.cResult.text
countF++
if (countF > 100) countF = 0
}
I have figured out how to use a function to generate a new shuffe of the list once I've hit > 100 shown.
My issue with making it a function was I was trying to use val variable in the function but the variable already existed so I didn't need to use val, just the name of the variable.
I have list of string say
var list = mutableListOf("clock", "kite", "rekt")
Now, I need to filter list according to character. Currently I am filtering by:
list.filter { it.contains("k") }
It gives me result
[clock, kite, rekt]
But this is where I have problem, I want to filter the list and also sort by its character position and expected result is
[kite, rekt, clock]
where the character "k" comes first is first at the list and so on.
I am confused what to do. Thank you in advance.
To sort it you have to use a Comparator. Comparator contract is described here in method compare here
Compares its two arguments for order. Returns zero if the arguments are equal, a negative number if the first argument is less than the second, or a positive number if the first argument is greater than the second.
So in our case we take two strings a and b and we look at what position the k is present.
Example:
a = "clock" // position of 'k' is 4
b = "kite" // position of 'k' is 0
Comparing those two 4 - 0 = 4 results in a positive number which means first argument is greater than the second.
a = "kite" // position of 'k' is 0
b = "rekt" // position of 'k' is 3
Comparing those two 0 - 3 = -3 results in a negative number which means first argument is less than the second.
Code:
var list = mutableListOf("clock", "kite", "rekt", "abcd")
list.retainAll { it.contains("k") }
// Ascending Order
list.sortWith(Comparator {a, b -> a.indexOf('k') - b.indexOf('k')})
println(list) // [kite, rekt, clock]
// Descending Order
list.sortWith(Comparator {a, b -> b.indexOf('k') - a.indexOf('k')})
println(list) // [clock, rekt, kite]
// Cleaner and more concise syntax thanks to #Tenfour04
list.sortWith( compareBy { it.indexOf('k') } )
list.sortWith( compareByDescending { it.indexOf('k') } )
Found the answer -> dataclass copy with field ArrayList - change the ArrayList of the copied class changes the original
I have this wired bug. I have an array list of size 3. The third item is a duplicate of the second item in the list. I differentiate them based on their position on the list. The problem is that when I do a for loop to change the properties of the item at index 1 the item at index 2 reflects the same change.
What I've tried so far. ...
I have confirmed that the list is being changed at only one spot in the app.
Added more fields to differentiate the duplicate items.
Set if statements to make sure that I am only changing the specific item.
What I think the solution is.
My assumption is that since the items at index 1 & 2 have a coupled relationship since they are duplicates of each other. (even with differentiating factors) I don't know what this relationship is.
My code snippet.
private fun testing(selectedNormalModifier: ToppingModel) {
var modIndex = -1
selectedNormalModifier.parentPosition = selectedModifierPosition
selectedNormalModifier.localToppingName = modifierGroupModel!!.itemModifierGroups[selectedModifierPosition].itemLocalTitle.toString()
val itemToEdit = modifierGroupModel!!.itemModifierGroups[selectedModifierPosition]
for (i in itemToEdit.modifierGroups.modifiers.indices) {
val mod = itemToEdit.modifierGroups.modifiers[i]
if (mod.title == selectedNormalModifier.toppingName) {
modIndex = i
}
}
itemToEdit.modifierGroups.modifiers[modIndex].isItemSelected = true
mSelectedNormalModifiers.add(selectedNormalModifier)
Log.e(TAG, "how many times did we get here $modifierGroupModel")
}
As you can see I am being very specific on the item that I want to edit. Regardless of this, the item at index 2 also gets edited and vice versa.
This is how I duplicate the items
for (i in modifierGroupModel!!.itemModifierGroups.indices) {
val item = modifierGroupModel!!.itemModifierGroups[i]
// only do this if the display count is greater than one
if (item.modifierGroups.displayCount.toInt() > 1) {
for(i in 0 until item.modifierGroups.displayCount.toInt()){
val localIndex = i + 1
item.itemIndex = localIndex
item.itemLocalTitle = getNumberOfName(localIndex) + " " + item.modifierGroups.modifierGroupTitle
tempItemModifierGroupModel.add(if (i > 0) item.copy() else item)
}
} else {
item.itemIndex = i
tempItemModifierGroupModel.add(item)
}
}
val newModidiferGroupModel = ModifierGroupsModel(
itemID = modifierGroupModel!!.itemID,
itemName = modifierGroupModel!!.itemName,
itemModifierGroups = ArrayList(tempItemModifierGroupModel.toMutableList())
)
modifierGroupModel = newModidiferGroupModel
The JSON object looks like this
"item" {
"nested list"[
"isSelected": "false"
]
},
"item" {
"nested list"[
"isSelected": "false" // when i change this to true
]
},
"item" {
"nested list"[
"isSelected": "false" // this one changes as well
]
}
]```
I'm guessing because you didn't show your Item data class but it looks like you are not editing the item in your list, but rather some indirectly referenced object. See this line:
itemToEdit.modifierGroups.modifiers[modIndex].isItemSelected = true
itemToEdit is not getting modified. Some indirectly referenced object in a collection called modifiers is what you're modifying.
When you copy an Item, it only copies all the property values. For a non-primitive property, the value is a reference to specific instance of a class. It does not perform a "deep copy". So your items at index 1 and 2 are different objects, but they reference the same instance of whatever is in the modifierGroups property.
I found the answer. ---> this other Stack Overflow question answered it. dataclass copy with field ArrayList - change the ArrayList of the copied class changes the original
The user #Andrey_yog
I am trying to extract a Map of headers (position of the header + text in the header) from a list of inputs. I have mapped the inputs to the header (or group) they belong in. I need a function that's more or less like distinctor distinctUntilChangedbut where I can obtain the position where the text changed.
So far I have this:
Observable.from(inputs)
.map(this::getSectionTitle) // Maps the inputs into the Header string
.distinct();
which obviously return a list of the headers. In case it wasn't clear I need the list of headers linked to the position where they should be placed.
Alternatively I can also accept a function in F# of type List<String> -> Set<(Int*String)>for example.
Edit:
This is the function I want to move from impeartive to a functionl approach:
for (int i = 0; i < inputs.size(); i++) {
Input pu = inputs.get(i);
String header = getSectionTitle(pu);
if (!mHeaders.containsValue(header)) {
mHeaders.put(i + mHeaders.size(), header);
}
}
Edit 2:
Example of an input/output
["a","a","a","b","b","c"] -> [("a",0),("b",3),("c",5)]
You could use something like this
Observable.from(inputs)
.map(this::getSectionTitle) // Maps the inputs into the Header string
.zipWith(Observable.range(0, 1000), Pair::create) // This is a weak point of the solution as you should know max number of emissions as if it exceeds the number in range, it will stop emitting ..
.distinctUntilChanged(pair -> pair.first) // keySelector function tells the operator what is the value object upon which it should determine whether it was changed
.map(pair -> /* Map to your desired format */)
The pair corresponds to an item which has changed and the order number, in which it was emitted...
EDIT: Instead of Observable.range() you can use timer with scan operator. You should not rely on direct emissions from timer observable as there is no guarantee that it will be a continuous sequence, therefore a scan operator.
Observable.interval(1, TimeUnit.MILLISECONDS)
.scan(0L, (index, emittedValue) -> index++)