8
\$\begingroup\$

I have this problem: Given a String of any length, return a new String where the last 2 chars, if present, are swapped. My question is, is there a more delicate way to do this than this way:

public String lastTwo(String str) {
   if(str.length() < 2 || str.equals("")){
      return str;
   }
   String lastTwo = str.substring(str.length() - 2);
   StringBuilder sb = new StringBuilder(lastTwo);
   String lastTwoReversed = sb.reverse().toString();
   return str.substring(0, str.length() - 2) + lastTwoReversed;
}
\$\endgroup\$
  • 2
    \$\begingroup\$ Do you want to swap the last two UTF-16 code units (possibly yielding an invalid string) or do you want to swap the last two graphemes (characters as the user perceives them)? \$\endgroup\$ – heinrich5991 Dec 9 '15 at 21:24
  • 2
    \$\begingroup\$ E.g. should the reversal of the string "😐😃" yield "😃😐" or "😐��"? It currently does the latter. \$\endgroup\$ – heinrich5991 Dec 9 '15 at 21:31
  • \$\begingroup\$ @heinrich5991 Your comment shows emojis on android mobile app : i.imgur.com/5xGWnTB.jpg . I dont know if should be commented in meta \$\endgroup\$ – rpax Dec 9 '15 at 21:36
  • 1
    \$\begingroup\$ @rpax These are emojis from Unicode, I believe it is intended that they're rendered this way on Android. Maybe I should pick different symbols. \$\endgroup\$ – heinrich5991 Dec 10 '15 at 0:30
  • \$\begingroup\$ @heinrich5991 Great point. If there is any chance at all your input string could contain surrogate-pair characters, this approach does not handle it, and you will need to use something like String.codePointCount. \$\endgroup\$ – funkybro Dec 10 '15 at 9:53
14
\$\begingroup\$

Rather than generating the substring for the last two chars and reversing it, you could simply add the last two chars in reverse order:

return str.substring(0, str.length() - 2)
    + str.charAt(str.length() - 1)
    + str.charAt(str.length() - 2);
\$\endgroup\$
  • \$\begingroup\$ Don't you think the string concatenation would reduce performance? \$\endgroup\$ – IEatBagels Dec 9 '15 at 16:05
  • 2
    \$\begingroup\$ @Barry Java (whether by the standard or just by the JVM I do not know) turns string addition into StringBuilder calls as you wrote, so as long as it's in one expression the two are equivalent. \$\endgroup\$ – Veedrac Dec 9 '15 at 17:42
  • 2
    \$\begingroup\$ @Veedrac This is done by the Java compiler. \$\endgroup\$ – Tavian Barnes Dec 9 '15 at 18:42
  • 3
    \$\begingroup\$ What's up with the indexing strings with brackets? \$\endgroup\$ – user2357112 Dec 10 '15 at 0:22
  • 2
    \$\begingroup\$ @WalterM No it would not since this is suppose to replace the last 4 lines of codes in the original question, the validation is still there to check for the length. \$\endgroup\$ – Marc-Andre Dec 10 '15 at 13:06
8
\$\begingroup\$

The second condition here

if(str.length() < 2 || str.equals("")){
   return str;
}  

is superflous. How should str be equal to "" and having >= 2 characters ?

The multiple calls to str.length() can be reduced to one call by storing the result in a variable.

Using a StringBuilder for concating 2 characters is a little bit over the top. I guess the creation of the StringBuilder will take more time and memory than just adding the chars.

\$\endgroup\$
5
\$\begingroup\$

I would check if the String is null first and either throw IllegalArgumentException and explain that the String must not be null or I would do nothing and return null. It's yours to decide which is better depending on what you need.

lastTwo is not the best name for your method since it's not really description on what the method is doing. Try to find a better way like : reverseLastTwoCharacters or something like that.

