Parsing structured data in a functional way (e.g. without mutating) - javascript

Imagine you have the following data in a file:
Group1
Thing1
Thing2
Group2
Thing1
Thing2
Thing3
Group3
Group4
Thing1
It's easy to write a "parser" which loops through the file line-by-line, remembering the current Group (in a variable) and then writing all the Things to an object, neatly grouped by their respective group:
// Very naive implementation for illustrative purposes only
let groups = {}
let currentGroup = null
data
.split(/\n/)
.forEach(entry => {
const matches = entry.match(/^(Group\d+)$/)
if (matches) {
currentGroup = matches[1]
groups[currentGroup] = []
} else {
groups[currentGroup].push(entry.trim())
}
})
which gives me:
{
Group1: [
'Thing1', 'Thing2'
],
Group2: [
'Thing1', 'Thing2', 'Thing3'
],
...
}
What's the best way to achieve this without mutating groups and currentGroup, in a purely functional way? Do I need to take a harder look at Array.reduce, because I've seen some (IMHO rather mind-boggling) use-cases to transform an Array into an Object, or is that not going to help here?

Yes, you'd want to use reduce here:
data
.split(/\n/)
.reduce(({groups, currentGroup}, entry) => {
const matches = entry.match(/^(Group\d+)$/)
if (matches) {
groups[matches[1]] = []
return {currentGroup: matches[1], groups};
} else {
groups[currentGroup] = groups[currentGroup].concat([entry.trim()]);
return {currentGroup, groups};
}
}, {groups: {}, currentGroup: null})
.groups
However, there is no reasonable way in JS to create a map object without mutation. As long as you keep your property assignments local, there's nothing wrong with that.

Related

How to write recursion where items depend on each other?

I want to write a function, which returns a list of people a person relies on. However, the people relied upon, could in turn rely on the person themselves. For example:
const people = {
'james': {
reliesOn: [
'gemma',
'jessica'
]
},
jessica: {
reliesOn: [
'gemma',
'peter'
]
},
peter: {
reliesOn: [
'james',
'gemma',
'jessica',
'ivon',
'jamie'
]
},
jamie: {
reliesOn: [
'ivon'
]
}
}
I am attempting to get the following result, in the simplest possible code:
james relies on:
gemma, jessica, peter, ivon, jamie
jessica relies on:
gemma, peter, james, ivon, jamie
peter relies on:
james, gemma, jessica, ivon, jamie
jamie relies on:
ivon
Apologies if this has already been asked. I simplified this example from real world, so I hope it makes sense
If you really want to do this with reduce then:
const getReliedOn = (people, name) =>
[...(people[name]?.reliesOn || []).reduce(function recur(acc, name) {
return acc.has(name) ? acc
: (people[name]?.reliesOn || []).reduce(recur, acc.add(name));
}, new Set([name]))].slice(1);
// Sample input
const people = {'james': { reliesOn: ['gemma','jessica']}, jessica: {reliesOn: ['gemma','peter']}, peter: {reliesOn: ['james','gemma','jessica','ivon','jamie']}, jamie: { reliesOn: ['ivon']}};
// Produce output for each person:
for (let name in people) console.log(name, "=>", ...getReliedOn(people, name));
Alternative without reduce, without recursion
This is a breadth-first search (BFS). It relies on the fact that when you iterate over a set, and during that iteration you add values to that set, the loop will also visit those added values in later iterations. So the set acts like the queue that is typical for a BFS:
function getReliedOn(people, name) {
let names = new Set([name]); // a set with only one entry
for (let name of names) { // this set will extend while we loop
if (!people[name]) continue;
for (let other of people[name].reliesOn) names.add(other);
}
// get normal array from set, without original name
return Array.from(names).slice(1);
}
// Sample input
const people = {'james': { reliesOn: ['gemma','jessica']}, jessica: {reliesOn: ['gemma','peter']}, peter: {reliesOn: ['james','gemma','jessica','ivon','jamie']}, jamie: { reliesOn: ['ivon']}};
// Produce output for each person:
for (let name in people) console.log(name, "=>", ...getReliedOn(people, name));
I find it useful to pass around a "memo" object when recursing that keeps track of where we've been already to avoid circular loops:
function *getDependenciesRecursive(people, person, alreadyVisited = new Set()) {
if (alreadyVisited.has(person)) {
return;
}
alreadyVisited.add(person);
if (!(person in people)) {
return;
}
for (const dependency of people[person].reliesOn) {
yield dependency;
yield* getDependenciesRecursive(people, dependency, alreadyVisited);
}
}
Essentially, in an approach like this the Set tells us what to skip.
const reliances = Object
.entries(people)
.reduce((results, [person, { reliesOn }]) => Object.assign(results, {
[person]: [...getDependenciesRecursive(people, person)]
}), {})

