0

Although a plethora of posts have been dedicated to the topic, I still couldn't find a satisfying idea how to subset object properties of any depth. More so, I would also like to rename the selected keys on the fly.

What I'm aiming to achieve is a generic function, let's call it select(), that accepts two inputs:

  • an object of data
  • an object in which keys represent the desired new name, and values specify the path to desired properties.

For example, consider the following data:

const earthData = {
  distanceFromSun: 149280000,
  continents: {
    asia: {
      area: 44579000,
      population: 4560667108,
      countries: { japan: { temperature: 62.5 } },
    },
    africa: { area: 30370000, population: 1275920972 },
    europe: { area: 10180000, population: 746419440 },
    america: { area: 42549000, population: 964920000 },
    australia: { area: 7690000, population: 25925600 },
    antarctica: { area: 14200000, population: 5000 },
  },
};

My goal is to call select() this way:

const earthDataSubset = select(earthData, {
  distanceFromSun: ['distanceFromSun'],
  asiaPop: ['continents', 'asia', 'population'],
  americaArea: ['continents', 'america', 'area'],
  japanTemp: ['continents', 'asia', 'countries', 'japan', 'temperature'],
});

where the resulted earthDataSubset is

// earthDataSubset
{
    distanceFromSun: 149280000,
    asiaPop: 4560667108,
    americaArea: 42549000,
    japanTemp: 62.5
}

At this point, one may ask why I don't simply do this:

const earthDataSubsetSimple = {
    distanceFromSun: earthData.distanceFromSun,
    asiaPop: earthData.continents.asia.population,
    americaArea: earthData.continents.america.area,
    japanTemp: earthData.continents.asia.countries.japan.temperature
}

This won't work because typically, my data arrives as an array of objects, so I need to map over the array and apply the same select procedure such as in:

const earthData = {
    distanceFromSun: 149280000,
    continents: {
      asia: {
        area: 44579000,
        population: 4560667108,
        countries: { japan: { temperature: 62.5 } },
      },
      africa: { area: 30370000, population: 1275920972 },
      europe: { area: 10180000, population: 746419440 },
      america: { area: 42549000, population: 964920000 },
      australia: { area: 7690000, population: 25925600 },
      antarctica: { area: 14200000, population: 5000 },
    },
  };

  const earthData2050 = {
    distanceFromSun: 149280000,
    continents: {
      asia: {
        area: 44579000,
        population: 4560767108,
        countries: { japan: { temperature: 73.6 } },
      },
      africa: { area: 30370000, population: 1275960972 },
      europe: { area: 10180000, population: 746419540 },
      america: { area: 42549000, population: 964910000 },
      australia: { area: 7690000, population: 25928600 },
      antarctica: { area: 14200000, population: 5013 },
    },
  };

const myEarthArr = [earthData, earthData2050]

Admittedly, I could have called .map() array method simply as:

const mapRes = myEarthArr.map((record) => ({
  distanceFromSun: record.distanceFromSun,
  asiaPop: record.continents.asia.population,
  americaArea: record.continents.america.area,
  japanTemp: record.continents.asia.countries.japan.temperature,
}));

And get the desired output:

// [ { distanceFromSun: 149280000,
//     asiaPop: 4560667108,
//     americaArea: 42549000,
//     japanTemp: 62.5 },
//   { distanceFromSun: 149280000,
//     asiaPop: 4560767108,
//     americaArea: 42549000,
//     japanTemp: 73.6 } ]

Nevertheless, I'm looking to create my own generic select() function that accepts one object, and subsets it. The benefit of such approach is its flexibility. I can use it standalone on a single object, plus allowing me to scale select() to an array of objects when needed, by doing something like:

// pseudo code
myEarthArr.map( (record) => select(record, {
  distanceFromSun: ['distanceFromSun'],
  asiaPop: ['continents', 'asia', 'population'],
  americaArea: ['continents', 'america', 'area'],
  japanTemp: ['continents', 'asia', 'countries', 'japan', 'temperature'],
}) )

From looking around in StackOverflow posts, I found this one to be the closest. But neither do I understand how to shape it to my needs, nor whether its recursive mechanism actually required in my situation. By contrast, this post offers a ton of solutions for the simple scenario of subsetting, but none is addressing the issue of nested properties.

