1. Character and String Processing

For storing characters and strings, Java provides the types char, Character, String and StringBuilder. The use of the data types must be practiced, because each type has its justification. The exercises help the readers to understand the advantages and disadvantages of each data type.

Prerequisites

  • be able to use API of String and StringBuilder safely

  • recognize when String and StringBuilder is more appropriate

Data types used in this chapter:

1.1. The String class and its properties

String is not only a data type that stands for immutable strings, but the class also provides a large number of methods. If you know the methods and how to use them, you can save yourself a lot of work.

1.1.1. Quiz: Is string a built-in keyword? ⭐

Java has built-in data types, including int, double, boolean. Is String also a primitive built-in data type? Is String a keyword in Java?

1.1.2. Building HTML elements with simple concatenation ⭐

As a reminder, tags are used in HTML for markup, an example is <strong><em>Emphasized and Italics</em></strong>.

Task:

  1. Write a new method htmlElement(String tag, String body) that encloses a String with a start and end tag. There is an extra handling:

    • If tag is equal to null or empty ("") then only the body is considered and no start-end tags are written.

    • If body is equal to null then it is considered like a passed empty string.

  2. Write two new methods strong(String) and emphasized(String) that work in the background with htmlElement(…​) and create a <strong> and <em> respectively.

Example:

  • htmlElement( "strong", "strong is bold" )"<strong>strong is bold</strong>"

  • strong( emphasized( "strong + emphasized" )"<strong><em>strong + emphasized</em></strong>"

  • htmlElement( "span", null )"<span></span>"

  • htmlElement( "", "no" )"no"

  • htmlElement( null, "not strong" )"not strong"

  • htmlElement( null, null )""

Note: There are several restrictions on tag names that a good program could check. For example, tag names may only contain the digits 0 to 9 and upper and lower case letters. These cases can be ignored.

1.1.3. Fill strings ⭐

Captain CiaoCiao loves freedom, and spacing is very important to him. Even with texts, he thinks, the letters could have a little more spacing.

Task:

  • Write a method mix(String, String) that spreads a string and puts fill characters between all characters.

  • The parameters may be null.

Examples:

  • mix("We’re out of rum!", "-")"W-e-'-r-e- -o-u-t- -o-f- -r-u-m-!"

  • mix("Blimey", "👻")"B👻l👻i👻m👻e👻y"

  • mix("👻", "👻")"👻"

  • mix("", "👻")""

1.1.4. Check safe transmission by doubling characters ⭐

Bamboo Blobfish uses a type telegraph to communicate important messages to Bonny Brain. Since every character matters, Bamboo sends all characters twice in a row for safety.

Task:

  • Write a method int isEveryCharcterTwice(String) that checks if each character in the string occurs twice in a row.

    • If the number of symbols is odd, the message is wrong and the method returns 0.

    • If each character occurs twice, the answer is any positive number. ˗ If a character does not occur twice in a row, the method returns the position with the first wrong digit, but negated.

Examples:

  • isEveryCharacterTwice("eehhrrwwüürrddiiggeerr$$ccaappttaaiinn")1

  • isEveryCharacterTwice("ccapptttaaiinn")-3

  • isEveryCharacterTwice("222")0

  • isEveryCharacterTwice(null)NullPointerException

The fact that a negative index marks certain locations can also be found in the Java library. The Arrays class provides binarySearch(…​), which searches for something in a sorted array, and if the method finds the element, returns the location; if binarySearch(…​) does not find the entry, it returns the negated position where the element could be inserted.

1.1.5. Swap Y and Z ⭐

Captain CiaoCiao types a longer text on his keyboard, and quite late he notices that instead of the English keyboard layout, the German one is activated. Now "y" and "z", or "Y" and "Z" are swapped. The text has to be corrected.

Task:

  1. Create a new class YZswapper.

  2. Set a new static method void printSwappedYZ(String string), which prints a given string to the screen, but prints the letter "y" as "z", "z" as "y", "Y" as "Z" and "Z" as "Y". The point is not to return a string from the method!

  3. Do not write only one variant, but try to program at least two variants. There is for example, the possibility to check the characters with if-else or with switch-case.

Examples:

  • printSwappedYZ("yootaxz") gives the output zootaxy on the screen and

  • printSwappedYZ("yanthoxzl") gives the output zanthoxyl.

1.1.6. Give defiant answers ⭐

Tony the Defiant is in charge of Captain CiaoCiao’s black market activities, but he gets caught and questioned by the police. To annoy the cops, he repeats everything they say and puts a "No idea!" at the end. If the policeman asks, "Where is the illegal whiskey distillery?", Tony says, "Where is the illegal whiskey distillery? No idea!"

Task:

  1. Create a new class, and ask for an input from the command line.

  2. Depending on the input, distinguish three cases:

    • If the input ends with a ?, then output to the screen whatever is coming from the input, but append " No idea!" at the end.

    • If no question is asked by the police — the input does not end with ? — Tony the Defiant keeps his mouth shut completely.

    • If the input is "No idea?", and regardless of the case, Tony defiantly replies "Aye!".

1.1.7. Quiz: String comparisons with == and equals(…​) ⭐

Strings are objects, and therefore there are basically two ways to compare them:

  • by comparing the references via ==.

  • using the method equals(Object), which is typical for objects.

What difference does this make?

1.1.8. Quiz: Is equals(…​) symmetric? ⭐

Assuming s is a string: is there a difference between s.equals("tutego") and "tutego".equals(s)?

1.1.9. Test strings for palindrome property ⭐

A palindrome is a word that reads the same from the front as from the back, such as "Otto" or even "121".

The fact that such words and even sentences exist at all amuses Captain CiaoCiao, since he can entertain the audience with it. However, he is always presented with strings that are not palindromes. Therefore, all words must be tested beforehand.

Task:

  • Write a Java program that examines whether a string is a palindrome.

    • Create a new class PalindromeTester.

    • Implement a static method boolean isPalindrome(String s).

    • Enhance the program with a class method isPalindromeIgnoringCase(String s), so that the test becomes case-insensitive.

    • Now all characters that are not letters or digits should also be ignored. Character.isLetterOrDigit(char) helps to detect this. This can be used to check sentences like A man a plan a canal Panama or Pepe in Tahiti never has pep or Be mean - always be mean! Let’s call the method `isPalindromeIgnoringNonLettersAndDigits(String).

1.1.10. Check if Captain CiaoCiao is in the middle ⭐

Captain CiaoCiao is the center of the world, so he expects to be in the center in all texts as well.

Task:

  • Write a method boolean isCiaoCiaoInMiddle(String) that returns true if the string "CiaoCiao" is in the middle.

Examples:

  • isCiaoCiaoInMiddle("CiaoCiao")true.

  • isCiaoCiaoInMiddle("!CiaoCiao!")true

  • isCiaoCiaoInMiddle("SupaCiaoCiaoCute")true

  • isCiaoCiaoInMiddle("x!_CiaoCiaoabc")true

  • isCiaoCiaoInMiddle("\tCiaoCiao ")true

  • isCiaoCiaoInMiddle("BambooCiaoCiaoBlop")false

  • isCiaoCiaoInMiddle("BabyTigerChristine")false

1.1.11. Find the shortest name in the array ⭐

Bonny Brain uses only the shortest call name for a person.

Task:

  • Write a method String shortestName(String... names) that returns the shortest partial string of all full names. The string can also contain exactly one space if the name is composed of parts. In other words, there are strings with one name or strings with two names.

  • If there are no names, the answer is an empty string.

  • The vararg array must not be null, and no string in the array must be null.

Example:

  • shortestName("Albert Tross", "Blowfish", "Nick Olaus", "Jo Ker")"Jo"

1.1.12. Count string occurrences ⭐

Captain CiaoCiao eliminated the developer Dev David in a careless action. He was in the process of writing a method; the Javadoc is ready, but the implementation is missing.

/**
 * Counts how many times the substring appears in the larger string.
 *
 * A {@code null} or empty ("") String input returns {@code 0}.
 *
 * <pre>
 * StringUtils.countMatches(null, *)       = 0
 * StringUtils.countMatches("", *)         = 0
 * StringUtils.countMatches("abba", null)  = 0
 * StringUtils.countMatches("abba", "")    = 0
 * StringUtils.countMatches("abba", "a")   = 2
 * StringUtils.countMatches("aaaa", "aa")  = 2
 * StringUtils.countMatches("abba", "ab")  = 1
 * StringUtils.countMatches("abba", "xxx") = 0
 * </pre>
 *
 * @param string  the String to check, may be null
 * @param other   the substring to count, may be null
 * @return the number of occurrences, 0 if either String is {@code null}
 */
public static int countMatches( String string, String other ) { return null; }

Note: The * in the Javadoc symbolizes an arbitrary argument.

Task:

  • Implement the method.

1.1.13. Determine the larger crew size ⭐

Bonny Brain is studying old logbooks that show the strength of her crew and captured ships:

|-|||
|-||
|||-|||
|||||-||

Each crew member is symbolized by a dash, a minus sign separates the crew size. On the left is the number of people on their own ship, on the right is the number on the raided ship.

Task:

  • The dashes are not easy to read for Bonny Brain. Write a program that makes the coding clear:

    |-|| => Raided ship had a larger crew, difference 2
    |-|| => Raided ship had a larger crew, difference 1
    ||-|| => Ships had the same crew size
    |||||-|| => Pirate ship had a larger crew, difference 3

1.1.14. Build diamonds ⭐⭐

Captain CiaoCiao likes diamonds, the bigger the better.

Task:

  • Write a program that generates the following output:

       A
      ABA
     ABCBA
    ABCDCBA
     ABCBA
      ABA
       A

    Using a prompt on the console, it should be possible to specify the maximum width of the diamond. In our example this is 7 — the length of the string ABCDCBA. Only input for the width that can be achieved with strings of ascending and descending uppercase letters should be accepted, i.e. at most the length of ABCDEFGHIJKLMNOPQRSTUVWXYZYXWVU…​BA.

1.1.15. Underline words ⭐⭐

Every now and then, Bonny Brain has to call the crew’s attention to rules. She does this by writing a message and underlining the important words. Bonny Brain has underlined the word "treasure" in the following message:

There is more treasure in books than in all the pirates' loot on Treasure Island
              -------- --------

Task:

  1. Create a new class PrintUnderline.

  2. Write a new static method printUnderline(String string, String search) that underlines each string search in string as shown in the example above. Keep in mind that search can occur more than once in the string or not at all.

  3. The case of the search string should not matter, as also shown in the example.

1.1.16. Check for a good password ⭐

All the dirty secrets are encrypted by Captain CiaoCiao, but too often his password was too simple and was guessed. He has learned that a secure password is important for his business, but he can’t quite remember the rules: a good password has a certain length, contains special characters, etc.

Task:

  1. Create a new class PasswordTester.

  2. Write a method isGoodPassword(String) that tests some criteria. The method should return false if the password is weak, and true if the password has a proper syntax. If a test fails, a message should appear via System.err and no further checks should take place.

1.1.17. Calculate sum of digits ⭐

Since Bonny Brain often instructs payments and fears that someone might change the amounts, she resorts to a trick: In addition to the amount, she transmits the cross sum in a separate channel.

The cross sum of a number is formed by adding each digit of the number. For example, if the number is 10938, the cross sum is 1 + 0 + 9 + 3 + 8 = 21.

Task:

  1. Create a new class SumOfTheDigits.

  2. Write a class method int digitSum(long value) that calculates the cross sum of a number.

  3. Add an overloaded class method int digitSum(String value) that takes the digits in a string.

Which method is easier to implement? Which method should call the other as a subroutine?

1.1.18. Decolumnize texts ⭐⭐

Captain CiaoCiao is scanning old logbooks, but they were originally in columns. After OCR text recognition, the columns are preserved.

Since this is hard to read, the two columns should be recognized and translated into a regular continuous text without columns.

Task:

  • Write a method decolumnize(String) that searches for the column and returns a text with one column from a text with two columns.

Example:

I’m dishonest, and a to watch out for,
dishonest man you    because you can
can always trust to  never predict when
be dishonest.        they’re going to do
Honestly, it’s the   something incredibly
honest ones you want stupid.

I’m dishonest, and a
dishonest man you
can always trust to
be dishonest.
Honestly, it’s the
honest ones you want
to watch out for,
because you can
never predict when
they’re going to do
something incredibly
stupid.

Each column is separated by at least one space. Note that the right and left column can have incomplete blank lines!

1.1.19. Draw a meadow with favorite flowers ⭐⭐

Captain CiaoCiao wants to beautify his ship and decorate it with flowers. He finds a graphic by Joan G. Stark to use as a template and thinks about how to tell the painters and decorators what patterns he wants in the cabin.

                _
              _(_)_                          wWWWw   _
  @@@@       (_)@(_)   vVVVv     _     @@@@  (___) _(_)_
 @@()@@ wWWWw  (_)\    (___)   _(_)_  @@()@@   Y  (_)@(_)
  @@@@  (___)     `|/    Y    (_)@(_)  @@@@   \|/   (_)\
   /      Y       \|    \|/    /(_)    \|      |/      |
\ |     \ |/       | / \ | /  \|/       |/    \|      \|/
\\|//   \\|//   \\\|//\\\|/// \|///  \\\|//  \\|//  \\\|//
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Task:

  1. Copy the flowers into a string. Tip: Create a string like String flower = "";, put the flower string on the clipboard, and paste it between the quotes in the IDE; IntelliJ and Eclipse will then independently encode the special characters like \ and \n. Java text blocks are also a good option for this multiline string.

  2. There are 8 kinds of flowers, and we can number them from 1 to 8. We encode the order as a string, for example "12345678". It should now be possible to change the order and have flowers appear more than once, for example by encoding "8383765432". If an identifier is wrong, the first flower will always appear automatical for it.

Examples:

  • "838" leads to

       _    _(_)_      _
     _(_)_ (_)@(_)   _(_)_
    (_)@(_)  (_)\   (_)@(_)
      (_)\      `|/   (_)\
         |      \|       |
        \|/      | /    \|/
      \\\|/   \\\|//  \\\|/
    ^^^^^^^^^^^^^^^^^^^^^^^
  • "ABC9" leads to

      @@@@   @@@@   @@@@   @@@@
     @@()@@ @@()@@ @@()@@ @@()@@
      @@@@   @@@@   @@@@   @@@@
       /      /      /      /
    \ |    \ |    \ |    \ |
    \\|//  \\|//  \\|//  \\|//
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Hold the locations for the transition between the flowers in an array.

1.1.20. Detect repetitions ⭐⭐⭐

Captain CiaoCiao flips through a book and finds patterns of the sort 🌼🌻🌼🌻🌸🌼🌻🌼🌻🌸🌼🌻🌼🌻🌸. How reassuring for him. He wants to make stamps for printing, so he can print such sequences of patterns himself. Of course, the cost should be reduced, but the stamp itself should not contain any repetitions of symbols. For a given pattern sequence, a program needs to be developed that determines the minimum sequence of symbols that need to be on the stamp.

Task:

  • Write a method String repeatingStrings(String) that returns the repeating string in case of a repetition, otherwise null if no substring repeats.

Examples:

  • repeatingStrings("🌼🌼🌼") returns "🌼".

  • repeatingStrings("🌼🌻"+"🌼🌻"+"🌼🌻") returns "🌼🌻".

  • repeatingStrings("Ciao "+"Ciao") returns "Ciao".

  • repeatingStrings("Captain CiaoCiaoCaptain CiaoCiao") returns "Captain CiaoCiao".

  • repeatingStrings("🌕🌔🌓🌑") returns null.

  • repeatingStrings("CaptainCiaoCiaoCaptain") return null

  • repeatingStrings("🌼") returns null

  • repeatingStrings("") return null

  • repeatingStrings(null) return null

Note: repeatingStrings(…​) should return the shortest repeating string.

1.1.21. Constrain line boundaries and wrap lines ⭐⭐

Bonny Brain is switching to carrier pigeons for communication, and there isn’t too much space on the paper. Now all texts must be reduced in width.

Task:

  • Write a class WordWrap with a static method String wrap(String string, int width) that splits a string without line breaks into small substrings of maximum width width and returns them separated by \n. Inside words — and punctuation marks belong to the word — should not be forcibly wrapped!

Example:

  • The call to

    String s = "Live now; make now always xxxxxxxxxxxx the most precious time. "
               + "Now will never come again.";
    System.out.println( wrap( s, 10 ) );

    returns the following output with a maximum line length of 30:

    Live now; make now always the
    most precious time. Now will
    never come again.

1.1.22. Quiz: How many string objects? ⭐

How many string objects are present in the following program code?

String str1 = "tutego";
String str2 = new String( "tutego" );
String str3 = "tutego";
String str4 = new String( "tutego" );

1.1.23. Test if the fruit is wrapped in chocolate ⭐⭐

Captain CiaoCiao likes fruit skewers covered in chocolate. Sara gets the job of frosting the fruit, making different layers of dark and white chocolate.

Bambi checks to make sure the layers are correct. If she sees dhFhd, she knows that the fruit F got first a layer of white chocolate, then a layer of dark chocolate. At dhhd the fruit is missing, and this does not match Captain CiaoCiao’s expectation. At ddhFh the layer is broken, which is also not right. And at F the chocolate is completely missing, what a disappointment!

Task:

  • Write a recursive method checkChocolate(String) that checks if the string is symmetric, that is, on the left and on the right is the same type of chocolate and in the middle is the fruit F.

1.1.24. From top to bottom, from left to right ⭐⭐⭐

In a cave Bonny Brain discovers a text, however the text does not run from left to right, but is written from top to bottom.

s u
ey!
ao

Written vertically, this is the string sea you! — and it’s much easier to read!

Task:

  • Write a method printVerticalToHorizontalWriting(String) that returns a string to its horizontal position and prints it. The argument is a string in which line feeds separate the lines.

Example:

  • Let’s stick with the string from above:

    String s = "s u\ney!\nao ";
    printVerticalToHorizontalWriting( s );

    The output on the screen will be sea you!.

Three important assumptions should hold:

  1. Lines are separated only with `\n .Each line is the same length; for example, the last line has a space so that all lines are 3 characters (in this example) long (not counting line breaks) .

  2. There is no \n at the end of the string.

1.2. Dynamic strings with StringBuilder

While String objects are immutable, objects of java.lang.StringBuilder can be modified. The same is true for StringBuffer, but this class is API-like and not relevant for the exercises.

1.2.1. Practicing the alphabet with a parrot ⭐

Captain CiaoCiao is teaching his parrot the alphabet. To save himself the time and effort, he uses a program that generates the alphabet for him.

Given is the following method:

static String abcz() {
  String result;
  result = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  return result;
}

The method returns a string with all characters in the alphabet from 'A' to 'Z'. So Captain CiaoCiao can copy the string to an input field at https://ttsreader.com/de/ and let it read aloud. However, there must then be spaces between the letters to make the sound better.

The method is performant in its task, but not very flexible, for example, when it comes to generating only certain ranges, such as from '0' to '9'. Because the parrot can already do ABC very well, but from G to Z it gets tough.

Task:

  • Change the method abcz() so that the String is dynamically generated via a loop.

  • Add a method String abcz(char start, char end) that generates a string with all symbols between the start character start and the end character end; the end character is included and belongs to the string.

  • Write a method String abcz(char start, int length) which returns length characters starting from start. Can one of the methods be mapped to the other?

  • Consider how to handle incorrect parameters, such as when the end index is before the start index.

The primitive data type char is nothing more than a numeric positive data type that can be converted to the data type int. Consequently, we can calculate with a char, and if we take the letter 'A' and add 1 to it, we come out with 'B'.

1.2.2. Quiz: lightly attached ⭐

If you want to build strings dynamically, you have basically two options in Java :

  1. via the class String and the String concatenation with +.

  2. via StringBuilder (we do not want to explicitly mention StringBuffer here — the two classes are API-identical).

In code:

String s = "";
s += "Ay ";
s += "Captain";

StringBuilder sb = new StringBuilder();
sb.append( "Ay " ).append( "Captain" );
String t = sb.toString();

How do the two solutions differ? How many objects are generated?

1.2.3. Convert number to textual unary encoding ⭐

The unary encoding represents a natural number n as follows:

nunäre Kodierung

0

0

1

10

2

110

3

1110

4

11110

A positive integer n is represented by n ones followed by a zero. By the way, a code of this kind is called prefix-free, because no word is the prefix of another word. In the encoding, we could also swap 0s and 1s without any problem, and the prefix characteristic does not change.

Unary encoding results in codes of different lengths.

Task:

  • Write a method String encode(int... values) that creates a string from a vararg array with integers, in which all unary encoded values from the array are concatenated.

  • Add a method int[] decode(String value) that turns an unary encoded string back into an int array.

Example:

  • encode( 0, 1, 2, 3, 0, 1 )"0101101110010"

  • encode( 0, 0, 0 )"0000"

  • encode()""

  • Arrays.toString( decode("0101101110010") )[0, 1, 2, 3, 0, 1]

1.2.4. Lose weight by moving digits ⭐

Bonny Brain has noticed that it’s great to cheat on freight charges. The people in charge in the office often forget the exact weight, but they can remember the occurring digits perfectly, and it is not noticed if at most two digits are swapped. Bonny Brain uses this to its advantage by putting the smallest digit in the number forward to achieve a smaller weight, but 0 must be ignored, otherwise the length of the number changes, and this is noticeable.

Task:

  • Write a method int cheatedWeight(int weight) that does exactly this transformation.

Examples:

  • cheatedWeight(0)0

  • cheatedWeight(1234)1234

  • cheatedWeight(4321)1324

  • cheatedWeight(100)100

  • cheatedWeight(987654321)187654329

1.2.5. Remove vowels ⭐

Captain CiaoCiao dictates his memoir and words coming out of his mouth quickly. Kiko Kokopu can’t take notes that fast! But can’t you leave out all the vowels and still understand it later? A linguist once said that a text remains understandable even after the vowel letters have been removed. Common vowel letters are: A, E, I, O, U. Is it true what the scientists say?

Task:

  1. Create a new class RemoveVowel.

  2. Write a class method String removeVowels(String string) that removes the vowel letters from a passed String.

  3. Solve the task with at least two different variants.

Examples:

  • "Hello Javanese""Hll Jvnsn"

  • "BE NICE""B NC"

1.2.6. Don’t shoot the Messenger ⭐

Bonny Brain is sending a secret message and is afraid that the messenger will be attacked and the message will be revealed. So she sends several messengers, each delivering a part of the message. The scheme is like this:

  • A text is broken down into letters.

  • Messenger 1 gets the 1st letter

  • Messenger 2 gets the 2nd letter

  • messenger 1 gets the 3rd letter

  • messenger 2 gets the 4th letter

  • etc.

The recipient of the message must now wait for the two messengers, know the original order, and reassemble the message.

Task:

  • Write a method String joinSplitMessages(String...) that takes any number of the split messages and returns the assembled string.

  • If message parts are missing, this should not cause an error.

Examples:

  • joinSplitMessages("Hoy", "ok")"Hooky"

  • joinSplitMessages("Hooky")"Hooky"

  • joinSplitMessages("Hk", "oy", "o")"Hooky"

  • joinSplitMessages( "H", "", "ooky" )"Hooky"

1.2.7. Compress repeated spaces ⭐⭐

Bubbles listens in on a conversation for Captain CiaoCiao and then transcribes it. But because Bubbles always peels so many peanuts, the space bar is stuck and doesn’t release well; so a space quickly doubles. In no way can she give the text Captain CiaoCiao this way.

Task:

  • Write a static method StringBuilder compressSpace(StringBuilder string) that merges more than two spaces in the passed string into one space.

  • The passed string should be returned with return string;, the change should be done directly at StringBuilder.

Example:

  • "Will ​ ​ you shut up, ​ man! ​ ​ This is the way!""Will you shut up, man! This is the way!"

1.2.8. Insert and remove crackles and pops ⭐

Messages over the radio often have crackling, which is distracting Captain CiaoCiao.

Task:

  • Write two methods:

    • String crackle(String) should insert "♬KNACK♪" at arbitrary intervals and return the crackle string.

    • String decrackle(String) shall remove the crackle again.

1.2.9. Split CamelCase strings ⭐

To save volume when transmitting text by telegraph, Funker Frogfish uses a trick: He capitalizes the next character after the space and then deletes the space. ciao ciao becomes ciaoCiao. If the next character is already an uppercase letter, only the space is removed, thus Ciao Ciao becomes CiaoCiao. Since the uppercase letters within the series of lowercase letters look like humps, the spelling was named CamelCase.

Captain CiaoCiao receives the strings, but it is not easy to read such a text.

Task:

  • Write a new method String camelCaseSplitter(String) that separates all CamelCase segments again.

Examples:

  • camelCaseSplitter("List")"List"

  • camelCaseSplitter("CiaoCiao")"Ciao Ciao"

  • camelCaseSplitter("numberOfElements")"number Of Elements"

  • camelCaseSplitter("CiaoCiaoCAPTAIN")"Ciao Ciao CAPTAIN"

If words are completely in uppercase, as in the last case, only the change between lowercase and uppercase applies.

1.2.10. Implement Caesar encryption ⭐⭐⭐

Captain CiaoCiao has learned about an encryption that is supposed to have already been used by Gaius Julius Caesar. Since he admires the Roman general, he also wants to encrypt his texts this way.

In the so-called Caesar encryption, one shifts each character by three positions in the alphabet, that is, A becomes D, B becomes E, and so on. At the end of the alphabet, we start over again, and so X becomes A, Y becomes B, Z becomes C.

Task:

  1. Create a new class Caesar.

  2. Implement a method String caesar(String s, int rotation) that does the encryption. rotation` is the shift, which should be arbitrary, not only 3 as from the input example.

  3. Write a method String decaesar(String s, int rotation) which takes back the encryption.

  4. Caesar encryption falls into the class of decryption ciphers. Is it Captain CiaoCiao to be recommended?

Example:

  • caesar( "abxyz. ABXYZ!", 13 )"noklm. NOKLM!"

  • decaesar( caesar( "abxyz. ABXYZ!", 13 ), 13 ) )"abxyz. ABXYZ!"

1.3. Suggested solutions

1.3.1. Quiz: Is string a built-in keyword?

String is not a built-in data type. All keywords in Java are lowercase, and all reference types are uppercase according to the naming convention. java.lang.String is a class, and String objects exist as instances at runtime. The String class inherits from java.lang.Object like all other classes.

1.3.2. Building HTML elements with simple concatenation

com/tutego/exercise/string/HtmlBuilder.java
public static String htmlElement( String tag, String body ) {
  if ( tag == null )
    tag = "";
  if ( body == null )
    body = "";
  if ( tag.isEmpty() )
    return body;
  else
    return "<" + tag + ">" + body + "</" + tag + ">";
}

public static String strong( String body ) {
  return htmlElement( "strong", body );
}

public static String emphasized( String body ) {
  return htmlElement( "em", body );
}

The general method String htmlElement(String tag, String body) can be used by the other methods.

Tag and body may be null according to the assignment, then they are treated like an empty string. Then comes the main check: if the tag is empty, only the body is returned; if the tag is not empty, our method creates a start tag, concatenates the body and appends an end tag.

For the strong(…​) and emphasized(…​) methods, we fall back to our previously defined method, fill the tag name with strong and em respectively, and pass the body.

1.3.3. Fill strings

com/tutego/exercise/string/StringFiller.java
private static String mix( String string, String fill ) {

  if ( string == null || string.isEmpty() )
    return "";

  if ( fill == null || fill.isEmpty() )
    return string;

  String result = "";

  for ( int i = 0; i < string.length() - 1; i++ ) {
    char c = string.charAt( i );
    result += c + fill;
  }

  result += string.charAt( string.length() - 1 );

  return result;
}

Let’s start in the method by checking the parameters. If the string is null, or empty, we return an empty string. If the string is not null and it has at least one character, but the fill string is null or empty, there is nothing to do, and we take a shortcut by returning string directly. In principle, a NullPointerException would also be appropriate if string or fill were equal to null, because if no valid objects are passed in, methods had better throw an exception.

We start with an empty string, which we fill in a loop. After we have extracted a character, we append this character and the string fill to the previous result. However, we do not run with the index to the very last character, but only to the second to last, so that we can always put the fill string, just not after the last element. Therefore the termination condition is < string.length() - 1. We append the last character to result at the end after the loop and then return result to the caller.

Via the first case distinction we have tested that the string is at least one character long, therefore we also have the possibility to run from 0 to the index < string.length() - 1, because if the string length is 1, 1 - 1 is equal to 0, and we have no loop pass, only the one "last" character is appended to result.

The fact that the fill string is placed between the characters can also be solved differently: we could query the index and see if it does not stand for the last character, and only then append the fill characters. If the index is on the last character, the fill characters are not appended.

1.3.4. Check safe transmission by doubling characters

com/tutego/exercise/string/RepeatingCharacters.java
private static int isEveryCharacterTwice( String string ) {

  int FAILURE_CODE = 0;
  int SUCCESS_CODE = 1;

  if ( string.length() % 2 != 0 )
    return FAILURE_CODE;

  for ( int i = 0; i < string.length(); i += 2 ) {
    char first  = string.charAt( i );
    char second = string.charAt( i + 1 );
    if ( first != second )
      return -(i + 1);
  }

  return SUCCESS_CODE;
}

The method gets a string and first tests if the input is correct. The number of characters must be even, because if the number is odd, it is not possible that each character occurs twice. If null is passed, there will be a NullPointerException when trying to retrieve the length; this is to be expected.

In the next step, we run two-steps over the array. We extract the character at location i and the following character at location i + 1. If the two characters do not match, then i + 1 is where the character is not like the character before it. We negate the expression and report the location. This always gives a negative return value, because we do not invert i (the index starts at 0), but i + 1, which leads to -1 in the result. The negative returns are always odd.

If there is no exit from the loop, the method returns 1.

1.3.5. Swap Y and Z

Java provides several options for comparisons: if-else, switch-case and the conditional operator. This reflects the solutions:

com/tutego/exercise/string/YZswapper.java
static void printSwappedYZ1( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    if ( c == 'y' ) c = 'z';
    else if ( c == 'z' ) c = 'y';
    else if ( c == 'Y' ) c = 'Z';
    else if ( c == 'Z' ) c = 'Y';
    System.out.print( c );
  }
}

In the first proposed solution, we run the string from front to back. We can query the number of characters with length() and each character at a position with chatAt(…​). If we have the character at a position, it can be checked and replaced by another character, which we finally output. This variant contains a screen output, and we do not temporarily create a new string.

com/tutego/exercise/string/YZswapper.java
static void printSwappedYZ2( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    switch ( string.charAt( i ) ) {
      case 'y': System.out.print( 'z' ); break;
      case 'z': System.out.print( 'y' ); break;
      case 'Y': System.out.print( 'Z' ); break;
      case 'Z': System.out.print( 'Y' ); break;
      default:  System.out.print( string.charAt( i ) );
    }
  }
}

The second variant also runs over the string by hand. However, instead of comparing the individual characters via ==, this solution uses a switch-case construction. The fact that System.out.print(char) occurs several times is not nice.

com/tutego/exercise/string/YZswapper.java
static void printSwappedYZ3( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    System.out.print( c == 'y' ? 'z' :
                      c == 'Y' ? 'Z' :
                      c == 'z' ? 'y' :
                      c == 'Z' ? 'Y' :
                      c );

  }
}

The third variant uses a nested conditional operator to compare if the letter is Y or Z.

As of Java 14, there are also new switch notations with a ->, which allows for more variations. Option 1:

com/tutego/exercise/string/YZswapper.java
static void printSwappedYZ5( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    switch ( string.charAt( i ) ) {
      case 'y' -> System.out.print( 'z' );
      case 'z' -> System.out.print( 'y' );
      case 'Y' -> System.out.print( 'Z' );
      case 'Z' -> System.out.print( 'Y' );
      default  ->  System.out.print( string.charAt( i ) );
    }
  }
}

Option 2:

com/tutego/exercise/string/YZswapper.java
static void printSwappedYZ6( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    System.out.print(
        switch ( string.charAt( i ) ) {
          case 'y' -> 'z'; case 'Y' -> 'Z';
          case 'z' -> 'y'; case 'Z' -> 'Y';
          default  -> string.charAt( i );
        } );
  }
}

Some reader may wish for a method to swap characters, but there is no such method for strings. There is a replace(…​) method, but it doesn’t help us, because if we replace for example "y" with "z", later the "original" "z" characters are no longer recognizable and must then be converted to "y". In principle, we could proceed in three steps and use, for example, a special character as a placeholder:

  1. Replace all "y" with "$".

  2. Replace all "z" with "y".

  3. Replace all "$" with "z".

This is not elegant, especially since we have to make sure that a placeholder symbol like "$" is free.

We have seen that there are several ways to solve the task. Whenever we need to check a variable against several constants known at compile time, switch is a good choice. If you can use the current Java versions, you should use the notation with switch and arrow. It is not only shorter, but also prevents unintentional fallthroughs.

1.3.6. Give defiant answers

com/tutego/exercise/string/TonyTheDefiant.java
String input = new Scanner( System.in ).nextLine().trim();
if ( input.equalsIgnoreCase( "no idea?" ) )
  System.out.println( "Aye!" );
else if ( input.endsWith( "?" ) ) {
  System.out.println( input + " No idea!" );
}

We query for a line from the command line using Scanner, and then cut white space front and back. If the input is No idea? the output is Aye!. For case-insensitive testing, we rely on equalsIgnoreCase(…​), which is faster than an input.toLowerCase().equals("no idea?"), because equalsIgnoreCase(…​) can compare directly, while toLowerCase() first creates a new String object to compare with equals(…​). But the String object is memory garbage again after the equals(…​). Building new objects unnecessarily costs memory and runtime and should be avoided.

If the input is not No idea?, we test if the input ends with a question mark. If so, we repeat the input and append No idea! at the end. If neither case is true, nothing happens. It is important that we do not first test whether the input ends with a question mark, because if we reverse the case distinctions, the program would recognize the question mark at the back when No idea? is entered and output No idea? No idea!, which is not correct.

1.3.7. Quiz: String comparisons with == and equals(…​)

In principle, strings can be compared with ==, but usually this will not work, because that would require that the String objects are only built internally in the virtual machine. Normally, however, we build objects ourselves, e.g. by reading a file line by line or by reading from the command line. Then each line and input are a new String object. Java developers should make it a habit to always compare strings with the equals(…​) method. There are also variants here, e.g. equalsIgnoreCase(…​).

Newcomers from other programming languages have difficulties at the beginning. Most programming languages allow to compare strings with ==.

1.3.8. Quiz: Is equals(…​) symmetric?

Comparisons with the equals(Object) method should be symmetric, that is, there should be no difference whether it says a.equals(b) or b.equals(b). However, the symmetry is broken by null. Thus, String s = null; s.equals("tutego") leads to a NullPointerException, but "tutego".equals(s) leads to false. In practice, therefore, it is more robust to call equals(…​) on a string literal and pass another string.

If null can occur in the comparison and NullPointerException is to be prevented, Objects.equals(…​) can be used well:

OpenJDK implementation of Objects.equals(…​).
public static boolean equals(Object a, Object b) {
    return (a == b) || (a != null && a.equals(b));
}

1.3.9. Test strings for palindrome property

com/tutego/exercise/string/PalindromeTester.java
public static boolean isPalindrome( String string ) {

  for ( int index = 0; index < string.length() / 2; index++ ) {
    char frontChar = string.charAt( index );
    char backChar  = string.charAt( string.length() - index - 1 );
    if ( frontChar != backChar )
      return false;
  }
  return true;
}

public static boolean isPalindromeIgnoringCase( String string ) {
  return isPalindrome( string.toLowerCase() );
}

public static boolean isPalindromeIgnoringNonLettersAndDigits(String string) {

  for ( int startIndex = 0, endIndex = string.length() - 1;
        startIndex < endIndex;
        startIndex++, endIndex-- ) {
    while ( ! Character.isLetterOrDigit( string.charAt( startIndex ) ) )
      startIndex++;
    while ( ! Character.isLetterOrDigit( string.charAt( endIndex ) ) )
      endIndex--;

    char frontChar = Character.toLowerCase( string.charAt( startIndex ) );
    char backChar  = Character.toLowerCase( string.charAt( endIndex ) );
    if ( frontChar != backChar )
      return false;
  }
  return true;
}

public static boolean isPalindromeRecursive( String string ) {

  if ( string.length() < 2 )
    return true;

  if ( string.charAt( 0 ) != string.charAt( string.length() - 1 ) )
    return false;

  return isPalindromeRecursive( string.substring( 1, string.length() - 1 ) );
}

The task of testing with palindromes is a classic in computer science. There are different approaches to this task; it can be answered relatively easily with a Java library method in a one-liner, or it can be solved iteratively via a loop and also recursively.

The solution shown here is simple and performant. If we want to test whether a string is a palindrome, all we have to do is compare the first character with the last, then the second with the second to last, and so on. We loop from 0 to half of the string and do exactly that: with string.charAt(index) we run from the left to the middle and with string.charAt( string.length() - index - 1 ) from the right to the middle. Once we have extracted the characters, we compare them; if the characters are not equal, we exit the method with return false. If the program survives the loop, string is a palindrome. The solution works for strings with an even and also odd number of characters.

Our method isPalindromeIgnoringCase(String) tests whether the string is also a palindrome regardless of case. In this case, we first convert the string to lowercase and pass it to our existing method isPalindrome(String) for testing.

For the isPalindromeIgnoringNonLettersAndDigits(String) method, we could proceed as we did for the isPalindromeIgnoringCase(String) method, that is, using a Java method to delete everything that is not a letter or digit, or we could dispense with an extra pass to clean up the string and do the query directly. This is the approach taken by the implementation shown. We run the string from left and right and define two loop counters startIndex and endIndex for it. The start index becomes larger and the end index smaller. We start with the start index on the first character and with the end index on the last character. Now it can happen that one of the two characters is neither letter nor digit. So we have to search from the left until we come to a valid symbol, and we have to do the same from the right. This is what the two while loops do. Since the check can now be case-insensitive, we convert the two characters to lowercase and then compare them. If the characters are not equal, return false terminates the method. If each comparison is true, the method’s response is return true.

Finally, we look at the recursive implementation. Here there are basically two possibilities. Here we show the possibility that first a test checks if there is no or a character in the string. In that case we have reached the end of the recursion and can return true. Next we extract the first and the last character and compare them. If the characters are not the same, we exit the method with false. If the characters match — case-insensitive here — we extract a substring and go down one recursion level. Since the recursion here is at the end, we also call it an end recursion, which in principle can be optimized by runtime environments, although the standard JVM does not do this optimization yet.

An alternative solution with a recursion would be to parameterize the method with a start and end index, which is then moved within the method each time, without generating temporary strings. That is, instead of forming a partial string, only the start and end index are adjusted. However, this solution is no longer very far from an iterative solution.

Finally, let’s look at the one-liner with a Java method:

String s = "otto";
boolean isPalindrome = new StringBuilder( s ).reverse().toString().equals( s );

1.3.10. Check if Captain CiaoCiao is in the middle

com/tutego/exercise/string/InMiddle.java
public static boolean isStringInMiddle( String string, String middle ) {

  if ( middle.length() > string.length() )
    return false;

  int start = string.length() / 2 - middle.length() / 2;
  return string.regionMatches( start, middle, 0 /* middle offset */, middle.length() );
}

public static boolean isCiaoCiaoInMiddle( String string ) {
  return isStringInMiddle( string, "CiaoCiao" );
}

The core idea of the algorithm is to determine the midpoint of the main string, then subtract half the length from the string you want in the middle, and compare whether the middle string is located from that position.

Finally, the question of whether the string "CiaoCiao" occurs is quite specific. Therefore, we generalize the question and write a general method isStringInMiddle(String string, String middle) that works for any string that should be in the middle.

In the first step, we check the parameters. If null was passed for one of the parameters, it throws a NullPointerException when accessing the length. The reaction is good. If both are valid String objects, the lengths are determined and compared. If the middle string is longer than the main string itself, this is an error and we return false directly.

The following lines follow the described algorithm. regionMatches(…​) is useful in this case. The signature of the method is:

boolean regionMatches(int toffset, String other, int ooffset, int len).

This is a better approach than using substring(…​) to first cut out the string and then comparing it with equals(…​).

1.3.11. Find the shortest name in the array

com/tutego/exercise/string/ShortName.java
private static final int INDEX_NOT_FOUND = -1;

private static String shortest( String s1, String s2 ) {
  return s1.length() <= s2.length() ? s1 : s2;
}

private static String shortestName( String... names ) {

  if ( names.length == 0 )
    return "";

  String result = names[ 0 ];

  for ( String name : names ) {
    int spacePos = name.indexOf( ' ' );
    if ( spacePos == INDEX_NOT_FOUND )
      result = shortest( result, name );
    else {
      String part1 = name.substring( 0, spacePos );
      String part2 = name.substring( spacePos + 1 );
      result = shortest( result, shortest( part1, part2 ) );
    }
  }
  return result;
}

In the algorithm, we will need to determine the shortest of two strings several times later, so we offload this task to its own small method. The method shortest(String, String) returns the shorter of the two strings passed in; if both strings are the same length, the method returns the first string &#8212 but the decision is arbitrary.

The actual shortestName(…​) method gets a vararg of strings, and as usual the parameter can be null. By accessing length it will throw a NullPointerException if null is passed — a desired behavior. If no element was passed to the method, the method can do nothing and returns the empty string.

Since there is at least one string in the array, we use it to initialize the variable result, which we will also return at the end. The extended for loop runs through all the strings in the array, and in the body result may be updated. First, let`s see if there is a space in the string. Now there are two alternatives:

  1. If there is no space in the string, then shortest(result, name) returns the shorter string, and overwrites result with the result.

  2. If there is a space in the name, substring(…​) splits the name into two parts part1 and part2. Here we nest shortest(…​), that first from part1 and part2 the shorter string is determined and then again from this result and result the shorter string is determined and stored as result in result .

At the end of the loop we have determined the shortest string, which we return.

1.3.12. Count string occurrences

com/tutego/exercise/string/StringUtils.java
private static final int INDEX_NOT_FOUND = -1;

public static int countMatches( String string, String other ) {

  if (    string == null || other == null
       || string.length() == 0 || other.length() == 0 )
    return 0;

  int result = 0;

  for ( int index = 0;
        (index = string.indexOf( other, index )) != INDEX_NOT_FOUND;
        index += other.length() )
    result++;

  return result;
}

The countMatches(…​) method must return 0 for incorrect values. It is erroneous if a string is null or contains no characters. This check is done at the beginning.

The variable result will later indicate the number of matches and form the return. In the for loop we repeatedly search for this partial string other with the String method indexOf(…​). For this purpose, the loop initializes a variable index, which always contains the last found locations, and if there are no more found locations, this is also a termination condition of the for loop. This index is then always incremented by the length of other, so that the indexOf(…​) method can continue searching behind the substring other at the next iteration.

If such a method is needed in real Java programs, developers could get it from Apache Commons. The implementation itself is at https://commons.apache.org/proper/commons-lang/apidocs/src-html/org/apache/commons/lang3/StringUtils.html.

1.3.13. Determine the larger crew size

First, let’s summarize what we need to accomplish: In a string, we need to find the separator character (a minus sign) and determine how many dashes are to the left and right of the minus sign. These values must be compared. There are three outputs for the comparison: the right value can be larger or smaller than the left one, or both values can be the same.

There are two simple solutions:

  • split the input with split("-") into two strings and then query this string length with length().

  • get the position of the minus sign in the string with indexOf(…​) and then calculate the length left and right of the separator character.

Here is a special solution for friends with the motto: "Why simple, if it can be done awkwardly". In practice, this is of little use and is only justified if the profiler indicates that a code position is a bottleneck that needs to be optimized. Otherwise, clarity always comes first!

Let us assume for a moment that we have found the separator character at the position i and delete it because we do not want to count the one separator character. The total length shrinks thereby by one.

diag f4eab94f3524f7ce7adb1dd1659f1563
Figure 1. Relationship between location of the minus sign and team sizes

If P (pirates) and R (raiders) are sets, then let |P| be the number of pirates and |R| the number of raiders. These three pieces of information (0, i, and length - 1) give us answers:

  • How many pirates are there? There are i - 0, so i many.

  • How many raiders are there? There are length - 1 - i many.

We could find these values and compare them and the problem would be solved.

However, more can be done with the information. The task asks for the difference in team strengths. Wouldn’t it be possible to calculate the difference and use the sign to find out which team is stronger? You can.

  • |P| < |R| it follows |P| - |R| < 0

  • |P| > |R| from this follows |P| - |R| > 0

  • |P| = |R| from this follows |P| - |R| = 0

When comparing two numbers, the difference can always be formed and the sign observed.

The expression |P| - |R| repeats, we can calculate that:

  • |P| - |R| = i - (length - 1 - i) = i - (-i + length - 1) = i + i - (length - 1) = 2×i - (length - 1).

This is the difference in team size and the sign tells us which team is larger.

It’s time for the Java program:

com/tutego/exercise/string/CrewSize.java
public static void printDecodedCrewSizes( String string ) {
  int index = string.indexOf( '-' );
  if ( index < 0 )
    throw new IllegalArgumentException( "Separator - is missing in " + string );
  System.out.print( string + " => " );
  int diff = 2 * index - (string.length() - 1);
  switch ( Integer.signum( diff ) ) {
    case -1 -> System.out.printf(
        "Raided ship had a larger crew, difference %d%n", -diff );
    case  0 -> System.out.println( "Ships had the same crew size" );
    case +1 -> System.out.printf(
        "Pirate ship had a larger crew, difference %d%n", diff );
  }
}

First, the location i is determined; if there is no minus sign, an exception follows. Then the difference is calculated. Now a case distinction with if-else can be added, but the program does something else: It maps all negative numbers to -1, all positive numbers to 1, and 0 remains 0. This leaves three possibilities that switch-case can handle. The last thing to do is to reverse the sign for the output in the case of a negative difference.

*Java 8 Backport

In Java 8, instead of using the arrow notation, we need to use the switch statement with labels.

1.3.14. Build diamonds

com/tutego/exercise/string/DiamondPrinter.java
private static void printDiamondIndentation( int indentation ) {
  for ( int i = 0; i < indentation; i++ )
    System.out.print( " " );
}

private static void printDiamondCore( char character, char stopCharacter ) {
  if ( character == stopCharacter ) {
    System.out.print( character );
    return;
  }
  System.out.print( character );
  printDiamondCore( (char) (character + 1), stopCharacter );
  System.out.print( character );
}

public static void printDiamond( int diameter ) {
  if ( diameter < 1 )
    return;

  diameter = Math.min( diameter, 2 * 26 - 1 );

  int radius = diameter / 2;
  for ( int indentation = radius; indentation >= -radius; indentation-- ) {
    int absIndentation = Math.abs( indentation );
    System.out.print( " ".repeat( absIndentation ) );
    printDiamondCore( 'A', (char) ('A' + radius - absIndentation) );
    System.out.println();
  }
}

The actual solution of the task is in the method printDiamond(int), however, we want to make use of a helper method to write a line of the diamond. For the indentation by spaces we fall back on the method repeat(int).

printDiamondCore(char, char) draws a diamond line on the screen, where the method gets a start character and a stop character. For example, if the start character is A and the stop character is C, then the method prints the sequence ABCBA on the screen. The implementation is recursive. There are two scenarios: If the start character is equal to the stop character, then the character is not set twice, but only once. Otherwise, the character is set and then the method is recursively called with the next character, leaving the end character the same. After the recursive call descends, the character is set once again. We could in principle reverse the case distinction to first ask if the character does not match the stop character and enter recursion earlier, but termination conditions of recursions are more common at the beginning of the method.

The actual method printDiamond(int) first checks if the circumference of diamonds is valid, and exits if not. Since we pass the diameter, but we are interested in the radius, we divide the diameter by 2. Now the actual program flow can start. But before we do so, let’s look at a special property, the relationship between indentation and diamond size. The diamond with diameter 7 shall serve as an illustration, and we give the diamond size as radius. The indentation by spaces is symbolized by the underscore _:

Table 1. Relationship diamond lines, indentation and diamond radius.
linesindentationradius

A

3

1

ABA

2

2

ABCBA

1

3

ABCDCBA

0

4

ABCBA

1

3

ABA

2

2

A

3

1

We can read that the sum of radius and indentation is always the same, 4 in our example. We can now use a loop to count down from 3 to 0 and back up to 3 and derive the radius. Or we can loop over the radius of the diamond from 1 to 4 and back to 1 and derive the indentation.

The proposed solution uses the indentation indentation as the loop counter. And a trick is used so that two loops are not necessary: The loop lets indentation start with the radius and end at the negative radius. We run once through the zero point with it. Inside the loop, we are not interested in the negative numbers, so we choose the absolute value and thus have a descending and ascending indentation. From these indentation values, as we have just seen, we can calculate the radius and thus output the line. At the beginning indentation is equal to radius, that means radius - indentation is 0 at the beginning. So the method printDiamondCore('A', 'A') writes only the A. In the next pass, the indentation is decreased by one, that is, the difference between the radius, which does not change, and indentation is A + 1` = B, so we draw the core ABA.

Java 8 Backport

The white space at the beginning is generated with the String method repeat(…​), which is new in Java 11. In Java 8 we could introduce a separate method:

private static void printDiamondIndentation( int indentation ) {
  for ( int i = 0; i < indentation; i++ )
    System.out.print( " " );
}

1.3.15. Underline words

com/tutego/exercise/string/PrintUnderline.java
public static void printUnderline( String string, String search ) {
  System.out.println( string );

  string = string.toLowerCase();
  search = search.toLowerCase();

  String secondLine = "";
  for ( int index = 0;
        (index = string.indexOf(search, index) ) >= 0;
        index += search.length() ) {
    secondLine += " ".repeat( index - secondLine.length() ) +
                  "-".repeat( search.length() );
  }
  System.out.println( secondLine );
}

The task has an interesting logic, which at first sight makes it difficult to understand. We have to manage the following: find all substrings and then put spaces wherever the search string does not appear and minus signs under all substrings.

The printUnderline(…​) method first prints the text followed by the newline. To make the search case-insensitive, we convert string and search to lowercase. indexOf(…​) then searches a lowercase string for a lowercase substring.

With secondLine we have a variable that grows over time so that it can be output at the end. The advantage is that once we want the printUnderline(…​) method to return String, we don`t have to change much code. The second advantage is the possibility to query the length — so we can find out how many characters have already been written.

A loop finds all positions with the searched string search. The for loop is complex because there is also an assignment in the conditional expression for breaking the loop. The idea is the following: We update the variable index with the found location, and if this is greater than or equal to 0, we have a found location and execute the body of the loop. At the moment when indexOf(…​) reports no more find, we abort the loop. So there are exactly as many runs as there are finds of search.

The body of the loop needs to do two things:

  • Produce spaces, and as many as we need to get from the last location (secondLine.length()) to the current find location (index). In other words: we put index - secondLine.length() many spaces.

  • append as many minus signs to the secondLine as the string search is long

Both can be implemented nicely with the repeat(…​) method, which quickly multiplies a desired character (or string).

Finally, we update the variable index in the for loop’s continuation expression by the length of the string to be searched, because after the last find we have to continue directly after the search string. In other words, indexOf(…​) starts the next search at the end of the last string, starting from index.

*Java 8 Backport

With the string method repeat(…​) added in Java 11, appending white space and minus signs can be written in a compact way, but in Java 8 we need a different way; like this:

for ( int i = secondLine.length(); i < index; i++ ) secondLine += " ";
for ( int i = 0; i < search.length(); i++ ) secondLine += "-";

1.3.16. Check for a good password

com/tutego/exercise/string/PasswordTester.java
public static final int MIN_PASSWORD_LEN = 8;

public static boolean isGoodPassword( String password ) {

  if ( password.length() < MIN_PASSWORD_LEN ) {
    System.err.println( "Password is too short" );
    return false;
  }

  if ( ! containsUppercaseLetter( password ) ) {
    System.err.println( "Must contain uppercase letters" );
    return false;
  }

  if ( ! containsLowercaseLetter( password ) ) {
    System.err.println( "Must contain lowercase letters" );
    return false;
  }

  if ( ! containsDigit( password ) ) {
    System.err.println( "Must contain a number" );
    return false;
  }

  if ( ! containsSpecialCharacter( password ) ) {
    System.err.println( "Must contain special characters like .," );
    return false;
  }

  return true;
}

private static boolean containsUppercaseLetter( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    if ( Character.isUpperCase( c ) )
      return true;
  }
  return false;
}

