43

So I have an array of objects like that:

var arr = [
  {uid: 1, name: "bla", description: "cucu"},
  {uid: 2, name: "smth else", description: "cucarecu"},
]

uid is unique id of the object in this array. I'm searching for the elegant way to modify the object if we have the object with the given uid, or add a new element, if the presented uid doesn't exist in the array. I imagine the function to be behave like that in js console:

> addOrReplace(arr, {uid: 1, name: 'changed name', description: "changed description"})
> arr
[
  {uid: 1, name: "bla", description: "cucu"},
  {uid: 2, name: "smth else", description: "cucarecu"},
]
> addOrReplace(arr, {uid: 3, name: 'new element name name', description: "cocoroco"})
> arr
[
  {uid: 1, name: "bla", description: "cucu"},
  {uid: 2, name: "smth else", description: "cucarecu"},
  {uid: 3, name: 'new element name name', description: "cocoroco"}
]

My current way doesn't seem to be very elegant and functional:

function addOrReplace (arr, object) {
  var index = _.findIndex(arr, {'uid' : object.uid});
  if (-1 === index) {
    arr.push(object);
  } else {
    arr[index] = object;
  }
} 

I'm using lodash, so I was thinking of something like modified _.union with custom equality check.

ganqqwerty
  • 1,894
  • 2
  • 23
  • 36

9 Answers9

65

In your first approach, no need for Lodash thanks to findIndex():

function upsert(array, element) { // (1)
  const i = array.findIndex(_element => _element.id === element.id);
  if (i > -1) array[i] = element; // (2)
  else array.push(element);
}

Example:

const array = [
  {id: 0, name: 'Apple', description: 'fruit'},
  {id: 1, name: 'Banana', description: 'fruit'},
  {id: 2, name: 'Tomato', description: 'vegetable'}
];

upsert(array, {id: 2, name: 'Tomato', description: 'fruit'})
console.log(array);
/* =>
[
  {id: 0, name: 'Apple', description: 'fruit'},
  {id: 1, name: 'Banana', description: 'fruit'},
  {id: 2, name: 'Tomato', description: 'fruit'}
]
*/

upsert(array, {id: 3, name: 'Cucumber', description: 'vegetable'})
console.log(array);
/* =>
[
  {id: 0, name: 'Apple', description: 'fruit'},
  {id: 1, name: 'Banana', description: 'fruit'},
  {id: 2, name: 'Tomato', description: 'fruit'},
  {id: 3, name: 'Cucumber', description: 'vegetable'}
]
*/

(1) other possible names: addOrReplace(), addOrUpdate(), appendOrUpdate(), insertOrUpdate()...

(2) can also be done with array.splice(i, 1, element)

Note that this approach is "mutable" (vs "immutable"): it means instead of returning a new array (without touching the original array), it modifies directly the original array.

tanguy_k
  • 11,307
  • 6
  • 54
  • 58
24

You can use an object instead of an array:

var hash = {
  '1': {uid: 1, name: "bla", description: "cucu"},
  '2': {uid: 2, name: "smth else", description: "cucarecu"}
};

The keys are the uids. Now your function addOrReplace is simple like this:

function addOrReplace(hash, object) {
    hash[object.uid] = object;
}

UPDATE

It's also possible to use an object as an index in addition to the array.
This way you've got fast lookups and also a working array:

var arr = [],
    arrIndex = {};

addOrReplace({uid: 1, name: "bla", description: "cucu"});
addOrReplace({uid: 2, name: "smth else", description: "cucarecu"});
addOrReplace({uid: 1, name: "bli", description: "cici"});

function addOrReplace(object) {
    var index = arrIndex[object.uid];
    if(index === undefined) {
        index = arr.length;
        arrIndex[object.uid] = index;
    }
    arr[index] = object;
}

