Группировка, массивы, карты, уменьшение и многое другое

Оператор SQL GROUP BY позволяет создавать группы связанных данных, а затем применять агрегатные функции, такие как SUM, MIN или MAX.

Рассмотрим следующую таблицу LocationsReport, в которой хранится количество офисов компании в каждом городе.

Country | City     | No
Italy   | Rome     | 2
Italy   | Genova   | 1
Spain   | Malaga   | 1
Spain   | Barcelona| 3

Следующий оператор SQL вычисляет общее количество местоположений в каждой стране.

SELECT Country, SUM(No)
FROM LocationsReport
GROUP BY Country
Country | No
Italy   | 3
Spain   | 4

В этой статье показано, как вычислить такой результат с помощью метода уменьшения массива.

Создание нового массива

Рассмотрим эквивалентный список объектов в JavaScript.

const locationsReport = [
 { country: 'Italy', city: 'Rome', no: 2 },
 { country: 'Italy', city: 'Genova', no: 1 },
 { country: 'Spain', city: 'Malaga', no: 1 },
 { country: 'Spain', city: 'Barcelona', no: 3 }
]

Чтобы вычислить количество местоположений в стране, мы можем создать новый массив с результатом. Это может выглядеть примерно так.

const perCountry = [
 { country: 'Italy', no: 3},
 { country: 'Spain', no: 4},
];

Давайте попробуем это сделать.

Метод массива reduce преобразует массив значений в одно значение с помощью функции редуктора. Единственное значение может быть новым массивом.

Рассмотрим следующую функцию редуктора.

function groupByCountry(newArr, line){
  const { country, no} = line;
  const countryTotal = 
    newArr.find(item => item.country === country);
  if(countryTotal) {
    countryTotal.no = countryTotal.no + no;
  } else {
    const newCountryTotal = { country, no };
    newArr.push(newCountryTotal);
  }
  
  return newArr;
}

Функция редуктора получает в качестве входных данных новый вычисляемый массив (newArr) и текущий элемент в списке (line). Он извлекает страну и свойства no из объекта строки в новые переменные, используя синтаксис деструктурирующего присваивания.

const { country, no} = line;

Затем он пытается найти ранее вычисленный объект результата для этой страны, используя метод поиска массива.

const countryTotal = 
    newArr.find(item => item.country === country);

Если такой объект найден, он просто добавляет новый no к вычисленной сумме на данный момент.

if(countryTotal) {
  countryTotal.no = countryTotal.no + no;
}

Если теперь найден общий информационный объект для страны, создается новый такой объект, и сумма инициализируется текущим значением no.

const newCountryTotal = { country, no };
newArr.push(newCountryTotal);

При вызове метода reduce мы передаем пустой массив в качестве начального значения.

const newReport = locationsReport.reduce(groupByCountry, []);
//[
//{country: "Italy", no: 3},
//{country: "Spain", no: 4}
//]

Далее попробуем сделать его более общим. Например, вместо использования свойства country внутри редьюсера мы можем передать дополнительный аргумент, сохраняющий это имя, с именем propName.

groupBy больше не функция редуктора, а построитель редуктора. groupBy принимает propName и возвращает функцию редуктора.

function groupBy(propName){
  return function (newArr, line){
    const { [propName] : name, no} = line;
    const totalInfo = 
      newArr.find(item => item[propName] === name);
    
    if(totalInfo) {
      totalInfo.no = totalInfo.no + no;
    } else {
      const newTotalInfo = { [propName] : name, no };
      newArr.push(newTotalInfo);
    }
    return newArr;
  }
}

Как уже было сказано, при вызове метода сокращения мы также передаем имя свойства, используемого для группировки связанных элементов.

const newReport = locationsReport.reduce(groupBy('country'), []);
//[
//{country: "Italy", no: 3},
//{country: "Spain", no: 4}
//]

Тем не менее, мы жестко задаем имя свойства, для которого выполняется добавление, то есть свойство no. Мы также можем передать это как аргумент функции редуктора (aggregateName).

