In Kotlin, what is the best way to make `String? + String?` return `null` if any of the inputs are `null`? - kotlin

The problem:
Consider the following code:
val key: String? = "key"
val value: String? = "label"
val row = key + ": " + value
I would like the variable row to be null if any of the supplied inputs in the concatenation is null.
By default, any null String will be converted to "null" and the concatenation will proceed. In example:
val value = null
"Height: " + value + "mm" // Produces: "Height: nullmm"
I can skip the showing "null" in the results by using value ?: "", but this solves only a part of my problem:
val value = null
"Height: " + (value ?: "") + "mm" // Produces: "Height: mm"
My best solution so far:
I understand that writing a simple function like the one below would do the job, but I still expect that something like this already exists in the language:
fun Array<String?>.nullSafeConcat(): String? {
val result = StringBuilder()
this.forEach {
if(it == null) return null
result.append(it)
}
return result.toString()
}
The ask:
Is there a better way to do this?
Post Scriptum:
I cannot understand why would a null string be converted to "null" by default, as I cannot find any use case where this would be actually usable.

I think matt freake's answer is correct for the question, but I would avoid overriding the + operator for Strings - it can cause tons of unexpected issues.
Instead, I suggest you to slightly modify your nullSafeConcat helper function to be a standalone function that takes vararg instead of being an extension function. Something like this:
fun nullSafeConcat(vararg strings: String?): String? {
val result = StringBuilder()
strings.forEach {
if(it == null) return null
result.append(it)
}
return result.toString()
}
Then you can use it like:
val merged = nullSafeConcat("foo", null, "bar", "baz")
Notes:
You might want to handle the empty case (when varargs argument strings is empty) specifically, depending on the outcome you want.
Additionally, if you want this to work for at least 2 strings (so a concatenation is actually meaningful), you can use a signature like nullSafeConcat(first: String?, second: String?, vararg rest: String?) instead.

I'm not sure if this solves the problem, you can override the + operator on a nullable String to get close to what you want. For example:
private operator fun String?.plus(otherString: String?): String? = if (this==null || otherString ==null ) "null" else this + otherString
Then:
fun main() {
val s1: String? = "Hello"
val s2: String? = null
val s3: String? = "Bye"
println(s1 + s2)
println(s2 + s1)
println(s1 + s3)
}
prints:
null
null
HelloBye
The problem is it will only work with variables of String? not String which your ":" value is. So you'd need to do something like:
s1 + colon + s2
where colon was also of type String?
EDIT:
There are two dangers with this approach. Firstly If you don't make it private (or even if you do) there is a risk that existing or new code tries to append two String? values and gets your new behaviour, which they don't expect. Secondly, someone reading the code where you call it may be surprised by the behaviour if they don't spot that you've overridden the + operator.

How about this
listOfNotNull("foo", null, "bar", "baz").joinToString()

Related

Jackson SNAKE_CASE How to generate underscore in field names before number

I have the next peace of code
#Test
fun `simple test`() {
val objectMapper = ObjectMapper()
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE)
.registerModule(KotlinModule())
val value = objectMapper.writeValueAsString(MyClass(myField1 = "something", myField2 = "something2"))
assertNotNull(value)
}
data class MyClass (
val myField1: String? = null,
#JsonProperty("my_field_2")
val myField2: String? = null,
)
the result of deserialization is next
{"my_field1":"something","my_field_2":"something2"}
Is it possible to configure objectMapper to automatically populate _ value, before digits in object property names, without specifying it in #JsonProperty?
Yes, this is possible using a PropertyNamingStrategy:
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
Note that you named your snake-case fields inconsistently, because there is my_field1 without a _ before the digit, and my_field_2 with a _ before the digit. The configuration above using PropertyNamingStrategies.SNAKE_CASE works fine for the first naming (like in my_field1).
If you want to use the second naming (like in my_field_2), then you would have to write your own naming strategy like this:
class MySnakeCaseStrategy : NamingBase() {
override fun translate(input: String?): String? =
if (input == null) null
else "([A-Z]+|[0-9]+)".toRegex().replace(input) { "_${it.groupValues[1]}".lowercase() }
}
That naming strategy can then be used to configure your object-mapper:
objectMapper.setPropertyNamingStrategy(MySnakeCaseStrategy())
I do not know if and how it would be possible to support both naming strategies at the same time.

Unable to replace string inside a String in Kotlin

