multiple conditions java list java-8 collectors

java - conditions - División de la lista en sublistas a lo largo de elementos



java stream filter multiple conditions (13)

Tengo esta lista ( List<String> ):

["a", "b", null, "c", null, "d", "e"]

Y me gustaría algo como esto:

[["a", "b"], ["c"], ["d", "e"]]

En otras palabras, quiero dividir mi lista en sublistas usando el valor null como separador, para obtener una lista de listas ( List<List<String>> ). Estoy buscando una solución Java 8. Lo he intentado con Collectors.partitioningBy pero no estoy seguro de que sea lo que estoy buscando. ¡Gracias!


Agrupe por token diferente cada vez que encuentre un nulo (o separador). Usé aquí un número entero diferente (usado atómico solo como titular)

Luego reasigne el mapa generado para transformarlo en una lista de listas.

AtomicInteger i = new AtomicInteger(); List<List<String>> x = Stream.of("A", "B", null, "C", "D", "E", null, "H", "K") .collect(Collectors.groupingBy(s -> s == null ? i.incrementAndGet() : i.get())) .entrySet().stream().map(e -> e.getValue().stream().filter(v -> v != null).collect(Collectors.toList())) .collect(Collectors.toList()); System.out.println(x);


Aquí está el código de AbacusUtil

List<String> list = N.asList(null, null, "a", "b", null, "c", null, null, "d", "e"); Stream.of(list).splitIntoList(null, (e, any) -> e == null, null).filter(e -> e.get(0) != null).forEach(N::println);

Declaración: Soy el desarrollador de AbacusUtil.


Aquí hay otro enfoque, que utiliza una función de agrupación, que utiliza índices de lista para la agrupación.

Aquí estoy agrupando el elemento por el primer índice que sigue a ese elemento, con un valor null . Entonces, en su ejemplo, "a" y "b" se asignarían a 2 . Además, estoy asignando un valor null al índice -1 , que debería eliminarse más adelante.

List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e"); Function<String, Integer> indexGroupingFunc = (str) -> { if (str == null) { return -1; } int index = list.indexOf(str) + 1; while (index < list.size() && list.get(index) != null) { index++; } return index; }; Map<Integer, List<String>> grouped = list.stream() .collect(Collectors.groupingBy(indexGroupingFunc)); grouped.remove(-1); // Remove null elements grouped under -1 System.out.println(grouped.values()); // [[a, b], [c], [d, e]]

También puede evitar obtener el primer índice de elemento null cada vez, almacenando en caché el índice mínimo actual en un AtomicInteger . La Function actualizada sería como:

AtomicInteger currentMinIndex = new AtomicInteger(-1); Function<String, Integer> indexGroupingFunc = (str) -> { if (str == null) { return -1; } int index = names.indexOf(str) + 1; if (currentMinIndex.get() > index) { return currentMinIndex.get(); } else { while (index < names.size() && names.get(index) != null) { index++; } currentMinIndex.set(index); return index; } };


Aunque la respuesta de Marks Stuart es concisa, intuitiva y segura en paralelo (y la mejor) , quiero compartir otra solución interesante que no necesita el truco de límites de inicio / fin.

Si observamos el dominio del problema y pensamos en el paralelismo, podemos resolverlo fácilmente con una estrategia de divide y vencerás. En lugar de pensar en el problema como una lista en serie que tenemos que recorrer, podemos ver el problema como una composición del mismo problema básico: dividir una lista en un valor null . Podemos ver intuitivamente con bastante facilidad que podemos analizar el problema de manera recursiva con la siguiente estrategia recursiva:

split(L) : - if (no null value found) -> return just the simple list - else -> cut L around ''null'' naming the resulting sublists L1 and L2 return split(L1) + split(L2)

En este caso, primero buscamos cualquier valor null y en el momento en que encontramos uno, inmediatamente cortamos la lista e invocamos una llamada recursiva en las sublistas. Si no encontramos null (el caso base), terminamos con esta rama y simplemente devolvemos la lista. Concatenando todos los resultados devolverá la lista que estamos buscando.

Una imagen vale mas que mil palabras:

El algoritmo es simple y completo: no necesitamos ningún truco especial para manejar los casos extremos del inicio / final de la lista. No necesitamos ningún truco especial para manejar casos extremos como listas vacías o listas con solo valores null . O listas que terminan en null o que comienzan con null .

