/*
* Copyright (C) 2013 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package cn.androidy.common.utils;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
/**
* Formats a list in a size-dependent way. List separators define how to separate two elements of
* the list. You can define 3 different separators:
* <ul>
* <li>separator for lists with exactly 2 elements (e.g. "first <b>and</b> second")
* <li>for lists with more than 2 elements, the separator for all but the last element (e.g.
* "first<b>,</b> second<b>,</b> …")
* <li>for lists with more than 2 elements, the separator for the second-last and last element
* (e.g. "second-last<b>, and</b> last")
* </ul>
*
* The {@code join} methods will throw exceptions if the list is null, or
* contains null or empty ("") elements. They will also throw if {@link Formatter#format(Object)}
* returns null or an empty string.
*
* <p> E.g.
* <pre>
* // Use the same separator for lists of all sizes.
* ListPhrase list = ListPhrase.from(", ");
* list.join(Arrays.asList("one")) → "one"
* list.join(Arrays.asList("one", "two")) → "one, two"
* list.join(Arrays.asList("one", "two", "three")) → "one, two, three"
* </pre>
* <pre>
* // Join English sentence-like lists.
* ListPhrase list = ListPhrase.from(
* " and ",
* ", ",
* ", and ");
* list.join(Arrays.asList("one")) → "one"
* list.join(Arrays.asList("one", "two")) → "one and two"
* list.join(Arrays.asList("one", "two", "three")) → "one, two, and three"
* </pre>
*/
public final class ListPhrase {
/**
* Entry point into this API.
*
* @param separator separator for all elements
*/
public static ListPhrase from(@NonNull CharSequence separator) {
checkNotNull("separator", separator);
return ListPhrase.from(separator, separator, separator);
}
/**
* Entry point into this API.
*
* @param twoElementSeparator separator for 2-element lists
* @param nonFinalElementSeparator separator for non-final elements of lists with 3 or more
* elements
* @param finalElementSeparator separator for final elements in lists with 3 or more elements
*/
@NonNull public static ListPhrase from(@NonNull CharSequence twoElementSeparator,
CharSequence nonFinalElementSeparator, CharSequence finalElementSeparator) {
return new ListPhrase(twoElementSeparator, nonFinalElementSeparator, finalElementSeparator);
}
/** Converts a list element to a {@link CharSequence}. */
public interface Formatter<T> {
CharSequence format(T item);
}
private final CharSequence twoElementSeparator;
private final CharSequence nonFinalElementSeparator;
private final CharSequence finalElementSeparator;
private ListPhrase(@NonNull CharSequence twoElementSeparator,
@NonNull CharSequence nonFinalElementSeparator, @NonNull CharSequence finalElementSeparator) {
this.twoElementSeparator = checkNotNull("two-element separator", twoElementSeparator);
this.nonFinalElementSeparator = checkNotNull("non-final separator", nonFinalElementSeparator);
this.finalElementSeparator = checkNotNull("final separator", finalElementSeparator);
}
/**
* Join 3 or more objects using {@link Object#toString()} to convert them to {@code Strings}.
*
* @throws IllegalArgumentException if any of the list elements are null or empty strings.
*/
@NonNull public <T> CharSequence join(@NonNull T first, @NonNull T second, @NonNull T... rest) {
return join(asList(first, second, rest));
}
/**
* Join a list of objects using {@link Object#toString()} to convert them to {@code Strings}.
*
* @throws IllegalArgumentException if any of the list elements are null or empty strings.
*/
@NonNull public <T> CharSequence join(@NonNull Iterable<T> items) {
checkNotNullOrEmpty(items);
return join(items, null);
}
/**
* A list of objects, converting them to {@code Strings} by passing them to {@link
* Formatter#format(Object)}.
*
* @throws IllegalArgumentException if any of the list elements are null or empty strings.
*/
@NonNull public <T> CharSequence join(@NonNull Iterable<T> items,
@Nullable Formatter<T> formatter) {
checkNotNullOrEmpty(items);
return joinIterableWithSize(items, getSize(items), formatter);
}
private <T> CharSequence joinIterableWithSize(Iterable<T> items, int size,
Formatter<T> formatter) {
switch (size) {
case 0:
// This case should be caught by the public join methods and this should never run.
throw new IllegalStateException("list cannot be empty");
case 1:
return formatOrThrow(items.iterator().next(), 0, formatter);
case 2:
return joinTwoElements(items, formatter);
default:
return joinMoreThanTwoElements(items, size, formatter);
}
}
private <T> CharSequence joinTwoElements(Iterable<T> items, Formatter<T> formatter) {
StringBuilder builder = new StringBuilder();
Iterator<T> iterator = items.iterator();
// Don't need to check hasNext since we know the size.
builder.append(formatOrThrow(iterator.next(), 0, formatter));
builder.append(twoElementSeparator);
builder.append(formatOrThrow(iterator.next(), 1, formatter));
return builder.toString();
}
private <T> CharSequence joinMoreThanTwoElements(Iterable<T> items, int size,
Formatter<T> formatter) {
StringBuilder builder = new StringBuilder();
int secondLastIndex = size - 2;
Iterator<T> iterator = items.iterator();
for (int i = 0; i < size; i++) {
// Don't need to check hasNext since we know the size.
builder.append(formatOrThrow(iterator.next(), i, formatter));
if (i < secondLastIndex) {
builder.append(nonFinalElementSeparator);
} else if (i == secondLastIndex) {
builder.append(finalElementSeparator);
}
}
return builder.toString();
}
private static int getSize(Iterable<?> iterable) {
if (iterable instanceof Collection) {
return ((Collection) iterable).size();
}
int size = 0;
Iterator<?> it = iterable.iterator();
while (it.hasNext()) {
size++;
it.next();
}
return size;
}
private static <T> List<T> asList(final T first, final T second, final T[] rest) {
return new AbstractList<T>() {
@Override
public T get(int index) {
switch (index) {
case 0:
return first;
case 1:
return second;
default:
return rest[index - 2];
}
}
@Override
public int size() {
return rest.length + 2;
}
};
}
/**
* Formats {@code item} by passing it to {@code formatter.format()} if {@code formatter} is
* non-null, else calls {@code item.toString()}. Throws an {@link IllegalArgumentException} if
* {@code item} is null, and an {@link IllegalStateException} if {@code formatter.format()}
* returns null.
*/
private static <T> CharSequence formatOrThrow(T item, int index, Formatter<T> formatter) {
if (item == null) {
throw new IllegalArgumentException("list element cannot be null at index " + index);
}
CharSequence formatted = formatter == null ? item.toString() : formatter.format(item);
if (formatted == null) {
throw new IllegalArgumentException("formatted list element cannot be null at index " + index);
}
if (formatted.length() == 0) {
throw new IllegalArgumentException(
"formatted list element cannot be empty at index " + index);
}
return formatted;
}
private static <T> T checkNotNull(String name, T obj) {
if (obj == null) {
throw new IllegalArgumentException(name + " cannot be null");
}
return obj;
}
private static <T> void checkNotNullOrEmpty(Iterable<T> obj) {
if (obj == null) {
throw new IllegalArgumentException("list cannot be null");
}
if (!obj.iterator().hasNext()) {
throw new IllegalArgumentException("list cannot be empty");
}
}
}