I'm trying to generate a list with several values that add up to the total value. The list works properly, but I still want to ask if there are possibilities to optimize this better. I have tried several attempts with different functionalities but unfortunately this does not give the exact result.
I don't need the exact solution just a step in the right direction will help me a lot.
This is how the code looks right now.
fun getTotal(amps:List<Int>): MutableMap<Int, Int>{
val listEqual = mutableMapOf<Int,Int>()
if(amps.size > 8) {
listEqual[0] = listOf(
amps[0],
amps[1],
amps[2],
amps[3],
amps[4],
amps[5],
amps[6],
amps[7],
amps[8],
amps[9]
).sum()
if(amps.size > 18) {
listEqual[1] = listOf(
amps[10],
amps[11],
amps[12],
amps[13],
amps[14],
amps[15],
amps[16],
amps[17],
amps[18],
amps[19]
).sum()
if(amps.size > 28) {
listEqual[2] = listOf(
amps[20],
amps[21],
amps[22],
amps[23],
amps[24],
amps[25],
amps[26],
amps[27],
amps[28],
amps[29]
).sum()
if(amps.size > 38) {
listEqual[3] = listOf(
amps[30],
amps[31],
amps[32],
amps[33],
amps[34],
amps[35],
amps[36],
amps[37],
amps[38],
amps[39]
).sum()
if(amps.size > 47) {
listEqual[4] = listOf(
amps[40],
amps[41],
amps[42],
amps[43],
amps[44],
amps[45],
amps[46],
amps[47],
amps[48]
).sum()
}
}
}
}
}
return listEqual
}
fun getTotal(amps: List<Int>): Map<Int, Int> {
return amps
.windowed(10, 10, true)
.mapIndexed { index: Int, ints: List<Int> -> index to ints.sum() }
.toMap()
}
The part with windowed() looks likes this with named arguments:
.windowed(size = 10, step = 10, partialWindows = true)
It 'slides' over a list by step and returns size elements. If partialWindows is set to true, the last part, if less than size, will be returned too, if set to false, it will be omitted.
The sizes you are checking seem arbitrary. Like if the input size is 9, it would crash at amps[9]. You should be checking >= multiples of 10 in your if statement since you are grouping chunks of 10.
Also, there doesn't seem much point in using a Map where the keys are consecutive Ints starting from 0. It might as well be a List. If you can return a List, your whole function could be:
return amps.chunked(10, Iterable<Int>::sum)
If you do need a Map, you can do
return amps.chunked(10, Iterable<Int>::sum)
.mapIndexed { i, list -> i to list }
.toMap()
If you need the option of dropping the last group if it's smaller than 10, then the other answer with windowed is more appropriate.
If chunked and windowed were not available, your code could be shortened using the subList() function and a loop. For example:
fun getTotal(amps:List<Int>): MutableMap<Int, Int>{
val listEqual = mutableMapOf<Int,Int>()
repeat(5) { chunkIndex ->
if (amps.size >= chunkIndex * 10 + 10) {
listEqual[chunkIndex] = amps.subList(chunkIndex * 10, chunkIndex * 10 + 10).sum()
}
}
return listEqual
}
Related
Having this object:
class Data (region: String, language: String, version: String)
with these instances:
var data1 = Data("Spain", "En, Pl", "")
var data2 = Data("Spain", "Ru, Es", "")
var data2 = Data("France", "Fr, En", "v1.0")
var data3 = Data("Germany", "", "v1.1")
and having an array of that object: var list = mutableListOf(data1, data2, data3, data3)
My idea is to order the list by these factors, ordered by importance, first factor is the most important, then, the others:
Order factor 1: if (region == "Spain") is the most important factor, anything with that condition must be before other regions. And if is not, then it should compare if (region == "Europe"). And if not, then should it compare if (region == "World")
Order factor 2: if (language.contains("Es")) is the second most important factor
Order factor 3: version. v1.1 is higher so must be in a more important position that v1.0. Numbers can be infinite, 1.1 and 1.0 are just samples. It must put higher versions before lower versions
How can this be implemented?
I tried this, with totally wrong results:
list.sortedWith(compareBy({ it.region == "Spain"}, { it.language?.contains("Es") }, { it.version }))
You can just provide 'numeric scores' for region/language/version -- exactly as you've described in your question. And because compareBy sorts elements in ascending order, lower numbers will be first.
val sorted = list.sortedWith(
compareBy<Data>(
{
when (it.region.lowercase()) {
"spain" -> 0
"europe" -> 1
"world" -> 2
else -> 3
}
},
{
when {
it.language.lowercase().contains("es") -> 0
else -> 1
}
}
).thenByDescending {
it.version
}
)
I have list of Work model elements.
Work has paremeters like:
id,
title,
number,
version number
I would like to have only Work elements where version number is not null and take the highest version number of existing elements.
For example my list has Work elements like (in order title, number, version)
Running (120:1)
Running (120:2)
Running (120:3)
Climbing (55:1)
Climbing (55:2)
Climbing (55:3)
Climbing (55:4)
My result in the list after filters should be
Running (120:3)
Climbing (55:4)
My code so far:
private fun List<Work>.filterByVersion(): List<Work> {
work.version_number != null
}.filter { work -> ("${work.title} ${work.number}:${work.version_number}").contains("no idea of regex here")
}
}
I was thinking something like inner search with {work.title} ${work.number}: then find in here the max value of {work.version_number}.
The following code works for me:
private fun List<Work>.filterByVersion(): List<Work> =
groupBy { it.number }.map {
it.value
.filter { work -> work.version_number != null }
.maxByOrNull { work -> work.version_number!! }
}.filterNotNull()
First we convert current list into a Map<Int, List<Work>>, where the key is Work.number then we map resulting Map into a List<Work> by filtering non null values of version_number and finding the max value of it.
Another variation:
private fun List<Work>.filterByVersion(): List<Work> =
groupBy { it.number }.values.mapNotNull { works ->
works
.filter { work -> work.version_number != null }
.maxByOrNull { work -> work.version_number!! }
}
data class Work(
val id: Int,
val title: String,
val number: Int,
val versionNumber: Int
)
val works = listOf(
Work(1, "Running", 120, 1),
Work(2, "Running", 120, 2),
Work(3, "Running", 120, 3),
Work(4, "Climbing", 55, 1),
Work(5, "Climbing", 55, 2),
Work(6, "Climbing", 55, 3),
Work(7, "Climbing", 55, 4)
)
fun List<Work>.filterByVersion() = this
.groupBy { it.number }
.map { (_, list) -> list.maxBy { it.versionNumber } }
val result = works.filterByVersion()
result.forEach(::println)
In the Android View system with a RecyclerView, one could have the GridLayoutManager decide at runtime the number of "Spans" each item used by using a callback called spanSizeLookup:
Imagine you have
val layoutManager = GridLayoutManager(this, 2) //Two Spans Max
layoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
// calculate based on whatever you want and return N
return if (adapter.getItem(position).xx = YY) 1 else 2 //e.g.
}
}
Now I'm attempting to convert this to Compose using LazyVerticalGrid where there's no Adapter, and certainly no LayoutManager to deal with. However, I'm having hard time finding the equivalent to the spanSizeLookup.
Initial Option: using the "DSL"
Imagine a #Composable that receives the "data" and does something like:
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
items(data) { anItem ->
// some composable to show "anItem"
}
}
This is fine, it will display each item in a "two" column (spans!) layout; what if you want to dynamically change the span for each item?
No problem, the items function in the DSL actually does take a span:
items(data, span = { // put your span here }) { anItem ->
// some composable to show "anItem"
}
This span is going to apply to all items and you cannot change it... so this doesn't solve the problem.
The function block for span is expecting a GridItemSpan(Int).
So the 1st question is: Would there be a way to change this from inside the content block? Think of something like this:
items(data) { anItem ->
// Lookup the correct span for this `item`
span = if (anItem.xx = YY) 1 else 2
// some composable to show "anItem"
}
This is obviously not possible like that...
The Alternative
What one CAN do, is create individual items which also accept a span and only apply to the actual item:
// Manually iterate all items
data.forEach { anItem ->
if (anItem.xx = YY) {
// Render the item with 1 span.
item(span = { GridItemSpan(1) }) {
// some composable to show...
}
} else {
// Render the item with 2 spans.
item(span = { GridItemSpan(1) }) {
// some composable to show...
}
}
}
This works (I've tested it) but it feels a bit convoluted.
Question 2 is then: Is this "ok" according to the current (1.3.0-alpha01) version of Compose? Is there a better way?
Please keep in mind I tried to abstract the irrelevant parts of all this, so it's a bit of pseudo-code here and there to illustrate a point.
I have seen this post and similar ones, but I'm not sure if that's the right approach either, seems like a lot of complexity for something the framework APIs could better handle. I'd love to hear more about it though.
I've naturally read the official documentation to no avail.
The documentation on this is a bit scarce, that is true.
What you might have missed is: while you can only provide one span lambda, it will be called for each item, with that item as argument. This means you are free to return different span sizes depending on the argument provided.
So the implementation path is almost identical to the classic SpanSizeLookup, with the advantage that you don't have to look up items by their index (but can still opt to do it):
// Just for visualization purposes
#Composable
fun GridItem(label: String) {
Box(
Modifier
.fillMaxWidth()
.height(56.dp)
.border(1.dp, Color.Gray, RoundedCornerShape(16.dp)),
contentAlignment = Alignment.Center
) {
Text(text = label)
}
}
#Preview
#Composable
fun GridSpansSample() {
LazyVerticalGrid(columns = GridCells.Fixed(3), modifier = Modifier.fillMaxSize()) {
// based on index
items(3, span = { index ->
val spanCount = if (index == 0) 3 else 1
GridItemSpan(spanCount)
}) { index ->
GridItem("Item #$index")
}
// based on list content
items(listOf("Foo", "Bar", "Baz"), span = { item ->
val spanCount = if (item == "Foo") 3 else 1
GridItemSpan(spanCount)
}) { item ->
GridItem(item)
}
// based on either content or index
itemsIndexed(listOf("Foo", "Bar", "Baz"), span = { index, item ->
val spanCount = if (item == "Foo" || index == 1) 3 else 1
GridItemSpan(spanCount)
}) { index, item ->
GridItem(item)
}
// Bonus: The span lambda receives additional information as "this" context, which allows for further customization
items(10 , span = {
// occupy the available remaining width in the current row, but at most 2 cells wide
GridItemSpan(this.maxCurrentLineSpan.coerceAtMost(2))
}) { index ->
GridItem("Item #$index")
}
}
}
In my case I needed to place a whole element at the bottom of the list, let's say the list is odd with 5 elements I would place 2 elements per column and then 1 at the bottom
LazyVerticalGrid(
modifier = Modifier
.fillMaxSize()
.background(CoffeeFoam),
columns = GridCells.Fixed(2),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
itemsIndexed(items = myItemList, span = { index, _ ->
val spanCount = if (myItemList.size % 2 == 0) 1 else if (index == myItemList.size - 1) 2 else 1
GridItemSpan(spanCount)
}) { index, uiState ->
CheckoutSection(
modifier = Modifier.wrapContentHeight(),
uiState = uiState,
onCardCtaClick = onCardCtaClick
)
}
}
Doing this I'm checking if the list is pair, if so I return 1 span sice (which will take 2 items per column) , then I check for the indexes and the list size if the list size is not pair, which could be 3 , 5 , 7, 9 ....
index = 0 != 4 return 1 as span size
index = 1 != 4 return 1 as span size
index = 2 != 4 return 1 as span size
index = 3 != 4 return 1 as span size
index = 4 == 4 return 2 as span size (full width element)
And in this way I'm rendering the last element with full column width on odd list sizes, we can make an exception for first element also with just one more check for index != 0
While exploring TextField in a Jetpack Compose, I came across a case where I have to modify input typed in the field. For example, adding a comma after entering 3 characters.
This is how I made it.
#Composable
fun TFDemo() {
var fieldValue by remember { mutableStateOf(TextFieldValue("")) }
TextField(
value = fieldValue,
onValueChange = {
val newMessage = it.text.let { text -> if (text.length == 3) "$text," else text }
fieldValue = it.copy(newMessage, selection = TextRange(newMessage.length))
},
keyboardOptions = KeyboardOptions(autoCorrect = false),
)
}
But after running it, I realized that after the comma is added, keyboard view changed back to alphabets from numbers/symbols which should not be the case. See video output below for clarity
As you can see in the below video when I typed "111" comma was appended and suddenly keyboard's numeric view changed to alphabets again.
Here I have modified the selection of TextFieldValue so that cursor always be at the end of the message whenever a comma is appended.
This kind of cases is exactly what VisualTransformation is intended for.
Here's a Googler's comment on another issue:
I don't think we can fix this issue easily.
The filtering text in onValueChanged callback is generally not recommended because the text state is shared with out process IME(software keyboard). The filtering text means the text content changes internally, then the new state is notified to IME. This is not a normal path to IME and different IME reacts differently to this unexpected state change. Some IME may try to reconstruct the composition, others may give up and start new session, etc. This is mostly due of the historical reason and hard to fix from now. So, please avoid filtering text in onValueChanged callback and consider following alternatives:
(Recommended) Don't filter it and show error message. (irrelevant here)
Use VisualTransformation for changing visual output without modifying edit buffer.
As per the mentioned answer above, VisualTransformation is the perfect solution for such cases and we should not directly modify TextField's buffer. Because VisualTransformation just changes the visual output of text and not actual text.
I've written an article on this scenario here where I've explained this in detail.
Solution:
#Composable
fun TextFieldDemo() {
var message by remember { mutableStateOf("") }
TextField(
value = message,
placeholder = { Text("Enter amount or message") },
onValueChange = { message = it },
visualTransformation = AmountOrMessageVisualTransformation()
)
}
class AmountOrMessageVisualTransformation : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val originalText = text.text
val formattedText = formatAmountOrMessage(text.text)
val offsetMapping = object : OffsetMapping {
override fun originalToTransformed(offset: Int): Int {
if (originalText.isValidFormattableAmount) {
val commas = formattedText.count { it == ',' }
return when {
offset <= 1 -> offset
offset <= 3 -> if (commas >= 1) offset + 1 else offset
offset <= 5 -> if (commas == 2) offset + 2 else offset + 1
else -> 8
}
}
return offset
}
override fun transformedToOriginal(offset: Int): Int {
if (originalText.isValidFormattableAmount) {
val commas = formattedText.count { it == ',' }
return when (offset) {
8, 7 -> offset - 2
6 -> if (commas == 1) 5 else 4
5 -> if (commas == 1) 4 else if (commas == 2) 3 else offset
4, 3 -> if (commas >= 1) offset - 1 else offset
2 -> if (commas == 2) 1 else offset
else -> offset
}
}
return offset
}
}
return TransformedText(
text = AnnotatedString(formattedText),
offsetMapping = offsetMapping
)
}
}
Given that we have returned separately a list of animals:
val animals = "cat, dog and mouse"
Which we then concat to our animalsMessage so it looks as following:
val animalsMessage = "You have identified cat, dog and mouse"
Given my default font colour is white and I only wanted to change the val animals font colour in my animalsMessage, I could do:
animalsMessage.setSpan(
ForegroundColorSpan(resources.getColor(R.color.yellow, null)),
animalsMessage.length - animals.length,
animalsMessage.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
However, say I wanted to ignore the , and the word and whilst spanning, so they remained the default white colour, how would I go about doing that? I am basing this question on the assumption that there might be an and in the animals string and there might be one , or many.
I believe the answer lies in using a pattern matcher and then ignoring whilst spanning based on finding a match.
Things I have tried:
First, before concat my val animals to my val animalsMessage I tried to format my val animals as described above, to do that, I created the below method:
private fun ignoreSeparators(animals: String): SpannableString {
val spannable = SpannableString(animals)
val matcher: Matcher = Pattern.compile(",\\\\and").matcher(animals)
while (!matcher.matches()) {
val animal = matcher.group(1)
val animalIndex: Int = animals?.indexOf(animal) - 1
spannable.setSpan(ForegroundColorSpan(resources.getColor(R.color.yellow, null)), 0, animalIndex, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return spannable
}
I then planned on returning the spanned text and then concating it to my val animalsMessage, however, I get a crash saying that no match is found.
I would recommend doing the following, first, remove the , and and change to a list as follows...
val animals = listOf("cat", "dog", "mouse")
Then, pass them to the following method that will handle the styling, followed by adding the necessary comma and the and. The rule followed was that the and would always be between the last and second from last animal and all other values no matter how large the list is, would be separated by a comma.
The second param, prefix, is simply our animalsMessage which we concat to, as mentioned in your question.
private fun formatAnimalStrings(animals: List<String>, prefix: String): SpannableStringBuilder {
val lastIndex = animals.size - 1
val secondLastIndex = lastIndex - 1
val result = SpannableStringBuilder(prefix)
animals.forEachIndexed { index, animal ->
val startIndex = result.length
result.append(animals[index])
result.setSpan(
ForegroundColorSpan(resources.getColor(R.color.yellow, null)),
startIndex,
startIndex + animal.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
if (index < lastIndex) {
if (index == secondLastIndex) {
result.append(" and ")
} else {
result.append(", ")
}
}
}
result.append(".")
return result
}
This would result in
You have identified cat, dog and mouse
(I have used bold to express yellow text colour, since I cannot make the text yellow)