Una implementación simple e ingenua de esta estrategia es la siguiente:

public List<List<String>> split(List<String> input) { OptionalInt index = IntStream.range(0, input.size()) .filter(i -> input.get(i) == null) .findAny(); if (!index.isPresent()) return asList(input); List<String> firstHalf = input.subList(0, index.getAsInt()); List<String> secondHalf = input.subList(index.getAsInt()+1, input.size()); return asList(firstHalf, secondHalf).stream() .map(this::split) .flatMap(List::stream) .collect(toList()); }

Primero buscamos el índice de cualquier valor null en la lista. Si no encontramos uno, devolvemos la lista. Si encontramos uno, dividimos la lista en 2 sublistas, las transmitimos y llamamos recursivamente al método de split nuevamente. Las listas resultantes del subproblema se extraen y se combinan para obtener el valor de retorno.

Observe que las 2 secuencias se pueden hacer fácilmente paralelas () y el algoritmo seguirá funcionando debido a la descomposición funcional del problema.

Aunque el código ya es bastante conciso, siempre se puede adaptar de muchas maneras. Por el bien de un ejemplo, en lugar de verificar el valor opcional en el caso base, podríamos aprovechar el método orElse en el OptionalInt para devolver el índice final de la lista, lo que nos permite reutilizar la segunda secuencia y adicionalmente filtrar las listas vacías:

public List<List<String>> split(List<String> input) { int index = IntStream.range(0, input.size()) .filter(i -> input.get(i) == null) .findAny().orElse(input.size()); return asList(input.subList(0, index), input.subList(index+1, input.size())).stream() .map(this::split) .flatMap(List::stream) .filter(list -> !list.isEmpty()) .collect(toList()); }

El ejemplo solo se da para indicar la mera simplicidad, adaptabilidad y elegancia de un enfoque recursivo. De hecho, esta versión introduciría una pequeña penalización de rendimiento y fallaría si la entrada estuviera vacía (y como tal podría necesitar una verificación vacía adicional) .

En este caso, la recursión probablemente no sea la mejor solución (el algoritmo de Stuart Marks para encontrar índices es solo O (N) y las listas de mapeo / división tienen un costo significativo), pero expresa la solución con un algoritmo paralelo paralelo simple e intuitivo sin efectos secundarios.

No profundizaré en la complejidad y las ventajas / desventajas, ni utilizaré casos con criterios de detención y / o disponibilidad de resultados parciales. Simplemente sentí la necesidad de compartir esta estrategia de solución, ya que los otros enfoques eran simplemente iterativos o usaban un algoritmo de solución demasiado complejo que no era paralelo.


Aunque ya hay varias respuestas y una respuesta aceptada, todavía faltan un par de puntos en este tema. Primero, el consenso parece ser que resolver este problema usando flujos es simplemente un ejercicio, y que es preferible el enfoque convencional de bucle for. En segundo lugar, las respuestas dadas hasta ahora han pasado por alto un enfoque que usa técnicas de matriz o de estilo vectorial que creo que mejora considerablemente la solución de flujos.

Primero, aquí hay una solución convencional, para propósitos de discusión y análisis:

static List<List<String>> splitConventional(List<String> input) { List<List<String>> result = new ArrayList<>(); int prev = 0; for (int cur = 0; cur < input.size(); cur++) { if (input.get(cur) == null) { result.add(input.subList(prev, cur)); prev = cur + 1; } } result.add(input.subList(prev, input.size())); return result; }

Esto es principalmente sencillo, pero hay un poco de sutileza. Un punto es que una sublista pendiente de prev a cur siempre está abierta. Cuando encontramos null lo cerramos, lo agregamos a la lista de resultados y avanzamos prev . Después del ciclo, cerramos la sublista incondicionalmente.

Otra observación es que este es un ciclo sobre índices, no sobre los valores en sí mismos, por lo tanto, usamos un ciclo for aritmético en lugar del ciclo mejorado "for-each". Pero sugiere que podemos transmitir usando los índices para generar subintervalos en lugar de transmitir sobre valores y poner la lógica en el recopilador (como lo hizo la solución propuesta de Joop Eggen ).