I am trying to replace a few sub strings inside a string. But my code doesn't seem to work.
val listOfMaleWords = listOf(" him", " he", " his")
val listOfFemaleWords = listOf(" her", " she", " her")
fun modifyIdeaForGender(rawIdea : String, desiredGender : String): String {
var theRawIdea = rawIdea
if (desiredGender == "FEMALE") {
println("desired gender is FEMALE")
listOfMaleWords.forEachIndexed { index, element ->
theRawIdea.replace(element, listOfFemaleWords[index])
}
} else {
println("desired gender is MALE")
listOfFemaleWords.forEachIndexed { index, element ->
theRawIdea.replace(element, listOfMaleWords[index])
}
}
return theRawIdea
}
fun main() {
var sampleString : String = "Tell him, he is special"
println(modifyIdeaForGender(sampleString, "FEMALE"))
}
Expected Output :
"Tell her, she is special"
Current Output :
"Tell him, he is special" // no change
Whats wrong with my code? The current output doesn't replace the string characters at all.
replace returns a new String that you are discarding immediately. It does not mutate theRawIdea itself, so you should assign it back to theRawIdea yourself. For example:
theRawIdea = theRawIdea.replace(element, listOfFemaleWords[index])
Though this would modify theRawIdea as you desire, it wouldn't replace the pronouns correctly. Once it replaces the "him"s with "her"s, it would try to replace the "he"s with "she"s. But note that "he" a substring of "her"! So this would produce:
Tell sher, she is special
This could be fixed by reordering the lists, putting the "he"-"she" pair first, or by using regex, adding \b word boundary anchors around the words:
// note that you should not have spaces before the words if you decide to use \b
val listOfMaleWords = listOf("him", "he", "his")
val listOfFemaleWords = listOf("her", "she", "her")
...
theRawIdea = theRawIdea.replace("\\b$element\\b".toRegex(), listOfFemaleWords[index])
Note that this doesn't account for capitalisation or the fact that changing from female gender pronouns to male ones is inherently broken. Your current code would change all her to him. It would require some more complicated natural language processing to accurately do this task in general.
Taking all that into account, I've rewritten your code with zip:
fun modifyMaleIdeaToFemaleGender(rawIdea : String): String {
var theRawIdea = rawIdea
// if you really want to do the broken female to male case, then this would be
// listOfFemaleWords zip listOfMaleWords
// and the loop below can stay the same
val zipped = listOfMaleWords zip listOfFemaleWords
zipped.forEach { (target, replacement) ->
theRawIdea = theRawIdea.replace("\\b$target\\b".toRegex(), replacement)
}
return theRawIdea
}
You can also use fold to avoid reassigning theRawIdea:
fun modifyIdeaToFemaleGender(rawIdea : String): String {
val zipped = listOfMaleWords zip listOfFemaleWords
return zipped.fold(rawIdea) { acc, (target, replacement) ->
acc.replace("\\b$target\\b".toRegex(), replacement)
}
}
Your code assumes that the replace() method performs an in-place mutation of the string. However, the string with the replaced values are returned by the replace(). So you need to change your code to contain something like:
theRawIdea = theRawIdea.replace(element, listOfFemaleWords[index])
To do this, you will have to use a conventional loop instead of listOfMaleWords.forEachIndexed style looping.

How to use putextra () in Kotin