private static boolean containsLowercaseLetter( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    if ( Character.isLowerCase( c ) )
      return true;
  }
  return false;
}

private static boolean containsDigit( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    if ( Character.isDigit( c ) )
      return true;
  }
  return false;
}

private static boolean containsSpecialCharacter( String string ) {
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    if ( c == '.' || c == ',' )
      return true;
  }
  return false;
}

public static void main( String[] args ) {
  System.out.println( isGoodPassword( "zukurz" ) );
  System.out.println( isGoodPassword( "nurkleinbuchstaben" ) );
  System.out.println( isGoodPassword( "keineziffern" ) );
  System.out.println( isGoodPassword( "Mit0Sonderzeichen" ) );
  System.out.println( isGoodPassword( "Mit 3 Sonderzeichen .$#&" ) );
}

Our method performs various tests one after the other. If a test fails, return false terminates the method. If all tests are positive, the method ends with return true.

Except for the first test, the individual tests are separated into methods. This increases the clarity. The individual methods get a string each, run it from front to back and test certain properties. The approach here is also that if we can make a decision, we can directly exit the method with a corresponding return true. Let’s take containsUppercaseLetter(String) as an example. The method runs the string from front to back and checks with Character.isUpperCase(char) if there is an uppercase letter. If so, we don’t need to test any more characters, we can exit the method immediately.

