web-dev-qa-db-fra.com

Meilleur boucle Idiom pour cas spécial le dernier élément

Je rencontre souvent ce cas lorsque je fais du traitement de texte simple et des instructions print où je passe en boucle sur une collection et que je veux mettre en cas particulier le dernier élément (par exemple, chaque élément normal sera séparé par une virgule, à l'exception du dernier cas).

Existe-t-il un idiome de bonne pratique ou une forme élégante qui n'exige pas de duplication de code ni d'insertion dans un if, sinon dans la boucle?.

Par exemple, j'ai une liste de chaînes que je souhaite imprimer dans une liste séparée par des virgules. (La solution do while suppose déjà que la liste contient 2 éléments ou plus, sinon elle serait aussi mauvaise que la plus correcte pour une boucle avec condition).

par exemple. Liste = ("chien", "chat", "chauve-souris")

Je veux imprimer "[chien, chat, chauve-souris]"

Je présente 2 méthodes le

  1. Pour boucle avec conditionnel

    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. faire en boucle amorçant la boucle

    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;
    }
    

    Classe de testeur:

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

Dans la classe Java AbstractCollection, l’implémentation est la suivante (un peu détaillé car il contient toutes les vérifications d’erreur de cas Edge, mais pas mauvais).

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
Dougnukem

Il y a beaucoup de boucles for dans ces réponses, mais je trouve qu'un Iterator et une boucle while sont beaucoup plus faciles à lire. Par exemple.:

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
  }
}

C'est l'approche adoptée par Joiner dans les collections de Google et je la trouve très lisible.

24
gk5885

Je l'écris habituellement comme ceci:

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
finnw
string value = "[" + StringUtils.join( items, ',' ) + "]";
14
Jerod Houghtelling

Ma prise habituelle est de tester si la variable d’index est zéro, par exemple:

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

Mais bien sûr, ce n’est que si nous parlons de langues qui n’ont pas de méthode Array.join (","). ;-)

7
mishoo

Je pense qu'il est plus facile de considérer le premier élément comme un cas spécial car il est beaucoup plus facile de savoir si une itération est le premier plutôt que le dernier. Aucune logique complexe ou coûteuse n'est nécessaire pour savoir si quelque chose est fait pour la première fois.

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
Jacob Tomaw

Comme votre cas traite simplement du texte, vous n'avez pas besoin de conditionnel dans la boucle. Un exemple 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] = ']';

Au lieu d’ajouter une condition pour éviter de générer la virgule de fin, continuez et générez-la (pour que votre boucle soit simple et sans condition) et remplacez-la simplement à la fin. Souvent, je trouve plus facile de générer le cas spécial comme toute autre itération de boucle, puis de le remplacer manuellement à la fin (bien que si le code "le remplacer" est plus que quelques lignes, cette méthode peut en réalité devenir plus difficile à utiliser. lis).

2
bta

J'irais avec votre deuxième exemple, à savoir. gérez le cas particulier en dehors de la boucle, écrivez-le un peu plus simplement:

String itemOutput = "[";

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

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

itemOutput += "]";
2
Secure

Solution Java 8, au cas où quelqu'un le chercherait:

String res = Arrays.stream(items).reduce((t, u) -> t + "," + u).get();
2
gatti

J'aime utiliser un drapeau pour le premier élément.

 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
Adam

En général, mon préféré est la sortie à plusieurs niveaux. Changement

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

à

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

Il élimine tout code en double ou les contrôles redondants.

Votre exemple:

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

Cela fonctionne pour certaines choses mieux que d'autres. Je ne suis pas un grand fan de cela pour cet exemple spécifique.

Bien sûr, cela devient très délicat pour les scénarios où la condition de sortie dépend de ce qui se passe dans doForAll() et pas seulement s2. Utiliser un Iterator est un tel cas.

Voici un article du prof qui l'a promu sans vergogne à ses étudiants :-). Lisez la section 5 pour exactement ce dont vous parlez.

1
Mark Peters

...

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

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

ou alors est assez lisible. Vous pouvez bien sûr placer le conditionnel dans une clause if distincte. Ce qui le rend idiomatique (du moins je le pense) est qu’il utilise une boucle foreach et n’utilise pas d’en-tête de boucle compliqué. 

De plus, aucune logique n’est dupliquée (c’est-à-dire qu’il n’ya qu’un seul endroit où un élément de items est en fait ajouté à la chaîne de sortie - dans une application réelle, ce pourrait être une opération de formatage plus compliquée et plus longue, donc je ne voudrais pas. Je ne veux pas répéter le code).

