DanielR
DanielR

Reputation: 596

Mapping over an array of objects without affecting all object fields

I have an array of objects which I need to parse in some way. The objects have a set of common fields and some optional fields that aren't present in all objects. For the sake of the example, a,b,c are common fields and d,e,f are optional. I want to perform some action on some of the fields, e.g double the value of a and capitalize d, while leaving the rest as they were. If an object was missing one or more of the optional fields, it should remain so in the result.

The catch is that I want to do it in a purely functional way, without declaring an empty array and pushing into it.

Example input:

const input = [
      {
        a: 3,
        b: 'test',
        c: 34,
        d: 'example'
      },
      {
        a: 6,
        b: 'another',
        c: 0,
        e: true,
        f: () => {}
      }
    ];

Expected result:

[
  {
    a: 6,
    b: 'test',
    c: 34,
    d: 'EXAMPLE'
  },
  {
    a: 12,
    b: 'another',
    c: 0,
    e: true,
    f: () => {}
  }
]

What I tried so far was using map like so:

const result = input.map(x => ({
  a: 2 * x.a,
  d: x.d.toUppercase()
});

Or like so:

const result = input.map(x => ({
  a: 2 * x.a,
  b,
  c,
  d: x.d.toUppercase(),
  e,
  f
});

But this results in objects that either contain only the fields which were manipulated, or all of them, regardless if they existed in the original object.

Any suggestions on how to accomplish that?

Upvotes: 1

Views: 1593

Answers (6)

user3297291
user3297291

Reputation: 23372

It might be worth googling for lenses. Many "functional" javascript libraries implement them to easily chain the kinds of data modifications you describe.

Under the hood, they might use the same Object.assign approach you've already found, but they allow for readable, reusable and easily chainable code.

A quick & dirty implementation to show the change in syntax (es6):

const propLens = prop => ({
  get: obj => obj[prop],
  // Return a new object with the property set to
  // a value
  set: (val, obj) => Object.assign({}, obj, {
    [prop]: val
  }),
  // Return a new object with the property mapped
  // by a function
  over: (fn, obj) => Object.assign({}, obj, {
    [prop]: fn(obj[prop])  
  })
});

// Helpers to create specific set/over functions
const set = (lens, val) => obj => lens.set(val, obj);
const over = (lens, fn) => obj => lens.over(fn, obj);

// Some utils for the example
const { pipe, double, toUpper } = utils();
  
// Now, you can define your modification by a set
// of lens + set/over combinations
const convertData = pipe(
  set(  propLens("d"),  "added" ),
  over( propLens("a"),  double  ),
  over( propLens("b"),  toUpper )
);

// Example data
const item = {a: 5, b: "hello", c: "untouched"};

// Converted copy
const convertedItem = convertData(item);

console.log("Converted:", convertedItem);
console.log("item !== convertedItem ->", item !== convertedItem);

// Utils
function utils() {
  return {
    pipe: (...fns) => x =>
      fns.reduce((acc, f) => f(acc), x),
    double: x => x * 2,
    toUpper: x => x.toUpperCase()
  };
}
  

(note that this example will run a bit slower than doing everything in one function, but you'll probably only notice for large amounts of data)

If you like the lenses-approach, I'd advice to use a well maintained and tested library (like Ramda). You'll even get more awesome features like index lenses and lens paths, and things are curried by default.

Upvotes: 0

castletheperson
castletheperson

Reputation: 33466

If you want your objects to be immutable, you will need to make a copy of the objects before making the needed changes. Object.assign allows you to do this in one expression:

const result = input.map(x => Object.assign({}, x,
  { a: 2 * x.a },
  ('d' in x) && { d: x.d.toUpperCase() }
));

const input = [{
    a: 3,
    b: 'test',
    c: 34,
    d: 'example'
  }, {
    a: 6,
    b: 'another',
    c: 0,
    e: true,
    f: () => {}
  }
];

const result = input.map(x => Object.assign({},
  x,
  { a: 2 * x.a },
  ('d' in x) && { d: x.d.toUpperCase() }
));

console.log(result);
.as-console-wrapper { min-height: 100%; }

Upvotes: 2

Ori Drori
Ori Drori

Reputation: 191946

This solution requires Babel's Object rest spread transform plugin because Object Rest/Spread Properties for ECMAScript is still in the proposals stage.

Create a new object. Spread the original object into the new one. If the properties you wish to change exist in the original object, return an object with the the new property value to the spread. If not, return null (spread ignores undefined and null). The new values will override the old.

const input = [{
    a: 3,
    b: 'test',
    c: 34,
    d: 'example'
  },
  {
    a: 6,
    b: 'another',
    c: 0,
    e: true,
    f: () => {}
  }
];

const result = input.map(x => ({
  ...x, // spread all props of x
  ...('a' in x ? { a: 2 * x.a } : null), // override a if exists
  ...('d' in x ? { d: x.d.toUpperCase() } : null) // override d if exists
}));

console.log(result);

Upvotes: 1

Anurag Singh Bisht
Anurag Singh Bisht

Reputation: 2753

Inside the map function, you can first add the conditions to manipulate the input and then you need to return the modified object.

const input = [
      {
        a: 3,
        b: 'test',
        c: 34,
        d: 'example'
      },
      {
        a: 6,
        b: 'another',
        c: 0,
        e: true,
        f: () => {}
      }
    ];
    
let output = input.map(o => {
  if(o.a) { o.a = o.a * 2; }
  if(o.d) { o.d = o.d.toUpperCase(); }
  return o;
});

console.log(output)

Output::

[
  {
    "a": 6,
    "b": "test",
    "c": 34,
    "d": "EXAMPLE"
  },
  {
    "a": 12,
    "b": "another",
    "c": 0,
    "e": true,
    "f": () => {}
  }
]

Upvotes: 0

brk
brk

Reputation: 50291

map function be still used.In this case you can check if a key a exist in the object then do the necessary operation only on it's value.

Hope this snippet will be useful

const input = [{
    a: 3,
    b: 'test',
    c: 34,
    d: 'example'
  },
  {
    a: 6,
    b: 'another',
    c: 0,
    e: true,
    f: () => {}
  }
];
input.map(function(elem, index) {
  if (elem.hasOwnProperty('a')) {
    elem.a = elem['a'] * 2;

  }

})
console.log(input)

Upvotes: 0

Christos
Christos

Reputation: 53958

You could use the map method and make a check in each object if the key, whose value you want to change, exists. If so, then yuo can alter correspondingly the value of the key.

const input = [
      {
        a: 3,
        b: 'test',
        c: 34,
        d: 'example'
      },
      {
        a: 6,
        b: 'another',
        c: 0,
        e: true,
        f: () => {}
      }
    ];

const result = input.map(function(obj){
    if(obj.a){
       obj.a = 2 * obj.a;
    }
    if(obj.d){
       obj.d = obj.d.toUpperCase()
    }
    return obj;
});

console.log(result);

Upvotes: 0

Related Questions