Сортировать массив по сравнению со значениями другого массива - PullRequest
0 голосов
/ 13 мая 2019

У меня есть два массива:

const tags = ['one', 'two', 'three'];
let posts = [
  { tags: ['four', 'five'] },
  { tags: ['one', 'six'] },
  { tags: ['seven'] },
  { tags: ['nine', 'two'] },
];

Мне нужно отсортировать массив posts следующим образом: элементы с хотя бы одним тегом из массива tags должны находиться в начале массива.Порядок остальных элементов (без соответствующих тегов) не важен.

Ожидаемый результат:

posts = [
  { tags: ['one', 'six'] },
  { tags: ['nine', 'two'] },
  { tags: ['four', 'five'] },
  { tags: ['seven'] },
];

Ответы [ 4 ]

1 голос
/ 13 мая 2019

Вот функциональный подход. Сначала мы начнем с некоторой функции, которая может сказать нам, содержит ли данный post какой-либо из данных tags -

const postHasAnyTags = (tags = []) => (post = {}) =>
  tags .some (t => post.tags .includes (t))

Далее, прежде чем мы сможем подключиться к sort, нам нужен базовый компаратор. Вот ascending -

const ascending = (a, b) =>
  a < b
    ? -1
    : a > b
      ? 1
      : 0

Однако данные нельзя сравнивать напрямую, используя < и >. Числа и строки можно сравнивать, используя > и <, но мы не имеем ни того, ни другого. Наши данные - это пост-объект и массив строк, а наша вспомогательная функция postHasAnyTags возвращает логическое значение. Мы должны map значения до их сравнения. Введите сцену, contramap и compose -

const contramap = (f, g) =>
  (a, b) =>
    f (g (a), g (b))

const compose = (f, g) =>
  x => f (g (x))

Повторно используемые утилиты позволяют нам преобразовывать функции осмысленным образом. contramap принимает двоичную функцию f и унарную функцию g и возвращает новую двоичную функцию, которая преобразует свои входные данные с использованием g перед передачей их в f. compose принимает две унарные функции, f и g, и возвращает новую функцию, которая последовательно вызывает f и g. Теперь у нас есть все, чтобы написать наш сортировщик -

posts .sort
  ( contramap
      ( ascending 
      , compose (Number, postHasAnyTags (tags))
      )
  )

Требуется одно небольшое изменение. Если сообщение содержит теги, которые мы ищем, postHasAnyTags возвращает true, в противном случае false. При преобразовании в число эти значения равны 1 и 0 соответственно. Компаратор ascending выставит 1 -значения после 0 -значений, потому что 1 больше 0. На самом деле нам нужен компаратор descending для возврата данных в нужном вам порядке

const descending = (a, b) =>
  ascending (a, b) * -1

posts .sort
  ( contramap
      ( descending
      , compose (Number, postHasAnyTags (tags))
      )
  )

И это все. Разверните фрагмент ниже, чтобы проверить результаты в своем браузере -

const postHasAnyTags = tags => post =>
  tags .some (t => post.tags .includes (t))

const contramap = (f, g) =>
  (a, b) =>
    f (g (a), g (b))
    
const compose = (f, g) =>
  x => f (g (x))
  
const ascending = (a, b) =>
  a < b
    ? -1
    : a > b
      ? 1
      : 0

const descending = (a, b) =>
  ascending (a, b) * -1

const tags =
  ['one', 'two', 'three']

const posts = 
  [ { tags: ['four', 'five'] }
  , { tags: ['one', 'six'] }
  , { tags: ['seven'] }
  , { tags: ['nine', 'two'] }
  ]

posts .sort
  ( contramap
      ( descending
      , compose (Number, postHasAnyTags (tags))
      )
  )
  
console .log (posts)
  

JavaScript свободно набран и позволяет вам делать такие вещи, как сложение и вычитание логических значений, например true + true // => 2 или true - false // => 1. В целом, неявные преобразования типов могут быть болезненным источником ошибок в вашей программе, поэтому решение, приведенное выше, требует особой тщательности для выполнения явных преобразований типов и обеспечения того, чтобы странное поведение не появлялось у нас.

Так что да, вы можете написать сортировщик как -

posts .sort
  ( (a, b) =>
      Number (tags .some (t => b.tags .includes (t)))
       - Number (tags .some (t => a.tags .includes (t)))
  )

Но это беспорядок дублирования и никоим образом не сообщает о своих намерениях читателю. При написании программ нам нужно управлять сложностью таким образом, чтобы мы могли изолировать и зафиксировать поведение по имени. Определив такие функции, как postHasAnyTags и descending, мы смогли разделить работу на разумные более мелкие части. Эти мелкие детали легче писать, тестировать и обслуживать. И самое главное, эти меньшие части могут быть повторно использованы в других областях вашей программы. Сравните это с гиперкомплексной лямбдой, описанной выше, которую сложно писать, тестировать, поддерживать и абсолютно невозможно использовать в других областях вашей программы.

1 голос
/ 13 мая 2019

Вы можете получить индекс, и если -1 взять Infinity для сортировки этого элемента до конца массива.

const
    getIndex = array => tags.findIndex(v => array.includes(v)),
    tags = ['one', 'two', 'three'];

let posts = [{ tags: ['four', 'five'] }, { tags: ['one', 'six'] }, { tags: ['seven'] }, { tags: ['nine', 'two'] }]

posts.sort((a, b) => ((getIndex(a.tags) + 1) || Infinity) - ((getIndex(b.tags) + 1) || Infinity))