Una vez que nos hemos dado cuenta de eso, podemos ver que cada posición de null en la entrada es el delimitador para una sublista: es el extremo derecho de la sublista a la izquierda, y (más uno) es el extremo izquierdo de la sublista para el derecho. Si podemos manejar los casos extremos, nos lleva a un enfoque en el que encontramos los índices en los que se producen elementos null , los asignamos a sublistas y recopilamos las sublistas.

El código resultante es el siguiente:

static List<List<String>> splitStream(List<String> input) { int[] indexes = Stream.of(IntStream.of(-1), IntStream.range(0, input.size()) .filter(i -> input.get(i) == null), IntStream.of(input.size())) .flatMapToInt(s -> s) .toArray(); return IntStream.range(0, indexes.length-1) .mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1])) .collect(toList()); }

Obtener los índices en los que se produce null es bastante fácil. El escollo agrega -1 a la izquierda y size en el extremo derecho. He optado por usar Stream.of para hacer los flatMapToInt y luego flatMapToInt para aplanarlos. (Intenté varios otros enfoques, pero este parecía el más limpio).

Es un poco más conveniente usar matrices para los índices aquí. Primero, la notación para acceder a una matriz es mejor que para una Lista: indexes[i] vs. indexes.get(i) . En segundo lugar, el uso de una matriz evita el boxeo.

En este punto, cada valor de índice en la matriz (excepto el último) es uno menos que la posición inicial de una sublista. El índice a su derecha inmediata es el final de la sublista. Simplemente transmitimos a través de la matriz y mapeamos cada par de índices en una sublista y recopilamos la salida.

Discusión

El enfoque de streams es ligeramente más corto que la versión for-loop, pero es más denso. La versión for-loop es familiar, porque hacemos todo esto en Java todo el tiempo, pero si aún no sabe lo que se supone que está haciendo este loop, no es obvio. Es posible que deba simular algunas ejecuciones de bucle antes de descubrir qué está haciendo prev y por qué la sublista abierta debe cerrarse después del final del bucle. (Inicialmente olvidé tenerlo, pero capté esto en las pruebas).

El enfoque de flujos es, creo, más fácil de conceptualizar lo que está sucediendo: obtener una lista (o una matriz) que indique los límites entre las sublistas. Esa es una transmisión fácil de dos líneas. La dificultad, como mencioné anteriormente, es encontrar una manera de agregar los valores de borde en los extremos. Si hubiera una mejor sintaxis para hacer esto, por ejemplo,

// Java plus pidgin Scala int[] indexes = [-1] ++ IntStream.range(0, input.size()) .filter(i -> input.get(i) == null) ++ [input.size()];

haría las cosas mucho menos desordenadas. (Lo que realmente necesitamos es la comprensión de la matriz o la lista). Una vez que tenga los índices, es simple asignarlos a sublistas reales y recopilarlos en la lista de resultados.

Y, por supuesto, esto es seguro cuando se ejecuta en paralelo.

ACTUALIZACIÓN 2016-02-06

Aquí hay una mejor manera de crear la matriz de índices de sublista. Se basa en los mismos principios, pero ajusta el rango del índice y agrega algunas condiciones al filtro para evitar tener que concatenar y mapear los índices.

static List<List<String>> splitStream(List<String> input) { int sz = input.size(); int[] indexes = IntStream.rangeClosed(-1, sz) .filter(i -> i == -1 || i == sz || input.get(i) == null) .toArray(); return IntStream.range(0, indexes.length-1) .mapToObj(i -> input.subList(indexes[i]+1, indexes[i+1])) .collect(toList()); }

ACTUALIZACIÓN 2016-11-23

Co-presenté una charla con Brian Goetz en Devoxx Antwerp 2016, "Thinking In Parallel" ( video ) que presentó este problema y mis soluciones. El problema presentado allí es una ligera variación que se divide en "#" en lugar de nulo, pero por lo demás es el mismo. En la charla, mencioné que tenía un montón de pruebas unitarias para este problema. Los he agregado a continuación, como un programa independiente, junto con mis implementaciones de bucles y transmisiones. Un ejercicio interesante para los lectores es ejecutar soluciones propuestas en otras respuestas contra los casos de prueba que he proporcionado aquí, y ver cuáles fallan y por qué. (Las otras soluciones tendrán que adaptarse para dividir en función de un predicado en lugar de dividir en nulo).

