Группировка
Здесь есть что рассказать, поэтому я собираюсь двигаться немного быстрее.Если вы застряли в какой-либо части, пожалуйста, оставьте комментарий, и я сделаю все возможное, чтобы рассказать о любых проблемных областях.
Во-первых, нет никакой гарантии, что allRecords
или newRecords
будут отсортированы до того, как ониобъединены.Эффективная группировка подобных предметов может быть легко обработана с помощью Map
.Однако, когда мы хотим распечатать элементы в нужном порядке, значения карты необходимо будет отсортировать.Мы рассмотрим это как вторую часть ответа.Мы начинаем с группировки allRecords
по свойству type
-
const allRecords =
[ { type: 'fruit', name: 'apple' }
, { type: 'vegetable', name: 'spinach' }
, { type: 'meat', name: 'chicken' }
, { type: 'fruit', name: 'raspberry' } // added this item
]
const m1 =
groupBy(x => x.type, allRecords)
console.log(m1)
// Map
// { 'fruit' =>
// [ { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// ]
// , 'vegetable' =>
// [ { type: 'vegetable', name: 'spinach' }
// ]
// , 'meat' =>
// [ { type: 'meat', name: 'chicken' }
// ]
// }
Далее мы группируем newRecords
таким же образом -
const newRecords =
[ { type: 'meat', name: 'pork' }
, { type: 'fruit', name: 'pear' }
, { type: 'vegetable', name: 'celery' }
, { type: 'dairy', name: 'milk' } // added this item
]
const m2 =
groupBy(x => x.type, newRecords)
console.log(m2)
// Map
// { 'meat' =>
// [ { type: 'meat', name: 'pork' }
// ]
// , 'fruit' =>
// [ { type: 'fruit', name: 'pear' }
// ]
// , 'vegetable' =>
// [ { type: 'vegetable', name: 'celery' }
// ]
// , 'dairy' =>
// [ { type: 'dairy', name: 'milk' }
// ]
// }
Прежде чем мы продолжим, давайте определимсяуниверсальная функция groupBy
-
const groupBy = (f, a = []) =>
a.reduce
( (map, v) => upsert(map, [ f (v), v ])
, new Map
)
// helper
const upsert = (map, [ k, v ]) =>
map.has(k)
? map.set(k, map.get(k).concat(v))
: map.set(k, [].concat(v))
Далее нам нужен способ объединения двух карт m1
и m2
-
const m3 =
mergeMap(m1, m2)
console.log(m3)
// Map
// { 'fruit' =>
// [ { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'fruit', name: 'pear' }
// ]
// , 'vegetable' =>
// [ { type: 'vegetable', name: 'spinach' }
// , { type: 'vegetable', name: 'celery' }
// ]
// , 'meat' =>
// [ { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// ]
// , 'dairy' =>
// [ { type: 'dairy', name: 'milk' }
// ]
// }
Мы можем легко определить mergeMap
дляподдержка объединения любого количества карт -
const mergeMap = (...maps) =>
maps.reduce(mergeMap1, new Map)
// helper
const mergeMap1 = (m1, m2) =>
Array.from(m2.entries()).reduce(upsert, m1)
Как мы видим, на карте красиво сгруппированы элементы.Давайте теперь соберем все значения -
const unsorted =
[].concat(...m3.values())
console.log(unsorted)
// [ { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'vegetable', name: 'spinach' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// , { type: 'dairy', name: 'milk' }
// ]
Сортировка
Этот раздел ответа не для сердца, но я настоятельно рекомендую вампридерживаться этого.Мы используем функциональный подход к написанию функций сравнения, но есть некоторые компромиссы с используемыми методами.Здесь мы используем много простых функций, которые легко написать, протестировать и поддерживать.В результате функции становятся более гибкими и могут быть повторно использованы в других областях вашей программы.Подробнее об этом подходе, а также о том, что происходит, когда эти методы не используются, см. этот недавний ответ по теме.
Хорошо, поэтому мы видимСписок в настоящее время заказывают фрукты , овощи , мясо , а затем молочные продукты .Это связано с тем, в каком порядке они были сгруппированы в исходных картах.Что, если бы вы хотели, чтобы они были заказаны другим способом?
unsorted.sort(orderByTypes("vegetable", "meat", "fruit"))
// [ { type: 'vegetable', name: 'spinach' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// , { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'dairy', name: 'milk' }
// ]
Хорошо, а что, если бы мы хотели, чтобы они заказали вместо name
?
unsorted.sort(orderByName)
// [ { type: 'fruit', name: 'apple' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'meat', name: 'chicken' }
// , { type: 'dairy', name: 'milk' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'meat', name: 'pork' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'vegetable', name: 'spinach' }
// ]
Было бы возможно *Сначала 1061 *, а затем выполнить вторичную сортировку, используя orderByName
?
unsorted.sort
( mergeComparator
( orderByTypes("meat", "fruit", "dairy") // primary sort
, orderByName // secondary sort (tie breaker)
)
)
// [ { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// , { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'dairy', name: 'milk' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'vegetable', name: 'spinach' }
// ]
Мы видим, что результат - первый порядок по типам, мясо , фрукты , и молочные продукты сначала.И мы также видим вторичную сортировку по name
.Мясо курица и свинина в порядке возрастания, как и фрукты яблоко , груша и малина .Обратите внимание, что хотя "vegetables"
не использовалось в orderByTypes
, вторичная сортировка по-прежнему применяется, поэтому сельдерей и шпинат в порядке.
Как вы можетевидите, мы можем определить гибкие функции компаратора, такие как orderByTypes
и orderByName
, и объединить их, используя mergeComparator
, чтобы добиться более сложного и сложного поведения.Начнем с более простого из двух: orderByName
-
const orderByName =
contramap
( ascending // transform base comparator
, x => x.name // by first getting object's name property
)
// base comparator
const ascending = (a, b) =>
a > b
? 1
: a < b
? -1
: 0
// functional utility
const contramap = (f, g) =>
(a, b) =>
f(g(a), g(b))
Компаратор orderByTypes
немного сложнее -
const orderByTypes = (...types) =>
contramap
( ascending // transform base comparator
, pipe // using a function sequence
( x => x.type // first get the item's type property
, x => matchIndex(types, x) // then get the index of the matched type
, x => x === -1 ? Infinity : x // then if it doesn't match, put it at the end
)
)
// helper
const matchIndex = (values = [], query) =>
values.findIndex(v => v === query)
// functional utility
const identity = x =>
x
// functional utility
const pipe = (f = identity, ...more) =>
more.reduce(pipe1, f)
// pipe helper
const pipe1 = (f, g) =>
x => g(f(x))
Мы определили два (2) отдельные компараторы orderByName
и orderByTypes
, и последнее, что нам нужно сделать, это определить, как их объединить -
const mergeComparator = (c = ascending, ...more) =>
more.reduce(mergeComparator1, c)
// helper 1
const mergeComparator1 = (c1, c2) =>
(a, b) =>
mergeComparator2(c1(a, b), c2(a, b))
// helper 2
const mergeComparator2 = (a, b) =>
a === 0 ? b : a
Собрать все вместе
Хорошо, давайте посмотрим, сможем ли мы положить на него лук -
const allRecords =
[ { type: 'fruit', name: 'apple' }
, { type: 'vegetable', name: 'spinach' }
, { type: 'meat', name: 'chicken' }
, { type: 'fruit', name: 'raspberry' }
]
const newRecords =
[ { type: 'meat', name: 'pork' }
, { type: 'fruit', name: 'pear' }
, { type: 'vegetable', name: 'celery' }
, { type: 'dairy', name: 'milk' }
]
// efficient grouping, can support any number of maps
const grouped =
mergeMap
( groupBy(x => x.type, allRecords)
, groupBy(x => x.type, newRecords)
)
const unsorted =
[].concat(...grouped.values())
// efficient sorting; can support any number of comparators
const sorted =
unsorted.sort
( mergeComparator
( orderByTypes("meat", "fruit", "dairy")
, orderByName
)
)
Вывод
console.log(sorted)
// [ { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// , { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'dairy', name: 'milk' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'vegetable', name: 'spinach' }
// ]
Разверните фрагмент ниже, чтобы проверить результаты в своем собственном браузере -
// ---------------------------------------------------
// STEP 1
const upsert = (map, [ k, v ]) =>
map.has(k)
? map.set(k, map.get(k).concat(v))
: map.set(k, [].concat(v))
const groupBy = (f, a = []) =>
a.reduce
( (map, v) =>
upsert(map, [ f (v), v ])
, new Map
)
const allRecords =
[ { type: 'fruit', name: 'apple' }
, { type: 'vegetable', name: 'spinach' }
, { type: 'meat', name: 'chicken' }
, { type: 'fruit', name: 'raspberry' }
]
const newRecords =
[ { type: 'meat', name: 'pork' }
, { type: 'fruit', name: 'pear' }
, { type: 'vegetable', name: 'celery' }
, { type: 'dairy', name: 'milk' }
]
const m1 =
groupBy(x => x.type, allRecords)
console.log("first grouping\n", m1)
// Map
// { 'fruit' =>
// [ { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// ]
// , 'vegetable' =>
// [ { type: 'vegetable', name: 'spinach' }
// ]
// , 'meat' =>
// [ { type: 'meat', name: 'chicken' }
// ]
// }
const m2 =
groupBy(x => x.type, newRecords)
console.log("second grouping\n", m2)
// Map
// { 'meat' =>
// [ { type: 'meat', name: 'pork' }
// ]
// , 'fruit' =>
// [ { type: 'fruit', name: 'pear' }
// ]
// , 'vegetable' =>
// [ { type: 'vegetable', name: 'celery' }
// ]
// , 'dairy' =>
// [ { type: 'dairy', name: 'milk' }
// ]
// }
// ---------------------------------------------------
// STEP 2
const mergeMap1 = (m1, m2) =>
Array.from(m2.entries()).reduce(upsert, m1)
const mergeMap = (...maps) =>
maps.reduce(mergeMap1, new Map)
const m3 =
mergeMap(m1, m2)
console.log("merged grouping\n", m3)
// Map
// { 'fruit' =>
// [ { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'fruit', name: 'pear' }
// ]
// , 'vegetable' =>
// [ { type: 'vegetable', name: 'spinach' }
// , { type: 'vegetable', name: 'celery' }
// ]
// , 'meat' =>
// [ { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// ]
// , 'dairy' =>
// [ { type: 'dairy', name: 'milk' }
// ]
// }
const unsorted =
[].concat(...m3.values())
console.log("unsorted\n", unsorted)
// [ { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'vegetable', name: 'spinach' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// , { type: 'dairy', name: 'milk' }
// ]
// ---------------------------------------------------
// STEP 3
const ascending = (a, b) =>
a > b
? 1
: a < b
? -1
: 0
const contramap = (f, g) =>
(a, b) =>
f(g(a), g(b))
const orderByName =
contramap(ascending, x => x.name)
const sorted1 =
unsorted.sort(orderByName)
console.log("sorted by name only\n", sorted1)
// [ { type: 'fruit', name: 'apple' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'meat', name: 'chicken' }
// , { type: 'dairy', name: 'milk' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'meat', name: 'pork' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'vegetable', name: 'spinach' }
// ]
// ---------------------------------------------------
// STEP 4
const identity = x =>
x
const pipe1 = (f, g) =>
x => g(f(x))
const pipe = (f = identity, ...more) =>
more.reduce(pipe1, f)
const matchIndex = (values = [], query) =>
values.findIndex(v => v === query)
const orderByTypes = (...types) =>
contramap
( ascending
, pipe
( x => x.type
, x => matchIndex(types, x)
, x => x === -1 ? Infinity : x
)
)
const sorted2 =
unsorted.sort(orderByTypes("vegetable", "meat", "fruit"))
console.log("sorted by types\n", sorted2)
// [ { type: 'vegetable', name: 'spinach' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// , { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'dairy', name: 'milk' }
// ]
// ---------------------------------------------------
// STEP 5
const mergeComparator = (c = ascending, ...more) =>
more.reduce(mergeComparator1, c)
const mergeComparator1 = (c1, c2) =>
(a, b) =>
mergeComparator2(c1(a, b), c2(a, b))
const mergeComparator2 = (a, b) =>
a === 0 ? b : a
const sorted3 =
unsorted.sort
( mergeComparator
( orderByTypes("meat", "fruit", "dairy")
, orderByName
)
)
console.log("sorted by types, then name\n", sorted3)
// [ { type: 'meat', name: 'chicken' }
// , { type: 'meat', name: 'pork' }
// , { type: 'fruit', name: 'apple' }
// , { type: 'fruit', name: 'pear' }
// , { type: 'fruit', name: 'raspberry' }
// , { type: 'dairy', name: 'milk' }
// , { type: 'vegetable', name: 'celery' }
// , { type: 'vegetable', name: 'spinach' }
// ]
Обратите внимание, что вам нужно открыть консоль разработчика вашего браузера, если вы хотите просмотреть содержимое карты