Saturday, July 23, 2022

Only String - based Competitive Programming Questions for Java , Kotlin and C++

 String based Competitive Programming Questions for Java , Kotlin and C++ 

I am sharing question that has been asked in companies like Google, Amazon , Microsoft , Facebook and may other companies. These question are based on only one topic that is "String" . So if you master these questions then I think you are ready for any string problem for competitive programming , interview questions etc.

Find the Questions Based on Strings :

1)Naive Pattern Search

2)Pattern Search

3)Binary String

4)Implement strstr

5)Anagram

6)Check if string is rotated by two places

7)Check if strings are rotations of each other or not

8)Isomorphic Strings

9)Check if a string is Isogram or not

10)Keypad typing

11)Repeating Character - First Appearance Leftmost

12)Non Repeating Character

13)Maximum Occuring Character

14)Remove common characters and concatenate

15)Reverse words in a given string

16)Sum of numbers in string

17)Pangram Checking

18)Minimum indexed character

19)Smallest window in a string containing all the characters of another string

20)Nth number made of prime digits

21)Check if a String is Subsequence of Other

22)Pattern Search KMP

23)Rabin Karp - Pattern Searching

24)Lexicographic Rank Of A String

25)Case-specific Sorting of Strings

26)The Modified String

These are not just questions these concepts to build the thought process for solving problems on Strings.


Sunday, July 17, 2022

KMP Algorithm for Pattern Searching in Java/Kotlin

KMP Algorithm for Pattern Searching in Java/Kotlin

Given a text txt[0..n-1] and a pattern pat[0..m-1], write a function search(char pat[], char txt[]) that prints all occurrences of pat[] in txt[]. You may assume that n > m.

Examples:

Input:  txt[] = "THIS IS A TEST TEXT"

        pat[] = "TEST"

Output: Pattern found at index 10

Input:  txt[] =  "AABAACAADAABAABA"

        pat[] =  "AABA"

Output: Pattern found at index 0

        Pattern found at index 9

        Pattern found at index 12


We have discussed the Naive pattern searching algorithm and the Rabin-Karp algorithm for searching patterns. The worst case complexity of both of the algorithms is O(n*m). Here, we will discuss a new algorithm for searching patterns, KMP algorithm. The time complexity of KMP algorithm is O(n) in the worst case.

KMP (Knuth Morris Pratt) Pattern Searching

The Naive pattern searching algorithm doesn’t work well in cases where we see many matching characters followed by a mismatching character. Following are some examples.

 txt[] = "AAAAAAAAAAAAAAAAAB"

   pat[] = "AAAAB"

   txt[] = "ABABABCABABABCABABABC"

   pat[] =  "ABABAC" (not a worst case, but a bad case for Naive)

The KMP matching algorithm uses degenerating property (pattern having the same sub-patterns appearing more than once in the pattern) of the pattern and improves the worst case complexity to O(n). The basic idea behind KMP’s algorithm is: whenever we detect a mismatch (after some matches), we already know some of the characters in the text of the next window. We take advantage of this information to avoid matching the characters that we know will anyway match. Let us consider the below example to understand this.

Matching Overview

txt = "AAAAABAAABA" 

pat = "AAAA"

We compare first window of txt with pat

txt = "AAAAABAAABA" 

pat = "AAAA"  [Initial position]

We find a match. This is same as Naive String Matching.

In the next step, we compare next window of txt with pat.

txt = "AAAAABAAABA" 

pat =  "AAAA" [Pattern shifted one position]

This is where KMP does optimization over Naive. In this 

second window, we only compare fourth A of pattern

with fourth character of current window of text to decide 

whether current window matches or not. Since we know 

first three characters will anyway match, we skipped 

matching first three characters. 

Need of Preprocessing?

An important question arises from the above explanation, 

how to know how many characters to be skipped. To know this, 

we pre-process pattern and prepare an integer array 

lps[] that tells us the count of characters to be skipped. 

Preprocessing Overview:

KMP algorithm preprocesses pat[] and constructs an auxiliary lps[] of size m (same as size of pattern) which is used to skip characters while matching.

name lps indicates longest proper prefix which is also suffix.. A proper prefix is prefix with whole string not allowed. For example, prefixes of "ABC" are "", "A", "AB" and "ABC". Proper prefixes are "", "A" and "AB". Suffixes of the string are "", "C", "BC" and "ABC".

We search for lps in sub-patterns. More clearly we focus on sub-strings of patterns that are either prefix and suffix.

For each sub-pattern pat[0..i] where i = 0 to m-1, lps[i] stores length of the maximum matching proper prefix which is also a suffix of the sub-pattern pat[0..i].

   lps[i] = the longest proper prefix of pat[0..i] 

              which is also a suffix of pat[0..i]. 

    Note : lps[i] could also be defined as longest prefix which is also proper suffix. We need to use properly at one place to make sure that the whole substring is not considered.

Examples of lps[] construction:
For the pattern “AAAA”, 
lps[] is [0, 1, 2, 3]

For the pattern “ABCDE”, 
lps[] is [0, 0, 0, 0, 0]

For the pattern “AABAACAABAA”, 
lps[] is [0, 1, 0, 1, 2, 0, 1, 2, 3, 4, 5]

For the pattern “AAACAAAAAC”, 
lps[] is [0, 1, 2, 0, 1, 2, 3, 3, 3, 4] 

For the pattern “AAABAAA”, 
lps[] is [0, 1, 2, 0, 1, 2, 3]

    Searching Algorithm: Unlike Naive algorithm, where we slide the pattern by one and compare all characters at each shift, we use a value from lps[] to decide the next characters to be matched. The idea is to not match a character that we know will anyway match.

    How to use lps[] to decide the next positions (or to know a number of characters to be skipped)?

    • We start comparison of pat[j] with j = 0 with characters of current window of text.
    • We keep matching characters txt[i] and pat[j] and keep incrementing i and j while pat[j] and txt[i] keep matching.
    • When we see a mismatch
      • We know that characters pat[0..j-1] match with txt[i-j...i-1] (Note that j starts with 0 and increment it only when there is a match).
      • We also know (from above definition) that lps[j-1] is count of characters of pat[0...j-1] that are both proper prefix and suffix.
      • From above two points, we can conclude that we do not need to match these lps[j-1] characters with txt[i-j...i-1] because we know that these characters will anyway match. Let us consider above example to understand this.

txt[] = "AAAAABAAABA" 
pat[] = "AAAA"
lps[] = {0, 1, 2, 3} 