import java.util.*; import java.util.function.*; import java.util.stream.*; import static java.util.Arrays.asList; public class ListSplitting { static final Map<List<String>, List<List<String>>> TESTCASES = new LinkedHashMap<>(); static { TESTCASES.put(asList(), asList(asList())); TESTCASES.put(asList("a", "b", "c"), asList(asList("a", "b", "c"))); TESTCASES.put(asList("a", "b", "#", "c", "#", "d", "e"), asList(asList("a", "b"), asList("c"), asList("d", "e"))); TESTCASES.put(asList("#"), asList(asList(), asList())); TESTCASES.put(asList("#", "a", "b"), asList(asList(), asList("a", "b"))); TESTCASES.put(asList("a", "b", "#"), asList(asList("a", "b"), asList())); TESTCASES.put(asList("#"), asList(asList(), asList())); TESTCASES.put(asList("a", "#", "b"), asList(asList("a"), asList("b"))); TESTCASES.put(asList("a", "#", "#", "b"), asList(asList("a"), asList(), asList("b"))); TESTCASES.put(asList("a", "#", "#", "#", "b"), asList(asList("a"), asList(), asList(), asList("b"))); } static final Predicate<String> TESTPRED = "#"::equals; static void testAll(BiFunction<List<String>, Predicate<String>, List<List<String>>> f) { TESTCASES.forEach((input, expected) -> { List<List<String>> actual = f.apply(input, TESTPRED); System.out.println(input + " => " + expected); if (!expected.equals(actual)) { System.out.println(" ERROR: actual was " + actual); } }); } static <T> List<List<T>> splitStream(List<T> input, Predicate<? super T> pred) { int[] edges = IntStream.range(-1, input.size()+1) .filter(i -> i == -1 || i == input.size() || pred.test(input.get(i))) .toArray(); return IntStream.range(0, edges.length-1) .mapToObj(k -> input.subList(edges[k]+1, edges[k+1])) .collect(Collectors.toList()); } static <T> List<List<T>> splitLoop(List<T> input, Predicate<? super T> pred) { List<List<T>> result = new ArrayList<>(); int start = 0; for (int cur = 0; cur < input.size(); cur++) { if (pred.test(input.get(cur))) { result.add(input.subList(start, cur)); start = cur + 1; } } result.add(input.subList(start, input.size())); return result; } public static void main(String[] args) { System.out.println("===== Loop ====="); testAll(ListSplitting::splitLoop); System.out.println("===== Stream ====="); testAll(ListSplitting::splitStream); } }


Bueno, después de un poco de trabajo, U ha encontrado una solución basada en la transmisión de una línea. En última instancia, utiliza reduce() para hacer la agrupación, lo que parecía la opción natural, pero fue un poco feo colocar las cadenas en la List<List<String>> requerida por reducir:

List<List<String>> result = list.stream() .map(Arrays::asList) .map(x -> new LinkedList<String>(x)) .map(Arrays::asList) .map(x -> new LinkedList<List<String>>(x)) .reduce( (a, b) -> { if (b.getFirst().get(0) == null) a.add(new LinkedList<String>()); else a.getLast().addAll(b.getFirst()); return a;}).get();

Sin embargo, es 1 línea!

Cuando se ejecuta con la entrada de la pregunta,

System.out.println(result);

Produce:

[[a, b], [c], [d, e]]


Con String se puede hacer:

String s = ....; String[] parts = s.split("sth");

Si todas las colecciones secuenciales (como la Cadena es una secuencia de caracteres) tuvieran esta abstracción, esto también podría ser posible para ellos:

List<T> l = ... List<List<T>> parts = l.split(condition) (possibly with several overloaded variants)

Si restringimos el problema original a la Lista de cadenas (e imponemos algunas restricciones en el contenido de sus elementos) podríamos hackearlo así:

String als = Arrays.toString(new String[]{"a", "b", null, "c", null, "d", "e"}); String[] sa = als.substring(1, als.length() - 1).split("null, "); List<List<String>> res = Stream.of(sa).map(s -> Arrays.asList(s.split(", "))).collect(Collectors.toList());

(Por favor, no te lo tomes en serio :))

De lo contrario, la vieja recursión simple también funciona:

