Лучшая Идиома Цикла для специального преобразования регистра последнего элемента

Я сталкиваюсь с этим случаем много времен при выполнении простой текстовой обработки и операторов печати, где я - цикличное выполнение по набору, и я хочу к особому случаю последний элемент (например, каждый нормальный элемент будет запятой, разделенной за исключением последнего случая).

Есть ли некоторая идиома лучшей практики или изящная форма, которая не требует кода дублирования или проталкивания если, еще в цикле.

Например, у меня есть список строк, которые я хочу распечатать в списке разделенных запятой значений. (в то время как решение уже принимает список, имеет 2 или больше элемента иначе, это было бы настолько же плохо как более корректное для цикла с условным выражением).

например, Список = ("собака", "кошка", "летучая мышь")

Я хочу распечатать" [собаку, кошку, летучую мышь]"

Я представляю 2 методики

  1. Для цикла с условным выражением

    public static String forLoopConditional(String[] items) {
    
    String itemOutput = "[";
    
    for (int i = 0; i < items.length; i++) {
        // Check if we're not at the last element
        if (i < (items.length - 1)) {
            itemOutput += items[i] + ", ";
        } else {
            // last element
            itemOutput += items[i];
        }
    }
    itemOutput += "]";
    
    return itemOutput;
     }
    
  2. сделайте воспламенение цикла с условием продолжения цикл

    public static String doWhileLoopPrime(String[] items) {
    String itemOutput = "[";
    int i = 0;
    
    itemOutput += items[i++];
    if (i < (items.length)) {
        do {
            itemOutput += ", " + items[i++];
        } while (i < items.length);
    }
    itemOutput += "]";
    
    return itemOutput;
    }
    

    Класс тестера:

    public static void main(String[] args) {
        String[] items = { "dog", "cat", "bat" };
    
        System.out.println(forLoopConditional(items));
        System.out.println(doWhileLoopPrime(items));
    
    }
    

В классе Java AbstractCollection это имеет следующую реализацию (немного подробный, потому что это содержит всю проверку ошибок пограничного случая, но не плохо).

public String toString() {
    Iterator<E> i = iterator();
if (! i.hasNext())
    return "[]";

StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
    E e = i.next();
    sb.append(e == this ? "(this Collection)" : e);
    if (! i.hasNext())
    return sb.append(']').toString();
    sb.append(", ");
}
}
48
задан Stargateur 25 April 2018 в 05:17
поделиться

16 ответов

В этих ответах много циклов for, но я нахожу, что цикл Iterator и while читается гораздо легче. Например:

Iterator<String> itemIterator = Arrays.asList(items).iterator();
if (itemIterator.hasNext()) {
  // special-case first item.  in this case, no comma
  while (itemIterator.hasNext()) {
    // process the rest
  }
}

Такой подход используется в Joiner в коллекциях Google, и я нахожу его очень читабельным.

24
ответ дан 26 November 2019 в 18:38
поделиться

Третья альтернатива следующая

StringBuilder output = new StringBuilder();
for (int i = 0; i < items.length - 1; i++) {
    output.append(items[i]);
    output.append(",");
}
if (items.length > 0) output.append(items[items.length - 1]);

Но лучше всего использовать join()-подобный метод. Для Java есть String.join в сторонних библиотеках, тогда ваш код станет таким:

StringUtils.join(items,',');

FWIW, метод join() (строка 3232 и далее) в Apache Commons использует if внутри цикла, хотя:

public static String join(Object[] array, char separator, int startIndex, int endIndex)     {
        if (array == null) {
            return null;
        }
        int bufSize = (endIndex - startIndex);
        if (bufSize <= 0) {
            return EMPTY;
        }

        bufSize *= ((array[startIndex] == null ? 16 : array[startIndex].toString().length()) + 1);
        StringBuilder buf = new StringBuilder(bufSize);

        for (int i = startIndex; i < endIndex; i++) {
            if (i > startIndex) {
                buf.append(separator);
            }
            if (array[i] != null) {
                buf.append(array[i]);
            }
        }
        return buf.toString();
    }
0
ответ дан 26 November 2019 в 18:38
поделиться

В этом случае вы, по сути, объединяете список строк с помощью некоторой строки-разделителя. Вы можете сами написать что-нибудь, что это делает. Тогда вы получите что-то вроде:

String[] items = { "dog", "cat", "bat" };
String result = "[" + joinListOfStrings(items, ", ") + "]"

with

public static String joinListOfStrings(String[] items, String sep) {
    StringBuffer result;
    for (int i=0; i<items.length; i++) {
        result.append(items[i]);
        if (i < items.length-1) buffer.append(sep);
    }
    return result.toString();
}

Если у вас есть Collection вместо String [] , вы также можете использовать итераторы и hasNext () метод, чтобы проверить, последнее это или нет.

1
ответ дан 26 November 2019 в 18:38
поделиться