Is destructuring this possible?

I am new to destructuring and need help finding the best solution. I have a pretty complicated object coming back as a response and would like to clean it up. Without doing anything, it looks like this:
const homeTeam = {
totalPlaysFor: res.data.stats.home.teamStats[0].miscellaneous.offensePlays,
totalPlaysAgainst: res.data.stats.away.teamStats[0].miscellaneous.offensePlays
}
I know I can do something like:
const { offensePlays } = res.data.stats.home.teamStats[0].miscellaneous;
but that only solves my problem for one of them and would leave the other still long and tough to read.
You can destructure the stats property in advance, then make a helper function that takes the .home or .away object and navigates to the nested .offensePlays:
const { stats } = res.data;
const getOffPlays = obj => obj.teamStats[0].miscellaneous.offensePlays;
const homeTeam = {
totalPlaysFor: getOffPlays(stats.home),
totalPlaysAgainst: getOffPlays(stats.away)
};
Without having a standalone helper function, you could also create the object by .mapping an array of properties (eg [['totalPlaysFor', 'home'], ['totalPlaysAgainst', 'away']]) and pass it to Object.fromEntries, but that would be significantly less readable IMO.
While you can destructure this directly, via something like
const {
data: {
stats: {
home: {
teamStats: [{
miscellaneous: {
offensePlays: totalPlaysFor
}
}]
},
away: {
teamStats: [{
miscellaneous: {
offensePlays: totalPlaysAgainst
}
}]
}
}
}
} = res
const homeTeam = {totalPlaysFor, totalPlaysAgainst}
that feels pretty ugly.
Regardless whether you think your code is ugly, I can see a much more important problem with it: it doesn't work when any of those properties doesn't exist. To solve that, you might want a feature not yet ubiquitous in the language, one of Optional Chaining that would avoid throwing errors for a nested path such as 'data?.stats?.home?.teamStats?.0?.miscellaneous?.offensePlays', simply returning undefined if one of the nodes mentioned does not exist.
An equivalent feature is available as a function in many libraries. Underscore's property, lodash's property, and Ramda's path offer slightly different versions of this. (Disclaimer: I'm a Ramda author.) However it's easy enough to write our own:
const getPath = (pathStr) => (obj) =>
pathStr .split ('.')
.reduce ((o, p) => (o || {}) [p], obj)
const res = {data: {stats: {home: {teamStats: [{miscellaneous: {offensePlays: "foo"}}]}, away: {teamStats: [{miscellaneous: {offensePlays: "bar"}}]}}}}
const homeTeam = {
totalPlaysFor: getPath ('data.stats.home.teamStats.0.miscellaneous.offensePlays') (res),
totalPlaysAgainst: getPath ('data.stats.away.teamStats.0.miscellaneous.offensePlays') (res)
}
console .log (homeTeam)
Note that the array is not delineated in this simple version as [0] but just as .0. Obviously we could make this more sophisticated if necessary.
You can destructure as much as you want!
const homeTeam = {
totalPlaysFor: res.data.stats.home.teamStats[0].miscellaneous.offensePlays,
totalPlaysAgainst: res.data.stats.away.teamStats[0].miscellaneous.offensePlays
}
You can even use a combination of array destructuring / object destructuring here:
const [ home ] = res.data.stats.home.teamStats
const [ away ] = res.data.stats.away.teamStats
const { offensePlays: totalPlaysFor } = home.miscellaneous
const { offensePlays: totalPlaysAgainst } = away.miscellaneous
const hometeam = { totalPlaysFor, totalPlaysAgainst }
Or, if you want a more reusable solution, you can use parameter destructuring:
const getTeam = (
{ miscellaneous: { offensePlays: totalPlaysFor } },
{ miscellaneous: { offensePlays: totalPlaysAgainst } }
) => ({
totalPlaysFor,
totalPlaysAgainst
})
Then you can use it like:
const [ home ] = res.data.stats.home.teamStats
const [ away ] = res.data.stats.away.teamStats
const homeTeam = getTeam(home, away)

Is there a way to traverse a possibly-self-containing object in JavaScript?