1.3.17. Calculate sum of digits

com/tutego/exercise/string/SumOfTheDigits.java
static int digitSum( long value ) {
  return digitSum( String.valueOf( value ) );
}

static int digitSum( String value ) {
  int sum = 0;

  for ( int i = 0; i < value.length(); i++ )
    // sum += value.charAt( i ) - '0';
    sum += Character.getNumericValue( value.charAt( i ) );

  return sum;
}

The first thing to note is that only one of the two methods needs to be implemented, because we can always call the other method. If we call digitSum(long), we can turn the integer into a string, and then call digitSum(String). Vice versa: if we call digitSum(String), we can convert the string to an integer with Long.parseLong(String) and call digitSum(long).

It is a bit of a matter of taste which of the two methods you implement. The approach is different. If we implement the method with the long parameter type, we always have to divide by 10 to decompose the number step-by-step. Here we need some mathematics, and this solution has a second disadvantage, namely that we get the result from right to left.This doesn’t matter for the sum of the digits, for some conversions it is rather impractical. So we implement digitSum(String).

As usual, we run the string from left to right with the for loop. We must now consider each character as a digit. To convert a Unicode character with a digit into a numeric value, we can use the method Character.getNumericValue(char). A char like '1' becomes 1, and '7' becomes 7. In principle, we could do this calculation ourselves by subtracting '0' from the Unicode character, but getNumericValue(…​) works in general on all Unicode characters. For example, getNumericValue('٢') returns the result 2.