Мне нравится использовать флаг для первого пункта.

 ArrayList<String> list = new ArrayList()<String>{{
       add("dog");
       add("cat");
       add("bat");
    }};
    String output = "[";
    boolean first = true;
    for(String word: list){
      if(!first) output += ", ";
      output+= word;
      first = false;
    }
    output += "]";
2
ответ дан 26 November 2019 в 18:38
поделиться

Я обычно проверяю, равна ли индексная переменная нулю, например:

var result = "[ ";
for (var i = 0; i < list.length; ++i) {
    if (i != 0) result += ", ";
    result += list[i];
}
result += " ]";

Но, конечно, это только если мы говорим о языках, в которых нет метода Array.join(", "). ;-)

.
7
ответ дан 26 November 2019 в 18:38
поделиться

Я обычно пишу так:

static String commaSeparated(String[] items) {
    StringBuilder sb = new StringBuilder();
    String sep = "";
    for (String item: items) {
        sb.append(sep);
        sb.append(item);
        sep = ",";
    }
    return sb.toString();
}
43
ответ дан 26 November 2019 в 18:38
поделиться

...

String[] items = { "dog", "cat", "bat" };
String res = "[";

for (String s : items) {
   res += (res.length == 1 ? "" : ", ") + s;
}
res += "]";

или так вполне читабельно. Конечно, вы можете поместить условие в отдельную if клаузулу. Что делает его идиоматичным (по крайней мере, я так думаю), так это то, что он использует цикл foreach и не использует сложный заголовок цикла.

Также не дублируется логика (т.е. есть только одно место, где элемент из items фактически добавляется к выходной строке - в реальном приложении это может быть более сложной и длительной операцией форматирования, поэтому я бы не хотел повторять код).

1
ответ дан 26 November 2019 в 18:38
поделиться

Я бы пошел со вторым примером, т.е. обрабатывать особый случай вне цикла, просто напишите его немного проще:

String itemOutput = "[";

if (items.length > 0) {
    itemOutput += items[0];

    for (int i = 1; i < items.length; i++) {
        itemOutput += ", " + items[i];
    }
}

itemOutput += "]";
3
ответ дан 26 November 2019 в 18:38
поделиться

Поскольку ваш случай просто обрабатывает текст, вам не нужно использовать условие внутри цикла. Пример C:

char* items[] = {"dog", "cat", "bat"};
char* output[STRING_LENGTH] = {0};
char* pStr = &output[1];
int   i;

output[0] = '[';
for (i=0; i < (sizeof(items) / sizeof(char*)); ++i) {
    sprintf(pStr,"%s,",items[i]);
    pStr = &output[0] + strlen(output);
}
output[strlen(output)-1] = ']';

Вместо того, чтобы добавлять условное выражение, чтобы избежать создания конечной запятой, продолжайте и сгенерируйте его (чтобы ваш цикл был простым и безусловным) и просто перезапишите его в конце. Во многих случаях мне было проще сгенерировать специальный случай, как и любую другую итерацию цикла, а затем вручную заменить его в конце (хотя, если код «заменить его» составляет более пары строк, этот метод может стать труднее читать).

2
ответ дан 26 November 2019 в 18:38
поделиться
string value = "[" + StringUtils.join( items, ',' ) + "]";
14
ответ дан 26 November 2019 в 18:38
поделиться

Если вам нужен просто список, разделенный запятыми, например: "[The, Cat, in, the, the, Hat]", даже не тратьте время на написание собственного метода. Просто используйте List.toString:

List<String> strings = Arrays.asList("The", "Cat", "in", "the", "Hat);

System.out.println(strings.toString());

При условии, что общий тип списка имеет toString со значением, которое вы хотите отобразить, просто вызовите List.toString:

public class Dog {
    private String name;

    public Dog(String name){
         this.name = name;
    }

    public String toString(){
        return name;
    }
}

Затем вы можете сделать:

List<Dog> dogs = Arrays.asList(new Dog("Frank"), new Dog("Hal"));
System.out.println(dogs);

И вы получите: [Frank, Hal]

0
ответ дан 26 November 2019 в 18:38
поделиться

Я думаю, что проще думать о первом элементе как об особом случае, потому что гораздо проще узнать, является ли итерация первой, а не последней. Не требуется никакой сложной или дорогостоящей логики, чтобы узнать, делается ли что-то в первый раз.

public static String prettyPrint(String[] items) {
    String itemOutput = "[";
    boolean first = true;

    for (int i = 0; i < items.length; i++) {
        if (!first) {
            itemOutput += ", ";
        }

        itemOutput += items[i];
        first = false;
    }

    itemOutput += "]";
    return itemOutput;
}
6
ответ дан 26 November 2019 в 18:38
поделиться

Обычно я пишу цикл for следующим образом:

public static String forLoopConditional(String[] items) {
    StringBuilder builder = new StringBuilder();         

    builder.append("[");                                 

    for (int i = 0; i < items.length - 1; i++) {         
        builder.append(items[i] + ", ");                 
    }                                                    

    if (items.length > 0) {                              
        builder.append(items[items.length - 1]);         
    }                                                    

    builder.append("]");                                 

    return builder.toString();                           
}       
0
ответ дан 26 November 2019 в 18:38
поделиться

Вообще, мой фаворит - многоуровневый выход. Изменить

for ( s1; exit-condition; s2 ) {
    doForAll();
    if ( !modified-exit-condition ) 
        doForAllButLast();
}

на

for ( s1;; s2 ) {
    doForAll();
if ( modified-exit-condition ) break;
    doForAllButLast();
}

Это устраняет любой дублирующийся код или лишние проверки.

Ваш пример:

for (int i = 0;; i++) {
    itemOutput.append(items[i]);
if ( i == items.length - 1) break;
    itemOutput.append(", ");
}

Для некоторых вещей это работает лучше, чем для других. Я не большой поклонник этого для данного конкретного примера.

Конечно, это становится действительно сложным для сценариев, где условие выхода зависит от того, что происходит в doForAll(), а не только s2. Использование итератора - это как раз такой случай.

Вот статья от профессора, который бесстыдно рекламировал ее своим студентам :-). Прочитайте раздел 5, чтобы понять, о чем именно идет речь.