List<List<String>> part(List<String> input, List<List<String>> acc, List<String> cur, int i) { if (i == input.size()) return acc; if (input.get(i) != null) { cur.add(input.get(i)); } else if (!cur.isEmpty()) { acc.add(cur); cur = new ArrayList<>(); } return part(input, acc, cur, i + 1); }

(tenga en cuenta que en este caso nulo debe agregarse a la lista de entrada)

part(input, new ArrayList<>(), new ArrayList<>(), 0)


En mi biblioteca StreamEx hay un método groupRuns que puede ayudarlo a resolver esto:

List<String> input = Arrays.asList("a", "b", null, "c", null, "d", "e"); List<List<String>> result = StreamEx.of(input) .groupRuns((a, b) -> a != null && b != null) .remove(list -> list.get(0) == null).toList();

El método groupRuns toma un BiPredicate que para el par de elementos adyacentes devuelve verdadero si se deben agrupar. Después de eso, eliminamos los grupos que contienen valores nulos y recopilamos el resto en la Lista.

Esta solución es compatible con paralelo: también puede usarla para flujo paralelo. También funciona bien con cualquier fuente de flujo (no solo listas de acceso aleatorio como en algunas otras soluciones) y es algo mejor que las soluciones basadas en recopiladores, ya que aquí puede usar cualquier operación de terminal que desee sin desperdicio de memoria intermedia.


Estaba viendo el video sobre Thinking in Parallel de Stuart. Entonces decidí resolverlo antes de ver su respuesta en el video. Actualizará la solución con el tiempo. por ahora

Arrays.asList(IntStream.range(0, abc.size()-1). filter(index -> abc.get(index).equals("#") ). map(index -> (index)).toArray()). stream().forEach( index -> {for (int i = 0; i < index.length; i++) { if(sublist.size()==0){ sublist.add(new ArrayList<String>(abc.subList(0, index[i]))); }else{ sublist.add(new ArrayList<String>(abc.subList(index[i]-1, index[i]))); } } sublist.add(new ArrayList<String>(abc.subList(index[index.length-1]+1, abc.size()))); });


Este es un problema muy interesante. Se me ocurrió una solución de una línea. Puede que no sea muy eficiente pero funciona.

List<String> list = Arrays.asList("a", "b", null, "c", null, "d", "e"); Collection<List<String>> cl = IntStream.range(0, list.size()) .filter(i -> list.get(i) != null).boxed() .collect(Collectors.groupingBy( i -> IntStream.range(0, i).filter(j -> list.get(j) == null).count(), Collectors.mapping(i -> list.get(i), Collectors.toList())) ).values();

Es una idea similar que se le ocurrió a @Rohit Jain. Estoy agrupando el espacio entre los valores nulos. Si realmente desea una List<List<String>> puede agregar:

List<List<String>> ll = cl.stream().collect(Collectors.toList());


La única solución que se me ocurre por el momento es implementar su propio recopilador personalizado.

Antes de leer la solución, quiero agregar algunas notas sobre esto. Tomé esta pregunta más como un ejercicio de programación, no estoy seguro de si se puede hacer con un flujo paralelo.

Por lo tanto, debe tener en cuenta que se romperá silenciosamente si la tubería se ejecuta en paralelo .

Este no es un comportamiento deseable y debe evitarse . Es por eso que lanzo una excepción en la parte del combinador (en lugar de (l1, l2) -> {l1.addAll(l2); return l1;} ), ya que se usa en paralelo al combinar las dos listas, para que tenga una excepción en lugar de un resultado incorrecto.

Además, esto no es muy eficiente debido a la copia de la lista (aunque utiliza un método nativo para copiar la matriz subyacente).

Así que aquí está la implementación del recopilador:

