`for` loop with dynamic condition


#1

In Java, I can say this:

StringBuilder text = new StringBuilder("testing");

int numIterations = text.length();
for (int i = 0; i < numIterations; i++) {
  System.out.print(text.charAt(i));
  if (someCondition()) {
  numIterations–;
  }
}


As far as I can see, there’s no equivalent in Kotlin.  for (i in 0..numIterations-1), for example, doesn’t respect changes in numIterations.

This is the closest I can imagine:

val text = StringBuilder("testing")

var i = 0
var numIterations = text.length
while (i < numIterations) {
  print(text[i])
  if (someCondition()) {
  numIterations
  }
  i++
}


Despite Kotlin’s nicer syntax, this seems more wordy than the Java version.  Is there a better solution?

This basic pattern comes up in my text-parsing code now and then, and it’s unfortunate when the original Java code was nicer than the new Kotlin code.


#2

No, there is no direct equivalent. If you could show a more realistic example, we could probably help you rewrite this in a more idiomatic Kotlin (which will very likely include changing the loop to use an iterator).


#3

I'm working on an Android app that deals with a strange API and has to convert "&gt;", "&lt;", and "&amp;" into ">", "<", and "&".  This method may be called hundreds of times (with StringBuilders that may have thousands of characters) in a short space of time, so I think using iterators is infeasible.

This is a little complicated (for performance reasons), but it’s the shortest example I can find right now.

 
public final class HtmlEntityResolver {
    private HtmlEntityResolver() {}

  private static final int NUM_END_CHARS_TO_IGNORE = 3;

  public static void resolveAll(StringBuilder text) {
  int numIterations = text.length() - NUM_END_CHARS_TO_IGNORE;

  for (int i = 0; i < numIterations; i++) {
           if (text.charAt(i) == ‘&’) {
           switch (text.charAt(i + 1)) {
                   case ‘g’:
                   if ((text.charAt(i + 2) == ‘t’) && (text.charAt(i + 3) == ‘;’)) {
                           text.replace(i, i + 4, “>”);
                           numIterations = text.length() - NUM_END_CHARS_TO_IGNORE;
                   }
                   break;
                   case ‘l’:
                   if ((text.charAt(i + 2) == ‘t’) && (text.charAt(i + 3) == ‘;’)) {
                           text.replace(i, i + 4, “<”);
                           numIterations = text.length() - NUM_END_CHARS_TO_IGNORE;
                   }
                   break;
                   case ‘a’:
                   if ((text.charAt(i + 2) == ‘m’) && (text.charAt(i + 3) == ‘p’) && ((i + 4) < text.length()) && (text.charAt(i + 4) == ‘;’)) {
                           text.delete(i + 1, i + 5);
                           numIterations = text.length() - NUM_END_CHARS_TO_IGNORE;
                   }
                   break;
           }
           }
  }
  }
}


#4

Maybe it's just me, but I find the while version more readable / understandable.


#5

The replace() method that you're using here will copy the entire tail of the string on each replacement. You waste much more time on unnecessary character copies that you save by not allocating a single iterator object.

A much more efficient way to implement this method is to use two indexes into the StringBuilder, one for source and one for destination, and to copy the characters manually instead of using the replace() function. The source index will iterate over all the characters in the original StringBuilder, so you can use a regular for loop with a range, and you don’t need to update the range dynamically.


#6

Thanks for the feedback.  I had considered this at first but rejected it because, in the vast majority of cases in my app, there will be no mutation of the StringBuilder, and the current code is a lot simpler than keeping track of a "source" pointer and a "destination" pointer.  If, however, mutation were frequent then I think the solution you propose would be the only practical one.

I realise that my code is an edge case, but I still feel it’s unfortunate that Kotlin doesn’t have the powerful for syntax that Java has.


#7

Also, I'm not sure whether the solution you describe would allow us to say `for (i in 0..text.length-1)`, because when we encounter tokens we'll want to jump `i` ahead (e.g., `i += 3`), and if I'm not mistaken this is incompatible with a fixed progression.


#8

Hi.
I have also come into such problem where loops don’t respect changes made to its condition while iterating through data set. I assume that it’s mainly due to 1..numIterations range expression being called only once. Then again, generating an array every iteration would cost quite a lot of performance.
But what was the reason behind original java loops not being available in kotlin?


#9

When a language is designed, it does not automatically gain all the features from any other language. Any feature added to the language needs to be designed, implemented and supported in all the tooling, which requires time and effort.

In this case, we did not see strong use cases for the for loops which couldn’t be supported in a more idiomatic way with range loops and higher-order functions on collections. (And actually, we still don’t.)


#10

It’s not just you :-). I think so, too. I once had to maintain tons of code that did that kind of manipulations with the loop counter. It was a night mare. I believe Kotlin does not allow for manipulating the loop counter for that reason. I humbly recommend to go with the while loop. The next developer having to maintain the code will thank you.


#11

For the loop , this snippet is no good ?

fun main(args: Array) {

val string = "print my characters"
for (char in string) {
    println(char)
}

}


#12

This topic is only discussing ‘for’ loops where an attempt is made to change the range within the loop itself. For example, if you have this:

var n = 10
for (i in 0..n) {
    print("$i ")
    n--
}
println("\n$n")

The output is:

0 1 2 3 4 5 6 7 8 9 10 
-1

which demonstrates that, in Kotlin, the range is fixed when the ‘for’ loop is entered and is not recalculated on each iteration based on the value of ‘n’ then.

If your example were changed to:

 var string = "print my characters"
 for (char in string) {
     print(char)
     string = "x" + string
 }
 println("\n$string")

then the output would be:

print my characters
xxxxxxxxxxxxxxxxxxxprint my characters

demonstrating that changing the ‘string’ variable mid-loop has no effect on the result.