JasperZelf
JasperZelf

Reputation: 2844

Elegant reduce function

I am trying to reduce an array of objects (config values in my case). My array looks like this:

const settings = [
  {room: null, key: 'radioEnabled', value: true},
  {room: 24,   key: 'radioEnabled', value: false},
  {room: 24,   key: 'name',         value: 'Jack'},
  {room: 23,   key: 'name',         value: 'Mike'},
  {room: 23,   key: 'radioEnabled', value: false},
  {room: null, key: 'tvEnabled',    value: false},
];

This array is not ordered in any way.
If a room is set to null, this means it is a global setting. Global settings can be overwritten by local settings.

I am trying to write a function to get all settings for a room. For room 24 it should return:

[
  {room: 24,   key: 'radioEnabled', value: false},
  {room: 24,   key: 'name',         value: 'Jack'},
  {room: null, key: 'tvEnabled',    value: false},
]

The order in which the values are returned is not important to me. I have been able to achieve this in more than one way, but the solutions just don't seem that elegant/readable to me. Can anybody suggest a more elegant idea?

My solutions are below and on jsfiddle.

const settings = [
  {room: null, key: 'radioEnabled', value: true},
  {room: 24,   key: 'radioEnabled', value: false},
  {room: 24,   key: 'name',         value: 'Jack'},
  {room: 23,   key: 'name',         value: 'Mike'},
  {room: 23,   key: 'radioEnabled', value: false},
  {room: null, key: 'tvEnabled',    value: false},
];

const getAll_1 = function(room){
	return settings.reduce( (a, b) => {
  	// remove all other rooms
  	if(b.room && b.room!== room){
    	return a;
  	}

		// see if the setting is already added
  	let found = a.find( (setting) => {
    	return setting.key === b.key;
  	})

		// we already have a local value in our return array, don't add/replace anything
  	if( found && found.room === room) {
    	return a;
  	} 
  
  	// we have a value, but it's not local. Replace the current value with the new one
	  if( found ) {
      const index = a.findIndex( (setting) => {
    		return setting.key === b.key;
  		})
  	  a[index] = b;
    	return a;
  	}
  
  	// we don't have this setting at all. add it. 
	  return a.concat(b);
	}, []);
}

const getAll_2 = function(room){
	return settings
    // first filter out all other room settings, only keep current room and global values
  	.filter( (setting) => {
		  return setting.room === null || setting.room === room;
		})
    // than sort em so all local (high prio) values are up top
		.sort( (a, b) => {
		  return (a.room > b.room) ? -1 : ( a.room < b.room ) ? 1 : 0;
		})
    // reduce the array, adding only global values if they are not already added as local value
		.reduce( (a, b) => {
		  const found = a.find( (setting) => {
  		  return setting.key === b.key;
  		})
  		if (found){
  			return a;
  		}
  		return a.concat(b);
		}, [])
}

console.log(`Stack Overflow does not support console.table. Open your console for better representation`);

console.log(`getAll_1 response:`);
console.table(getAll_1(24));
console.log(`getAll_2 response:`);
console.table(getAll_2(24));
Check your console

Upvotes: 2

Views: 113

Answers (6)

Scott Sauyet
Scott Sauyet

Reputation: 50787

Another approach, which might or might not help with your fundamental requirements, is to convert this to a more useful format:

const roomSettings = settings => {
  const globals = settings.filter(s => s.room == null)
    .reduce((all, {key, value}) => ({...all, [key]: value}), {})
  
  return settings.filter(s => s.room != null)
    .reduce((all, {room, key, value}) => ({
      ...all, 
      [room]: {...(all[room] || globals), [key]: value}
    }), {} )
}

const settings = [{"key": "radioEnabled", "room": null, "value": true}, {"key": "radioEnabled", "room": 24, "value": false}, {"key": "name", "room": 24, "value": "Jack"}, {"key": "name", "room": 23, "value": "Mike"}, {"key": "radioEnabled", "room": 23, "value": false}, {"key": "tvEnabled", "room": null, "value": false}, {"key": "name", "room": 25, "value": "Beth"}]

console.log(roomSettings(settings))

Note that this returns something like the following:

{
  23: {
    radioEnabled: false,
    tvEnabled: false,
    name: "Mike"
  },
  24: {
    radioEnabled: false,
    tvEnabled: false,
    name: "Jack"
  },
  25: {
    radioEnabled: true,
    tvEnabled: false,
    name: "Beth"
  } 
}