private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) { final List<String> current = new ArrayList<>(); return Collector.of(() -> new ArrayList<List<String>>(), (l, elem) -> { if (sep.test(elem)) { l.add(new ArrayList<>(current)); current.clear(); } else { current.add(elem); } }, (l1, l2) -> { throw new RuntimeException("Should not run this in parallel"); }, l -> { if (current.size() != 0) { l.add(current); return l; } ); }

Y cómo usarlo:

List<List<String>> ll = list.stream().collect(splitBySeparator(Objects::isNull));

Salida:

[[a, b], [c], [d, e]]

Como la respuesta de Joop Eggen está fuera , parece que se puede hacer en paralelo (¡dale crédito por eso!). Con eso reduce la implementación del recopilador personalizado a:

private static Collector<String, List<List<String>>, List<List<String>>> splitBySeparator(Predicate<String> sep) { return Collector.of(() -> new ArrayList<List<String>>(Arrays.asList(new ArrayList<>())), (l, elem) -> {if(sep.test(elem)){l.add(new ArrayList<>());} else l.get(l.size()-1).add(elem);}, (l1, l2) -> {l1.get(l1.size() - 1).addAll(l2.remove(0)); l1.addAll(l2); return l1;}); }

que dejó el párrafo sobre paralelismo un poco obsoleto, sin embargo, lo dejé ya que puede ser un buen recordatorio.

Tenga en cuenta que la API Stream no siempre es un sustituto. Hay tareas que son más fáciles y más adecuadas usando los flujos y hay tareas que no lo son. En su caso, también podría crear un método de utilidad para eso:

private static <T> List<List<T>> splitBySeparator(List<T> list, Predicate<? super T> predicate) { final List<List<T>> finalList = new ArrayList<>(); int fromIndex = 0; int toIndex = 0; for(T elem : list) { if(predicate.test(elem)) { finalList.add(list.subList(fromIndex, toIndex)); fromIndex = toIndex + 1; } toIndex++; } if(fromIndex != toIndex) { finalList.add(list.subList(fromIndex, toIndex)); } return finalList; }

y List<List<String>> list = splitBySeparator(originalList, Objects::isNull); como List<List<String>> list = splitBySeparator(originalList, Objects::isNull); .

Se puede mejorar para comprobar casos extremos.


La solución es usar Stream.collect . Crear un recopilador utilizando su patrón de construcción ya se ofrece como solución. La alternativa es que la otra collect sobrecargada es un poco más primitiva.

List<String> strings = Arrays.asList("a", "b", null, "c", null, "d", "e"); List<List<String>> groups = strings.stream() .collect(() -> { List<List<String>> list = new ArrayList<>(); list.add(new ArrayList<>()); return list; }, (list, s) -> { if (s == null) { list.add(new ArrayList<>()); } else { list.get(list.size() - 1).add(s); } }, (list1, list2) -> { // Simple merging of partial sublists would // introduce a false level-break at the beginning. list1.get(list1.size() - 1).addAll(list2.remove(0)); list1.addAll(list2); });

Como se ve, hago una lista de listas de cadenas, donde siempre hay al menos una última lista de cadenas (vacía).

  • La primera función crea una lista inicial de listas de cadenas. Especifica el objeto resultado (escrito).
  • La segunda función se llama para procesar cada elemento. Es una acción sobre el resultado parcial y un elemento.
  • El tercero no se usa realmente, entra en juego para paralelizar el procesamiento, cuando se deben combinar resultados parciales.

Una solución con un acumulador:

Como señala @StuartMarks, el combinador no llena el contrato de paralelismo.

Debido al comentario de @ArnaudDenoyelle, una versión que usa reduce .

List<List<String>> groups = strings.stream() .reduce(new ArrayList<List<String>>(), (list, s) -> { if (list.isEmpty()) { list.add(new ArrayList<>()); } if (s == null) { list.add(new ArrayList<>()); } else { list.get(list.size() - 1).add(s); } return list; }, (list1, list2) -> { list1.addAll(list2); return list1; });

  • El primer parámetro es el objeto acumulado.
  • La segunda función se acumula.
  • El tercero es el ya mencionado combinador.

Por favor no vote. No tengo suficiente lugar para explicar esto en los comentarios .

Esta es una solución con un Stream y un foreach pero esto es estrictamente equivalente a la solución de Alexis o un bucle foreach (y menos claro, y no pude deshacerme del constructor de copias):

List<List<String>> result = new ArrayList<>(); final List<String> current = new ArrayList<>(); list.stream().forEach(s -> { if (s == null) { result.add(new ArrayList<>(current)); current.clear(); } else { current.add(s); } } ); result.add(current); System.out.println(result);

Entiendo que desea encontrar una solución más elegante con Java 8, pero realmente creo que no ha sido diseñado para este caso. Y como dijo el Sr. Spoon, prefiero la manera ingenua en este caso.