1
Alexander Gessler

Si vous construisez une chaîne de cette manière de manière dynamique, vous ne devriez pas utiliser l'opérateur + =. La classe StringBuilder fonctionne beaucoup mieux pour la concaténation dynamique répétée de chaînes.

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();
}

Alors l'appel est comme ça

String[] items = {"one","two","three"};
StringBuilder bob = new StringBuilder();
bob.append("[");
bob.append(commaSeperate(items,","));
bob.append("]");
System.out.print(bob.toString());
1
Rambo Commando

Je pense qu'il y a deux réponses à cette question: le meilleur idiome pour ce problème dans n'importe quelle langue et le meilleur idiome pour ce problème en Java. Je pense également que l'intention de ce problème n'était pas de réunir des chaînes, mais plutôt le motif en général, donc cela n'aide pas vraiment de montrer les fonctions de bibliothèque qui peuvent le faire.

Premièrement, bien que les actions entourant une chaîne avec [] et créant une chaîne séparée par des virgules soient deux actions distinctes, et idéalement, deux fonctions distinctes.

Quelle que soit la langue, je pense que la combinaison de la récursivité et de l'appariement de modèles fonctionne le mieux. Par exemple, dans haskell je ferais ceci:

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]"

L’avantage de l’écrire ainsi est qu’il énumère clairement les différentes situations auxquelles la fonction sera confrontée et comment elle la gérera. 

Une autre façon très agréable de faire cela est avec une fonction de type accumulateur. Par exemple:

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

Cela peut aussi être fait dans d'autres langues, par exemple c #:

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

Pas très efficace dans cette situation, mais peut être utile dans d'autres cas (ou l'efficacité peut ne pas avoir d'importance).

Malheureusement, Java ne peut utiliser aucune de ces méthodes. Donc, dans ce cas, je pense que le meilleur moyen est d'avoir des contrôles en haut de la fonction pour les cas exceptionnels (0 ou 1 éléments), puis d'utiliser une boucle for pour gérer le cas avec plus d'un élément:

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();
}

Cette fonction montre clairement ce qui se passe dans les deux cas Edge (0 ou 1 élément). Il utilise ensuite une boucle pour tous les éléments sauf le dernier et ajoute enfin le dernier élément sans virgule. La méthode inverse de gestion de l'élément sans virgule au début est également facile à utiliser.

Notez que la ligne if (items.length == 1) return items[0]; n'est pas réellement nécessaire, mais je pense que cela facilite la tâche de la fonction.

(Notez que si quelqu'un veut plus d'explications sur les fonctions haskell/c #, demandez-le et je l'ajouterai) 

1
David Miani

Dans ce cas, vous concaténez essentiellement une liste de chaînes à l'aide d'une chaîne de séparation. Vous pouvez peut-être écrire quelque chose vous-même qui fait cela. Ensuite, vous obtiendrez quelque chose comme:

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

avec

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();
}

Si vous avez un Collection au lieu d'un String[], vous pouvez également utiliser des itérateurs et la méthode hasNext() pour vérifier si c'est le dernier ou non.

1
muksie

Il peut être réalisé en utilisant Java 8 lambda et Collectors.joining () en tant que -

List<String> items = Arrays.asList("dog", "cat", "bat");
String result = items.stream().collect(Collectors.joining(", ", "[", "]"));
System.out.println(result);
1
Pankaj

Une troisième alternative est la suivante

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]);

Mais le mieux est d'utiliser une méthode de type join (). Pour Java, il existe un String.join dans des bibliothèques tierces, ainsi votre code devient:

StringUtils.join(items,',');

FWIW, la méthode join () (à partir de la ligne 3232) dans Apache Commons utilise un if si dans une boucle si: 

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
Vinko Vrsalovic

J'écris habituellement une boucle for comme ceci:

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
frm

Si vous cherchez simplement une liste séparée par des virgules, comme celle-ci: "[The, Cat, in, the, Hat]", ne perdez même pas de temps à écrire votre propre méthode. Il suffit d'utiliser List.toString:

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

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

Si le type générique de la liste a une chaîne toString avec la valeur que vous souhaitez afficher, appelez simplement List.toString:

public class Dog {
    private String name;

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

    public String toString(){
        return name;
    }
}

Ensuite, vous pouvez faire:

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

Et vous aurez: [Frank, Hal]

0
Ian Dallas