2015-04-29 11 views
7

danej listy jak:Jak dołączyć do elementów listy, ale do ostatniego elementu użyć innego ogranicznika?

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

Chciałbym przekształcić go na ciąg jak poniżej:

George, John, Paul and Ringo

mogę zrobić to z dość niezdarny StringBuilder rzeczy tak:

Czy istnieje nieco bardziej eleganckie podejście? W razie potrzeby nie mam nic przeciwko korzystaniu z biblioteki.

UWAGI:

  • mogę użyć starego for pętlę z indeksem, ale ja nie szukam takiego rozwiązania
  • Brak przecinków w wartościach (nazwy)
+3

Czy przecinki są zawsze w obrębie wartości? – Bohemian

+0

@Behemian nope (zauważył, że w pytaniu) – Xorty

+0

Dlaczego nie enkapsulować jego funkcjonalności w metodzie zewnętrznej? Coś w stylu 'public String mixListOfNamesAndReplaceLastCommaWithAnd (lista nazw )' – Anatoly

Odpowiedz

2

Jak już większość z nich nie chciałbym wprowadzić inną metodę „replaceLast”, który nie jest w JDK dla java.lang.String tej pory:

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

Można zmienić separatory i params jak dobrze. Oto test dla wymagań tej pory:

@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

Nieźle, jeśli nie trzeba sparametryzować ograniczników, może to być 'return names.stream(). Collect (łączenie (", ")). ReplaceFirst (" (? S), (?!. * ?,) "," i "); – Xorty

+0

Tak, oczywiście, możesz zmodyfikować go do jednolinijkowego :-) – swinkler

2

Możesz dołączyć wszystkie elementy z wyjątkiem ostatniego za pomocą podmenu:

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

Osobiście nie podoba mi się to podejście, ponieważ dla implementacji list nieskładkowych, czas List#get może być O (n).

Testowany tutaj:

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 
} 

Oto alternatywna implementacja lub joinList, które mogą uczynić go bardziej zrozumiałym, co faktycznie dzieje:

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

Trudno byłoby użyć go do pustej listy i listy tylko jednego elementu – Xorty

+0

@Xorty w przypadku pustą listę, zwraca pusty ciąg;). –

+0

w przypadku 1 elementu, który powróci "i George" zamiast "George" (w przeciwieństwie do oryginalnego rozwiązania) – Xorty

0

Jeśli przecinki nie są w wartościach, to jedno-liner :

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

Może to stać się koszmarem dla kogoś, kto wesprze ten kod :( – Anatoly

+1

Podczas gdy jesteśmy na temat wspierania tego kodu ... Czy możesz złamać odpowiedź i wyjaśnić jej części? – CubeJockey

-2

Możesz napisać niestandardową funkcję, aby dodać ostatni separator, ale dla separator pomiędzy można użyć StringUtils.join(), aby osiągnąć swój task.Check link do api

+0

Nie potrzebuję tego, ja już dołączyłem do nich z prostym językiem Java 8 - patrz 'zbieraj (dołączanie (", "))' część – Xorty

1

Jeśli nie przeszkadza iterator, to działa:

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

To może prawdopodobnie być zapisane jako kolektor wystarczająco łatwo przez kogoś, kto jest zaznajomiony z tym api.

+0

Może to być subiektywne, ale ja rzeczywiście twierdzę, że oryginalny kod jest lepszy niż to :) – Xorty

+1

To jest rozwiązanie _elegant_, ponieważ nie przechodzi przez kolekcję więcej niż jeden raz i nie zależy od zawartości ostatniej wartości na liście. :-) –

+0

Napisałem inną odpowiedź, używając strumieni/zbierania api. Ale w prawdziwym życiu użyłbym tej opartej na Iteratorze metody. –

2

Nie jestem pewien, jaki to jest elegancki, ale działa. Denerwujące jest to, że musisz odwrócić 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); 

Jednak myślę, że najlepszym rozwiązaniem jest to.

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

Jest wyraźny, wydajny i oczywiście działa. Nie sądzę, że Stream s są dobre dla tego problemu.

1

Oto eleganckie rozwiązanie używając API strumieni:

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

Unfortunatley, zależy to od tej funkcji, które można schować w jakimś klasie użytkowej:

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

    }; 
} 

... i także w tej klasie, która jest wewnętrznym stateholder w powyższej funkcji, ale które API Collector chce narażone:

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

Kolektor niestandardowy rzeczywiście jest eleganckim podejściem. Teraz to tylko kwestia gustu, czy większość programistów jest szczęśliwsza w zrozumieniu kilku linii łączenia ciągów lub niestandardowych kolektorów. – Xorty

+0

To jest moja pierwsza próba napisania niestandardowego kolektora, więc prawdopodobnie można go poprawić na ... –

+1

@RasmusKaj Myślę, że to naprawdę dobrze. To na pewno działa. Jedyną poprawą, jaką mogę uczynić, jest to, że wszystkie te anonimowe klasy w "naturalCollector" można zastąpić lambdami. Kod dla 'dostawca()' może być po prostu 'return Ack :: new;'. –

Powiązane problemy