(I added 'Beth' to have at least one that wasn't false/false.)

This format looks more useful, but it certainly might not be for you.

Upvotes: 2

Shidersz
Shidersz

Reputation: 17190

You could first get the specific settings, and then add the general ones if there isn't already a specific one for the key:

const settings = [
  {room: null, key: 'radioEnabled', value: true},
  {room: 24,   key: 'radioEnabled', value: false},
  {room: 24,   key: 'name',         value: 'Jack'},
  {room: 23,   key: 'name',         value: 'Mike'},
  {room: 23,   key: 'radioEnabled', value: false},
  {room: null, key: 'tvEnabled',    value: false},
];

const generalSettings = settings.filter(x => x.room === null);

const getSettings = (roomID) =>
{
    let keysAdded = new Set();

    // Get specific settings and add keys on the set.

    let res = settings.filter(x => x.room == roomID && keysAdded.add(x.key));
    
    // Add general settings.

    return generalSettings.reduce(
        (acc, curr) => keysAdded.has(curr.key) ? acc : [...acc, curr],
        res
    );
}


console.log(getSettings(23));
console.log(getSettings(24));

Upvotes: 1

Prince Hernandez
Prince Hernandez

Reputation: 3721

basically it is easier to use filter over your array, this filter function wrapped in a higher function to receive the room number as a parameter.

edit: forgot about the reduce to remove the duplicates.

const settings = [{
    room: 24,
    key: 'radioEnabled',
    value: false
  },
  {
    room: null,
    key: 'radioEnabled',
    value: true
  },
  {
    room: 24,
    key: 'name',
    value: 'Jack'
  },
  {
    room: 23,
    key: 'name',
    value: 'Mike'
  },
  {
    room: 23,
    key: 'radioEnabled',
    value: false
  },
  {
    room: null,
    key: 'tvEnabled',
    value: false
  },
];


const getConfigs = (room = null, settings = []) => {
  return settings
    // filter all the options that match our criteria.
    .filter(setting => setting.room === room || setting.room === null)
    // using reduce we will remove the duplicated key entries.
    .reduce((accum, currentVal) => {
      // if the index is -1 it means it is not on the array, so we add it. 
      const index = accum.findIndex(accumSetting => accumSetting.key === currentVal.key)
      if (index === -1) {
        accum.push(currentVal);
      } else { // it means that we have the entry. replace if we have a local one.
        if(currentVal.room === room && accum[index].room === null){
          accum[index] = currentVal;
        }
        
      }
      return accum;
    }, [])
}

const result24 = getConfigs(24, settings);
const result23 = getConfigs(23, settings);

console.log(result24);
console.log(result23);

Upvotes: 1

Nenad Vracar
Nenad Vracar

Reputation: 122027

You can do this with one reduce loop if you store result in object based on key field and then get its values to return an array as a result.

  • Check if room is null or target value
  • Check if key property for that setting doesn't exist or if its room value is null

const settings = [{"room":null,"key":"radioEnabled","value":true},{"room":24,"key":"radioEnabled","value":false},{"room":24,"key":"name","value":"Jack"},{"room":23,"key":"name","value":"Mike"},{"room":23,"key":"radioEnabled","value":false},{"room":null,"key":"tvEnabled","value":false}]

const getAll = room => {
  return Object.values(settings.reduce((r, e) => {
    if (e.room == null || e.room == room) {
      if (!r[e.key] || r[e.key].room == null) r[e.key] = e
    }
    return r;
  }, {}))
}

console.log(getAll(24))

Upvotes: 0

trincot
trincot

Reputation: 350137

I would suggest:

  • Filter first for where room is null
  • Then filter for where room is the desired room number
  • Concatenate both results in that order
  • Add that to a Map to get only one entry per key
  • Output as an array

The fact that the first two filter-results are concatenated in that order will ensure that null room entries get lower priority than non-null entries.

function getAll(room){
    return [...new Map([...settings.filter(a => a.room === null), 
                        ...settings.filter(a => a.room === room)]
               .map(a => [a.key, a])).values()]
}

const settings = [{room: null, key: 'radioEnabled', value: true},{room: 24,   key: 'radioEnabled', value: false},{room: 24,   key: 'name', value: 'Jack'},{room: 23,   key: 'name',         value: 'Mike'},{room: 23,   key: 'radioEnabled', value: false},{room: null, key: 'tvEnabled', value: false}];

console.log(getAll(24));

Upvotes: 1

Mulan
Mulan

Reputation: 135197

Here's a possible alternative -

const globals =
  { radioEnabled: true
  , tvEnabled: false
  }

const settings =
  [ { room: 24, name: 'Jack' }
  , { room: 24, radioEnabled: false }
  , { room: 25, name: 'Mike' }
  , { room: 25, tvEnabled: true }
  ]

const assign = (o1, o2) =>
  ({ ...o1, ...o2 })

const getConfig = (room, settings = []) =>
  settings
    .filter (s => s.room === room)
    .reduce (assign, globals)

console .log
  ( getConfig (24, settings)
    // { radioEnabled: false
    // , tvEnabled: false
    // , room: 24
    // , name: 'Jack'
    // }

  , getConfig (25, settings)
    // { radioEnabled: true
    // , tvEnabled: true
    // , room: 25
    // , name: 'Mike'
    // }
  )

Upvotes: 1

Related Questions