I want to send data (number) from "edit text section" of Sub-Activity1 (users input a simple number)and receive in another Sub-activity2, and depending on the number I want to show different sets of text. I am a beginner and I am stuck where in Sub-Activity 2 as it returns error for val str where I want to receive and manipulate the number received from Sub-Activity 1 editText.
Sub-Activity 1 :
<Send & Open Sub-Activity2>
getResult.setOnClickListener {
val intent = Intent(this, subactivity::class.java)
val name: String = editTextID.getText().toString()
intent.putExtra(name:"editTextID",value:"7.0")
startActivity(intent)
This returns no error.
Sub-Activity 2: <Receive & Manipulate the text>
class subactivity2 : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_subactivity2)
val str =intent.getStringExtra("editTextID")
when str == 7.0 {
infoTextView.textview.text= "textIwannaShow"
}
}
Franz might be right about you wanting to pass a Double, but since you're passing it as a String and also naming your val str, I'll assume you do want to pass a string (that just happens to represent a number). In which case you need to compare to a string
if (str == "7.0") {
infoTextView.textview.text = "textIwannaShow"
}
or if you do want a when block
when(str) {
"7.0" -> infoTextView.textview.text = "textIwannaShow"
}
If you actually want to work with numbers you'll have to call toDouble() on your string at some point to convert it to one. toDoubleOrNull would be better if you're taking that number from a text input (in case the user doesn't enter a valid number), but you're not actually using the value taken from the EditText
In your Sub-Activity 2, you are receiving a String not an Integer.
So, you should change your code from this
val str =intent.getStringExtra("editTextID")
To this
val str =intent.getIntExtra("editTextID", 0)
Anyway, in the example you are passing 7.0 which is Double, so you probably need this instead of above code
val str =intent.getDoubleExtra("editTextID", 0.0)

null check for vetoable delegates in Kotlin

I need to validate a certain String which a birthdate in YYMMDD format.
The way I did it in my data class is:
val dateOfBirth: String by Delegates.vetoable(
text.split("\n")[1].substring(13, 19),
onChange = { _: KProperty<*>, _: String, newValue: String ->
"\\d{6}".toRegex().matches(newValue)
})
Basically my class gets instantiate with a certain text such as val clz = MyClass(text = "")
However, I'm writing some tests and I'm checking what would happen with an empty String "" and it crashes with
java.lang.IndexOutOfBoundsException: Index: 1, Size: 1
which is the line where text.split("\n")[1].substring(13, 19) because basically, text is empty.
Is there a way in Kotlin to avoid or improve this? In my validation, I'm assuming that the text is not empty, but it can be.
Thanks!
Well you could simply use if to check if string has sufficient length, and depending on business logic assign some default value/raise exception etc...
Something like this:
val dateOfBirth: String by Delegates.vetoable(
if(text.length >= 19)
text.split("\n")[1].substring(13, 19)
else // Handle invalid text length
"" ,
onChange = { _: KProperty<*>, _: String, newValue: String ->
"\\d{6}".toRegex().matches(newValue)
})

Kotlin data classes JSON Deserialization

I am trying convert ApiEmployee to Employee and have written a test around it. I am confused about nulls in Kotlin as I am new to it.
ApiEmployee would be used for JSON conversion so it can have missing name field or or empty or can come as null. In that case, I don't want to add into list and safely ignore it.
I am getting Method threw 'kotlin.KotlinNullPointerException at exception. at apiEmployee.name!!.isNotBlank()
ApiEmployee
data class ApiEmployee(val image: String? = "image",
val name: String? = "name test",
val description: String? = "",
val id: String? = "")
Employee
data class Employee(val imagePath: String, val id: String)
EmployeeConverter(converts ApiEmployee to Employee)
fun apply(apiEmployees: List<ApiEmployee>): List<Employee> {
val employees = mutableListOf<Employee>()
for (apiEmployee in apiEmployees) {
if (apiEmployee.name!!.isNotBlank()){
employees.add(Employee(apiEmployee.image!!, apiEmployee.id!!)
}
}
}
EmployeeConverterTest
#Test
fun `should not add employee without name into employee list`() {
val invalidApiEmployee = ApiEmployee("image", null, "description", "id")
val convertedEmployees : List< Employee > = employeeConverter.apply(listOf( invalidApiEmployee))
assertThat(convertedEmployees.size).isEqualTo(0)
}
What you want to do is check if the name is null first and then if it is empty.
val employeeNameIsNotEmpty = apiEmployee.name?.isNotBlank() ?: false
if (employeeNameIsNotEmpty) {
// do stuff
}
The apiEmployee.name?.isNotBlank() will run and return a value only if name is not null. If name is null then the statment on the right side of ?: will return its value, which in this case should be false.
In this case however Kotlin has already put this particular example into an extension function
.isNullOrBlank()
So you could change it to:
if (!apiEmployee.name.isNullOrBlank()) {
// do stuff
}
As a side note you really don't whant to do this Employee(apiEmployee.image!!, apiEmployee.id!!).
Because image and id could still be null and crash your code with the same error.
Either pass the value for name.
ApiEmployee("image", "name", "description", "id")
(or)
Change the if condition as mentioned below (with ? operator):-
if (apiEmployee.name?.isNotBlank()){
?. performs a safe call (calls a method or accesses a property if the
receiver is non-null)
!! asserts that an expression is
non-null
The code asserts that name is not null and checking for not blank.
Probably, I think you are trying to do null and not blank check. You can use ? operator (safe call) for that. This means isNotBlank() gets executed only if the name is not null.