I want to descend an object in Javascript looking for a specific string. Unfortunately, this object is built in such a way that it'd be impossible to simply use the source and Ctrl-F for that string, and it's also built in such a way that recursive functions trying to descend it risk getting trapped inside of it forever.
Basically, this object contains itself. Not just once, but in very many areas. I cannot simply say "exclude these keys", as the object is obfuscated and therefore we'd be here all day listing keys, and once we were done we wouldn't have looked at all the data.
As well, I need to be able to descend __proto__ and prototype, as useful strings are hidden in there too. (But only for functions and objects.)
While I'd prefer something along the lines of findStuff(object, /string/ig), that may be hard, so any function that simply has areas clearly marked that the control flow falls to once it's found specific objects (function, string, etc.)
Thank you, and sorry for such a pain in the butt question.
Edit: In case it helps, I'm trying to traverse a compiled Construct2 runtime object. I'm not going to post the full thing here as it's not going to fit in any pastebin no matter how forgiving, and also I don't want to accidentally post resources I don't have the permission to provide. (Don't worry though, I'm not trying to pirate it myself, I'm simply trying to figure out some user-facing functionality)
You could use a WeakSet to keep track of the objects that were already traversed:
function traverseOnce(obj, cb) {
const visited = new WeakSet();
(function traverse(obj) {
for(const [key, value] of Object.entries(obj)) {
if(typeof value === "object" && value !== null) {
if(visited.has(value)) continue;
visited.add(value);
cb(value);
traverse(value);
}
}
})(obj);
}
Through the WeakSet you got O(1) lookup time, and are also sure that this will never leak.
Usable as:
const nested = { other: { a: 1 } };
nested.self = nested;
traverseOnce(nested, console.log);
// nested: { other, self }
// other: { a: 1 }
You could also use a Symbol to flag traversed objects, for that replace new WeakSet() with Symbol(), visited.has(value) with value[visited] and visuted.add(value) with value[visited] = true;
Any time you're traversing a potentially cyclical object, keeping a memo of already traversed objects and breaking if you've seen the current object before is a standard technique. You can use Set to do so.
Keep a list of objects you have recursed into, and then check each new object against that list.
const data = {
foo: {
bar: 1
},
one: 1,
jaz: {
hello: {
x: 1
}
}
};
data.bar = data.foo;
data.foo.foo = data.foo;
data.jaz.hello.foo = data;
function search_for_1() {
const seen = [];
search(data);
function search(object) {
Object.values(object).forEach(value => {
if (typeof value === "object") {
if (seen.includes(value)) {
console.log("Seen this already");
} else {
seen.push(value);
search(value);
}
} else {
if (value === 1) {
console.log("Found 1");
}
}
});
}
}
search_for_1();
Don't reinvent the wheel There are libraries for this kind of stuff.
We use object-scan for all our data processing. It's very powerful once you wrap your head around it. Here is how it would work for your questions
// const objectScan = require('object-scan');
const traverse = (data) => objectScan(['**'], {
filterFn: ({ key, value, parent }) => {
// do something here
},
breakFn: ({ isCircular }) => isCircular === true
})(data);
const circular = { name: 'Max', age: 5, sex: undefined, details: { color: 'black', breed: undefined } };
circular.sex = circular;
circular.details.breed = circular;
console.log(traverse(circular));
/* =>
[ [ 'details', 'breed' ],
[ 'details', 'color' ],
[ 'details' ],
[ 'sex' ],
[ 'age' ],
[ 'name' ] ]
*/
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="https://bundle.run/object-scan#13.8.0"></script>
Disclaimer: I'm the author of object-scan

How do I turn the values from an object into an array?

