如何在具有相同属性值的对象旁边的数组中插入对象

时间:2019-05-15 17:29:00

标签: arrays typescript sorting

我有一个对象数组

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 个答案:

答案 0 :(得分:2)

我会完全使用地图。一个例子如下。

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 :(得分:2)

分组

这里有很多要讨论的内容,所以我要快一点。如果您有任何卡住的地方,请发表评论,我会尽力扩大任何有问题的领域。

首先,不能保证allRecordsnewRecords在合并之前将被排序。使用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))

接下来,我们需要一种方法来合并两个地图m1m2-

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' }
// ]

排序

答案的这一部分不是为了heart弱,但我强烈建议您坚持使用。我们采用一种功能性方法来编写比较函数,但是在使用的技术上需要权衡取舍。在这里,我们使用了许多易于编写,测试和维护的简单功能。因此,这些功能更加灵活,可以在程序的其他区域中重复使用。有关此方法背后的更多原因以及使用这些技术时会发生什么的详细信息,请参阅主题上的this recent answer

好,所以我们看到列表当前是按 fruit vegetable meat dairy 。这是由于它们在原始地图中的分组顺序。如果您希望它们以其他方式订购怎么办?

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' }
// ]

是否可以先orderByTypes然后使用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,二级排序仍然适用,因此 celery spinach 是按顺序排列的。

如您所见,我们可以定义诸如orderByTypesorderByName之类的灵活比较器函数,并使用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)个独立的比较器orderByNameorderByTypes,我们要做的最后一件事是确定如何组合它们-

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 :(得分:1)

假设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))

Try it online!

如果您不想修改allRecords,则可以这样称呼它:

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

答案 3 :(得分:0)

这应该可以完成工作:

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);
   });
}

答案 4 :(得分:0)

不确定这是否是最佳解决方案性能,但这是:

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);