1.3.18. Decolumnize texts

The algorithm must do the following: In the first step, it must determine where there is a space in the same place in all the lines, which is an indication of the column. In the next step, we have to separate at this place and first put all the rows of the left columns under each other, then all the rows of the right column.

The actual method decolumnize(…​) falls back on the internal method findColumnIndex(String[]), which finds the column with the blank for an array of strings. Also findColumnIndex(…​) accesses another internal method isSpaceAt(…​), which we want to start with.

com/tutego/exercise/string/Decolumnizer.java
private static boolean isSpaceAt( String string, int index ) {
  if ( index >= string.length() )
    return true;
  return string.charAt( index ) == ' ';
}

isSpaceAt(String, int) checks if there is a space in the passed string string at the position index or not. In addition, this method evaluates everything that is "behind" the string as white space. That means that there are an infinite number of white spaces behind the actual string.

This method is used for the actual method findColumnIndex(String[]):

com/tutego/exercise/string/Decolumnizer.java
private final static int COLUMN_NOT_FOUND = -1;

private static int findColumnIndex( String[] lines ) {
  int length = lines[ 0 ].length();
  for ( String line : lines )
    length = Math.max( length, line.length() );

  mainLoop:
  for ( int column = 1; column < length - 1; column++ ) {
    for ( String line : lines )
      if ( ! isSpaceAt( line, column ) )
        continue mainLoop;
    return column;
  }

  return COLUMN_NOT_FOUND;
}

