Группировка, массивы, карты, уменьшение и многое другое
Оператор 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}
Спасибо за чтение.