1

I have an array of objects

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

I want to insert objects from another array such that the elements are placed next to elements of the same type.

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

So that a call like this:

allRecords.sortAndInsert(newRecords)

returns something like this:

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

In my case, I can't compare "types" to determine where it should go in the array alphabetically or by length ((vegetables come before meat, but after fruit) . In addition, there's no ID attribute that could numerically position things. I just want to group things by the same type.

I've found that I can insert into the correct index by getting the index using the length of the arrays:

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

However, it is not very readable or obvious what the function is doing. In addition, it assumes that the new records have one of each type.

I would like a function that, instead, looks at the properties and groups them together. Ideally, it should also be able to sort the groups in some order (such as, specifying that the 'fruit' group goes first, the 'vegetable' group goes next, then the 'meat' group.

7
  • Are we assuming that the allRecords is already "sorted" by the type? Commented May 15, 2019 at 17:40
  • Assume that it isn't. In my code, I actually start with an empty array that gets populated with the splicing code at the end of the question. Commented May 15, 2019 at 17:49
  • Why not use map of arrays of objects? Type would be key of the map. Commented May 15, 2019 at 18:04
  • @TonyStark That sounds like a good idea. I'm trying to wrap my head around how that should be written. If you know, feel free to leave an answer. Commented May 15, 2019 at 18:05
  • if allRecords and newRecords are not guaranteed to be sorted, I would simply concat the two arrays and sort thereafter. Either way, it sounds like you need a custom sort comparator to group things in an arbitrary order - ie, "fruit first, then vegetables, then meats, then dairy, then carbohydrates ...". So you're either using the comparator to sort allRecords and newRecords before the merge, or you merge first and sort after. You have two distinct needs, and mashing them into one function is a mistake. Commented May 15, 2019 at 19:38

5 Answers 5

2

I'd totally use maps for that. An example would be as follows.

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

Sign up to request clarification or add additional context in comments.

11 Comments

that's not a real Map object though.
@user633183 Should be one now.
myMap['fruit'] = fruitObject defines a property fruit on the myMap object, however this is not how Map is intended to be used. The add method should be used, ie myMap.add('fruit', fruitObject), to add a key/value pair to myMap.
@user633183 Could you link the docs of the add method? Is this an alias for set?
@TonyStark great work on the updated version. The program output is easy on the eyes too.
|
2

Grouping

There's a lot to cover here, so I'm going to move somewhat quickly. If you get stuck at any part, please leave a comment and I'll do my best to expand on any problematic areas.

First, there's no guarantee that allRecords or newRecords will be sorted before they are combined. Grouping like items efficiently can be handled easily using a Map. When we want to print out the items in a desired order however, the Map's values will need to be sorted. We'll handle that as the second part of this answer. We start by grouping allRecords by the type property -

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

Next, we group newRecords the same way -

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

Before we go on, let's define generic function 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))

Next we need a way to combine the two maps m1 and 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' }
//       ]
//   }

We can define mergeMap easily to support merging any number of maps together -

const mergeMap = (...maps) =>
  maps.reduce(mergeMap1, new Map)

// helper
const mergeMap1 = (m1, m2) =>
  Array.from(m2.entries()).reduce(upsert, m1)

As we can see, the Map has nicely grouped the items together. Let's collect all the values now -

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

Sorting

This section of the answer is not for the feint of heart, but I strongly encourage you to stick with it. We take a functional approach to writing comparison functions, but there are trade-offs with the techniques used. Here, we use many simple functions that are easy to write, test, and maintain. As a result, the functions are more flexible and can be reused in other areas of your program. For more reasoning behind this approach as well as what happens when these techniques are not used, see this recent answer on the topic.

Ok, so we see the list is currently ordered by fruit, vegetable, meat, and then dairy. This is due to the order in which they were grouped in the original Maps. What if you wanted them ordered in a different way?

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

Ok, and what if we wanted them ordered by name instead?

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

Would it be possible to orderByTypes first and then do a secondary sorting using 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' }
// ]

We see the result is first order by types, meat, fruit, and dairy first. And we also see secondary sorting by name. Meats chicken and pork are in ascending order as are fruits apple, pear and raspberry. Note, even though "vegetables" wasn't used in orderByTypes, the secondary sort still applies so celery and spinach are in order.

As you can see, we can define flexible comparator functions like orderByTypes and orderByName and the combine them using mergeComparator to achieve a more intricate and complex behavior. We'll start with the simpler of the two, 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))

The orderByTypes comparator is a little more involved -

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

We've defined two (2) separate comparators orderByName and orderByTypes and the last thing we have to do is determine how to combine them -

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

Putting it all together

Okay, let's see if we can put a bow on it -

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

Output

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

Expand the snippet below to verify the results in your own browser -

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

Note, you will need to open your browser's developer console if you wish to view the Map contents

Comments

1

Assuming that allRecords is already ordered by type such that the values with any particular type are located in one contiguous segment of the array (or that type doesn't yet exist at all in the array), then the following will work very similarly to 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!

If you don't want to modify allRecords, you can call it like this instead:

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

Comments

0

This should do the job:

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

Comments

0

Not sure if it's the best solution performance-wise but here it is:

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.