i = 0, j = 0
txt[] = "AAAAABAAABA" 
pat[] = "AAAA"
txt[i] and pat[j] match, do i++, j++

i = 1, j = 1
txt[] = "AAAAABAAABA" 
pat[] = "AAAA"
txt[i] and pat[j] match, do i++, j++

i = 2, j = 2
txt[] = "AAAAABAAABA" 
pat[] = "AAAA"
pat[i] and pat[j] match, do i++, j++

i = 3, j = 3
txt[] = "AAAAABAAABA" 
pat[] = "AAAA"
txt[i] and pat[j] match, do i++, j++

i = 4, j = 4
Since j == M, print pattern found and reset j,
j = lps[j-1] = lps[3] = 3

Here unlike Naive algorithm, we do not match first three 
characters of this window. Value of lps[j-1] (in above 
step) gave us index of next character to match.
i = 4, j = 3
txt[] = "AAAAABAAABA" 
pat[] =  "AAAA"
txt[i] and pat[j] match, do i++, j++

i = 5, j = 4
Since j == M, print pattern found and reset j,
j = lps[j-1] = lps[3] = 3

Again unlike Naive algorithm, we do not match first three 
characters of this window. Value of lps[j-1] (in above 
step) gave us index of next character to match.
i = 5, j = 3
txt[] = "AAAAABAAABA" 
pat[] =   "AAAA"
txt[i] and pat[j] do NOT match and j > 0, change only j
j = lps[j-1] = lps[2] = 2

i = 5, j = 2
txt[] = "AAAAABAAABA" 
pat[] =    "AAAA"
txt[i] and pat[j] do NOT match and j > 0, change only j
j = lps[j-1] = lps[1] = 1 

i = 5, j = 1
txt[] = "AAAAABAAABA" 
pat[] =     "AAAA"
txt[i] and pat[j] do NOT match and j > 0, change only j
j = lps[j-1] = lps[0] = 0

i = 5, j = 0
txt[] = "AAAAABAAABA" 
pat[] =      "AAAA"
txt[i] and pat[j] do NOT match and j is 0, we do i++.

i = 6, j = 0
txt[] = "AAAAABAAABA" 
pat[] =       "AAAA"
txt[i] and pat[j] match, do i++ and j++

i = 7, j = 1
txt[] = "AAAAABAAABA" 
pat[] =       "AAAA"
txt[i] and pat[j] match, do i++ and j++

We continue this way...

Example Java Code :

public class Main {

void KMPSearch(String pat, String txt) {
int M = pat.length();
int N = txt.length();

// create lps[] that will hold the longest
// prefix suffix values for pattern
int lps[] = new int[M];
int j = 0; // index for pat[]

// Preprocess the pattern (calculate lps[]
// array)
computeLPSArray(pat, M, lps);

int i = 0; // index for txt[]
while (i < N) {
if (pat.charAt(j) == txt.charAt(i)) {
j++;
i++;
}
if (j == M) {
System.out.println("Found pattern "
+ "at index " + (i - j));
j = lps[j - 1];
}

// mismatch after j matches
else if (i < N && pat.charAt(j) != txt.charAt(i)) {
// Do not match lps[0..lps[j-1]] characters,
// they will match anyway
if (j != 0)
j = lps[j - 1];
else
i = i + 1;
}
}
}

void computeLPSArray(String pat, int M, int lps[]) {
// length of the previous longest prefix suffix
int len = 0;
int i = 1;
lps[0] = 0; // lps[0] is always 0

// the loop calculates lps[i] for i = 1 to M-1
while (i < M) {
if (pat.charAt(i) == pat.charAt(len)) {
len++;
lps[i] = len;
i++;
} else // (pat[i] != pat[len])
{
// This is tricky. Consider the example.
// AAACAAAA and i = 7. The idea is similar
// to search step.
if (len != 0) {
len = lps[len - 1];

// Also, note that we do not increment
// i here
} else // if (len == 0)
{
lps[i] = len;
i++;
}
}
}
}

public static void main(String[] args) {
String txt = "ABABDABACDABABCABAB";
String pat = "ABABCABAB";
new Main().KMPSearch(pat, txt);
}
}

Output: Found pattern at index 10

    Preprocessing Algorithm: In the preprocessing part, we calculate values in lps[]. To do that, we keep track of the length of the longest prefix suffix value (we use a len variable for this purpose) for the previous index. We initialize lps[0] and len as 0. If pat[len] and pat[i] match, we increment len by 1 and assign the incremented value to lps[i]. If pat[i] and pat[len] do not match and len is not 0, we update len to lps[len-1]. See computeLPSArray () in the below code for details.
Illustration of preprocessing (or construction of lps[])

pat[] = "AAACAAAA"

len = 0, i  = 0.
lps[0] is always 0, we move 
to i = 1

len = 0, i  = 1.
Since pat[len] and pat[i] match, do len++, 
store it in lps[i] and do i++.
len = 1, lps[1] = 1, i = 2

len = 1, i  = 2.
Since pat[len] and pat[i] match, do len++, 
store it in lps[i] and do i++.
len = 2, lps[2] = 2, i = 3

len = 2, i  = 3.
Since pat[len] and pat[i] do not match, and len > 0, 
set len = lps[len-1] = lps[1] = 1

len = 1, i  = 3.
Since pat[len] and pat[i] do not match and len > 0, 
len = lps[len-1] = lps[0] = 0

len = 0, i  = 3.
Since pat[len] and pat[i] do not match and len = 0, 
Set lps[3] = 0 and i = 4.
We know that characters pat
len = 0, i  = 4.
Since pat[len] and pat[i] match, do len++, 
store it in lps[i] and do i++.
len = 1, lps[4] = 1, i = 5

len = 1, i  = 5.
Since pat[len] and pat[i] match, do len++, 
store it in lps[i] and do i++.
len = 2, lps[5] = 2, i = 6

len = 2, i  = 6.
Since pat[len] and pat[i] match, do len++, 
store it in lps[i] and do i++.
len = 3, lps[6] = 3, i = 7

len = 3, i  = 7.
Since pat[len] and pat[i] do not match and len > 0,
set len = lps[len-1] = lps[2] = 2

len = 2, i  = 7.
Since pat[len] and pat[i] match, do len++, 
store it in lps[i] and do i++.
len = 3, lps[7] = 3, i = 8

We will stop here as we have constructed the whole lps[].



Friday, July 15, 2022

