Ir al contenido
wannacode

Métodos de arrays

En resumen: Los métodos de arrays procesan los datos como una serie de pasos: filter criba, map transforma, toSorted ordena y reduce construye un único resultado.

Problema

Un informe con un bucle

Ahora los pedidos también tienen una ciudad:

Order
class Order {
constructor(customer, city, total) {
this.customer = customer;
this.city = city;
this.total = total;
}
}
const orders = [
new Order("María", "Madrid", 75.5),
new Order("Jorge", "Valencia", 12.9),
new Order("Elena", "Madrid", 40),
new Order("Diego", "Granada", 120),
new Order("Ana", "Valencia", 64),
];

Quieres los clientes con pedidos de al menos 50 €, en orden alfabético:

const result = [];
for (const o of orders) {
if (o.total >= 50) {
result.push(o.customer);
}
}
result.sort();
console.log(result);
Salida
[ 'Ana', 'Diego', 'María' ]

Ocho líneas, y la pregunta es una sola. El código dice cómo: crea una lista vacía, recorre, comprueba, añade, ordena. Para entender qué hace, tienes que ejecutarlo en tu cabeza. Añade «solo los tres primeros» o «sin repetidos», y el bucle crecerá.

La idea

Una cadena de pasos

El mismo informe, escrito como pasos:

const result = orders
.filter((o) => o.total >= 50)
.map((o) => o.customer)
.toSorted();
console.log(result);
Salida
[ 'Ana', 'Diego', 'María' ]

Se lee de arriba abajo: toma los pedidos, quédate con los de al menos 50 €, toma el cliente, ordena. Cada paso es un método de array y recibe una función flecha de la lección anterior:

  • filter(condición) se queda solo con los elementos para los que la condición es verdadera;
  • map(función) convierte cada elemento en otro: crea una cadena a partir de un Order;
  • toSorted() ordena alfabéticamente, y toSorted(función de comparación) según tu regla.

Cada método devuelve un array nuevo, y sobre él se llama al siguiente. Por eso los pasos se alinean en una cadena. El array orders no cambia.

Más precisamente, para curiosos
  • El antiguo sort ordena el propio array y lo devuelve. toSorted es el más nuevo y devuelve un array nuevo. Verás por qué importa la diferencia en la tarjeta «Encuentra el error».
  • Cada paso recorre el array entero antes de que empiece el siguiente. Una cinta transportadora por la que los elementos avanzan de uno en uno es lo que te da un iterador. De él trata la siguiente tarjeta.

Míralo

Qué pasa el filtro

Aquí los pasos no se hacen sobre el array, sino sobre el iterador que da values(). El iterador tiene los mismos filter y map, pero es una cinta transportadora: da los valores de uno en uno, cuando alguien los pide. toArray() los reúne en un array.

const prices = [12, 75, 40, 130, 58];
const labels = prices
.values()
.filter((p) => p >= 50)
.map((p) => `${p} €`)
.toArray();
console.log(labels);
Paso a paso, en palabras
  1. Cinco precios. values() da un iterador: un camino por el que todavía no ha pasado nada.
  2. 12 no pasa p >= 50 y se queda fuera. Nunca llega a map.
  3. 75 pasa filter, map lo convierte en el texto «75 €», toArray() lo reúne.
  4. 40 también se queda fuera. Cada elemento recorre todo el camino antes de que salga el siguiente.
  5. 130 y 58 pasan. El array prices no cambia.
  6. El resultado es un array nuevo con tres cadenas.

Los cinco precios avanzan de uno en uno. El 12 llega a filter y se queda fuera. El 75 pasa, map lo convierte en el texto «75 €» y toArray() lo recoge. Después el 40 se queda fuera, y el 130 y el 58 pasan.

Cada elemento hace todo el recorrido antes de que empiece el siguiente. Y nada empieza hasta que toArray() pide los valores.

Más precisamente, para curiosos

Esto se llama evaluación perezosa. Los métodos del iterador solo anotan lo que hay que hacer. Míralo con un print en cada paso:

const check = (p) => {
console.log("filter " + p);
return p >= 50;
};
const double = (p) => {
console.log("map " + p);
return p * 2;
};
console.log([12, 75, 40].filter(check).map(double));
const stream = [12, 75, 40].values().filter(check).map(double);
console.log("El iterador está listo, todavía no pasó nada por él.");
console.log(stream.toArray());
Salida
filter 12
filter 75
filter 40
map 75
[ 150 ]
El iterador está listo, todavía no pasó nada por él.
filter 12
filter 75
map 75
filter 40
[ 150 ]
  • El array (el primer console.log) lo hace todo enseguida y paso a paso: filter recorre los tres precios, y solo después map recorre el que queda.
  • El iterador está listo antes de que un solo elemento haya pasado por él. Después map 75 llega justo después de filter 75: los elementos no esperan a que todos pasen por filter.
  • Por eso puedes tomar solo los primeros valores del iterador con take, sin comprobar el resto:
const check = (p) => {
console.log("filter " + p);
return p >= 50;
};
console.log([75, 130, 58, 12, 99].values().filter(check).take(2).toArray());
Salida
filter 75
filter 130
[ 75, 130 ]

Para las tareas de todos los días basta con el array, y es más fácil de leer. El iterador es para muchos datos, o cuando solo necesitas los primeros.

La idea

Suma, cantidad, media

El resultado puede ser un número en lugar de una lista. La suma de los números pares:

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.filter((n) => n % 2 === 0).reduce((total, n) => total + n, 0);
console.log(`Suma de los números pares: ${sum}`);
Salida
Suma de los números pares: 6
reduce(función, inicio) reduce el array a un único valor. Empieza desde 0, y la función recibe la suma acumulada y el siguiente elemento y devuelve la nueva suma.

Lo mismo para los pedidos:

const big = orders.filter((o) => o.total >= 50).length;
const revenue = orders.reduce((sum, o) => sum + o.total, 0);
const average = orders.length > 0 ? revenue / orders.length : 0;
const maxTotal = Math.max(...orders.map((o) => o.total));
console.log(`Pedidos de desde 50 €: ${big}`);
console.log(`Ingresos: ${revenue.toFixed(2)} €`);
console.log(`Media: ${average.toFixed(2)} €`);
console.log(`Mayor: ${maxTotal.toFixed(2)} €`);
Salida
Pedidos de desde 50 €: 3
Ingresos: 312.40 €
Media: 62.48 €
Mayor: 120.00 €
  • La length del array filtrado es la cantidad.
  • La media es la suma dividida entre la cantidad. Si no hay pedidos, 0 / 0 es NaN («no es un número»), de ahí la comprobación orders.length > 0.
  • Math.max(...array) da el número más grande. ... despliega el array en parámetros separados: Math.max(75.5, 12.9, ...). Si el array está vacío, el resultado es -Infinity. Qué hacer con un resultado que puede faltar es la siguiente idea.
Más precisamente, para curiosos

Con un array vacío:

const prices = [];
console.log(prices.reduce((sum, p) => sum + p, 0));
console.log(Math.max(...prices));
console.log(0 / prices.length);
Salida
0
-Infinity
NaN

Sin valor inicial, reduce sobre un array vacío se detiene con TypeError: Reduce of empty array with no initial value. Así que escribe siempre el valor inicial.

La idea

Ordenar, recortar, sin repetidos

const ratings = [5, 3, 5, 4, 2, 4, 5];
console.log([...new Set(ratings)].toSorted((a, b) => a - b));
const top = orders
.toSorted((a, b) => b.total - a.total)
.slice(0, 3)
.map((o) => o.customer);
console.log(top);
Array.from({ length: 3 }, (_, i) => i + 1).forEach((i) => console.log(`Mesa ${i}`));
Salida
[ 2, 3, 4, 5 ]
[ 'Diego', 'María', 'Ana' ]
Mesa 1
Mesa 2
Mesa 3
  • new Set(ratings) quita los repetidos: en un conjunto cada valor está una sola vez. [...set] lo vuelve a convertir en un array.
  • toSorted((a, b) => b.total - a.total) ordena empezando por el pedido más grande, con una función de comparación de la lección anterior.
  • slice(0, 3) toma los elementos desde el índice 0 hasta el índice 3, sin incluirlo: los tres primeros. Junto con la ordenación, eso es un «top 3».
  • JavaScript no tiene range. Array.from({ length: 3 }, (_, i) => i + 1) crea un array con 3 elementos llamando a la función para cada índice i: 1, 2, 3. _ es el nombre de un parámetro que no necesitamos.
Más precisamente, para curiosos

Un Set mantiene los valores en el orden en que se añadieron, así que sin ordenar obtienes el orden de primera aparición:

const ratings = [5, 3, 5, 4, 2, 4, 5];
console.log([...new Set(ratings)]);
Salida
[ 5, 3, 4, 2 ]

Pruébalo

¿Qué llega al final?

¿Qué imprimirá el programa?

const drinks = ["café", "té", "café", "vino", "té", "jugo"];
const result = [...new Set(drinks)]
.toSorted()
.slice(0, 3)
.map((d) => d.toUpperCase());
console.log(result);
Respuesta

La idea

undefined: un resultado que puede faltar

¿Cuál es el pedido más grande? Si el array está vacío, no hay respuesta. Entonces el resultado es undefined: «ningún valor». Pero quien llama tiene que acordarse de comprobarlo. Si se le olvida, el programa se detiene con un TypeError en algún sitio más adelante.

Java tiene para esto Optional: una caja que o bien contiene un valor, o bien está vacía. JavaScript no tiene una caja así, pero tiene dos operadores que hacen corta la comprobación:

const biggest = orders.toSorted((a, b) => b.total - a.total)[0];
if (biggest !== undefined) {
console.log(`Mayor: ${biggest.customer}`);
}
const huge = orders.find((o) => o.total > 500);
console.log(huge !== undefined);
console.log(huge?.customer ?? "no existe ese pedido");
Salida
Mayor: Diego
false
no existe ese pedido
  • [0] de un array vacío es undefined.
  • find(condición) da el primer elemento para el que la condición es verdadera, o undefined.
  • huge?.customer: si huge es undefined o null, toda la expresión es undefined, en lugar de que el programa se detenga. Esto se llama encadenamiento opcional.
  • x ?? y da y si x es undefined o null, y x en caso contrario.

Tus propias funciones normalmente devuelven null cuando no tienen respuesta: «falta a propósito». ?? también funciona con él:

function nickname(user) {
// Devuelve el apodo, o null si la persona no tiene.
return user === "maria" ? "Mimi" : null;
}
const maria = nickname("maria");
if (maria !== null) {
console.log(`Apodo: ${maria}`);
}
const jorge = nickname("jorge");
console.log(jorge ?? "sin apodo");
Salida
Apodo: Mimi
sin apodo
Más precisamente, para curiosos
  • Sin ?.:
const huge = orders.find((o) => o.total > 500);
console.log(huge.customer);
Error
TypeError: Cannot read properties of undefined (reading 'customer')
  • x || y también da un valor de reserva, pero trata una cadena vacía y el 0 como si faltaran. ?? no (la tercera línea es una cadena vacía):
console.log(null ?? "sin apodo");
console.log("" || "sin apodo");
console.log("" ?? "sin apodo");
console.log(0 ?? 5);
Salida
sin apodo
sin apodo
0
  • some y every responden con true o false si alguno o todos los elementos cumplen la condición:
console.log(orders.some((o) => o.total > 100));
console.log(orders.every((o) => o.total >= 10));
Salida
true
true

Pruébalo

Encuentra el error

El programa debería imprimir el precio más caro y el primer precio introducido, 12. El segundo sale mal. ¿En qué línea está el error?

Elige la línea con el error.

La idea

Agrupar y unir

¿Cuántos pedidos hay de cada ciudad, y cuáles son los ingresos?

const byCity = Map.groupBy(orders, (o) => o.city);
const cities = [...byCity.keys()].toSorted();
const countByCity = Object.fromEntries(cities.map((city) => [city, byCity.get(city).length]));
console.log(countByCity);
for (const city of cities) {
const sum = byCity.get(city).reduce((total, o) => total + o.total, 0);
console.log(`${city}: ${sum.toFixed(2)} €`);
}
const customers = orders
.map((o) => o.customer)
.toSorted()
.join(", ");
console.log(customers);
Salida
{ Granada: 1, Madrid: 2, Valencia: 2 }
Granada: 120.00 €
Madrid: 115.50 €
Valencia: 76.90 €
Ana, Diego, Elena, Jorge, María
  • Map.groupBy(array, función) da un Map: para cada ciudad, un array de sus pedidos.
  • [...byCity.keys()] crea un array con las claves, y toSorted() las ordena alfabéticamente.
  • Object.fromEntries(pares) crea un objeto a partir de pares [clave, valor]. Aquí la clave es la ciudad, y el valor es la cantidad de sus pedidos (length).
  • join(", ") une las cadenas en una sola, con una coma entre ellas.