I have a function that is polling for temperature data:
{"a":"43",
"b":"43",
"c":"42",
"d":"43",
"e":"40",
"f":"41",
"g":"100",
"h":"42.6"}
I want to be able to graph that data over time, but I can't figure out the best way to map the above data, to something like the below data:
temps: [{
name: "a",
data: ["43","42","43"]
},
name: "b",
data: ["43","42","43"]
},
etc...
]
I have tried the code below, and tried to figure out the javascript map function, but I keep running into scoping problems where "this" isn't the same thing as it was in the parent:
this.temp_names.forEach(function(e){
if(typeof this.temps[e] == "undefined") {
this.temps[e] = []
}
this.temps.e.unshift(this.sys_telemetry.S.temps)
if (this.temps.e.length > 10) {
this.temps.e.pop()
}
})
where "temp_names" was an array of the keys.
I'm doing this in VueJS, so the "this" is accessing the data in my component.
Using Array#from, Object#entries, Array#map and destructuring you could do something like this.
const data={"a":"43","b":"43","c":"42","d":"43","e":"40","f":"41","g":"100","h":"42.6"}
const res = Object.entries(data)
.map(([name, data])=>({name, data:[data]}));
console.log(res);
Alternative using Array#reduce, Map,
const data={"a":"43","b":"43","c":"42","d":"43","e":"40","f":"41","g":"100","h":"42.6"}
const res = Array.from(Object
.entries(data)
.reduce((a,[k,v])=>{
if(!a.has(k)) a.set(k, []);
a.get(k).push(v);
return a;
}, new Map()))
.map(([name, data])=>({name, data}));
console.log(res);
graph that data over time
Because you want to do this over time, it would make sense to create an array and then using Object.entries, & Array.find, update the results.
Here is an example.
const values1 =
{"a":"43", "b":"43", "c":"42", "d":"43", "e":"40", "f":"41",
"g":"100", "h":"42.6"};
const values2 =
{"c":"44", "e":"39"};
const results = [];
function addData(data) {
Object.entries(data).forEach(([k, v]) => {
let find = results.find(f => f.name === k);
if (!find) {
find = {
name: k,
data: []
}
results.push(find);
}
find.data.push(v);
});
}
addData(values1); //data packet one arrives
addData(values2); //data packet two arrives
console.log(results); //results contains both data packet one & two.
You might be able to get away with a simpler data structure like, eg. { a: [43, 42, 43], b: [1, 2, 3] }
ie. instead of having separate name and data keys, you could use name as the key, and the data array as the value.
If this would work to represent the timeline for each key, and your initial data is structured like, eg. [{ a: 43, b: 1, c: 3 }, { a: 42, b: 2, c: 3 }], then something like this might be suitable to transform the latter into the former:
const output = {};
temp_data.forEach(x => {
for (const key in x) {
const y = x[key];
if (typeof output[key] === 'undefined') {
output[key] = [];
}
output[key].push(y);
}
});
This produces an object whose keys match the keys in your data points (eg. "a", "b", "c", etc), and whose values are an array of all the values for each of these keys, which might be suitable for plotting a timeline.
(Incidentally, if you want to plot these as values on a graph, it might be better to treat the values as numbers - eg. 1, 2, 3 - rather than strings - eg. "1", "2", "3").
There are probably more elegant, functional-style ways of doing this, but this might do the job!
It seems to me that you want to be able to add multiple datasets to the data object. One approach is to have a data object with methods that know how to do things like add data to themselves, maybe something like the following. You might want to keep the index property private, and maybe sort it so it's always in a particular order regardless of the order the values are added.
var data0 = {"a":"43",
"b":"43",
"c":"42",
"d":"43"};
var data1 = {"a":"53",
"b":"53",
"c":"52",
"d":"53",
"e":"65"
};
class DataObject {
constructor (data) {
this.index = [];
this.data = [];
if (data) {
this.addData(data);
}
}
addData (data) {
Object.keys(data).forEach(key => {
let idx = this.index.indexOf(key);
if (idx == -1) {
idx = this.index.push(key) - 1;
this.data.push({name:key, data:[]});
}
this.data[idx].data.push(data[key]);
});
}
}
// Initialise object with some data
let myData = new DataObject(data0);
console.log(JSON.stringify(myData.data));
// Add more data
myData.addData(data1);
console.log(JSON.stringify(myData.data));

Can the props in a destructuring assignment be transformed in place?

This works…
const { prop1:val1, prop2:val2 ) = req.query
val1 = val1.toLowerCase()
Though, I'm more inclined to do something like
const { prop1.toLowerCase():val1, prop2:val2 } = req.query
or
const { prop1:val1.toLowerCase(), prop2:val2 } = req.query
neither of which work. Is there a syntax similar to this or must manipulations be done outside of the destructing assignment?
No, this is not possible. A destructuring assignment does only assign, it does not do arbitrary transformations on the value. (Setters are an exception, but they would only complicate this).
I would recommend to write
const { prop1, prop2:val2 ) = req.query;
const val1 = prop1.toLowerCase();
or, in one statement:
const { prop1, prop2:val2 ) = req.query, val1 = prop1.toLowerCase();
The trouble with the temporary variable solutions is that they introduce different versions of the same data into the scope, which can lead to bugs.
This solution creates a utility function that receives the object to be destructured as well as a second object that is a mapping of property names to transformation functions. It's a little more verbose, but does the trick.
// Utility functions to perform specified transformations on an object
function transformProps(obj, trans) {
return Object.assign({}, obj, ...Object.entries(trans).map(([prop, fn]) =>
prop in obj ? {[prop]: fn(obj[prop])} : null
));
}
const { prop1:val1, prop2:val2 } = transformProps(
{prop1: "FOO", prop2: "BAR"},
{prop1: v => v.toLowerCase()} // Transformations to be made
);
console.log(val1, val2);

Categories

Resources