Rabin-Karp Algorithm for Pattern Searching - Algorithm in Java/Kotlin

 Rabin-Karp Algorithm for Pattern Searching - Algorithm in Java/Kotlin

Given a text txt[0..n-1] and a pattern pat[0..m-1], 

write a function search(char pat[], char txt[]) that prints all occurrences of pat[] in txt[]. 

You may assume that n > m.

Examples:

Input:  txt[] = "THIS IS A TEST TEXT"

        pat[] = "TEST"

Output: Pattern found at index 10



Input:  txt[] =  "AABAACAADAABAABA"

        pat[] =  "AABA"

Output: Pattern found at index 0

        Pattern found at index 9

        Pattern found at index 12

The Naive String Matching algorithm slides the pattern one by one. After each slide, one by one it checks characters at the current shift and if all characters match then it prints the match.

Like the Naive Algorithm, Rabin-Karp algorithm also slides the pattern one by one. But unlike the Naive algorithm, Rabin Karp algorithm matches the hash value of the pattern with the hash value of current substring of text, and if the hash values match then only it starts matching individual characters. So Rabin Karp algorithm needs to calculate hash values for following strings.

Pattern itself.

All the substrings of the text of length m, that is of the length of pattern string.

Since we need to efficiently calculate hash values for all the substrings of size m of text, we must have a hash function which has the following property.