console.log(posts);
0 голосов
/ 13 мая 2019

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

const countMatches = (target) => ({tags}) =>
  tags .reduce ( (n, tag) => target .includes (tag) ? n + 1 : n, 0)

const descendBy = (fn) => (xs) => 
  xs .slice (0)
     .sort( (a, b) => fn (b) - fn (a) )

const sortByTagMatches = (target) => descendBy ( countMatches (target) )

sortByTagMatches (tags) (posts) //=> sorted results

countMatches

Функция countMatches просто подсчитывает количество совпадений между целью (tags) и сообщением tagsимущество.Всегда существует противоречие между использованием наиболее конкретного кода, который выполняет свою работу, и более общей, многократно используемой версией.Но здесь разница между более общей функцией:

const countMatches = (target, name) => (o) =>
  o[name] .reduce ( (n, x) => target .includes (x) ? n + 1 : n, 0)

и конкретной:

const countMatches = (target) => ({tags}) =>
  tags .reduce ( (n, tag) => target .includes (tag) ? n + 1 : n, 0)

настолько мала, а разница в использовании между ними почти такая же незначительная -что если есть любой шанс, что я захочу использовать эту функциональность в другом месте моего приложения, я бы выбрал эту универсальную.

Есть еще одно упрощение, которое мы могли бы включить:

const countMatches = (target, name) => (o) =>
  o[name] .filter ( (x) => target .includes (x) ) .length

Это немного более простой код.Функция, переданная в filter, несомненно, чище, чем функция, переданная в reduce.Но есть компромисс.filter создает новый массив сообщений, использует его только для получения length, а затем выбрасывает его.В горячем коде это может быть проблемой производительности, но в большинстве случаев это просто неправильно, если вызов reduce не , который намного сложнее, чем filter.

descendBy

descendBy просто.Вы передаете ей целочисленную функцию, и она возвращает функцию, которая сама принимает массив значений и возвращает отсортированную версию этого массива.Он не изменяет массив на месте.Если вы действительно этого хотите, вы можете просто удалить вызов slice.Эта функция основана на функции, которую я часто использую, под названием sortBy, только с вычитанием, обратным для снижения.Я вполне мог бы включить оба в проект, хотя, если бы я это сделал, я мог бы переименовать sortBy в ascendBy, чтобы прояснить параллель.

Нетрудно сделать более общую версию этой функции.если нам нравитсяВместо того, чтобы принимать функцию, которая возвращает число, мы могли бы иметь функцию, которая принимает функцию, которая возвращает любое упорядоченное значение, Даты, Строки, Числа и все, что реализует valueOf, по существу, все, что может быть полезнопо сравнению с "<".(Это иногда называют упорядоченным - или Ord - типом).Эта версия может выглядеть так:

const descendBy = (fn) => (xs) => 
  xs .slice (0)
     .sort( (a, b, x = fn (a), y = fn (b) ) => x < y ? 1 : x > y ? -1 : 0 )

Здесь я стою перед вопросом о том, стоит ли предпочитать универсальную версию во время моего первого прохода.Конкретный выше действительно проще.Я бы, вероятно, использовал конкретный, зная, что он совместим с общим, если мне когда-нибудь понадобится его заменить.

Дальнейшее упрощение

descendBy, кажется, выполняет слишком много работы.Он преобразует Ord функцию возврата в компаратор, а затем сортирует список с использованием этого компаратора.Было бы неплохо разбить эти два шага на части, сделав результат descendBy еще более пригодным для повторного использования.Здесь название descend кажется более правильным:

const descend = (fn) => 
  (a, b) => fn(b) - fn(a)

const sortByTagMatches = (target) => (xs) =>
  xs .slice(0) .sort (descend (countMatches (target, 'tags') ) )

Мы переместили нарезку и сортировку в основную функцию, оставив descend довольно простым.И вот тут я думаю, что оставлю это.Код теперь выглядит так:

const countMatches = (target, name) => (o) =>
  o[name] .filter ( (x) => target .includes (x) ) .length

const descend = (fn) => 
  (a, b) => fn(b) - fn(a)

const sortByTagMatches = (target) => (xs) =>
  xs .slice(0) .sort (descend (countMatches (target, 'tags') ) )

const tags = ['one', 'two', 'three']
const posts = [{tags: ['four', 'five']}, {tags: ['one', 'six']}, {tags: ['seven']}, {tags: ['one', 'three', 'five']}, {tags: ['nine', 'two']}]

console .log (
  sortByTagMatches (tags) (posts)
)

(Обратите внимание, что я добавил дополнительный пост с двумя совпадающими тегами для демонстрации дополнительных функций сортировки.)

Почему?

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

0 голосов
/ 13 мая 2019

Вы можете проверить, существует ли какой-либо из тегов каждого объекта в массиве tags, используя some и includes. Затем вычтите значение для 2 сравниваемых объектов.

const tags = ['one', 'two', 'three'];
let posts = [
  { tags: ['four', 'five'] },
  { tags: ['one', 'six'] },
  { tags: ['seven'] },
  { tags: ['nine', 'two'] },
];

posts.sort((a, b) => 
  tags.some(t => b.tags.includes(t)) - tags.some(t => a.tags.includes(t))
)

console.log(posts)

Если a имеет соответствующий тег, а b нет, то compareFunction возвращает -1 (false - true) и a имеет приоритет относительно b.

Для обратной ситуации возвращается 1

Если оба из a и b имеют соответствующий тег или не имеют тега, compareFunction вернет ноль. Итак, они неподвижны относительно друг друга

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...