1
ответ дан 26 November 2019 в 18:38
поделиться

Если вы динамически создаете строку таким образом, вам не следует использовать оператор + =. Класс StringBuilder намного лучше подходит для повторяющейся динамической конкатенации строк.

public String commaSeparate(String[] items, String delim){
    StringBuilder bob = new StringBuilder();
    for(int i=0;i<items.length;i++){
        bob.append(items[i]);
        if(i+1<items.length){
           bob.append(delim);
        }
    }
    return bob.toString();
}

Тогда вызов будет таким

String[] items = {"one","two","three"};
StringBuilder bob = new StringBuilder();
bob.append("[");
bob.append(commaSeperate(items,","));
bob.append("]");
System.out.print(bob.toString());
1
ответ дан 26 November 2019 в 18:38
поделиться

Я думаю, что есть два ответа на этот вопрос: лучшая идиома для этой проблемы на любом языке и лучшая идиома для этой проблемы на java. Я также думаю, что цель этой проблемы заключалась не в задачах объединения строк, а в шаблоне в целом, поэтому показ библиотечных функций, которые могут это сделать, не очень помогает.

Во-первых, хотя окружение строки символом [] и создание строки, разделенной запятыми, - это два отдельных действия, и в идеале это были бы две отдельные функции.

Я думаю, что для любого языка лучше всего работает комбинация рекурсии и сопоставления с образцом. Например, в haskell я бы сделал следующее:

join [] = ""
join [x] = x
join (x:xs) = concat [x, ",", join xs]

surround before after str = concat [before, str, after]

yourFunc = surround "[" "]" . join

-- example usage: yourFunc ["dog", "cat"] will output "[dog,cat]"

Преимущество такого написания состоит в том, что он четко перечисляет различные ситуации, с которыми функция может столкнуться, и то, как она с этим справится.

Еще один очень хороший способ сделать это - использовать функцию типа аккумулятора. Например:

join [] = ""
join strings = foldr1 (\a b -> concat [a, ",", b]) strings 

Это можно сделать и на других языках, например, c #:

public static string Join(List<string> strings)
{
    if (!strings.Any()) return string.Empty;
    return strings.Aggregate((acc, val) => acc + "," + val);
}

Не очень эффективно в этой ситуации, но может быть полезно в других случаях (или эффективность может не иметь значения).

К сожалению, java не может использовать ни один из этих методов. Поэтому в этом случае я думаю, что лучший способ - это проверить в верхней части функции случаи исключения (0 или 1 элемент), а затем использовать цикл for для обработки случая с более чем одним элементом:

public static String join(String[] items) {
    if (items.length == 0) return "";
    if (items.length == 1) return items[0];

    StringBuilder result = new StringBuilder();
    for(int i = 0; i < items.length - 1; i++) {
        result.append(items[i]);
        result.append(",");
    }
    result.append(items[items.length - 1]);
    return result.toString();
}

Это функция ясно показывает, что происходит в двух крайних случаях (0 или 1 элемент).Затем он использует цикл для всех элементов, кроме последних, и, наконец, добавляет последний элемент без запятой. Обратный способ обработки элемента, отличного от запятой, в начале также легко сделать.

Обратите внимание, что строка if (items.length == 1) return items [0]; на самом деле не нужна, однако я думаю, что она упрощает определение того, что функция делает, с первого взгляда. .

(Обратите внимание, что если кто-то хочет более подробных объяснений по функциям haskell / C #, спрашивайте, и я добавлю их)

1
ответ дан 26 November 2019 в 18:38
поделиться
Другие вопросы по тегам:

Похожие вопросы: