JavaScript reduce() / map() patterns

Patterns pentru Array.prototype.reduce()

Daniel Turcu

9 minute read

JavaScript reduce() / map() patterns

Introducere

.map() si .reduce() sunt piese de baza ale programarii functionale in JS. Cand vorbim de programare functionala, contrastam cu programarea imperativa. Codul devine mult mai expresiv si de obicei este mai concis. Odata ce ne obisnuim cu acest mod de programare, va fi si mai usor de inteles codul aplicatiilor.

Sa vedem un exemplu de programare imperativa versus programare functionala in JS.

// imperativ

var arr = [1,10,100];
var sum = 0;
for (var i = 0; i < arr.length; i++) {
	sum += arr[i]	
}

console.log(sum); // 111
// declarativ (programare functionala)

var arr = [1,10,100];
var sum = arr.reduce( function (sum, current) { return sum + current }, 0 )

console.log(sum); // 111

sau varianta ES6:


var sum = arr.reduce( (sum, current) => sum + current, 0 )

Practic, in cel de-al doilea mod, este ca si cum am spune ce vrem, fara sa ne intereseze detaliile de implementare: ‘Vreau sa-mi reduci array-ul la o valoare dupa regula…'. Regula este data de primul parametru al lui .reduce() - adica o functie al carei return este instructiunea care se aplica pentru fiecare element din array.

Al doilea parametru al lui reduce este valoarea initiala cu care se pleaca la drum.

reduce() ne va pune la dispozitie valoarea acumulata, de fiecare data cand trece la un alt element.

Haideti sa rulam urmatorul cod modificat:


var arr = [1,10,100];
var sum = arr.reduce( function (sum, current, index) { 
        console.log('Pasul: ' + index + 
            '; sum: '+ sum, 
            '; current: ' + current + 
            '. Voi returna ' + Number(sum+current) ); 
        return sum + current;
    }, 0 );

  • am folosit Number() ca sa evitam mecanismul de coercion to string, adica concatenare in loc de suma

Cam multe concatenari in codul de mai sus… vom folosi template string (ES6) ca sa simplificam constructia mesajului din consola:

// versiunea ES6

let arr = [1,10,100];
let sum = arr.reduce( function (sum, current, index) { 
    console.log(`Pasul: ${index}; sum: ${sum} ; current: ${current}. Voi returna ${sum+current}`);
        return sum + current;
    }, 0 );

  • observam ca nu mai este nevoie de parsing cu Number(). Ceea ce avem intre ${...} este vazut ca o expresie JavaScript care este mai intai evaluata si apoi concatenata cu stringul literal.

Grupari folosind reduce()

Daca aveti experienta in SQL, stiti sintaxa GROUP BY si utilitatea ei. Ceva similar putem sa obtinem in JavaScript folosind reduce.

Pornind de la datele de mai jos:


let products = [
   { id: 1,  sku: 'p01', name: 'Product One',   category: 10, weight: 2, price: 7,  active: true }, 
   { id: 2,  sku: 'p02', name: 'Product Two',   category: 10, weight: 2, price: 3,  active: true }, 
   { id: 3,  sku: 'p03', name: 'Product Three', category: 20, weight: 2, price: 12, active: true }, 
   { id: 4,  sku: 'p04', name: 'Product Four',  category: 10, weight: 2, price: 9,  active: true }, 
   { id: 5,  sku: 'p05', name: 'Product Five',  category: 20, weight: 2, price: 1,  active: true }, 
   { id: 6,  sku: 'p06', name: 'Product Six',   category: 20, weight: 2, price: 9,  active: true }, 
   { id: 7,  sku: 'p07', name: 'Product Seven', category: 30, weight: 2, price: 21, active: true }, 
   { id: 8,  sku: 'p08', name: 'Product Eight', category: 20, weight: 2, price: 16, active: true }, 
   { id: 9,  sku: 'p09', name: 'Product Nine',  category: 40, weight: 2, price: 6,  active: true }, 
   { id: 10, sku: 'p10', name: 'Product Ten',   category: 40, weight: 2, price: 5,  active: true }, 
];

let categories = [
    { id: 10,  code: 'c10', name: 'Category A' }, 
    { id: 20,  code: 'c20', name: 'Category B' }, 
    { id: 30,  code: 'c30', name: 'Category C' }, 
    { id: 40,  code: 'c40', name: 'Category D' }, 
];

Prima problema unde putem utiliza reduce:

Cate articole are fiecare categorie (vom utiliza id-ul categoriei pentru grupare)

Mai intai o functie helper re-utilizabila:


// data - setul de date
// prop - proprietatea dupa care facem gruparea (ex: 'category')
const groupBy = (data, prop) => {   
    
    let grouped_data = data.reduce((acc,current) => {
        let grouping_prop = current[prop];   // current['category'] = 10;
        console.log(acc, current, prop ,current[prop])
        // daca este prima data cand intalnim aceasta valoare (ex: 10)
        if (!acc[grouping_prop]) { acc[grouping_prop] = 0 }  // initializare { '10': 0 }

        acc[grouping_prop]++; // incrementam numarul de produse din categoria respectiva

        return acc; // Ex: { '10': 2, '20': 1 }
    }, {})

    return grouped_data;
}

let groupedByCategory = groupBy(products, 'category');

console.log(groupedByCategory);
// {10: 3, 20: 4, 30: 1, 40: 2}

Am aflat ca avem 3 produse in categoria cu id-ul ‘10’, 4 in categoria ‘20’, etc.

Denormalizarea datelor si afisarea denumirii categoriei in locul id-ului

Practic trebuie sa construim un join intre doua seturi de date. Campurile dupa care vom face aceasta legatura sunt: category(products) si id(category). Prima o mai putem denumi cheia straina iar a doua este cheia primara pentru setul de date categories.

Scopul ar fi sa afisam datele de mai sus astfel:

{ 'Category A': 3, 'Category B': 4, 'Category C': 1, 'Category D': 2 }

Pentru asta, ne vom uita mai intai la functia: .map() (Array.prototype.map)

map()

O foarte utila functie in JavaScrip este map(). Inainte sa incercam sa gasim o metoda pentru combinarea array-urilor dupa o cheie data, sa vedem un exemplu care foloseste map().

Vom folosi setul de date products de mai sus. Pornind de la un array de obiecte, vrem sa obtinem toate id-urile intr-un nou array.

id este proprietate a unui obiect; putem folosi map pentru a furniza o valoare corespondenta pentru fiecare obiect din lista products (mapare).

Mai jos avem codul pentru a obtine toate id-urile (puse intr-un array).


const getIds = (arr) => {

    let ids = arr.map( (item) => {
        return item.id;     // pentru noul array returnam un corespondent al lui item 
    } )

    return ids;
}

console.log( getIds(products) ); // [1,2,3...]

Deci, folosind map(), am putut sa inlocuim fiecare obiect cu o noua valoare. map() va crea un nou array, array-ul original ramane nealterat.

Acum, dupa ce am vazut cum actioneaza map, sa incercam sa facem acel join intre array-uri.

Combinarea datelor din doua array-uri dupa o cheie comuna


// obiectele din noul array vor avea si campul categoryName
let productsCategory = products.map( (item) => {
    let category = categories.find( cat => cat.id === item.category )

    // folosind Object.assign putem sa adaugam proprietati noi obiectului item
    return Object.assign( item, { categoryName: category.name } ) 
} );

// acum vom rescrie codul de grupare
let groupedByCategoryName = groupBy(products, 'categoryName');

console.log(groupedByCategoryName);
// {Category A: 3, Category B: 4, Category C: 1, Category D: 2}

Crearea unui index peste un set de date

Inainte sa cautam solutia pentru crearea unui index, sa discutam putin despre un feature ES6: ... (spread operator). Acesta ne ajuta sa transformam obiectele si array-urile in elemente:

let obj = { a: 10, b: 20 };
let newObj = { ...obj, c: 30 };  
// {a: 10, b: 20, c: 30} 
  • am creat un nou obiect care contine toate proprietatile lui obj plus o noua proprietate c: 30

La array-uri putem folosi spread operator pentru clonari si concatenari:

let arr = [10, 20, 30];
let arrClone = [ ...arr ];
let newExtendedArray = [ ...arr, 40, 50 ]; // [10, 20, 30, 40, 50]

Acum sa revenim la indexare…

Sa presupunem ca am ajuns la concluzia ca o indexare a datelor dupa categorie ar fi utila. Vom considera setul de date products. Vrem sa grupam sub umbrela categoriei toate obiectele din acea categorie. Cand vom folosi sintaxa indexByCategory[20] vom obtine un array de produse din acea categorie.