Un Map mantiene las claves en el orden en que se añadieron: Madrid, Valencia, Granada. Por eso las ciudades se ordenan explícitamente.

Más precisamente, para curiosos
  • Para cada ciudad, un array de los nombres. Aquí las ciudades van en el orden en que se añadieron:
const namesByCity = new Map();
for (const [city, list] of Map.groupBy(orders, (o) => o.city)) {
namesByCity.set(city, list.map((o) => o.customer));
}
console.log(namesByCity);
Salida
Map(3) {
'Madrid' => [ 'María', 'Elena' ],
'Valencia' => [ 'Jorge', 'Ana' ],
'Granada' => [ 'Diego' ]
}
  • Si una clave se repite en los pares para Object.fromEntries, gana el último valor, sin ningún error (en Java toMap lanza una excepción):
const lastByCity = Object.fromEntries(orders.map((o) => [o.city, o.customer]));
console.log(lastByCity);
Salida
{ Madrid: 'Elena', Valencia: 'Ana', Granada: 'Diego' }

Ejercicio

Informes con métodos de arrays

Resuelve las tareas en tu editor. Lee la entrada con ask, como antes.

1. Ventas por categoría. Una clase Sale con un producto, una categoría, una cantidad y un precio unitario, y un método revenue() (cantidad por precio). Agrupa por categoría con Map.groupBy, suma cada grupo con reduce, imprime las categorías en orden alfabético y los ingresos totales. Las ventas: café en grano, bebidas, 12 a 18,50 €; té, bebidas, 30 a 4,20 €; chocolate, dulces, 40 a 2,80 €; galletas, dulces, 25 a 3,10 €; miel, comida, 8 a 12 €; otra vez café en grano, 5 a 18,50 €.

Salida
Bebidas: 440.50 €
Comida: 96.00 €
Dulces: 189.50 €
Total: 726.00 €

2. Los 3 mejores productos. Con las mismas ventas, encuentra los tres productos con más ingresos. El café está en dos ventas, así que primero agrupa por producto y suma, y luego ordena las entradas del mapa por valor. Pista: [...byProduct].toSorted((a, b) => b[1] - a[1]).slice(0, 3): cada elemento de un Map es un par [producto, ingresos].

Salida
Café en grano: 314.50 €
Té: 126.00 €
Chocolate: 112.00 €

3. Nota media. Lee una línea de notas separadas por espacios. Imprime la media, la más alta y cuántas son excelentes (5,50 o más). Pista: (await ask("")).split(" ").map(Number).

Entrada
4.50 5.75 6.00 3.00 5.50
Salida
Media: 4.95
Máxima: 6.00
Excelentes: 3

4. Las palabras de un texto. Lee una línea de texto e imprime sus palabras sin repetidos, en orden alfabético, en minúsculas y separadas por comas. Divide por todo lo que no sea una letra: split(/[^\p{L}]+/u), y elimina las cadenas vacías.

Entrada
Café con leche, café sin azúcar. ¡Té con miel!
Salida
azúcar, café, con, leche, miel, sin, té

Más tareas

  • La palabra más larga. Con reduce, encuentra la palabra más larga de una frase. ¿Qué pasará con una línea vacía sin valor inicial?
  • Media por grupo. Estudiantes con un nombre, un grupo y una nota: agrúpalos con Map.groupBy e imprime la nota media de cada grupo.
  • Pedidos grandes y pequeños. Con some, comprueba si hay un pedido de más de 1000 €, y con every si todos son de al menos 10 €.

Test

Ponte a prueba

1. ¿Qué le pasa al array orders después de orders.filter((o) => o.total > 50)?
2. ¿Cuál de estos métodos cambia el propio array?
3. orders.find((o) => o.total > 500) no encuentra ningún pedido. ¿Qué devuelve?
4. ¿Qué da huge?.customer ?? "no existe ese pedido" si huge es undefined?
5. orders.reduce((sum, o) => sum + o.total, 0). ¿Qué es el 0?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·