2015-04-29 13 views
7

lista data come:Come unire elementi di elenco, ma utilizzare un delimitatore diverso per l'ultimo elemento?

List<String> names = Lists.newArrayList("George", "John", "Paul", "Ringo")

vorrei trasformarlo in una stringa come questa:

George, John, Paul and Ringo

posso farlo con un po 'goffa StringBuilder cosa in questo modo:

String nameList = names.stream().collect(joining(", ")); 
     if (nameList.contains(",")) { 
      StringBuilder builder = new StringBuilder(nameList); 
      builder.replace(nameList.lastIndexOf(','), nameList.lastIndexOf(',') + 1, " and"); 
      return builder.toString(); 
     } 

C'è un approccio un po 'più elegante? Non mi dispiace usare una libreria, se necessario.

NOTE:

  • potevo usare un vecchio for ciclo con un indice, ma non sono alla ricerca di una soluzione del genere
  • ci sono virgole all'interno dei valori (nomi)
+3

Le virgole sono mai all'interno dei valori? – Bohemian

+0

@Bohemian nope (notato nella domanda) – Xorty

+0

Perché non incapsulare la sua funzionalità in un metodo esterno? Qualcosa come 'public String mixListOfNamesAndReplaceLastCommaWithAnd (Lista nomi)' – Anatoly

risposta

2

Come già ha fatto la maggior parte di essa vorrei introdurre un secondo metodo "replaceLast" che non è nel JDK per java.lang.String finora:

import java.util.List; 
import java.util.stream.Collectors; 

public final class StringUtils { 
private static final String AND = " and "; 
private static final String COMMA = ", "; 

// your initial call wrapped with a replaceLast call 
public static String asLiteralNumeration(List<String> strings) { 
    return replaceLast(strings.stream().collect(Collectors.joining(COMMA)), COMMA, AND); 
} 

public static String replaceLast(String text, String regex, String replacement) { 
    return text.replaceFirst("(?s)" + regex + "(?!.*?" + regex + ")", replacement); 
} 
} 

Si potrebbe modificare i delimitatori e params come bene. Ecco il test per le vostre esigenze finora:

@org.junit.Test 
public void test() { 
List<String> names = Arrays.asList("George", "John", "Paul", "Ringo"); 
assertEquals("George, John, Paul and Ringo", StringUtils.asLiteralNumeration(names)); 

List<String> oneItemList = Arrays.asList("Paul"); 
assertEquals("Paul", StringUtils.asLiteralNumeration(oneItemList)); 

List<String> emptyList = Arrays.asList(""); 
assertEquals("", StringUtils.asLiteralNumeration(emptyList)); 

} 
+0

Non male, se non ho bisogno di parametrizzare i delimitatori può essere 'return names.stream(). Collect (join (", ")). ReplaceFirst (" (? S), (?!. * ?,) "," e ");' – Xorty

+0

Sì, certo, è possibile modificarlo in un unico rivestimento :-) – swinkler

2

Si può unire tutti gli elementi tranne l'ultima utilizzando un elenco secondario:

String nameList = 
    names.isEmpty() ? "" : 
     names.subList(0, names.size() - 1) 
      .stream() 
      .collect(Collectors.joining(firstDelimiter)) 
      + ((names.size() > 1) ? secondDelimiter + names.get(names.size() - 1) : names.get(0)) 
     ; 

Personalmente, questo approccio non mi piace perché per le implementazioni di elenchi di backup non array, il tempo di List#get può essere O (n).

provato qui:

static String joinList(List<String> names) { 
    return joinList(names, ", ", " and "); 
} 

static String joinList(List<String> names, String firstDelimiter, String secondDelimiter) { 
    return names.isEmpty() ? "" : 
     names.subList(0, names.size() - 1) 
     .stream() 
     .collect(Collectors.joining(firstDelimiter)) 
     + ((names.size() > 1) ? secondDelimiter+ names.get(names.size() - 1) : names.get(0)) 
     ; 
} 

public static void main(String[] args) { 
    System.out.println(joinList(Arrays.asList("George", "John", "Paul", "Ringo"))); 
    System.out.println(joinList(Arrays.asList("Ringo"))); 
    System.out.println(joinList(Arrays.asList())); 
    System.out.println(joinList(null)); //this one throws NPE as OP oddly requested 
} 

Ecco un'implementazione alternativa o joinList, che possono rendere più chiaro ciò che effettivamente accade:

static String joinList(List<String> names, String firstDelimiter, String secondDelimiter) { 
    if (names.isEmpty()) { 
     return ""; 
    } else if (names.size() == 1) { 
     return names.get(0); 
    } else { 
     return names.subList(0, names.size() - 1) 
        .stream().collect(Collectors.joining(firstDelimiter)) 
      + secondDelimiter + names.get(names.size() - 1); 
    } 
} 
+1

Sarebbe difficile da usare per una lista vuota e una lista di 1 elemento solo – Xorty

+0

@Xorty in caso di una lista vuota, restituisce una stringa vuota;). –

+0

in caso di 1 elemento sarebbe restituito "e George" invece di "George" (a differenza della soluzione originale) – Xorty

0

Se virgole sono mai nei valori, è una battuta :

String all = names.toString().replaceAll("^.|.$", "").replaceAll(",(?!.*,)", " and"); 
+5

