How can I implement text completion,Like Gmail's smart compose?
I've an edit text where the user enters server address and I want to detect when they start typing the domain suffix and suggest completion.
Thanks.
First you need an algorithm to get suggestion from a given dictionary.
I've created a simple class named SuggestionManager to get suggestion from a given dictionary for a string input. Instead of returning the full match, it'll only return the remaining part of the given input. Below given a simple unit test along with full source code of the class. You can also go here to run the test online.
SuggestionManager.kt
class SuggestionManager(private val dictionary: Array<String>) {
companion object {
private val WORD_SPLIT_REGEX = Regex("[^A-Za-z0-9'\\-]")
/**
* To get reversed list
*/
private fun getReversedList(list: List<String>): MutableSet<String> {
val reversed = mutableSetOf<String>()
for (item in list.withIndex()) {
if (item.index != 0) {
val rev = list.subList(list.size - item.index, list.size).joinToString(" ")
reversed.add(rev)
}
}
// finally, add the full string
reversed.add(list.joinToString(" "))
return reversed
}
}
fun getSuggestionFor(_text: String?): String? {
var text = _text
// empty text
if (text.isNullOrBlank()) {
return null
}
// Getting last line only
if (text.contains("\n")) {
text = text.split("\n").last()
if (text.trim().isEmpty()) {
return null
}
}
// Splitting words by space
val words = text.split(WORD_SPLIT_REGEX).filter { it.isNotBlank() }
// Getting last word
val lastWord = if (text.endsWith(" ")) "${words.last()} " else words.last()
// Matching if last word exist in any dictionary
val suggestions = mutableSetOf<String>()
for (dicItem in dictionary) {
if (dicItem.contains(lastWord, true)) {
// Storing founded matches
suggestions.add(dicItem)
}
}
// Reverse ordering split-ed words
val pyramidWords = getReversedList(words)
val matchList = mutableListOf<String>()
for (pw in pyramidWords) {
for (sug in suggestions) {
// Storing suggestions starts with reversed word
if (sug.startsWith(pw, true)) {
matchList.add("$pw:$sug")
}
}
}
// Checking if second level match is not empty
if (matchList.isNotEmpty()) {
// Ordering by matched reversed word - (largest key first)
matchList.sortBy { -it.split(":")[0].length }
// Looping through in ascending order
for (m in matchList) {
val match = m.split(":")
val selPw: String = match[0]
var selSug: String = match.subList(1, match.size).joinToString(":")
// trimming to
selSug = selSug.replace(selPw, "", true)
if (text.endsWith(" ")) {
selSug = selSug.trim()
}
return selSug
}
}
return null
}
}
Unit Test
class SuggestionManagerUrlTest {
private val suggestionManager by lazy {
val dictionary = arrayOf(
"google.com",
"facebook.com",
"gmail.com",
"yahoo.com",
"192.168.354.45"
)
SuggestionManager(dictionary)
}
#Test
fun test() {
// null of empty and null input
assertNull(suggestionManager.getSuggestionFor(null))
assertNull(suggestionManager.getSuggestionFor(""))
// matching
assertEquals("ogle.com", suggestionManager.getSuggestionFor("go"))
assertEquals("book.com", suggestionManager.getSuggestionFor("face"))
assertEquals(".168.354.45", suggestionManager.getSuggestionFor("192"))
// no match
assertNull(suggestionManager.getSuggestionFor("somesite"))
}
}
Then, you'll have to set text in EditText with two colors. One for input and other for the suggestion. You may use the Html.fromHtml method to do this.
val text = "<font color=#cc0029>$input</font> <font color=#ffcc00>$suggestion</font>";
yourEditText.setText(Html.fromHtml(text));
Combining these two aspects, you can create a custom EditText.
Related
I'm working on creating a way to input an amount and format it from left to right with placeholder zeros.
For example, pressing 1 and 2 would show $0.12 pressing 3 would give $1.23. Pressing backspace would give $0.12.
Instead, I am getting $1,0002.00
binding.keypad.btnBackspace.setOnClickListener {
val text = binding.tvTotalValue.text.toString()
if(text.isNotEmpty()) {
binding.tvTotalValue.text = text.drop(1)
}
binding.tvTotalValue.text = ""
}
binding.keypad.onNumberSelected = {
processNewAmount(it.toString())
}
private fun processNewAmount(newValue: String) {
val text = binding.tvTotalValue.text.toString().plus(newValue)
val result = text.replace("[^0-9]".toRegex(), "") // remove any characters
val amount = result.toDouble()
binding.tvTotalValue.text = NumberFormat.getCurrencyInstance().format(amount)
}
What am I doing wrong?
Is there a better way to do this?
I advise keeping a property that stores the entered value without formatting. Each time a number is added, you can add it to this entered number and then format it for the screen. That will be a lot simpler than trying to move/remove existing symbols around in the String.
private var enteredNumber = ""
//...
binding.keypad.btnBackspace.setOnClickListener {
enteredNumber = enteredNumber.dropLast(1)
refreshTotal()
}
binding.keypad.onNumberSelected = {
if(!(it == 0 && enteredNumber == "0")) { // avoid multiple zeros or backspace will act unexpectedly
enteredNumber += it.toString()
refreshTotal()
}
}
//...
private fun refreshTotal() {
val amount = enteredNumber.toDouble() / 100.0
binding.tvTotalValue.text =
NumberFormat.getCurrencyInstance().format(amount)
}
For example, I load data into a List, it`s wrapped by MutableStateFlow, and I collect these as State in UI Component.
The trouble is, when I change an item in the MutableStateFlow<List>, such as modifying attribute, but don`t add or delete, the UI will not change.
So how can I change the UI when I modify an item of the MutableStateFlow?
These are codes:
ViewModel:
data class TestBean(val id: Int, var name: String)
class VM: ViewModel() {
val testList = MutableStateFlow<List<TestBean>>(emptyList())
fun createTestData() {
val result = mutableListOf<TestBean>()
(0 .. 10).forEach {
result.add(TestBean(it, it.toString()))
}
testList.value = result
}
fun changeTestData(index: Int) {
// first way to change data
testList.value[index].name = System.currentTimeMillis().toString()
// second way to change data
val p = testList.value[index]
p.name = System.currentTimeMillis().toString()
val tmplist = testList.value.toMutableList()
tmplist[index].name = p.name
testList.update { tmplist }
}
}
UI:
setContent {
LaunchedEffect(key1 = Unit) {
vm.createTestData()
}
Column {
vm.testList.collectAsState().value.forEachIndexed { index, it ->
Text(text = it.name, modifier = Modifier.padding(16.dp).clickable {
vm.changeTestData(index)
Log.d("TAG", "click: ${index}")
})
}
}
}
Both Flow and Compose mutable state cannot track changes made inside of containing objects.
But you can replace an object with an updated object. data class is a nice tool to be used, which will provide you all copy out of the box, but you should emit using var and only use val for your fields to avoid mistakes.
Check out Why is immutability important in functional programming?
testList.value[index] = testList.value[index].copy(name = System.currentTimeMillis().toString())
Suppose I have Double number which I want to convert to String.
I want to have an option which based on that I would have String number without trailing zeroes.
So for example:
option with trailing zeros 123.00 -> "123,00", 123.324 -> "123,32"
option without trailing zeroes 123.00 -> "123", 123.324 -> "123,32"
Is there a nice way to do this in Kotlin?
This is code which I have which I feel is rather ugly:
private const val VALUE_DIVIDER = 100
private const val DIGITS_AMOUNT = 2
private val defaultLocale = Locale("us")
private val currency = "$"
private val cents = 10000
fun print(withoutTrailingZeros: Boolean = true): String {
val price = (cents.toDouble() / VALUE_DIVIDER)
.valueToString()
.let { if (withoutTrailingZeros) it.removeSuffix(",00") else it }
return "$price $currency"
}
private fun Double.valueToString() = round(DIGITS_AMOUNT).replace(".", ",")
private fun Double.round(digits: Int): String =
NumberFormat.getInstance(defaultLocale).apply {
maximumFractionDigits = digits
minimumFractionDigits = digits
isGroupingUsed = false
}.format(this)
UPDATE: The solution provided by #Roma Pochanin works partially, but strangely only as jUnit tests.
After I am running integration tests on Android emulator using this logic this is not working for 0 (it is formatted as "0,00" even when the withoutTrailingZeros flag is true). I heard about some bug related to that Why does new BigDecimal("0.0").stripTrailingZeros() have a scale of 1?
but how it is connected with my case? Can anyone explain?
Please, see the exact sessions from debugger:
working, as jUnit tests: https://ibb.co/HN9n41T
bug, when running instrumentation tests on Android emulator: https://ibb.co/VCrmrMh
There is no function for that in the Kotlin standard library, but you can specify the number of decimal places and the decimal format symbol using Java's DecimalFormat:
val formatSymbols = DecimalFormatSymbols.getInstance().apply {
decimalSeparator = ','
}
val twoDecimalDigitsFormat = DecimalFormat("#.##").apply {
decimalFormatSymbols = formatSymbols
}
val twoTrailingZerosFormat = DecimalFormat("#.00").apply {
decimalFormatSymbols = formatSymbols
}
fun formatPrice(price: Double, withDecimalZeros: Boolean) = if (withDecimalZeros) {
twoTrailingZerosFormat
} else {
// Is number still the same after discarding places?
if (price.toInt().toDouble() == price) {
twoDecimalDigitsFormat
} else {
twoTrailingZerosFormat
}
}.format(price)
println(formatPrice(123.00, true)) // 123,00
println(formatPrice(123.324, true)) // 132,32
println(formatPrice(123.00, false)) // 123
println(formatPrice(123.324, false)) // 123,32
Why don't you use BigDecimal? It's like the default way to deal with prices and similar stuff. You also can consider using BigDecimal's method stripTrailingZeros:
private const val VALUE_DIVIDER = 100
private const val DIGITS_AMOUNT = 2
private val currency = "$"
private val cents = 1298379
fun formatPrice(withoutDecimalZeros: Boolean = true) =
BigDecimal(cents)
.divide(BigDecimal(VALUE_DIVIDER), DIGITS_AMOUNT, RoundingMode.UP)
.let { if (withoutDecimalZeros) it.stripTrailingZeros() else it }
.toString().replace(".", ",")
.let { "$it $currency" }
I'm doing validation on an EditText. I want the CharSequence to be invalid if it's empty or it doesn't begin with "https://". I'm also using RxBinding, specifically RxTextView. The problem is that when there is one character left, and I then delete it leaving no characters left in the the CharSequence the map operator doesn't fire off an emission. In other words I want my map operator to return false when the EditText is empty. I'm beginning to think this may not be possible the way I'm doing it. What would be an alternative?
Here is my Observable / Disposable:
val systemIdDisposable = RxTextView.textChanges(binding.etSystemId)
.skipInitialValue()
.map { charSeq ->
if (charSeq.isEmpty()) {
false
} else {
viewModel.isSystemIdValid(charSeq.toString())
}
}
.subscribe { isValid ->
if (!isValid) {
binding.systemIdTextInputLayout.isErrorEnabled = true
binding.systemIdTextInputLayout.error = viewModel.authErrorFields.value?.systemId
} else {
binding.systemIdTextInputLayout.isErrorEnabled = false
binding.systemIdTextInputLayout.error = viewModel.authErrorFields.value?.systemId
}
}
And here is a function in my ViewModel that I pass the CharSequence to for validation:
fun isSystemIdValid(systemId: String?): Boolean {
return if (systemId != null && systemId.isNotEmpty()) {
_authErrors.value?.systemId = null
true
} else {
_authErrors.value?.systemId =
getApplication<Application>().resources.getString(R.string.field_empty_error)
false
}
}
After sleeping on it, I figured it out.
I changed RxTextView.textChanges to RxTextView.textChangeEvents. This allowed me to query the CharSequence's text value (using text() method provided by textChangeEvents) even if it's empty. Due to some other changes (not really relevant to what I was asking in this question) I was also able to reduce some of the conditional code too. I'm just putting that out there in case someone comes across this and is curious about these changes. The takeaway is that you can get that empty emission using RxTextView.textChangeEvents.
Here is my new Observer:
val systemIdDisposable = RxTextView.textChangeEvents(binding.etSystemId)
.skipInitialValue()
.map { charSeq -> viewModel.isSystemIdValid(charSeq.text().toString()) }
.subscribe {
binding.systemIdTextInputLayout.error = viewModel.authErrors.value?.systemId
}
And here is my validation code from the ViewModel:
fun isSystemIdValid(systemId: String?): Boolean {
val auth = _authErrors.value
return if (systemId != null && systemId.isNotEmpty()) {
auth?.systemId = null
_authErrors.value = auth
true
} else {
auth?.systemId =
getApplication<Application>().resources.getString(R.string.field_empty_error)
_authErrors.value = auth
false
}
}
Lastly, if anyone is curious about how I'm using my LiveData / MutableLiveData objects; I create a private MutableLiveData object and only expose an immutable LiveData object that returns the values of the first object. I do this for better encapsulation / data hiding. Here is an example:
private val _authErrors: MutableLiveData<AuthErrorFields> by lazy {
MutableLiveData<AuthErrorFields>()
}
val authErrors: LiveData<AuthErrorFields>
get() { return _authErrors }
Hope this helps someone! 🤗
I'm trying to parse some files this way:
File(tentConfig.getPathRepository())
.walkTopDown()
.forEach { file -> processFile(file) }
the path of this file is: /communications/email/begginer/.file
I have to convert that path to object like this:
my communications should be my category, email should be a subcategory of communications and beginner subcategory of email.
my process method is responsible to serialize this path to object but I'm pretty sure there is a better solution.
private fun processCategory(currentFile: File) {
val listOfDirectory = currentFile.path.split("/".toRegex())
listOfDirectory.forEachIndexed { index, folderName ->
if (index == 0) {
val currentCategory = parseYmlFile(currentFile, Category::class)
lesson.categories.forEach { itCategory ->
if (itCategory.title != currentCategory.title) lesson.categories.add(currentCategory)
}
} else {
val subCategory = parseYmlFile(currentFile, Category::class)
lesson.categories[subCategory.index].subcategories.add(subCategory)
}
}
}
For the sake of demo/testing purposes, my implementation of Category might be different from yours. Here's the one I was using:
inner class Category(val s: String, var subCategory: Category? = null)
Now that being said, here's a little function that will loop through the path of the given File, and construct a Category hierarchy, placing each element in the right order:
private fun processCategory(currentFile: File): Category? {
val listOfDirectory = currentFile.path.split("/".toRegex())
//The root category (in your example, communications)
var rootCategory: Category? = null
//A reminder of the current Category, so we can attach the next one to it
var currentCategory: Category? = null
listOfDirectory.forEach {
if (rootCategory == null) {
//First element, so I need to create the root category
rootCategory = Category(it)
currentCategory = rootCategory
} else {
//Other elements are simply created
val nextCategory = Category(it)
//Added as a subCategory of the previous category
currentCategory!!.subCategory = nextCategory
//And we progress within the chain
currentCategory = nextCategory
}
}
//In the end, my root category will contain :
// Category("communications", Category("email", Category("Beginner", null)))
return rootCategory
}
You can surely adapt this code to your needs, by replacing the constructor that I'm using with your YmlParser