Take a look at the jsfiddle-demo (an object-oriented solution you'll find here)

friedi
  • 4,350
  • 1
  • 13
  • 19
  • yeah, I like this structure too, but some libraries I use doesn't support it. Like [Angular typeahead](https://github.com/angular-ui/bootstrap/tree/master/src/typeahead), which can traverse only arrays. – ganqqwerty Sep 10 '14 at 12:47
  • 1
    You can use this structure as a index only. This way you've got fast lookups and a additional array for things like a array is needed. – friedi Sep 10 '14 at 12:50
  • upvoted! there are 2 problems, i need the keys in sorted order according to timestamp and i need to be able to access all items in an array manner to compute statistical measures – PirateApp Jan 07 '19 at 06:14
19

If you do not mind about the order of the items in the end then more neat functional es6 approach would be as following:

function addOrReplace(arr, newObj){ 
 return [...arr.filter((obj) => obj.uid !== newObj.uid), {...newObj}];
}

// or shorter one line version 

const addOrReplace = (arr, newObj) => [...arr.filter((o) => o.uid !== newObj.uid), {...newObj}];

If item exist it will be excluded and then new item will be added at the end, basically it is replace, and if item is not found new object will be added at the end.

In this way you would have immutable list. Only thing to know is that you would need to do some kind of sort in order to keep list order if you are for instance rendering list on the screen.

Hopefully, this will be handy to someone.

IngoP
  • 465
  • 5
  • 11
  • In my case I had to update array on array. I like your example the most so based on it here is oneliner solution if you have to merge two arrays together: `return arr1.map(ar => [...arr2.filter(arr => ar.id !== arr.id), ...[{...ar}] ]).flat()` – michal-mad Dec 27 '22 at 12:56
9

I personally do not like solutions that modify the original array/object, so this is what I did:

function addOrReplaceBy(arr = [], predicate, getItem) {
  const index = _.findIndex(arr, predicate);
  return index === -1
    ? [...arr, getItem()]
    : [
      ...arr.slice(0, index),
      getItem(arr[index]),
      ...arr.slice(index + 1)
    ];
}

And you would use it like:

var stuff = [
  { id: 1 },
  { id: 2 },
  { id: 3 },
  { id: 4 },
];

var foo = { id: 2, foo: "bar" };
stuff = addOrReplaceBy(
  stuff,
  { id: foo.id },
  (elem) => ({
    ...elem,
    ...foo
  })
);

What I decided to do was to make it more flexible:

  1. By using lodash -> _.findIndex(), the predicate can be multiple things
  2. By passing a callback getItem(), you can decide whether to fully replace the item or do some modifications, as I did in my example.

Note: this solution contains some ES6 features such as destructuring, arrow functions, among others.


There is a second approach to this. We can use JavaScript Map object which "holds key-value pairs and remembers the original insertion order of the keys" plus "any value (both objects and primitive values) may be used as either a key or a value."

let myMap = new Map(
  ['1', { id: '1', first: true }] // key-value entry
  ['2', { id: '2', second: true }]
)

myMap = new Map([
  ...myMap, 
  ['1', { id: '1', first: true, other: '...' }]
  ['3', { id: '3', third: true }]
])

myMap will have the following entries in order:

['1', { id: '1', first: true, other: '...' }]
['2', { id: '2', second: true }]
['3', { id: '3', third: true }]

We can use this characteristic of Maps to add or replace other elements:

function addOrReplaceBy(array, value, key = "id") {
  return Array.from(
    new Map([
      ...array.map(item => [ item[key], item ]),
      [value[key], value]
    ]).values()
  )
}
pgarciacamou
  • 1,602
  • 22
  • 40
4

Maybe

_.mixin({
    mergeById: function mergeById(arr, obj, idProp) {
        var index = _.findIndex(arr, function (elem) {
            // double check, since undefined === undefined
            return typeof elem[idProp] !== "undefined" && elem[idProp] === obj[idProp];
        });

        if (index > -1) {
            arr[index] = obj; 
        } else {
            arr.push(obj);
        }

        return arr;
    }
});

and

var elem = {uid: 3, name: 'new element name name', description: "cocoroco"};

_.mergeById(arr, elem, "uid");
Tomalak
  • 332,285
  • 67
  • 532
  • 628
3

Old post, but why not use the filter function?

// If you find the index of an existing uid, save its index then delete it
//      --- after the filter add the new object.
function addOrReplace( argh, obj ) {
  var index = -1;
  argh.filter((el, pos) => {
    if( el.uid == obj.uid )
      delete argh[index = pos];
    return true;
  });

  // put in place, or append to list
  if( index == -1 ) 
    argh.push(obj);
  else 
    argh[index] = obj;
}

Here is a jsfiddle showing how it works.

Chris Sullivan
  • 1,011
  • 9
  • 11
3

really complicated solutions :D Here is a one liner:

const newArray = array.filter(obj => obj.id !== newObj.id).concat(newObj)
MrP
  • 74
  • 2
  • This replaces the object with the given id if it already exists, instead of updating it. – Julian Feb 23 '22 at 14:35
  • thats true, but obviously the new object has the updated values already. But yes if you want to keep the order of the items in the array, he has to go with findindex – MrP Feb 24 '22 at 15:25
  • As I understand the question, the properties of the new object need to be merged with the properties of the old object. Some of the properties that need to be retained might not be present in the new object. – Julian Feb 25 '22 at 14:18
1

What about having the indexes of the array same as the uid?, like:

arr = [];
arr[1] = {uid: 1, name: "bla", description: "cucu"};
arr[2] = {uid: 2, name: "smth else", description: "cucarecu"};

that way you could just simply use

arr[affectedId] = changedObject;
Drecker
  • 1,215
  • 10
  • 24
  • will work well if I have the guarantee that uuid is always int. I think sometimes it's not the case. But even if I've had this guarantee, I feel uncomfortable to have arrays with missing elements like that: `arr[2134], arr[2135]`, when arr[0] doesn't exist. – ganqqwerty Sep 10 '14 at 12:11
  • @ganqqwerty so insted of classic array use associative one (object), this way you can use non-integer indexes and have no "holes" in your array – Drecker Sep 10 '14 at 12:25
0

Backbone.Collection provides exactly this functionality. Save yourself the effort when you can!

var UidModel = Backbone.Model.extend({
    idAttribute: 'uid'
});

var data = new Backbone.Collection([
    {uid: 1, name: "bla", description: "cucu"},
    {uid: 2, name: "smth else", description: "cucarecu"}
], {
    model: UidModel
});

data.add({uid: 1, name: 'changed name', description: "changed description"}, {merge: true});

data.add({uid: 3, name: 'new element name name', description: "cocoroco"});

console.log(data.toJSON());
Julian
  • 4,176
  • 19
  • 40