Hash at the next shift must be efficiently computable from the current hash value and next character in text or we can say hash(txt[s+1 .. s+m]) must be efficiently computable from hash(txt[s .. s+m-1]) and txt[s+m] i.e., hash(txt[s+1 .. s+m])= rehash(txt[s+m], hash(txt[s .. s+m-1]) and rehash must be O(1) operation.

The hash function suggested by Rabin and Karp calculates an integer value. The integer value for a string is numeric value of a string. For example, if all possible characters are from 1 to 10, the numeric value of "122" will be 122. The number of possible characters is higher than 10 (256 in general) and pattern length can be large. So the numeric values cannot be practically stored as an integer. Therefore, the numeric value is calculated using modular arithmetic to make sure that the hash values can be stored in an integer variable (can fit in memory words). To do rehashing, we need to take off the most significant digit and add the new least significant digit for in hash value. Rehashing is done using the following formula.

hash( txt[s+1 .. s+m] ) = ( d ( hash( txt[s .. s+m-1]) - txt[s]*h ) + txt[s + m] ) mod q

Where,

hash( txt[s .. s+m-1] ) : Hash value at shift s.

hash( txt[s+1 .. s+m] ) : Hash value at next shift (or shift s+1)

d: Number of characters in the alphabet

q: A prime number

h: d^(m-1)

Below is the implementation of the above approach:

public class Main {
// d is the number of characters in
// the input alphabet
public final static int d = 256;

/* pat -> pattern
txt -> text
q -> A prime number
*/
static void search(String pat, String txt, int q) {
int M = pat.length();
int N = txt.length();
int i, j;
int p = 0; // hash value for pattern
int t = 0; // hash value for txt
int h = 1;

// The value of h would be "pow(d, M-1)%q"
for (i = 0; i < M - 1; i++)
h = (h * d) % q;

// Calculate the hash value of pattern and first
// window of text
for (i = 0; i < M; i++) {
p = (d * p + pat.charAt(i)) % q;
t = (d * t + txt.charAt(i)) % q;
}

// Slide the pattern over text one by one
for (i = 0; i <= N - M; i++) {

// Check the hash values of current window of text
// and pattern. If the hash values match then only
// check for characters on by one
if (p == t) {
/* Check for characters one by one */
for (j = 0; j < M; j++) {
if (txt.charAt(i + j) != pat.charAt(j))
break;
}

// if p == t and pat[0...M-1] = txt[i, i+1, ...i+M-1]
if (j == M)
System.out.println("Pattern found at index " + i);
}

// Calculate hash value for next window of text: Remove
// leading digit, add trailing digit
if (i < N - M) {
t = (d * (t - txt.charAt(i) * h) + txt.charAt(i + M)) % q;

// We might get negative value of t, converting it
// to positive
if (t < 0)
t = (t + q);
}
}
}

public static void main(String[] args) {
String txt = "AEEKS FOR AEEKS";
String pat = "AEEK";
int q = 101; // A prime number
search(pat, txt, q);
}
}

Output : 

Pattern found at index 0

Pattern found at index 10

Time Complexity: The average and best case running time of the Rabin-Karp algorithm is O(n+m), but its worst-case time is O(nm). Worst case of Rabin-Karp algorithm occurs when all characters of pattern and text are the same as the hash values of all the substrings of txt[] match with the hash value of pat[]. For example pat[] = "AAA" and txt[] = "AAAAAAA".

Naive Pattern Searching - Strings Algorithm - Problems in Java/Kotlin

 Naive Pattern Searching - Strings Algorithm

Given a text txt[0..n-1] and a pattern pat[0..m-1],

 write a function search(char pat[], char txt[]) that prints all occurrences of pat[] in txt[]. 

 You may assume that n > m.

 Example : 

 Input:  txt[] = "THIS IS A TEST TEXT"

        pat[] = "TEST"

Output: Pattern found at index 10

Input:  txt[] =  "AABAACAADAABAABA"

        pat[] =  "AABA"



Output: 

       Pattern found at index 0

        Pattern found at index 9

        Pattern found at index 12

Pattern searching is an important problem in computer science. When we do search for a string in notepad/word file or browser or database, pattern searching algorithms are used to show the search results.

Naive Pattern Searching: The idea is to slide the pattern over text one by one and check for a match. If a match is found, then slides by 1 again to check for subsequent matches.

That is check for the match of the first character of the pattern in the string, if it matches then check for the subsequent characters of the pattern with the respective characters of the string. If a mismatch is found then move forward in the string.

Below is the implementation of the above approach:

public class Main {

    public static void search(String txt, String pat)

    {

        int M = pat.length();

        int N = txt.length();

        /* A loop to slide pat one by one */

        for (int i = 0; i <= N - M; i++) {

            int j;

            /* For current index i, check for pattern

              match */

            for (j = 0; j < M; j++)

                if (txt.charAt(i + j) != pat.charAt(j))

                    break;

            if (j == M) // if pat[0...M-1] = txt[i, i+1, ...i+M-1]

                System.out.println("Pattern found at index " + i);

        }

    }

    public static void main(String[] args) {

        String txt = "AABAACAADAABAAABAA";

        String pat = "AABA";

        search(txt, pat);

    }

}

Output :

Pattern found at index 0

Pattern found at index 9

Pattern found at index 13

What is the best case? The best case occurs when the first character of the pattern is not present in text at all.

txt[] = "AABCCAADDEE";

pat[] = "FAA";

The number of comparisons in best case is O(n).

What is the worst case ? The worst case of Naive Pattern Searching occurs in following scenarios.

txt[] = "AAAAAAAAAAAAAAAAAA";

pat[] = "AAAAA";

When all characters of the text and pattern are same.

txt[] = "AAAAAAAAAAAAAAAAAB";

pat[] = "AAAAB";

Worst case also occurs when only the last character is different.

The number of comparisons in the worst case is O(m*(n-m+1)). Although strings which have repeated characters are not likely to appear in English text, they may well occur in other applications (for example, in binary texts). The KMP matching algorithm improves the worst case to O(n). We will be covering KMP in the next post. Also, i will be writing more posts to cover all pattern searching algorithms and data structures.

Peeling concept and Problems on Strings - Java/Kotlin Developer

 Peeling concept and Problems on Strings -  Java/Kotlin Developer

Basics: Brief introduction to strings.

Algorithms: We'll look at various pattern matching algorithms in strings.

Implementation: How to use strings in Kotlin and Java.

Objective: The objective of this topic is to familiarize the learners with Strings.

1. Introduction of Strings

Strings are defined as a stream of characters. Strings are used to represent text and are generally represented by enclosing text within quotes as: "This is a sample string!".

Different programming languages have different ways of declaring and using Strings.

In C/C++, Strings are defined as an array of characters. The difference between a character array and a string is that the string is terminated with a special character ‘\0’.

In java , String is a sequence of characters. objects of String are immutable which means a constant and cannot be changed once created.

Kotlin String Example: 

import java.util.*

fun main() {

    stringExample()

}

fun stringExample() {

    val s = "GeeksforGeeks"

    // or String s= new String ("GeeksforGeeks");

    // Returns the number of characters in the String.

    // or String s= new String ("GeeksforGeeks");

    // Returns the number of characters in the String.

    println("String length = " + s.length)

    // Returns the character at ith index.

    // Returns the character at ith index.

    println(

        "Character at 3rd position = "

                + s[3]

    )

    // Return the substring from the ith index character

    // to end of string

    // Return the substring from the ith index character

    // to end of string

    println("Substring " + s.substring(3))

    // Returns the substring from i to j-1 index.

    // Returns the substring from i to j-1 index.

    println("Substring = " + s.substring(2, 5))

    // Concatenates string2 to the end of string1.

    // Concatenates string2 to the end of string1.

    val s1 = "Geeks"

    val s2 = "forGeeks"

    println("Concatenated string = $s1$s2")

    // Returns the index within the string

    // of the first occurrence of the specified string.

    // Returns the index within the string

    // of the first occurrence of the specified string.

    val s4 = "Learn Share Learn"

    println(

        "Index of Share " +

                s4.indexOf("Share")

    )

    // Returns the index within the string of the

    // first occurrence of the specified string,

    // starting at the specified index.

    // Returns the index within the string of the

    // first occurrence of the specified string,

    // starting at the specified index.

    println(

        "Index of a = " +

                s4.indexOf('a', 3)

    )

    // Checking equality of Strings

    // Checking equality of Strings

    var out = ("Geeks" == "geeks")

    println("Checking Equality $out")

    out = ("Geeks" == "Geeks")

    println("Checking Equality $out")

    out = "Geeks".equals("gEeks ", ignoreCase = true)

    println("Checking Equality $out")

    val out1 = s1.compareTo(s2)

    println("If s1 = s2 $out")

    // Converting cases

    // Converting cases

    val word1 = "GeeKyMe"

    println(

        "Changing to lower Case " +

                word1.lowercase(Locale.getDefault())

    )

    // Converting cases

    // Converting cases

    val word2 = "GeekyME"

    println(

        "Changing to UPPER Case " +

                word1.uppercase(Locale.getDefault())

    )

    // Trimming the word

    val word4 = " Learn Share Learn "

    println("Trim the word " + word4.trim { it <= ' ' })

    // Replacing characters

    val str1 = "feeksforfeeks"

    println("Original String $str1")

    val str2 = "feeksforfeeks".replace('f', 'g')

    println("Replaced f with g -> $str2")

}

Output : 

String length = 13

Character at 3rd position = k

Substring ksforGeeks

Substring = eks

Concatenated string = GeeksforGeeks

Index of Share 6

Index of a = 8

Checking Equality false

Checking Equality true

Checking Equality false

If s1 = s2 false

Changing to lower Case geekyme

Changing to UPPER Case GEEKYME

Trim the word Learn Share Learn

Original String feeksforfeeks

Replaced f with g -> geeksgorgeeks

Java Example:

public class Main {

    public static void main(String[] args) {

        String s= "GeeksforGeeks";

        // or String s= new String ("GeeksforGeeks");

        // Returns the number of characters in the String.

        System.out.println("String length = " + s.length());

        // Returns the character at ith index.

        System.out.println("Character at 3rd position = "

                + s.charAt(3));

        // Return the substring from the ith index character

        // to end of string

        System.out.println("Substring " + s.substring(3));

        // Returns the substring from i to j-1 index.

        System.out.println("Substring = " + s.substring(2,5));

        // Concatenates string2 to the end of string1.

        String s1 = "Geeks";

        String s2 = "forGeeks";

        System.out.println("Concatenated string = " +

                s1.concat(s2));

        // Returns the index within the string

        // of the first occurrence of the specified string.

        String s4 = "Learn Share Learn";

        System.out.println("Index of Share " +

                s4.indexOf("Share"));

        // Returns the index within the string of the

        // first occurrence of the specified string,

        // starting at the specified index.

        System.out.println("Index of a = " +

                s4.indexOf('a',3));

        // Checking equality of Strings

        Boolean out = "Geeks".equals("geeks");

        System.out.println("Checking Equality " + out);

        out = "Geeks".equals("Geeks");

        System.out.println("Checking Equality " + out);

        out = "Geeks".equalsIgnoreCase("gEeks ");

        System.out.println("Checking Equality " + out);

        int out1 = s1.compareTo(s2);

        System.out.println("If s1 = s2 " + out);

        // Converting cases

        String word1 = "GeeKyMe";

        System.out.println("Changing to lower Case " +

                word1.toLowerCase());

        // Converting cases

        String word2 = "GeekyME";

        System.out.println("Changing to UPPER Case " +

                word1.toUpperCase());

        // Trimming the word

        String word4 = " Learn Share Learn ";

        System.out.println("Trim the word " + word4.trim());

        // Replacing characters

        String str1 = "feeksforfeeks";

        System.out.println("Original String " + str1);

        String str2 = "feeksforfeeks".replace('f' ,'g') ;

        System.out.println("Replaced f with g -> " + str2);

    }

}

Output  :

String length = 13

Character at 3rd position = k

Substring ksforGeeks

Substring = eks

Concatenated string = GeeksforGeeks

Index of Share 6

Index of a = 8

Checking Equality false

Checking Equality true

Checking Equality false

If s1 = s2 false

Changing to lower Case geekyme

Changing to UPPER Case GEEKYME

Trim the word Learn Share Learn

Original String feeksforfeeks

Replaced f with g -> geeksgorgeeks

Note : Run my code and you will understand all the strings concept. There is nothing beyond this in String. In Next topic I will cover string algos and other manipulations of strings.


Thursday, July 14, 2022

What's new in Kotlin 1.7.0 for Kotlin Developers?

 What's new in Kotlin 1.7.0 for Kotlin Developers?

Kotlin 1.7.0 has been released. It unveils the Alpha version of the new Kotlin/JVM K2 compiler, stabilizes language features, and brings performance improvements for the JVM, JS, and Native platforms.

Here is a list of the major updates in this version:

The new Kotlin K2 compiler is in Alpha now, and it offers serious performance improvements. It is available only for the JVM, and none of the compiler plugins, including kapt, work with it.

A new approach to the incremental compilation in Gradle. Incremental compilation is now also supported for changes made inside dependent non-Kotlin modules and is compatible with Gradle.

We've stabilized opt-in requirement annotations, definitely non-nullable types, and builder inference.

There's now an underscore operator for type args. You can use it to automatically infer a type of argument when other types are specified.

This release allows implementation by delegation to an inlined value of an inline class. You can now create lightweight wrappers that do not allocate memory in most cases.

New Kotlin K2 compiler for the JVM in Alpha

This Kotlin release introduces the Alpha version of the new Kotlin K2 compiler. The new compiler aims to speed up the development of new language features, unify all of the platforms Kotlin supports, bring performance improvements, and provide an API for compiler extensions.

We've already published some detailed explanations of our new compiler and its benefits:

The Road to the New Kotlin Compiler

K2 Compiler: a Top-Down View

It's important to point out that with the Alpha version of the new K2 compiler we were primarily focused on performance improvements, and it only works with JVM projects. It doesn't support Kotlin/JS, Kotlin/Native, or other multi-platform projects, and none of compiler plugins, including kapt, work with it.

You can check out the performance boost on your JVM projects and compare it with the results of the old compiler. To enable the Kotlin K2 compiler, use the following compiler option:

-Xuse-k2

Also, the K2 compiler includes a number of bugfixes. Please note that even issues with State: Open from this list are in fact fixed in K2.

The next Kotlin releases will improve the stability of the K2 compiler and provide more features, so stay tuned!

If you face any performance issues with the Kotlin K2 compiler, please report them to our issue tracker.

Language Update

Kotlin 1.7.0 introduces support for implementation by delegation and a new underscore operator for type arguments. It also stabilizes several language features introduced as previews in previous releases:

Implementation by delegation to inlined value of inline class

Underscore operator for type arguments

Stable builder inference

Stable opt-in requirements

Stable definitely non-nullable types

Allow implementation by delegation to an inlined value of an inline class

If you want to create a lightweight wrapper for a value or class instance, it's necessary to implement all interface methods by hand. Implementation by delegation solves this issue, but it did not work with inline classes before 1.7.0. This restriction has been removed, so you can now create lightweight wrappers that do not allocate memory in most cases.

interface Bar {

    fun foo() = "foo"

}

@JvmInline

value class BarWrapper(val bar: Bar): Bar by bar

fun main() {

    val bw = BarWrapper(object: Bar {})

    println(bw.foo())

}

Underscore operator for type arguments

Kotlin 1.7.0 introduces an underscore operator, _, for type arguments. You can use it to automatically infer a type argument when other types are specified:

abstract class SomeClass<T> {

    abstract fun execute(): T

}

class SomeImplementation : SomeClass<String>() {

    override fun execute(): String = "Test"

}

class OtherImplementation : SomeClass<Int>() {

    override fun execute(): Int = 42

}

object Runner {

    inline fun <reified S: SomeClass<T>, T> run(): T {

        return S::class.java.getDeclaredConstructor().newInstance().execute()

    }

}

fun main() {

    // T is inferred as String because SomeImplementation derives from SomeClass<String>

    val s = Runner.run<SomeImplementation, _>()

    assert(s == "Test")

    // T is inferred as Int because OtherImplementation derives from SomeClass<Int>

    val n = Runner.run<OtherImplementation, _>()

    assert(n == 42)

}

You can use the underscore operator in any position in the variables list to infer a type argument.

Stable builder inference

Builder inference is a special kind of type inference that is useful when calling generic builder functions. It helps the compiler infer the type arguments of a call using the type information about other calls inside its lambda argument.

Starting with 1.7.0, builder inference is automatically activated if a regular type inference cannot get enough information about a type without specifying the -Xenable-builder-inference compiler option, which was introduced in 1.6.0.

Learn how to write custom generic builders.

Stable opt-in requirements

Opt-in requirements are now Stable and do not require additional compiler configuration.

Before 1.7.0, the opt-in feature itself required the argument -opt-in=kotlin.RequiresOptIn to avoid a warning. It no longer requires this; however, you can still use the compiler argument -opt-in to opt-in for other annotations, module-wise.

Stable definitely non-nullable types

In Kotlin 1.7.0, definitely non-nullable types have been promoted to Stable. They provide better interoperability when extending generic Java classes and interfaces.

You can mark a generic type parameter as definitely non-nullable at the use site with the new syntax T & Any. The syntactic form comes from the notation for intersection types and is now limited to a type parameter with nullable upper bounds on the left side of & and a non-nullable Any on the right side:

fun <T> elvisLike(x: T, y: T & Any): T & Any = x ?: y

fun main() {

    // OK

    elvisLike<String>("", "").length

    // Error: 'null' cannot be a value of a non-null type

    elvisLike<String>("", null).length

    // OK

    elvisLike<String?>(null, "").length

    // Error: 'null' cannot be a value of a non-null type

    elvisLike<String?>(null, null).length

}

Kotlin/JVM

This release brings performance improvements for the Kotlin/JVM compiler and a new compiler option. Additionally, callable references to functional interface constructors have become Stable. Note that since 1.7.0, the default target version for Kotlin/JVM compilations is now 1.8.

Compiler performance optimizations

New compiler option -Xjdk-release

Stable callable references to functional interface constructors

Removed the JVM target version 1.6

Compiler performance optimizations

Kotlin 1.7.0 introduces performance improvements for the Kotlin/JVM compiler. According to our benchmarks, compilation time has been reduced by 10% on average compared to Kotlin 1.6.0. Projects with lots of usages of inline functions, for example, projects using kotlinx.html, will compile faster thanks to the improvements to the bytecode postprocessing.

New compiler option: -Xjdk-release

Kotlin 1.7.0 presents a new compiler option, -Xjdk-release. This option is similar to the javac's command-line --release option. The -Xjdk-release option controls the target bytecode version and limits the API of the JDK in the classpath to the specified Java version. For example, kotlinc -Xjdk-release=1.8 won't allow referencing java.lang.Module even if the JDK in the dependencies is version 9 or higher.

This option is not guaranteed to be effective for each JDK distribution.

Stable callable references to functional interface constructors

Callable references to functional interface constructors are now Stable. Learn how to migrate from an interface with a constructor function to a functional interface using callable references.

Removed JVM target version 1.6

The default target version for Kotlin/JVM compilations is 1.8. The 1.6 target has been removed.

Please migrate to JVM target 1.8 or above. Learn how to update the JVM target version for:

Gradle

Maven

The command-line compiler

Kotlin/Native

Kotlin 1.7.0 includes changes to Objective-C and Swift interoperability and stabilizes features that were introduced in previous releases. It also brings performance improvements for the new memory manager along with other updates:

Performance improvements for the new memory manager

Unified compiler plugin ABI with JVM and JS IR backends

Support for standalone Android executables

Interop with Swift async/await: returning Void instead of KotlinUnit

Prohibited undeclared exceptions through Objective-C bridges

Improved CocoaPods integration

Overriding of the Kotlin/Native compiler download URL

Performance improvements for the new memory manager

The new Kotlin/Native memory manager is in Alpha. It may change incompatibly and require manual migration in the future.

The new memory manager is still in Alpha, but it is on its way to becoming Stable. This release delivers significant performance improvements for the new memory manager, especially in garbage collection (GC). In particular, concurrent implementation of the sweep phase, introduced in 1.6.20, is now enabled by default. This helps reduce the time the application is paused for GC. The new GC scheduler is better at choosing the GC frequency, especially for larger heaps.

Also, we've specifically optimized debug binaries, ensuring that the proper optimization level and link-time optimizations are used in the implementation code of the memory manager. This helped us improve execution time by roughly 30% for debug binaries on our benchmarks.

Unified compiler plugin ABI with JVM and JS IR backends

Starting with Kotlin 1.7.0, the Kotlin Multiplatform Gradle plugin uses the embeddable compiler jar for Kotlin/Native by default. This feature was announced in 1.6.0 as Experimental, and now it's stable and ready to use.

This improvement is very handy for library authors, as it improves the compiler plugin development experience. Before this release, you had to provide separate artifacts for Kotlin/Native, but now you can use the same compiler plugin artifacts for Native and other supported platforms.

This feature might require plugin developers to take migration steps for their existing plugins.

Support for standalone Android executables

Kotlin 1.7.0 provides full support for generating standard executables for Android Native targets. It was introduced in 1.6.20, and now it's enabled by default.

If you want to roll back to the previous behavior when Kotlin/Native generated shared libraries, use the following setting:

binaryOptions["androidProgramType"] = "nativeActivity"

Interop with Swift async/await: returning Void instead of KotlinUnit

Kotlin suspend functions now return the Void type instead of KotlinUnit in Swift. This is the result of the improved interop with Swift's async/await. This feature was introduced in 1.6.20, and this release enables this behavior by default.

You don't need to use the kotlin.native.binary.unitSuspendFunctionObjCExport=proper property anymore to return the proper type for such functions.

Prohibited undeclared exceptions through Objective-C bridges

When you call Kotlin code from Swift/Objective-C code (or vice versa) and this code throws an exception, it should be handled by the code where the exception occurred, unless you specifically allowed the forwarding of exceptions between languages with proper conversion (for example, using the @Throws annotation).

Previously, Kotlin had another unintended behavior where undeclared exceptions could "leak" from one language to another in some cases. Kotlin 1.7.0 fixes that issue, and now such cases lead to program termination.

So, for example, if you have a { throw Exception() } lambda in Kotlin and call it from Swift, in Kotlin 1.7.0 it will terminate as soon as the exception reaches the Swift code. In previous Kotlin versions, such an exception could leak to the Swift code.

The @Throws annotation continues to work as before.

Improved CocoaPods integration

Starting with Kotlin 1.7.0, you no longer need to install the cocoapods-generate plugin if you want to integrate CocoaPods in your projects.

Previously, you needed to install both the CocoaPods dependency manager and the cocoapods-generate plugin to use CocoaPods, for example, to handle iOS dependencies in Kotlin Multiplatform Mobile projects.

Now setting up the CocoaPods integration is easier, and we've resolved the issue when cocoapods-generate couldn't be installed on Ruby 3 and later. Now the newest Ruby versions that work better on Apple M1 are also supported.

See how to set up the initial CocoaPods integration.

Overriding the Kotlin/Native compiler download URL

Starting with Kotlin 1.7.0, you can customize the download URL for the Kotlin/Native compiler. This is useful when external links on the CI are forbidden.

To override the default base URL https://download.jetbrains.com/kotlin/native/builds, use the following Gradle property:

kotlin.native.distribution.baseDownloadUrl=https://example.com

The downloader will append the native version and target OS to this base URL to ensure it downloads the actual compiler distribution.

Standard library

In Kotlin 1.7.0, the standard library has received a range of changes and improvements. They introduce new features, stabilize experimental ones, and unify support for named capturing groups for Native, JS, and the JVM:

min() and max() collection functions return as non-nullable

Regular expression matching at specific indices

Extended support of previous language and API versions

Access to annotations via reflection

Stable deep recursive functions

Time marks based on inline classes for default time source

New experimental extension functions for Java Optionals

Support for named capturing groups in JS and Native

min() and max() collection functions return as non-nullable

In Kotlin 1.4.0, we renamed the min() and max() collection functions to minOrNull() and maxOrNull(). These new names better reflect their behavior – returning null if the receiver collection is empty. It also helped align the functions' behavior with naming conventions used throughout the Kotlin collections API.

The same was true of minBy(), maxBy(), minWith(), and maxWith(), which all got their *OrNull() synonyms in Kotlin 1.4.0. Older functions affected by this change were gradually deprecated.

Kotlin 1.7.0 reintroduces the original function names, but with a non-nullable return type. The new min(), max(), minBy(), maxBy(), minWith(), and maxWith() functions now strictly return the collection element or throw an exception.

fun main() {

    val numbers = listOf<Int>()

    println(numbers.maxOrNull()) // "null"

    println(numbers.max()) // "Exception in... Collection is empty."

}

Regular expression matching at specific indices

The Regex.matchAt() and Regex.matchesAt() functions, introduced in 1.5.30, are now Stable. They provide a way to check whether a regular expression has an exact match at a particular position in a String or CharSequence.

matchesAt() checks for a match and returns a boolean result:

fun main() {

    val releaseText = "Kotlin 1.7.0 is on its way!"

    // regular expression: one digit, dot, one digit, dot, one or more digits

    val versionRegex = "\\d[.]\\d[.]\\d+".toRegex()

    println(versionRegex.matchesAt(releaseText, 0)) // "false"

    println(versionRegex.matchesAt(releaseText, 7)) // "true"

}

matchAt() returns the match if it's found, or null if it isn't:

fun main() {

    val releaseText = "Kotlin 1.7.0 is on its way!"

    val versionRegex = "\\d[.]\\d[.]\\d+".toRegex()


    println(versionRegex.matchAt(releaseText, 0)) // "null"

    println(versionRegex.matchAt(releaseText, 7)?.value) // "1.7.0"

}

Extended support for previous language and API versions

To support library authors developing libraries that are meant to be consumable in a wide range of previous Kotlin versions, and to address the increased frequency of major Kotlin releases, we have extended our support for previous language and API versions.

With Kotlin 1.7.0, we're supporting three previous language and API versions rather than two. This means Kotlin 1.7.0 supports the development of libraries targeting Kotlin versions down to 1.4.0. For more information on backward compatibility, see Compatibility modes.

Access to annotations via reflection

The KAnnotatedElement.findAnnotations() extension function, which was first introduced in 1.6.0, is now Stable. This reflection function returns all annotations of a given type on an element, including individually applied and repeated annotations.

@Repeatable

annotation class Tag(val name: String)

@Tag("First Tag")

@Tag("Second Tag")

fun taggedFunction() {

    println("I'm a tagged function!")

}

fun main() {

    val x = ::taggedFunction

    val foo = x as KAnnotatedElement

    println(foo.findAnnotations<Tag>())

    // [@Tag(name=First Tag), @Tag(name=Second Tag)]

}

Stable deep recursive functions

Deep recursive functions have been available as an experimental feature since Kotlin 1.4.0, and they are now Stable in Kotlin 1.7.0. Using DeepRecursiveFunction, you can define a function that keeps its stack on the heap instead of using the actual call stack. This allows you to run very deep recursive computations. To call a deep recursive function, invoke it.

In this example, a deep recursive function is used to calculate the depth of a binary tree recursively. Even though this sample function calls itself recursively 100,000 times, no StackOverflowError is thrown:

class Tree(val left: Tree?, val right: Tree?)

val calculateDepth = DeepRecursiveFunction<Tree?, Int> { t ->

    if (t == null) 0 else maxOf(

        callRecursive(t.left),

        callRecursive(t.right)

    ) + 1

}

fun main() {

    // Generate a tree with a depth of 100_000

    val deepTree = generateSequence(Tree(null, null)) { prev ->

        Tree(prev, null)

    }.take(100_000).last()

    println(calculateDepth(deepTree)) // 100000

}

Consider using deep recursive functions in your code where your recursion depth exceeds 1000 calls.

Time marks based on inline classes for default time source

Kotlin 1.7.0 improves the performance of time measurement functionality by changing the time marks returned by TimeSource.Monotonic into inline value classes. This means that calling functions like markNow(), elapsedNow(), measureTime(), and measureTimedValue() doesn't allocate wrapper classes for their TimeMark instances. Especially when measuring a piece of code that is part of a hot path, this can help minimize the performance impact of the measurement:

@OptIn(ExperimentalTime::class)

fun main() {

    val mark = TimeSource.Monotonic.markNow() // Returned `TimeMark` is inline class

    val elapsedDuration = mark.elapsedNow()

}

This optimization is only available if the time source from which the TimeMark is obtained is statically known to be TimeSource.Monotonic.

New experimental extension functions for Java Optionals

Kotlin 1.7.0 comes with new convenience functions that simplify working with Optional classes in Java. These new functions can be used to unwrap and convert optional objects on the JVM and help make working with Java APIs more concise.

The getOrNull(), getOrDefault(), and getOrElse() extension functions allow you to get the value of an Optional if it's present. Otherwise, you get a default value, null, or a value returned by a function, respectively:

val presentOptional = Optional.of("I'm here!")

println(presentOptional.getOrNull())

// "I'm here!"

val absentOptional = Optional.empty<String>()

println(absentOptional.getOrNull())

// null

println(absentOptional.getOrDefault("Nobody here!"))

// "Nobody here!"

println(absentOptional.getOrElse {

    println("Optional was absent!")

    "Default value!"

})

// "Optional was absent!"

// "Default value!"

The toList(), toSet(), and asSequence() extension functions convert the value of a present Optional to a list, set, or sequence, or return an empty collection otherwise. The toCollection() extension function appends the Optional value to an already existing destination collection:

val presentOptional = Optional.of("I'm here!")

val absentOptional = Optional.empty<String>()

println(presentOptional.toList() + "," + absentOptional.toList())

// ["I'm here!"], []

println(presentOptional.toSet() + "," + absentOptional.toSet())

// ["I'm here!"], []

val myCollection = mutableListOf<String>()

absentOptional.toCollection(myCollection)

println(myCollection)

// []

presentOptional.toCollection(myCollection)

println(myCollection)

// ["I'm here!"]

val list = listOf(presentOptional, absentOptional).flatMap { it.asSequence() }

println(list)

// ["I'm here!"]

These extension functions are being introduced as Experimental in Kotlin 1.7.0. You can learn more about Optional extensions in this KEEP. As always, we welcome your feedback in the Kotlin issue tracker.

Gradle

This release introduces new build reports, support for Gradle plugin variants, new statistics in kapt, and a lot more:

A new approach to incremental compilation

New build reports for tracking compiler performance

Changes to the minimum supported versions of Gradle and the Android Gradle plugin

Support for Gradle plugin variants

Updates in the Kotlin Gradle plugin API

Availability of the sam-with-receiver plugin via the plugins API

Changes in compile tasks

New statistics of generated files by each annotation processor in kapt

Deprecation of the kotlin.compiler.execution.strategy system property

Removal of deprecated options, methods, and plugins

Compatibility guide for Kotlin 1.7.0

Kotlin 1.7.0 is a feature release and can, therefore, bring changes that are incompatible with your code written for earlier versions of the language. Find the detailed list of such changes in the Compatibility guide for Kotlin 1.7.0.


The Quickest Way to Become a Better Developer

 The Quickest Way to Become a Better Developer

What if I tell you that stopping using the else statement will make you a better developer and increase the quality of your code?



Let’s be honest… even though we are always trying to follow standards and best practices, we are only humans (after all)!

We all have different preferences, backgrounds, and experiences that make our code look slightly different from the one of our colleagues, in some way we can say that we all have our style of coding.

Most of the time this “style” consists of little things, like using a ternary operator instead of an if statement for a simple check, or using explicit function declaration instead of arrow functions; Some other times these things are close to being guilty pleasures.

Like everyone, I also have some preferences when I code, and as you could guess by the subtitle of this article, there is one in particular that I’d like to talk about: I never use the else statement!

❓ What is the problem with the else statement?

The answer is pretty simple: I write better code without it!

I know I’m not alone in this “battle”, but for those that are asking why I’m not using a fundamental piece of every programming language, here is an explanation:

We can detect 3 code smells that are often related to the else statement:

if (condition()) {

  // Then Block

} else {

  // Else Block

}

The first smell is related to the condition itself, in fact, if the condition is complicated, the else is twice as complicated because the reader has to invert the condition:

if(something > somethingElse || (!anotherVariable && something === 0)) {

  // then block

} else {

  // else block

  // If this is executed it means that the previous condition was false

  // which means that one of the following conditions was false:

  // - something > somethingElse

  // - !anotherVariable && something === 0

  // Possible scenarios:

  // ---> "something" equals to "somethingElse"

  // ---> "something" greater than "somethingElse"

  // ---> "anotherVariable" true and "something" === 0?

  // ---> "anotherVariable" false and "something" !== 0?

  // ---> "anotherVariable" true and "something" !== 0?

}

The second smell, instead, is somehow related to the visibility of the condition itself: If the “Then Block” contains more than a reasonable amount of lines it’s easy to forget what the condition was.

Last but not least, The third smell is when we have nested if-else blocks because they easily become really — really — hard to read:

if (conditionOne) {

  // then block condition one

  // conditionOne is truthy

  if (conditionTwo) {

    // then block condition two

    // conditionOne is truthy and conditionTwo is truthy

  } else if (conditionThree) {

    // else block condition two

    // but also then block of condition three

    // conditionOne is truthy and conditionTwo is falsy and 

    // conditionThree is truthy... I guess

  } else {

    // Is becoming hard to understand how I got here

    if (conditionFour) {

      // then block condition four

      // I hope no one will enter this block because I don't

      // know how to go back!

    }

  }

}

In all the previous examples, refactoring the code to avoid the usage of the else branch will result in a better and more readable code. This led me to a thought — What if the else branch was never invented?

 Would it be a better world without it?

What if the else branch was never invented?

Would it be a better world without the else statement?

Short answer: Yes.

Long answer:

The problem is not the else statement, the problem is you .

If we analyze again those 3 big “smells”, we can see that the real problem is not the else statement, the real problem is the whole if-else block that, if used in the wrong way, can quickly become a mess.

Forcing yourself not to use the else statement is just an easy trick to better structure your code because you’re forced to extract that piece of code into a separate function — which is always a good idea, whether you’re using the else branch or not.

So… What if there was no “else”?

Let’s start from a fact: the else statement is unnecessary! A world without else is possible!

Even though we wrote millions of conditions in our code, there are 2 possible scenarios for an if-else statement: when there is code after the if-else statement and when there is not:

First scenario: There isn’t any code to be executed after the condition:

function mainFunction() {

  if (condition())  {

    executeIfTheConditionIsTruthy();

  } else {

    executeIfTheConditionIsFalsy();

  }

}

Second scenario: when some code will be executed in any case, no matter the result of the condition.

function mainFunction() {

  if (condition()) {

    executeIfTheConditionIsTruthy();

  } else {

    executeIfTheConditionIsFalsy();

  }

  executeAnyway();

}

In this scenario, extracting the if-else block into a separate function is the way. There is also a name for this pattern: Extract Method.

You can always iterate the process for nested if-else and apply the Return early and Extract method together.

Let me also give you a real-world example, so everything will be much easier to understand:

function isEven(number: number) {

  if (number % 2 !== 0) {

    return false;

  } else {

   return true; 

  }

}

Here we don’t even need the if statement at all, because returning directly "number % 2 === 0 “ is enough, but this is not the point.

We don’t need any explicit else branch — if the code below the if statement is executed, it means that the condition is false.

And so, the rest of the function is an implicit else branch!

The resulting code is more clear and more concise.

Second scenario

function sendNotification(numberOfPeople: number) {

  let message = '';

  if (numberOfPeople === 0) {

    message = "No one will attend your talk 🥲";

  } else {

    message = `${numberOfPeople} people will attend your talk!`;

  }

  NotificationService.send(message);

}

Also in this case not using the else branch ended up in an improvement of the code quality, in fact, the function before was doing 2 things:

- creating the content of the message;

- calling the API to send the notification;

Now instead, the creation of the message is delegated to a dedicated function and the resulting code is easier to read and better structured.

In conclusion

By the way, I don’t have anything against the else statement and I’m not suggesting to always avoid it, all I wanted to point out is:

If you feel lost in a condition, try to refactor the code to avoid the else branches — you’ll have a better code as a result just because you’ll be forced to follow best practices and split the complexity into different functions— but if you don’t feel the smell, everything is fine 😉.