var indexByCategory = products.reduce((acc, current) => {
    return { ...acc,                        // { 10: [{1},{2}], 20: [{...}] }
        [current.category]: [               //  10: [
            ...(acc && acc[current.category] || [] ), // {1},{2} 
            current                                   // {3}  
        ]                                   //      ]
    } 
}, {}) // acumularea o vom face pe un obiect (fiecare proprietate pe obiect este categoryid)

Rezultatul arata cam asa:


{10: Array(3), 20: Array(4), 30: Array(1), 40: Array(2)}
    10: Array(3)
        0: {id: 10, sku: "p01", name: "Category A", category: 10, weight: 2, }
        1: {id: 10, sku: "p02", name: "Category A", category: 10, weight: 2, }
        2: {id: 10, sku: "p04", name: "Category A", category: 10, weight: 2, }
...

Acum putem accesa toate produsele dintr-o categorie apeland direct:

console.log(indexByCategory[10])

[
    {id: 10, sku: "p01", name: "Category A", category: 10, weight: 2, }
    {id: 10, sku: "p02", name: "Category A", category: 10, weight: 2, }
    {id: 10, sku: "p04", name: "Category A", category: 10, weight: 2, }
] 

Acum datele sunt organizate ca intr-un dictionar, unde in loc de litera avem id-ul categoriei. Similar am putea sa grupam produsele in functie de preturi: de exemplu sa avem grupele 0-10, 10-20, 20+.

Indexarea datelor imbricate dintr-un array de obiecte

Sa presupunem ca avem noi date pe obiectele din products:


let products = [
   { id: 1,  name: 'Product One',   category: 10, price: 7,  tags: ['t1', 't3'] }, 
   { id: 2,  name: 'Product Two',   category: 10, price: 3,  tags: ['t2']  }, 
   { id: 3,  name: 'Product Three', category: 20, price: 12, tags: ['t2', 't4']  }, 
   { id: 4,  name: 'Product Four',  category: 10, price: 9,  tags: ['t6', 't7', 't8'] }, 
   { id: 5,  name: 'Product Five',  category: 20, price: 1,  tags: ['t8'] }, 
   { id: 6,  name: 'Product Six',   category: 20, price: 9,  tags: ['t5', 't7'] }, 
   { id: 7,  name: 'Product Seven', category: 30, price: 21, tags: ['t3', 't2'] }, 
   { id: 8,  name: 'Product Eight', category: 20, price: 16, tags: ['t1', 't4', 't6', 't8'] }, 
   { id: 9,  name: 'Product Nine',  category: 40, price: 6,  tags: ['t3', 't6'] }, 
   { id: 10, name: 'Product Ten',   category: 40, price: 5,  tags: ['t7'] }
];

Vrem sa obtinem lista cu toate tag-urile. Aceasta lista poate fi pusa la dispozitia utilizatorului in interfata.

Pentru asta vom incepe cu urmatorul cod:


let allTags = products.reduce((acc,current) => {
    return [ ...acc, ...current.tags ]      // concatenare de array-uri
}, []);

console.log( allTags ); 
// ["t1", "t3", "t2", "t2", "t4", "t6", "t7", "t8", "t8", "t5", "t7", "t3", "t2", "t1", "t4", "t6", "t8", "t3", "t6", "t7"]

Am obtinut un array cu toate tag-urile, insa nu sunt unice.


let allTags = products
    .reduce((acc,current) => {
        return [ ...acc, ...current.tags ]      // concatenare de array-uri
    }, [])
    .reduce((acc, current) => {
        if (acc.indexOf(current) === -1) { // daca tag-ul nu exista, il adaugam
            acc.push(current)
        }
        return acc
    }, [])

console.log( allTags ); 
// 

Mai avem un singur pas. Sortarea alfabetica:


// ...

allTags.sort( );

console.log( allTags ); 

sort() poate primi o functie comparator, care sa ofere instructiuni despre cum vrem sa sorteze array-ul. Insa, pentru cazul nostru, sortarea implicita este ceea ce ne trebuie (alfabetic).

sort() actioneaza asupra array-ului si il modifica. Daca nu dorim sa modificam array-ul original putem crea o clona folosind spread operator:

let sorted = [ ...allTags ].sort()

console.log(sorted, allTags);

//sorted: ["t1", "t2", "t3", "t4", "t5", "t6", "t7", "t8"] 
//allTags: ["t1", "t3", "t2", "t4", "t6", "t7", "t8", "t5"]

comments powered by Disqus