Как вставить объекты в массив рядом с объектами с одинаковым значением свойства - PullRequest
1 голос
/ 15 мая 2019

У меня есть массив объектов

const allRecords = [
  {
    type: 'fruit',
    name: 'apple'
  },
  {
    type: 'vegetable',
    name: 'celery'
  },
  {
    type: 'meat',
    name: 'chicken'
  }
]

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

const  newRecords = [
  {
    type: 'fruit',
    name: 'pear'
  },
  {
    type: 'vegetable',
    name: 'spinach'
  },
  {
    type: 'meat',
    name: 'pork'
  }
]

Так что звонок такой:

allRecords.sortAndInsert(newRecords)

возвращает что-то вроде этого:

[
  {
    type: 'fruit',
    name: 'apple'
  },
  {
    type: 'fruit',
    name: 'pear'
  },
  {
    type: 'vegetable',
    name: 'celery'
  },
  {
    type: 'vegetable',
    name: 'spinach'
  },
  {
    type: 'meat',
    name: 'chicken'
  },
  {
    type: 'meat',
    name: 'pork'
  },

В моем случае я не могу сравнить "типы", чтобы определить, куда он должен идти в массиве по алфавиту или по длине ((овощи идут перед мясом, но после фруктов). Кроме того, нет атрибута ID, который мог бы численно позиционировать вещи. Я просто хочу сгруппировать вещи по одному типу.

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

// This gives the amount of records for each group. 
//In our example, this would be 2 for 'apple' and 'pear', etc
const multiplier = (allRecords.length + newRecords.length) / 
   (newRecords.length);
for (let i = 0; i < newRecords.length; i++){
    // Insert the record at 1 + i + multiplier. 'pear' will go to 1 + 0 * 2 = 1
    allRecords.splice(1 + i * multiplier, 0, newRecords[i]);
  }
return allRecords;

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

Мне нужна функция, которая вместо этого просматривает свойства и группирует их вместе. В идеале, он также должен иметь возможность сортировать группы в некотором порядке (например, указав, что группа «фрукты» идет первой, затем группа «овощи», затем группа «мясо».

Ответы [ 5 ]

2 голосов
/ 16 мая 2019

Группировка

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

Во-первых, нет никакой гарантии, что 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' }
// ]

Обратите внимание, что вам нужно открыть консоль разработчика вашего браузера, если вы хотите просмотреть содержимое карты

2 голосов
/ 15 мая 2019

Я бы полностью использовал карты для этого.Пример будет следующим:

let myMap = new Map();

myMap.set('fruit', [{
  name: 'apple',
  type: 'fruit'
}]);
myMap.set('vegetable', [{
  name: 'celery',
  type: 'vegetable'
}]);
myMap.set('meat', [{
  name: 'chicken',
  type: 'meat'
}]);

const newRecords = [{
  type: 'fruit',
  name: 'pear'
}, {
  type: 'vegetable',
  name: 'spinach'
}, {
  type: 'meat',
  name: 'pork'
}]

newRecords.forEach(function(el) {
  let arr = myMap.get(el.type);
  arr.push(el);
  myMap.set(el.type, arr);
});

for (let [k, v] of myMap) {
  console.log(k);
  console.log(v);
}
1 голос
/ 15 мая 2019

Предполагая, что allRecords уже упорядочен по type, так что значения с любым конкретным type находятся в одном непрерывном сегменте массива (или что type еще не существует в массиве вообще), то следующее будет работать очень похоже на Object.assign():

function spliceBy<T, K extends keyof T> (key: K, target: T[], ...sources: Iterable<T>[]) {
  const groups: Map<T[K], T[]> = new Map()

  for (const source of sources) {
    for (const entry of source) {
      const value = entry[key]
      const oldEntries = groups.get(value)
      const entries = oldEntries || []

      if (!oldEntries) groups.set(value, entries)

      entries.push(entry)
    }
  }

  for (const [value, entries] of groups) {
    // find the end of a group of entries
    let found = false
    const index = target.findIndex(
      entry => entry[key] === value ? (found = true, false) : found
    )

    if (found) target.splice(index, 0, ...entries)
    else target.push(...entries)
  }

  return target
}

const allRecords = [{type:'fruit',name:'apple'},{type:'vegetable',name:'celery'},{type:'meat',name:'chicken'}]
const newRecords = [{type:'fruit',name:'pear'},{type:'vegetable',name:'spinach'},{type:'meat',name:'pork'}]

console.log(spliceBy('type', allRecords, newRecords))

Попробуйте онлайн!

Если вы не хотите изменять allRecords, вы можете назвать его так:

console.log(spliceBy('type', [], allRecords, newRecords))
0 голосов
/ 15 мая 2019

Не уверен, что это лучшее решение с точки зрения производительности, но вот оно:

const allRecords = [
  {
    type: 'fruit',
    name: 'apple'
  },
  {
    type: 'vegetable',
    name: 'celery'
  },
  {
    type: 'meat',
    name: 'chicken'
  }
]

const  newRecords = [
  {
    type: 'fruit',
    name: 'pear'
  },
  {
    type: 'vegetable',
    name: 'spinach'
  },
  {
    type: 'meat',
    name: 'pork'
  }
]

function sortAndInsert(...records){
    let totalRecords = [];
    for(let record of records){
        totalRecords = totalRecords.concat(record);
    }
    totalRecords.sort((rec1, rec2)=>{
        if(rec1.type == rec2.type)
            return 0;
        else if(rec1.type > rec2.type)
            return 1;
        else
            return -1;
    })
    return totalRecords;
}

let completeRecords = sortAndInsert(newRecords, allRecords);
0 голосов
/ 15 мая 2019

Это должно сделать работу:

interface Record {
   type: string;
   name: string;
}

interface TypedRecords {
   [type: string]: records[];
}

private _recordsByType: TypedRecords = {};

sortAndInsert(allRecords: Record[], newRecords: Record[]): Record[] {
   const records: Record[] = [];
   this.insert(allRecords);
   this.insert(newRecords);
   Object.keys(this._recordsByType).forEach(type => {
      this._recordsByType[type].forEach(name => {
         records.push({type, name});
      });
   });

   return records;
}

private insert(records: Record[]) {
   records.forEach(record => {
      if (!this._recordsByType[record.type]) {
         this._recordsByType[record.type] = [];
      }
      this._recordsByType[record.type].push(record.value);
   });
}

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