\$\endgroup\$
  • \$\begingroup\$ What if there is only one character? That would error, it is not null but it is also not valid. \$\endgroup\$ – Alfie Goodacre Dec 9 '15 at 16:58
  • 1
    \$\begingroup\$ It's already covered in the code : if(str.length() < 2 || str.equals("")){ return str; \$\endgroup\$ – Marc-Andre Dec 9 '15 at 17:23
1
\$\begingroup\$

There isn't much you can do if it's a String, at the very least you will have to copy the entire string again. You can use a StringBuilder for the variable that you want to change, but Note that every time you call toString() it returns a new String meaning str.toString() == str.toString() will always be false.

public StringBuilder lastTwo(CharSequence str, boolean allowModify){
    //if allowModify is true then we can change the StringBuilder and save precious CPU time.
    //otherwise we create a new StringBuilder
    StringBuilder builder = !allowModify || !(str instanceof StringBuilder) ? new StringBuilder(str) : (StringBuilder)str;

    final int strLen = str.length();
    if (strLen >= 2) {
        //simple swap
        char lastChar = builder.charAt(strLen - 1);
        builder.setCharAt(strLen - 1, builder.charAt(strLen - 2));
        builder.setCharAt(strLen - 2, lastChar);
    }
    return builder;
}

To explain what it's doing

StringBuilder str = new StringBuilder("123");
String str2 = "123";

System.out.printf("%-9s %-9s%n", "New", "Original");
System.out.printf("%-9s %-9s%n", lastTwo(str, false), str);
System.out.printf("%-9s %-9s%n", lastTwo(str2, false), str2);
System.out.println("Allow Modify");
System.out.printf("%-9s %-9s%n", lastTwo(str, true), str);
System.out.printf("%-9s %-9s%n", lastTwo(str2, true), str2);

Output

New       Original 
132       123      
132       123      
Allow Modify
132       132      //original is changed here as it is a StringBuilder
132       123      //this is a String, so it will remain
\$\endgroup\$
1
\$\begingroup\$

Using regex

You can make use of capturing groups to do the reversing. The regex to match the pattern you are looking for is (.)(.)$. This implies to match the last two characters

In code it looks like this:

str.replaceFirst("(.)(.)$", "$2$1")

The symbols $1, $2, simply refer to each of those brackets you have placed in the initial regex. So this means that to accomplish what you want, you simply interchange the positions of the $1 and $2 and the swapping is complete.

Putting it all together

public String reverseLastTwo(String str) {
   if(str != null){
      return str.replaceFirst("(.)(.)$", "$2$1");
   }
   return str;
}
\$\endgroup\$
  • 3
    \$\begingroup\$ -1 - not enough jQuery. More seriously though: programmers.stackexchange.com/a/10999/88637 blog.codinghorror.com/… etc using RegEx in such trivial case hogs both CPU/mem and programmer's brainpower... if I see str.substr(0, len - 2) + str.charAt(len - 1) + str.charAt(len - 2);, I can tell what it does in milliseconds. When I see str.replaceFirst("(.)(.)$", "$2$1");, I actually have to decode the regex first. First notation can be understood by any person with any programming background - second forces you to know regex. \$\endgroup\$ – vaxquis Dec 10 '15 at 1:53
  • 3
    \$\begingroup\$ @vaxquis I think the word you are looking for is documentation, besides there is enough context behind this code that you don't even need to know what the regex does to understand what the code is doing. It is well known fact that if you didn't write the code then there is a high chance you will not understand it. Also what dictates trivial code? This code is trivial to me, what does that mean? \$\endgroup\$ – smac89 Dec 10 '15 at 3:12
  • 1
    \$\begingroup\$ obligatory xkcd.com/1343 - the word you are looking for is KISS - if the regex code appeared outside of a single method dedicated for it, I highly doubt if the intention would be as clear. It's not my opinion that regexes are Write Once, Run Away code. Also, It is well known fact that if you didn't write the code then there is a high chance you will not understand it. {citation needed} {weasel words} BTW, what does [trivial] mean? thefreedictionary.com/trivial \$\endgroup\$ – vaxquis Dec 10 '15 at 13:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.