Emman
  • 3,695
  • 2
  • 20
  • 44
  • Perhaps [JSONpath](https://www.ietf.org/id/draft-ietf-jsonpath-base-05.html) is what you're looking for? – tromgy Apr 29 '22 at 13:09
  • @tromgy, thanks. Not sure I understand how this interacts with JavaScript. Could you demonstrate? – Emman Apr 29 '22 at 13:20

2 Answers2

1

You can do something like this

const select = (data, filters) => Object.entries(filters)
.reduce((res, [key, path]) => {
  return {
   ...res,
   [key]: path.reduce((current, segment) => current[segment] ?? undefined , data)
  }

}, {})
  

const earthData = {
  distanceFromSun: 149280000,
  continents: {
    asia: {
      area: 44579000,
      population: 4560667108,
      countries: { japan: { temperature: 62.5 } },
    },
    africa: { area: 30370000, population: 1275920972 },
    europe: { area: 10180000, population: 746419440 },
    america: { area: 42549000, population: 964920000 },
    australia: { area: 7690000, population: 25925600 },
    antarctica: { area: 14200000, population: 5000 },
  },
};


const earthDataSubset = select(earthData, {
  distanceFromSun: ['distanceFromSun'],
  asiaPop: ['continents', 'asia', 'population'],
  americaArea: ['continents', 'america', 'area'],
  japanTemp: ['continents', 'asia', 'countries', 'japan', 'temperature'],
});

console.log(earthDataSubset)

Explanation inner reduce part

path.reduce((current, segment) => current[segment] ?? undefined , data)

path is an array of property nested inside data

path.reduce cycle all these property name

example

path = ['continents', 'asia', 'population']

in the first iteration

  • current is data your object (I omit it because it's a bit long)
  • segment is 'continents'
  • return data['continents']

second iteration

  • current is data['continents']
  • segment is 'asia'
  • return data['continents']['asia']

You got the idea

R4ncid
  • 6,944
  • 1
  • 4
  • 18
  • Awesome! Looks very good and simple. – Emman Apr 29 '22 at 13:20
  • @Emman thanks man :) I can add some explanation if you need it – R4ncid Apr 29 '22 at 13:21
  • Thanks, yes I need some explanation, and I can even frame where I struggle to understand. If we define a utility function `const prop = (obj, key) => obj[key] `, and `const somePath = ['continents', 'asia', 'countries', "japan", "temperature"]`. Then `const res = somePath.reduce(prop, earthData)` gives that `res` is `62.5`. **I don't understand how `reduce()` works here**. If you can please explain this, the rest of your solution seems more simple to comprehend. – Emman Apr 29 '22 at 14:00
  • 1
    @Emman I tried to explain it in the answer – R4ncid Apr 29 '22 at 14:13
0

Here's how you can do it using JSONPath.

Install the jsonpath-plus npm package:

npm install jsonpath-plus

And then you can write your function like this (assuming you're using modules):

import { JSONPath } from "jsonpath-plus";

function select(data, query) {
    let result = {};

    for (let key of Object.keys(query)) {
        result[key] = JSONPath(`$.${query[key].join('.')}`, data)[0];
    }

    return result;
}

Using your data (myEarthArr, which I omit here for brevity) and the query object:

const myQuery = {
    distanceFromSun: ['distanceFromSun'],
    asiaPop: ['continents', 'asia', 'population'],
    americaArea: ['continents', 'america', 'area'],
    japanTemp: ['continents', 'asia', 'countries', 'japan', 'temperature'],
};

Running this loop:

for (let dataItem of myEarthArr) {
    console.log(select(dataItem, myQuery));
}

produces:

{
  distanceFromSun: 149280000,
  asiaPop: 4560667108,
  americaArea: 42549000,
  japanTemp: 62.5
}
{
  distanceFromSun: 149280000,
  asiaPop: 4560767108,
  americaArea: 42549000,
  japanTemp: 73.6
}
tromgy
  • 4,937
  • 3
  • 17
  • 18
  • You can simplify it by using actual JSONPath expressions in the query object instead of arrays. – tromgy Apr 29 '22 at 15:18