function groupBy(propName, aggregateName){
  return function (newArr, line){
    const { [propName] : name, [aggregateName]: no} = line;
    const totalInfo = 
      newArr.find(item => item[propName] === name);
    
    if(totalInfo) {
      totalInfo[aggregateName] = 
        totalInfo[aggregateName] + no;
    } else {
      const newTotalInfo = 
        { [propName] : name, [aggregateName]: no };
      newArr.push(newTotalInfo);
    }
    return newArr;
  }
}

Метод reduce вызывается с передачей как имени свойства для группировки элементов, так и имени свойства, используемого в вычислении.

const newReport = locationsReport
   .reduce(groupBy('country', 'no'), []);
//[
//{country: "Italy", no: 3},
//{country: "Spain", no: 4}
//]

Мы можем еще больше обобщить построитель редукторов. Вместо того, чтобы всегда выполнять сложение, мы можем передать саму функцию вычисления. Это означает, что мы можем передать функции sum, min или max.

Рассмотрим функцию sum

function sum(a, b){
  return a + b;
}

Вот новая функция построения редуктора, принимающая функцию вычисления в качестве параметра (computeAggregate).

function groupBy(propName, computeAggregate, aggregateName){
  return function (newArr, line){
    const { [propName] : name, [aggregateName]: no} = line;
    const totalInfo = 
      newArr.find(item => item[propName] === name);
    if(totalInfo) {
      totalInfo[aggregateName] =    
        computeAggregate(totalInfo[aggregateName], no);
    } else {
      const newTotalInfo = 
        { [propName] : name, [aggregateName]: no };
      newArr.push(newTotalInfo);
    }
    return newArr;
  }
}

В следующем примере построитель редуктора вызывается с помощью агрегатной функции sum.

const newReport = 
  locationsReport.reduce(groupBy('country', sum,  'no'), []);
//[
//{country: "Italy", no: 3},
//{country: "Spain", no: 4}
//]

Ниже приведен еще один пример, в котором построитель редуктора вызывается с помощью агрегатной функции min.

function min(a, b){
 return Math.min(a, b);
}
const newReport = 
  locationsReport.reduce(groupBy('country', min,  'no'), []);
//[
//{country: "Italy", no: 1}
//{country: "Spain", no: 1}
//

Далее приведен пример вызова с использованием агрегатной функции max.

function max(a, b){
 return Math.max(a, b);
}
const newReport = 
  locationsReport.reduce(groupBy('country', max,  'no'), []);
//[
//{country: "Italy", no: 2},
//{country: "Spain", no: 3}
//]

Использование другой коллекции

Теперь давайте попробуем универсальный конструктор редукторов на новой коллекции информации о рейтингах игроков.

const ranking = [
  { country: 'France', name: 'Cleam', rating: 1447 },
  { country: 'Korea', name: 'Maru', rating: 1394 },
  { country: 'Korea', name: 'Rogue', rating: 1353 }
]

Следующий вызов показывает максимальный рейтинг по стране.

const newReport = ranking
  .reduce(groupBy('country', max,  'rating'), []);
//[
//{country: "France", rating: 1447},
//{country: "Korea", rating: 1394}
//]

Создание карты

Во всех других примерах мы фактически получили результат ключ-значение. Ключом было значение, хранящееся в propName, и значение, которое мы вычислили, используя свойство computeAggregate вместо aggregateName.

Вместо создания списка с результатом мы можем создать карту. Вот редуктор, создающий новую карту.

function groupBy(propName, computeAggregate, aggregateName){
  return function (newMap, line){
    const { [propName] : name, [aggregateName]: no} = line;
    const total = newMap.get(name);
    if(total) {
      newMap.set(name, computeAggregate(total, no));
    } else {
      newMap.set(name, no);
    }
    return newMap;
  }
}

Ниже приведен пример вызова нового построителя сокращения поверх коллекции ранжирования.

const newReport = ranking
  .reduce(groupBy('country', max,  'rating'), new Map);
//Map(2) {"France" => 1447, "Korea" => 1394}

Спасибо за чтение.