Può diventare un incubo per qualcuno supportare questo codice :( – Anatoly

+1

Mentre siamo sul tema di supportare questo codice ... potresti rompere la risposta e spiegare le sue parti? – CubeJockey

-2

Si può avere scrivere una funzione personalizzata per aggiungere l'ultimo delimitatore, ma per delimitatore tra voi può usare StringUtils.join() per realizzare il vostro task.Check questo link per api

+0

Non ne ho nemmeno bisogno, io li ho già uniti con Java 8 semplice - vedi 'collect (joining (", "))' parte – Xorty

1

Se non ti dispiace utilizzando un Iterator, questo funziona:

private static String specialJoin(Iterable<?> list, String sep, String lastSep) { 
    StringBuilder result = new StringBuilder(); 
    final Iterator<?> i = list.iterator(); 
    if (i.hasNext()) { 
     result.append(i.next()); 
     while (i.hasNext()) { 
      final Object next = i.next(); 
      result.append(i.hasNext() ? sep : lastSep); 
      result.append(next); 
     } 
    } 
    return result.toString(); 
} 

Probabilmente può essere riscritta come un collezionista abbastanza facilmente da qualcuno che ha familiarità con quella API.

+0

Questo può essere soggettivo, ma direi che il codice originale è migliore di questo :) – Xorty

+1

Questa è una soluzione _elegante_, poiché non attraversa la raccolta più di una volta e non dipende dal contenuto dell'ultimo valore nell'elenco. :-) –

+0

Ho scritto un'altra risposta, usando i flussi/raccogli le API. Ma nella vita reale, userei questo metodo basato su Iterator. –

2

Non sono sicuro di quanto sia elegante, ma funziona. La parte fastidiosa è che è necessario invertire il List.

List<String> list = Arrays.asList("George", "John", "Paul", "Ringo"); 
String andStr = " and "; 
String commaStr = ", "; 
int n = list.size(); 
String result = list.size() == 0 ? "" : 
     IntStream.range(0, n) 
       .mapToObj(i -> list.get(n - 1 - i)) 
       .reduce((s, t) -> t + (s.contains(andStr) ? commaStr : andStr) + s) 
       .get(); 
System.out.println(result); 

Tuttavia, penso che la soluzione migliore sia questa.

StringBuilder sb = new StringBuilder(); 
int n = list.size(); 
for (String string : list) { 
    sb.append(string); 
    if (--n > 0) 
     sb.append(n == 1 ? " and " : ", "); 
} 
System.out.println(sb); 

È chiaro, efficiente e ovviamente funziona. Non credo che gli Stream siano adatti a questo problema.

1

Ecco una soluzione elegante con il flussi di api:

String nameList = names.stream().collect(naturalCollector(", ", " and ")); 

Unfortunatley, dipende da questa funzione, che potrebbe essere messo da parte in qualche classe di utilità:

public static Collector<Object, Ack, String> naturalCollector(String sep, String lastSep) { 
    return new Collector<Object, Ack, String>() { 

     @Override public BiConsumer<Ack, Object> accumulator() { 
      return (Ack a, Object o) -> a.add(o, sep); 
     } 

     @Override public Set<java.util.stream.Collector.Characteristics> characteristics() { 
      return Collections.emptySet(); 
     } 

     @Override public BinaryOperator<Ack> combiner() { 
      return (Ack one, Ack other) -> one.merge(other, sep); 
     } 

     @Override public Function<Ack, String> finisher() { 
      return (Ack a) -> a.toString(lastSep); 
     } 

     @Override public Supplier<Ack> supplier() { 
      return Ack::new; 
     } 

    }; 
} 

... e anche in questa classe, che è uno stateholder interna in funzione sopra, ma che l'API collettore vuole esposta:

class Ack { 
    private StringBuilder result = null; 
    private Object last; 

    public void add(Object u, String sep) { 
     if (last != null) { 
      doAppend(sep, last); 
     } 
     last = u; 
    } 

    private void doAppend(String sep, Object t) { 
     if (result == null) { 
      result = new StringBuilder(); 
     } else { 
      result.append(sep); 
     } 
     result.append(t); 
    } 

    public Ack merge(Ack other, String sep) { 
     if (other.last != null) { 
      doAppend(sep, last); 
      if (other.result != null) { 
       doAppend(sep, other.result); 
      } 
      last = other.last; 
     } 
     return this; 
    } 

    public String toString(String lastSep) { 
     if (result == null) { 
      return last == null ? "" : String.valueOf(last); 
     } 
     result.append(lastSep).append(last); 
     return result.toString(); 
    } 
} 
+0

Un collezionista personalizzato è davvero un approccio elegante. Ora è solo una questione di gusti se la maggior parte degli sviluppatori è più felice di comprendere poche righe di concatenazioni di stringhe o collezionisti personalizzati. – Xorty

+0

Questo è il mio primo tentativo di scrivere un raccoglitore personalizzato, quindi può probabilmente essere migliorato in ... –

+1

@RasmusKaj Penso che sia davvero buono. Funziona sicuramente. L'unico miglioramento che farei è che tutte quelle classi anonime in 'naturalCollector' possono essere sostituite da lambda. Il codice per 'supplier()' può essere semplicemente 'return Ack :: new;'. –

Problemi correlati