Before we run through all the lines and ask if each line has a space at some point, we need to figure out how far we are allowed to run. Therefore, the first step is to find the longest line. It must be the longest line, because some lines may be completely empty, and some lines may be short, in which case there is white space behind these short lines again.

Once we have determined the longest line, we run through all possible columns with a loop. It cannot be the column with index `0, nor can it be the last column, because there cannot be a real column to the left or right of these positions. Regardless of this, it makes little sense if the columns are only one symbol ready, so one could certainly start with another index, perhaps in the neighborhood of the center.

The function of the two nested loops is the following: the outer loop runs all possible columns, while the inner loop makes sure that for each column all possible rows are examined. If there is no space in a row in the column column, then we don’t even need to consider the other rows, but continue with the next column. This possibility is achieved with the keyword continue — we have to use a jump label here, because without it continue would only continue in the inner loop, but we want to continue in the outer loop.

If the program overcomes the inner loop, we have found a place for all lines where there is a space. The variable column contains the found position, which is returned. The program doesn’t care if this location is in the middle, maybe there is a column with blanks at the beginning, then a column is recognized that shouldn’t be a column. If we run over all columns and rows and there are no blanks among them, the return is COLUMN_NOT_FOUND, i.e. -1.

Now we can get to the actual method decolumnize(String):

com/tutego/exercise/string/Decolumnizer.java
public static void decolumnize( String string ) {
  String[] lines = string.split( "\n" );
  if ( lines.length < 2 ) {
    System.out.println( string );
    return;
  }

  int column = findColumnIndex( lines );

  if ( column == COLUMN_NOT_FOUND ) {
    System.out.println( string );
    return;
  }

  // Left column
  for ( String line : lines )
    System.out.println(
        line.substring( 0, Math.min( line.length(), column ) ).trim() );

  // Right column
  for ( String line : lines )
    if ( column < line.length() )
      System.out.println( line.substring( column + 1 ).trim() );
    else
      System.out.println();
}

It first splits the large string into many lines using the split(…​) method. Splitting into columns only makes sense if there are at least two lines, so if there is one line, it will be output as such without starting the search for a column.

Otherwise the real work begins, the search for the column. If the method findColumnIndex(…​) does not find a column, then we output the string and end the method.

If we have found a column index, a first loop outputs the left column and the second loop outputs the right column. So the two loops run over all lines twice, but consider in the first case only everything to the left of the column index, and in the second case only everything to the right of the column index.

For the left column, consider that the rows can be shorter than the column index, because the rows as a whole can be shorter; we remember: isSpaceAt(…​) is programmed so that everything after the actual string is considered white space. If we output the line of the left column, the substring(…​) method must not go from 0 to the column index, but from the smaller value, the line length and the column index. We still cut possible blanks at the back and front for the output.

For the right column we proceed similarly. Only now we have to check if the right row exists at all. If yes, substring(…​) returns a substring starting at the column index to the end of the line. Otherwise we output an empty line.

The program does not care about truncating unnecessary blank lines at the end. If the columns are not balanced and there are the same number of lines on the left and on the right, as many blank lines will appear for the right column as there are lines in the left column.

1.3.19. Draw a meadow with favorite flowers

com/tutego/exercise/string/Flowers.java
private static final String FLOWERS = """
                    _
                  _(_)_                          wWWWw   _
      @@@@       (_)@(_)   vVVVv     _     @@@@  (___) _(_)_
     @@()@@ wWWWw  (_)\\    (___)   _(_)_  @@()@@   Y  (_)@(_)
      @@@@  (___)     `|/    Y    (_)@(_)  @@@@   \\|/   (_)\\
       /      Y       \\|    \\|/    /(_)    \\|      |/      |
    \\ |     \\ |/       | / \\ | /  \\|/       |/    \\|      \\|/
    \\\\|//   \\\\|//   \\\\\\|//\\\\\\|/// \\|///  \\\\\\|//  \\\\|//  \\\\\\|//
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    """;

private static final int[] FLOWER_START_POS={0, 7, 13, 22, 29, 37, 44, 50, 57};

private static final String[] FLOWER_LINES = FLOWERS.split( "\n" );
private static final int FLOWER_HEIGHT = FLOWER_LINES.length;
private static final int LONGEST_LINE_LEN=FLOWER_LINES[FLOWER_HEIGHT-1].length();

private static String flowerLine( int flower, int line ) {
  String s = FLOWER_LINES[ line ] + " ".repeat( LONGEST_LINE_LEN );
  return s.substring( FLOWER_START_POS[flower], FLOWER_START_POS[flower + 1] );
}

private static int flowerFromId( char id ) {
  return id >= '1' && id <= '8' ? Character.getNumericValue( id ) - 1 : 0;
}

public static void printFlowers( String order ) {
  for ( int line = 0; line < FLOWER_HEIGHT; line++ ) {
    for ( char id : order.toCharArray() )
      System.out.print( flowerLine( flowerFromId( id ), line ) );
    System.out.println();
  }
}

For the flowers we use multiline strings which are available since Java 15. The string is in a static variable in the proposed solution; a local variable is also possible in principle, but it is inconvenient.

We want to declare a few additional static variables. So that we can refer to the individual flowers later, we keep the positions at which the flowers start in a separate array FLOWER_START_POS. For example, the first flower starts from index 0, the second flower from index 7 and so on. Another constant FLOWER_LINES results from the flower string and stores the lines of all flowers in an array — so we can easily ask for a line later. The number of lines also tells us the height of the flowers. The last line is also the longest line, which we also want to remember in a constant LONGEST_LINE_LEN. If the flowers should ever change, we would have to adjust or recalculate some of these constants.

Let’s look at the individual methods. String flowerLine(int flower, int line) internally accesses the array FLOWER_LINES and extracts from a desired flower exactly this partial string from the line with all flowers. To do this, the method takes into account a peculiarity, namely some lines may be shorter than the longest line (i.e. not all lines are the same length), and then forming a substring would quickly lead to an exception. The trick is to first append spaces to each line. We know the number of spaces, because there are at most LONGEST_LINE_LEN many. The string with many spaces is formed by the repeat(…​) method of the String class.

The flowerLine(…​) method thus returns the individual line for each flower. Now we have to do a decoding from a character to the flower. Flower 1 must be mapped to 0. This is done by a new method int flowerFromId(char id). We translate a character with the flower id to an integer, so we can access the array internally later.

The last method is printFlowers(String). It falls back on the other two methods, so only a few lines are needed for the actual algorithm. The basic idea is simple: we go over all the lines and then set the partial string for each flower and each line in turn. This means we have to nest two loops. The outer loop goes over all the lines. For each line, we then split the string with the order into individual characters, which gives us the flower identifiers in id. toCharArray() returns an array of all the characters that the extended for loop can run. Now we run over all the flower identifiers through the inner loop and all the lines through the outer loop. In the inner loop, we first convert the flower identifier to the internal position and then get the partial string of the flower for the line, which we output with System.out.print(…​). After the internal loop, we end the line with a line feed.

*Java 8 Backport

Putting multi-line strings into code is messy in Java 8; but here the IDE helps us, as the task describes: We first create a new string in double quotes, copy from the task the string to the clipboard, and paste the clipboard contents in the editor. In order to repeat a string several times, an alternative must also be sought under Java 8, for example by means of a loop, because the repeat(int) method has only been available since Java 11.

1.3.20. Detect repetitions

Before we look at the solution in code, the algorithm that solves the problem must be clear. Let us take the simple string aaa as an example. As humans, we have no problem immediately recognizing the pattern that a repeats three times. A program could do the following: it could take the first character of aaa — that is, a — and repeat it until the string has length three. Then we compare whether it resembles the initial string aaa. It does! What about ababab? If the program also starts repeating the first character a, aaaa is created, and the comparison with the original ababab turns out negative. In the next step, the program can take the first two characters ab and repeat them, and the result is ababab, which matches the original. We have a match.

With the simple algorithm, we can test strings of any length: form repetitions of substrings of length 1, 2, 3, 4. …​ However, it helps us to consider what format the valid solutions will have.

Table 2. Valid repetitions of length 1, 2, 3, 4, 5, 6
partial sequenceof length 123456

a

a

aa

aaa

aaaa

aaaaa

aaaaaa

ab

ab

abab

ababab

abc

abc

abcabc

abcd

abcd

 — 

abcde

abcde

 — 

abcdef

abcdef

From the table, we can see that ab, for example, cannot be used to form strings of length 3, and abcd cannot be used to form strings of length 2.

If we have a substring with two characters, then the strings generated from it can be a multiple of 2, so can be 2 characters long, can be 4 characters long, can be 6 characters long, and so on. If we have a string with three characters, we can build repeats from it with 3 characters, 6 characters, 9 characters, and so on.

Let’s turn the game around. We don’t know what the substring looks like and how long it is, but we know the output string. For example, if the output string is 12 characters long, what are the possible repeating strings?

aaaaaaaaaaaa
abababababab
abcabcabcabc
abcdabcdabcd
abcdefabcdef

These are repetitions of a, ab, abc, abcd, abcdef; the length of these strings are the divisors of 12, which are 1, 2, 3, 4 and 6. Other combinations are not possible. In particular, the substring cannot be longer than half the string length, because otherwise the duplication is longer than the original string. So, in a naive approach, we could go up from 1 to n/2 for a total string length of n; if we want to do better, we go up only the divisors.

The solution consists of two methods: repeatingStrings(String) and an internal method int[] lengths(int) that returns the lengths for the strings to be concatenated. Let’s start with repeatingStrings(…​).

com/tutego/exercise/string/RepeatingStrings.java
public static String repeatingStrings( String string ) {

  if ( string == null || string.length() < 2 )
    return null;

  // Step 1: generate substrings, of length 1, length x, ...

  for ( int length : lengths( string.length() ) ) {
    String substring = string.substring( 0, length );

    // Step 2: check if repetitions of substring are equals to this text

    String repeatedSubstring = substring;
    while ( repeatedSubstring.length() < string.length() )
      repeatedSubstring += substring;

    if ( repeatedSubstring.equals( string ) )
      return substring;
  }

  return null;
}

If the null reference comes into the method, or strings that consist of only a single character, the method returns null directly.

In the following we can assume that the strings are longer than two characters. The custom method lengths(int) returns an array of all lengths, which we loop through with the extended for loop. The for loop generates substring substring for these lengths. These substrings are concatenated via repeatedSubstring until the length of the original string is reached. Since lengths(int) gives us divisors, multiplication will always result in a length equal to that of the input string, no longer. In the last step, we compare the string repeatedSubstring built from repetitions with the original string, and if they match, we can exit the method with substring. If the two strings do not match, we must return to the loop. If the for loop has been run completely and all variants have been tried without a hit, the method returns null.

The program uses the method lengths(…​), which determines the divisors for a string length. Since every number n has at least the divisors 1 and n, but for us n itself is uninteresting, the method does not return the number itself, but only 1 and real divisors.

com/tutego/exercise/string/RepeatingStrings.java
static int[] lengths( int length ) {

  int[] dividers  = new int[ length / 2 ];
  int dividersIndex = 0;

  for ( int i = 1; i <= length / 2; i++ )
    if ( length % i == 0 )
      dividers[ dividersIndex++ ] = i;

  return Arrays.copyOf( dividers, dividersIndex );
}

Parameter checking is not necessary with our method; the incoming strings are not null and also at least one character long.

All dividers are collected in the array dividers. Since it is unclear in advance how many elements dividers will contain, we pessimistically estimate that there can be at most length / 2 many dividers. This array dividers is just a buffer that we later transfer to a new array of the right length.

To find all dividers we could reach into the mathematical bag of tricks, but here we use the dumbest algorithm imaginable. This is fine for our program, in program runtime concatenating the strings is the expensive part. A loop generates all conceivable divisors and then uses the remainder operator to check whether we have a divisor or not. If so, we put the divisor into the array dividers. At the end of the loop, Arrays.copyOf(…​) creates an array that is the same size as the number of found divisors we remembered in dividersIndex.

1.3.21. Constrain line boundaries and wrap lines

com/tutego/exercise/string/WordWrap.java
public static String wrap( String string, int width ) {
  if ( string.length() <= width )
    return string;
  int breakIndex = string.lastIndexOf( ' ', width );
  if ( breakIndex == -1 )
    breakIndex = width;
  String firstLine = string.substring( 0, breakIndex );
  String remaining = wrap( string.substring( breakIndex ).trim(), width );
  return firstLine + "\n" + remaining;
}

1.3.22. Quiz: How many string objects?

All double-quoted strings are automatically String objects. The Java Virtual Machine stores strings of this type in a so-called constant pool. Such strings exist as objects only once. That is, str1, str3 and the string passed in the constructor are identical. If we command the runtime environment to build a new object with new, we end up with a new object as well, that is, str2 and str4 are new objects. In total, in the scenario we would have one string object in the constant pool and two new built objects by the new keyword, which would also disappear by the GC if not referenced; the strings in the constant pool remain until the end of the runtime.

1.3.23. Test if the fruit is wrapped in chocolate

com/tutego/exercise/string/ChocolateCovered.java
private static final String FRUIT = "F";

public static boolean checkChocolate( String string ) {
  return checkChocolate( string, 0 );
}

private static boolean checkChocolate( String string, int layer ) {

  if ( string.isEmpty() )
    return false;

  if ( string.length() == 1 )
    return string.equals( FRUIT ) && layer != 0;

  if ( string.charAt( 0 ) != string.charAt( string.length() - 1 ) )
    return false;

  return checkChocolate( string.substring( 1, string.length() - 1 ),
                         layer + 1 );
}

A recursive solution is perfect for the task. We have a public method, as requested by the task, checkChocolate(string), and a second private method checkChocolate(String, int), which is used by the recursion. Additionally, we declared a variable FRUIT for the fruit itself, so we could change the symbol.

The internal method is called with a string and with an integer that is incremented at each nesting. We need this variable so that we can distinguish if the string contains only the fruit, but no chocolate.

In the method checkChocolate(String, int) we check if the string is empty, in which case the chocolate, fruit and all are missing, and we return false. Passing null escalates to a NullPointerException. If the string is one character long, there are two things to test: first, whether it is the desired fruit — please no nuts —, but also whether the method has been called recursively at least once, that is, at least one layer of chocolate exists. Because if the very first call is with a string of length 1, the method must return false.

If the string is more than one character long, we check the first and last character, similar to a palindrome test. If the characters do not match, false is returned. If the first and last characters match, then we move on to the next round of recursion. We build a substring that goes from the second to the second last character, increase the nesting depth by 1, and use it to call checkChocolate(…​) recursively.

1.3.24. From top to bottom, from left to right

To solve the task, we study the given string again:

s u
ey!
ao

There are several possible solutions. One would be to split the string into lines, for example with String[] lines = string.split("\n");, and then run two nested loops first over all columns (width lines[0].length()), then over the lines.

Here we will show another way. Let’s put the lines next to each other and look at which symbol is at which index:

Table 3. Characters at an index

index

0

1

2

3

4

5

6

7

character

s

u

e

y

!

a

o

In the end, we want to see the result sea you!, so the question is which indexes we need to address for this. The answer:

0, 3, 6, 1, 4, 7, 2, 5

The sequence is not arbitrary, it follows a pattern. We need to find a mapping that transfers a number to the index so that we can read the character under the index:

Table 4. Derivation of i/3 + i%3 * 3
i01234567

i / 3

0

0

0

1

1

1

2

2

i % 3

0

1

2

0

1

2

0

1

i % 3 × 3

0

3

6

0

3

6

0

3

i/3 + i % 3 × 3

0

3

6

1

4

7

2

5

Now we have everything together to program the solution. The solution consists of two steps:

com/tutego/exercise/string/VerticalToHorizontalWriting.java
static void printVerticalToHorizontalWriting( String string ) {
  String oneliner   = string.replace( "\n", "" );
  int numberOfLines = string.length() - oneliner.length() + 1;
  for ( int i = 0; i < oneliner.length(); i++ ) {
    char c = oneliner.charAt(   (i / numberOfLines)
                              + (i % numberOfLines) * numberOfLines );
    System.out.print( c );
  }
}

The line break (newline) is encoded by the escape sequence \n. Since the string consists of several lines separated by \n, we want to remove these \n characters in the first step.

For the algorithm, we need another metric: the number of lines. Here we can resort to a nice trick. If we have deleted the \n characters, the string is shorter by exactly the number that there are \n characters. So if we take the difference between the original length and the length of the string without \n characters, we have the number of lines. Since the last line is not terminated with a \n, we have to add one. For our string from the example, that would be two \n characters + 1, so 3 lines.

The for loop creates a counter, which we transfer to the position of the character via the formula. The character is read and output to the screen.

1.3.25. Practicing the alphabet with a parrot

com/tutego/exercise/string/ABCZ.java
static String abcz() {
  StringBuilder result = new StringBuilder();

  for ( char c = 'A'; c <= 'Z'; c++ )
    result.append( c );

  return result.toString();
}

static String abcz( char start, char end ) {

  if ( end < start )
    return "";

  StringBuilder result = new StringBuilder( end - start + 1 );
  for ( char c = start; c <= end; c++ )
    result.append( c );

  return result.toString();
}

static String abcz( char start, int length ) {
  return abcz( start, (char) (start + length - 1) );
}

For the solution of the abcz() method, we resort to the convenient feature of Java that char is a numeric data type that can be used to count. A loop can thus generate 'A' to 'Z', and the entire alphabet is created. All characters are first concatenated in a StringBuilder, until finally the StringBuilder is converted to a String via toString() and returned. StringBuilder as return type is unusual and rather impractical, since other places usually expect String objects again.

In the second method, abcz(char start, char end), we simply parameterize start and end, and now we could even rewrite the first method abcz() to internally call abcz('a', 'z'). Since abcz(char start, char end) could be passed incorrect arguments, we check them in the first step: end must not be lesser than start. The assignments may well be equal, because if, for example, the method abcz('a', 'a') is called, an 'a' should appear at the end. In case of incorrect values we could also throw an exception, here we decide to use an empty string as return. We build the StringBuilder with an int parameterized constructor. The constructor is passed a starting size of the internal buffer; in our case the total length is known.

The third method delegates to the second method by adding the length to the start character and subtracting 1, because the last character is already included. The call to abcz('a', 1) should result in "a" and not in "ab".

The last two methods are related. In fact, it doesn’t matter which of the two methods we implement; one method can always be mapped to the other. Here we have implemented the method with the two characters in the parameter list, and the other method merely delegates to that implementation.

In principle, methods that do not differ significantly in the number of parameters and whose parameter types are very close to each other are error-prone. char and int are both numeric data types. Developers must be very careful not to accidentally call the wrong method. Therefore, the implementation of abcz(char, int) also has a type conversion to char, because the addition of a char and an int yields an int and not a char. If we didn’t use the type conversion there, we would starve endlessly in a recursion. Callers of the method may not have this in mind and write, for example, abcz('a', 'b' + 1) — the call is unlikely to be intentional. With good API design, we can reduce errors.

1.3.26. Quiz: lightly attached

Concatenation of String objects always creates new String objects temporarily. String objects internally reference another object, an array of characters. This means that there is always another object behind a String object, which is also created and must be removed again by the garbage collector. If you have loops with many passes, you should avoid using the + operator for concatenation.

The situation is different with the StringBuilder method append(…​). It also uses an array internally for the characters, but no new objects are built when appending (in the best case). Of course, the internal buffer of the StringBuilder object may not be enough, so a new internal array must be built for the characters, but if you can estimate the size from the StringBuilder, no temporary objects are created during concatenation. Of course, if there has to be a String object at the end, toString() has to be called again, which again leads to a new object. So in total there are three objects in our scenario: we build a StringBuilder object ourselves with new, internally the StringBuilder builds an array for the symbols, and finally we have a third object with toString().

1.3.27. Convert number to textual unary encoding

com/tutego/exercise/string/UnaryCoding.java
private static int ensurePositive( int value ) {
  if ( value < 0 )
    throw new IllegalArgumentException(
        "value is negative, but must be positive" );
  return value;
}

static String encode( int... values ) {
  StringBuilder codes = new StringBuilder( values.length );
  for ( int value : values ) {
    for ( int i = 0, len = ensurePositive( value ); i < len; i++ )
      codes.append( '1' );
    codes.append( '0' );
  }
  return codes.toString();
}

To notify developers of erroneous input, each entry in the array is checked to see if it is positive. This is handled by the helper method ensurePositive(int), which raises an exception if the value is negative; otherwise, the method returns the passed value.

encode(int... values) builds an internal StringBuilder. It is unclear how large the compound string will grow, because it grows depending on the values passed. However, we know that it is at least as large as the number of array elements, which means we can use that as the starting capacity of the StringBuilder object. The outer extended for loop runs over the array and extracts each value. The inner loop counts from 0 up to that value, puts value many ones into the StringBuilder, and appends a zero after passing through the inner loop. In the initialization part of the inner for loop we declare two variables: i for the loop counter and len for the length, where we have to test if it is positive. In principle, we could have written this test in the operation part of the loop, but then this test would have to be done on every loop pass, and that is unnecessary.

com/tutego/exercise/string/UnaryCoding.java
static int[] decode( String string ) {
  if ( string.isEmpty() )
    return new int[0];

  if ( ! string.endsWith( "0" ) )
    throw new IllegalArgumentException(
          "string must end with 0 but did end with "
        + string.charAt( string.length() - 1 ) );

  int arrayLength = 0;

  for ( int i = 0; i < string.length(); i++ ) {
    if ( string.charAt( i ) == '0' )
      arrayLength++;
    else if ( string.charAt( i ) != '1' )
      throw new IllegalArgumentException(
          "string can only contain 0 or 1 but found " + string.charAt( i ) );
  }

  int[] result = new int[ arrayLength ];
  int resultIndex = 0;

  int count = 0;
  for ( int i = 0; i < string.length(); i++ ) {
    if ( string.charAt( i ) == '1' )
      count++;
    else {
      result[ resultIndex++ ] = count;
      count = 0;
    }
  }

  return result;
}

The decode(String) method first checks the incoming string. If it is empty, we don’t even need to start our algorithm and can return with an empty array. Also, it is mandatory that the string is terminated with a zero — we check this as well and throw an exception otherwise.

If the incoming string is correct, we are faced with the problem that by looking at the string we don’t know how big the array is for the return. Therefore the first loop counts the number of zeros, because it matches the number of entries in the array to be created. Character by character, the for loop runs over the string and counts up arrayLength whenever a zero is found. If the other character is not a one, the method raises an exception.

After the first loop pass, the size of the array is known, and the array can be created with that size. Another for loop follows, which counts the number of ones. If a zero follows, the sequence of ones finds its termination, and the counter count is written into the array. The counter is reset, and a new search for the ones begins.

1.3.28. Lose weight by moving digits

com/tutego/exercise/string/WeightCheater.java
private static void swap( StringBuilder string, int i, int j ) {
  if ( i == j ) return;
  char temp = string.charAt( i );
  string.setCharAt( i, string.charAt( j ) );
  string.setCharAt( j, temp );
}

public static int cheatedWeight( int weight ) {
  StringBuilder weightString = new StringBuilder().append( weight );
  char smallestDigit = weightString.charAt( 0 );
  int  smallestDigitIndex = 0;
  for ( int i = 1; i < weightString.length(); i++ ) {
    char c = weightString.charAt( i );
    if ( c != '0' && c < smallestDigit ) {
      smallestDigit = c;
      smallestDigitIndex = i;
    }
  }

  swap( weightString, smallestDigitIndex, 0 );

  return Integer.parseInt( weightString, 0, weightString.length(), 10 );
}

Our method cheatedWeight(…​) gets an integer and returns an integer. In principle, the problem can be solved by an arithmetic way, but this is cumbersome. It is easier to convert the integer to a string, find the smallest digit and put it in front. So that we can swap symbols in a string, we resort to a mutable StringBuilder. We declare a helper method swap(…​), which swaps two symbols at the given positions. Although in our case we always swap with the first place, this utility method could perhaps become relevant for later fields of application. Therefore, the method is generic. It checks at the beginning if the two positions are not perhaps the same, then nothing has to be swapped. Otherwise, the character at position i is extracted and stored in an intermediate variable, then the character at position j is placed at position i and then the cached symbol is placed at position j.

With the method cheatedWeight(…​) we find the digit with the smallest value in the loop and also remember the position. However, we have to ignore 0; it is of course smaller than all other digits, but the task prohibits the prefixing of 0.

After running the loop, we swap the digit at the position smallestDigitIndex with the first digit. Finally, we need to convert the StringBuilder to an integer; the Integer class provides the method for this:

static int parseInt(CharSequence s, int beginIndex, int endIndex, int radix) throws NumberFormatException

StringBuilder is a special CharSequence. The method basically says the following: Give me any string, a start position, an end position, and a radix — 10 for the ordinary decimal system —, and I’ll convert that region to an integer for you.

*Java 8 backport

The method parseInt(CharSequence, int, int) is new as of Java 9. Alternatively, we have to convert the StringBuilder to a String and then use Integer.parseInt(…​). The disadvantage of this variant is a temporary String object that is not actually needed.

1.3.29. Remove vowels

Different approaches to the solution are to be listed:

com/tutego/exercise/string/RemoveVowel.java
public static String removeVowels1( String string ) {
  string = string.replace( "a", "" ).replace( "A", "" );
  string = string.replace( "ä", "" ).replace( "Ä", "" );
  string = string.replace( "e", "" ).replace( "E", "" );
  string = string.replace( "o", "" ).replace( "O", "" );
  string = string.replace( "ö", "" ).replace( "Ö", "" );
  string = string.replace( "u", "" ).replace( "U", "" );
  string = string.replace( "ü", "" ).replace( "Ü", "" );
  string = string.replace( "i", "" ).replace( "I", "" );
  string = string.replace( "y", "" ).replace( "Y", "" );
  return string;
}

The first solution is quite simple, but also with a high amount of code duplication. The replace(…​) method is overloaded: With one variant we can replace characters by other characters, with the other variant we can replace strings by strings. The replace(char, char) method is not able to delete a character, but the second variant is, where we can replace a string, no matter how long it is, with an empty string and thus remove it.

Variant number 2:

com/tutego/exercise/string/RemoveVowel.java
public static String removeVowels2( String string ) {
  char[] chars = new char[string.length()];
  int len = 0;

  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );

    if ( "aeiouöäüyAEIOUÄÖÜY".indexOf( c ) < 0 )
      chars[ len++ ] = c;
  }

  return new String( chars, 0, len );
}

The second method builds a temporary char buffer by first collecting all characters that are not vowels. This buffer of characters can be smaller than the string, but not larger. To start, we therefore build a char[] with the maximum number of characters expected, and that is the length of the incoming string. In a new variable len we note the size of the new resulting array. A loop now runs over all characters of the string. In the next step we have to test if the character is a vowel or not. This solution, as well as the following ones, use quite different approaches here. A good way is to test with indexOf(char). We first collect all the characters we want to find in a string. Then indexOf(char) tests whether the character we are looking at is in that substring or not. If indexOf(…​) answers with a positive result, we know that the character was in the string, that is, it was a vowel. Since we want to remove all vowels, we simply turn the condition around; indexOf(char) returns - 1 if the character was not in the string. And if the character was not in the string, we put the character in the array and increment the position. At the end of the loop, we have gone over the input string once, and put selected characters into the array. Now we need to convert the array back to a string. For this, the String class provides a suitable constructor.

The third variant differs in two details from the previous variant:

com/tutego/exercise/string/RemoveVowel.java
public static String removeVowels3( String string ) {
  StringBuilder result = new StringBuilder( string.length() );
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    switch ( c ) {
      case 'a', 'e', 'i', 'o', 'u', 'y', 'ä', 'ö', 'ü' -> { }
      default -> result.append( c );
    }
  }
  return result.toString();
}

The first difference is that no array is used as buffer, but a StringBuilder, to which append(…​) is used to append the character that is not a vowel. The second change concerns whether the character is a vowel. Here we resort to the modern switch statement with the arrow notation.

The fourth solution resorts to a custom method isVowel(char) that tests whether a character is a vowel.

com/tutego/exercise/string/RemoveVowel.java
private static boolean isVowel( char c ) {
  return "aeiouyäöüAEIOUYÄÖÜ".indexOf( c ) >= 0;
}

public static String removeVowels4( String string ) {
  StringBuilder result = new StringBuilder( string.length() );
  for ( int i = 0; i < string.length(); i++ ) {
    char c = string.charAt( i );
    if ( ! isVowel( c ) )
      result.append( c );
  }
  return result.toString();
}

Indeed, the consideration is whether a method that removes vowels should also decide what a vowel is. If we want to program well, then a single method should not be able to do too much. Therefore, it is reasonable to have one method that tests whether a character is a vowel and another method that can remove vowels from a string. Both have different tasks.

The following two solutions anticipate a bit thematically and use regular expressions quite cleverly.

com/tutego/exercise/string/RemoveVowel.java
public static String removeVowels5( String string ) {
  return string.replaceAll( "[aeiouyäöüAEIOUYÄÖÜ]", "" );
}

With the corresponding replaceAll(…​) method the task can be solved with a one-liner. replaceAll(String, String) gets as first argument a regular expression, which here stands for a group of characters. If the regular expression matches a character, the character is replaced by an empty string, i.e. removed.

The last solution goes a different, quite creative way.

com/tutego/exercise/string/RemoveVowel.java
public static String removeVowels6( String string ) {
  String result = "";
  String[] tokens = string.split( "[aeiouyäöüAEIOUYÄÖÜ]" );
  for ( String value : tokens )
    result += value;
  return result;
}

Instead of replacing the characters with nothing, the vowels here are separators. The split(…​) method consequently returns us all substrings before or after a vowel. We can reassemble these substrings into a result string.

1.3.30. Don’t shoot the Messenger

com/tutego/exercise/string/Messenger.java
private static String charAtOrEmpty( String string, int index ) {
  return index < string.length() ? string.substring( index, index + 1 ) : "";
}

private static String joinSplitMessages( String... parts ) {
  int maxStringLength = 0;

  for ( String part : parts )
    maxStringLength = Math.max( maxStringLength, part.length() );

  StringBuilder result = new StringBuilder();
  for ( int index = 0; index < maxStringLength; index++ )
    for ( String part : parts )
      result.append( charAtOrEmpty( part, index ) );

  return result.toString();
}

Later, when we run over all parts of the messenger, characters might be missing because the transfer is not complete; an example is "H", "", "ooky", where the second string has no character at the 0 position. To counter possible exceptions, we introduce a separate method charAtOrEmpty(String, int), which will fall back from a String to a character at a position; if the character does not exist at that position because the string is not that long, an empty string will be returned.

The charAtOrEmpty(…​) method mimics the behavior of JavaScript. There is also a charAt(…​) function here; it returns an empty string if the character at that index does not exist.

The actual joinSplitMessages(…​) method takes a vararg of strings. We do not check for null, but let it come to a NullPointerException, which will follow when the extended for loop accesses the array.

The algorithm consists of two steps. In the first step, the maximum string length is determined from all parts. The background is that messengers might transmit less data, so we go by the messenger that has the largest number of characters. So the query is meant for a scenario like joinSplitMessages("H", "", "ooky"). At the end of the loop, the variable maxStringLength contains the length of the longest string.

In the second step we ask for the first character of the first part, then the first character of the second part, the first character of the third part and so on. In the next step we learn the second character of the first part, the second character of the second part and so on. The outer loop generates indices from 0 to the maximum length of all the strings, and the inner loop runs over all the parts of the messengers. To request a character of the part’s string, we resort to our own method, which ensures that no exception occurs if no character exists at the index position.

After passing the loops, we convert the StringBuilder to a String, and return the String.

1.3.31. Compress repeated spaces

com/tutego/exercise/string/CompressSpace.java
public static final String TWO_SPACES = "  ";

static String compressSpace( String string ) {
  return compressSpace( new StringBuilder( string ) ).toString();
}

static StringBuilder compressSpace( StringBuilder string ) {
  int index = string.lastIndexOf( TWO_SPACES );

  while ( index >= 0 ) {
    string.deleteCharAt( index );
    index = string.lastIndexOf( TWO_SPACES );
  }
  return string;
}

For practical reasons, we write an overloaded compressSpace(…​) method, one variant with the String parameter type and the String return type, a second time with the StringBuilder parameter type, and this variant returns a StringBuilder. The String variant is more convenient for users, because strings are more common as a type than StringBuilder. The variant with the String parameter then takes the trouble of converting to a StringBuilder and back. In addition to the two methods, we create a constant TWO_SPACES that contains two spaces.

If we need to make changes to a string, we basically have two options: One is to rebuild a new string character by character, in our case including all characters but not two spaces in a row. The other option is to modify an existing string. This is the solution we choose.

We go with lastIndexOf(…​) from right to left, thus from back to front over the StringBuilder, and search for two blanks. If we have a result greater than or equal to 0, then we delete exactly one character at the position where the two spaces occurred, so that the first space disappears. Now we also search for the next occurrence of two spaces with lastIndexOf(…​). At some point, we have gone completely over the StringBuilder from right to left and can’t find any more two spaces. This is the time to stop.

It is perhaps strange that the lastIndexOf(…​) method is used and not indexOf(…​) which runs from left to right. Both works. However, for delete operations, it is more performant if less data is deleted. So, if we were to run over the string from the left and would find two spaces, we would have to move everything behind it one position to the left. But since there may be another two spaces on the right, we would move them too, even if they disappear later anyway. If we run from right to left, we won’t find two more spaces to the right of our current position, which means we won’t move any unnecessary spaces.

1.3.32. Insert and remove crackles and pops

com/tutego/exercise/string/Crack.java
private static final String CRACK = "♬KNACK♪";

public static String crackle( String string ) {
  StringBuilder result = new StringBuilder( string );

  for ( int i = string.length() - 1; i >= 0; i-- )
    if ( Math.random() < 0.1 )
      result.insert( i, CRACK );

  return result.toString();
}

public static String decrackle( String string ) {
  return string.replace( CRACK, "" );
}

We first declare a private final constant CRACK with the crackle string, so we can easily change the string. Later, two places refer to CRACK, when inserting the crackle and also when deleting the crackle.

The crackle(…​) method gets a String and returns a String. There are different approaches to the task. The solution here copies the passing string into a dynamic StringBuilder to insert the crackle at appropriate places. We generate potential insertion positions with a for loop and decide randomly whether to insert the crackle or not. There are two special features to the program. The first is that we do not generate the indexes from left to right, that is, not from the beginning to the end, but we go from right to left and work our way forward. The reason for this approach is that it allows us to avoid having one crackle overwrite another crackle. This is because if we insert a crackle at one point, the string will grow to the right as seen from the index, but not to the left. If we continue to run to the left with the index and insert a crackle there later, there can never be an overlap. Of course, if we run from left to right and insert a crackle, we could also increase the index by the length of CRACK.

The second trick is to decide to insert a crackle. A case decision with Math.random() < 0.1 is executed with a ten percent probability. The insertion is done by the insert(…​) method of the StringBuilder object.

The decrackle(…​) method is simpler, because here we can fall back on the well-known replace(…​) method of the String class. We let search for the crackle string and replace it with the empty string.

1.3.33. Split CamelCase strings

com/tutego/exercise/string/CamelCaseSplitter.java
private static String camelCaseSplitter( String string ) {
  StringBuilder result = new StringBuilder( string );

  for ( int i = 1; i < result.length(); i++ ) {
    char previousChar = result.charAt( i - 1 );
    char currentChar  = result.charAt( i );
    boolean isPreviousCharLowercase = Character.isLowerCase( previousChar );
    boolean isCurrentCharUppercase  = Character.isUpperCase( currentChar );
    if ( isPreviousCharLowercase && isCurrentCharUppercase ) {
      result.insert( i, " " );
      i++;
    }
  }

  return result.toString();
}

There are different solutions for this task. The approach chosen here copies the string into a StringBuilder and inserts spaces if necessary. There is no correction from upper to lower case letters, only spaces have to be inserted in the right place. The place we need to find is a change between a lowercase and an uppercase letter. If a lowercase letter follows another lowercase letter or an uppercase letter follows another uppercase letter, we can ignore it.

If we have built the StringBuilder results with the original string, then the constructor will throw a NullPointerException if the passed string was null. This is an acceptable response. We then start to run the string, but since the length of the string changes due to the added spaces, we do not walk the string parameter, but result instead, the StringBuilder. Now we always consider pairs. The loop generates us indices i and at the place i is the current character and at the place i - 1 is the previous character. Therefore the loop must also start at 1, otherwise we would have generated an index of -1, which leads to an error at the beginning. If the argument is none or one character long, the loop body is not entered.

The two characters previousChar and currentChar are now tested to see if the previous character was a lowercase letter and the following character was an uppercase letter. We rely on the answer of the Character methods, which provide correct answers for all Unicode characters. Case discrimination checks the criterion that an uppercase letter must follow a lowercase letter. In this case, we put a space at that position. This shifts the whole following block of characters one place to the right. If we have detected a shift, then the index i points to the upper case letter. If we have inserted a space at this position, the index will point to the space afterwards. However, we do not have to check spaces via our algorithm. We can therefore increase the index for the space by one, so that the index is then on the following uppercase letter. Since it continues afterwards in the continuation expression of the loop, the index is increased once again. Thus the index goes behind the capital letter, and the test continues for the following characters. If i++ is missing, this will not be noticed, since spaces are not letters, but by incrementing i, the algorithm saves a comparison.

At the end of the loop, the dynamic StringBuilder is converted to a String and returned.

Alternative implementations may be to build a StringBuilder first and not modify it later, there is also a regular expression solution.

A solution with a regular expression is possible, but not trivial:

String regex = "(?<=\p{javaLowerCase})(?=\p{javaUpperCase})";
String s = String.join( " ", "CiaoCiaoCAPTAINCiaoCiao".split( regex ) );

\p{javaLowerCase} and \p{javaUpperCase} stand for the character methods. ?<= is called "zero-width positive lookbehind" and ?= is a "zero-width positive lookahead". "Lookahead" says already quite well what it is about: Just look, don’t touch anything! We can use it to find characters, but they don’t become part of the match; the number of matched characters is 0. The spelling determines the transition from a lowercase letter to an uppercase letter, but since it’s not a match, no characters are missing in the split(…​) result.

In Java, the usual notation for identifiers is CamelCase; each new segment starts with an uppercase letter. Examples are: ArrayList, numberOfElements.

1.3.34. Implement Caesar encryption

The proposed solution consists of the methods caesar(…​) for encryption and decaesar(…​) for decryption and another private method named rotate(…​):

com/tutego/exercise/string/CaesarCipher.java
public static final int ALPHABET_LENGTH = 26;

private static int rotate( int c, int rotation ) {
  if ( rotation < 0 )
    throw new IllegalArgumentException(
        "rotation is not allowed to be negative, but was " + rotation );

  if ( c >= 'A' && c <= 'Z' )   // Character.isUpperCase( c ) is too broad
    return 'A' + (c - 'A' + rotation) % ALPHABET_LENGTH;
  else if ( c >= 'a' && c <= 'z' )
    return 'a' + (c - 'a' + rotation) % ALPHABET_LENGTH;
  else
    return c;
}

The private method int rotate(int c, int rotation) moves a character a certain number of positions — we also call this distance. Since we want to consider upper as well as lower case characters, there are two case distinctions. Also, the character may be neither an uppercase nor a lowercase letter, and then the original character is returned unchanged. In the constant ALPHABET_LENGTH we store the length of the alphabet, i.e. 26. Negative shifts are not allowed and lead to an exception.

The program logic is basically the same for uppercase and lowercase letters, so let’s look at the expression representative of uppercase letters. At first glance, the solution is simple: we add the distance to the Unicode position of the character c. If we have a character like 'W' and add 3, we end up with 'Z'. We have problems with the wrapping, so that after 'Z' we have to continue with 'A' again. Of course, a case distinction could check whether we run beyond 'Z', and then subtract the length of the alphabet, i.e. 26, but there is another solution to the problem which does not require a case distinction: In this solution, we do not add the distance to the character c. The consideration is rather what we have to add to the starting letter 'A to get to the letter c, and then also shifted by the distance. These are two parts. With c - 'A' we have calculated exactly the distance we have to add to get from the starting letter 'A' to c. 'A' + (c - 'A') is equal to c. Since we want to have a distance of rotation from the start letter, we add the distance, so 'A' + (c - 'A' + rotation). This looks shortened like c + rotation, but there is a subtle difference, namely that we can now take the parenthesized expression % ALPHABET_LENGTH, so that 'Z' + 1 leads us back to 'A'.

com/tutego/exercise/string/CaesarCipher.java
public static String caesar( String s, int rotation ) {
  StringBuilder result = new StringBuilder( s.length() );

  for ( int i = 0; i < s.length(); i++ )
    result.append( (char) rotate( s.charAt( i ), rotation ) );

  return result.toString();

  // Freaky solution
  // IntUnaryOperator rotation = c -> rotate( c, rotation );
  // return s.chars().map( rotation ).mapToObj( Character::toString )
  //         .collect( Collectors.joining() );
}

public static String decaesar( String s, int rotation ) {
  return caesar( s, ALPHABET_LENGTH - rotation );
}

The String caesar(String s, int rotation) method is then itself without surprise. We build an internal StringBuilder in which we collect the result, then run over the input string once from front to back, grab each character and rotate it, then put it in those containers. Finally, we convert the StringBuilder to a String and return it.

The method decaesar(…​) uses a nice property, and that is that after a certain number of shifts we end up back at the original character. And this number of shifts is just the size of the alphabet, so 26. What happens if we don’t shift the character by 26 positions, but only by 25? Then we would not have shifted the character to the "right", but to the "left"; a B would then no longer become a C, but the B would become an A. We can therefore decode with the position ALPHABET_LENGTH - rotation, which moves the character back to the left to its original position. One difference to caesar(…​) is, however, that we must not go into the negative by the subtraction, because otherwise an exception threatens. This is of course not symmetrical to caesar(…​), because the distance may be arbitrary.