LinkedList in JavaScript, how to append changes to list? - javascript

I'm running into a referential issue with variables in JavaScript, and I've been banging my head against the wall trying to figure this out.
I'm preparing to teach a class on data structures, and I'm reviewing the material after not looking at it for at least 10 years.
I know Linked Lists in theory, but for some reason, I'm struggling to come up with code that actually works in JavaScript (I chose JavaScript because that's what my class knows best)
This is my code:
let LinkedList = {
head: {},
tail: {}
};
let Node = {
data: {},
next: {}
}
function isObjectEmpty(obj1) {
return Object.keys(obj1).length === 0 && obj1.constructor === Object;
}
function count(node, counter) {
if (node.next) {
return 1 + count(node.next, counter);
}
return counter;
}
/**
* Adds data to LinkedList
* #param {LinkedList} list
* #param {Node} data
*/
function add_node(list, data) {
let temp = Object.assign({}, Node);
temp.data = data;
temp.next = {};
if (Object.keys(list.head).length === 0) {
list.head = temp;
list.tail = temp;
} else {
list.tail.next = temp;
list.tail = temp;
}
return list;
}
function insert(l, n, position) {
if (position <= 0) {
position = 0;
} else {
position = position - 1;
}
var list = Object.assign({}, l);
var node = Object.assign({}, Node);
node.data = n;
// this only counts elements on the list.
var elements = count(list.head, 0);
if (position > elements) {
return list;
}
var currentPosition = list.head;
var counter = 0;
while (!isObjectEmpty(currentPosition)) {
if (position === counter) {
var tmp = currentPosition;
currentPosition = node;
currentPosition.next = tmp.next;
return list;
}
currentPosition = currentPosition.next;
counter++;
}
return list;
}
// how to use the function
let songs = [
{id: '1', name: 'Kamikaze', artist: 'Eminem', releaseDate: '2018-08-31'},
{id: '2', name: 'despacito', artist: 'Luis Fonsi', releaseDate: '2018-08-31'},
{id: '3', name: 'La tortura', artist: 'Shakira', releaseDate: '2018-08-31'},
{id: '4', name: 'Roar', artist: 'Roar', releaseDate: '2018-08-31'},
];
let list = Object.assign({}, LinkedList);
songs.forEach((song) => {
add_node(list, song); // nothing special, just builds the linkedlist
});
list = insert(list, {id: '5', name: 'Havana', artist:'who knows', releaseDate:'2018-01-01'}, 3);
console.log(list); // new object isn't there.
This function is supposed to insert an element in an arbitrary position in the linked list. It kinda works. The problem is that the returned list keeps the reference to the old object before the re-association.
If you put a debugger in this block:
if (position === counter) {
var tmp = currentPosition;
currentPosition = node;
currentPosition.next = tmp.next;
return list;
}
You can see that I'm actually successfully inserting the new node where I want to.
But if you console.log the list structure, you'll see that the newly inserted Node is nowhere to be found.
I'm not sure where I'm failing or why the list keeps the old references and doesn't follow the new "path".
Any pointers in the right direction is greatly appreciated.

If you put a debugger in this block:
if (position === counter) {
var tmp = currentPosition;
currentPosition = node;
currentPosition.next = tmp.next;
return list;
}
You can see that I'm actually successfully inserting the new node
where I want to
No, you don't. If we drop the tmp and currentPosition assignment confusion, that bit of code is equivalent to
if (position === counter) {
node.next = currentPosition.next;
return list;
}
All that happens is that you are copying the tail of the list onto the new node, but you never really insert the node as the next of the one currently in the list. It's missing a
currentPosition.next = node;
A couple of other points:
Don't use isObjectEmpty and empty objects to represent "no node". Use null instead. If for some didactic reason you don't want to introduce null, use a boolean .isNode property on the objects to distinguish a node with data from an empty one.
Avoid Object.assign. Your usage is really unidiomatic. In
let temp = Object.assign({}, Node);
temp.data = data;
temp.next = {};
you are directly overwriting the values that you just copied from Node - better simplify to using an object literal:
let temp = {data, next: {}};
In var list = Object.assign({}, l); you don't really want to create a new object at all. You are going to mutate the passed in list, so you should just keep that. (If you wanted to make pure functions with immutable data structures, you would have to make all the nodes immutable as well, and for inserting clone the entire list until the desired position).
If your intention for Object.assign was to create new objects that later might involve other properties (or methods) that you weren't going to overwrite, use factory functions instead.
Don't count the list beforehand. Do the insertion in a single pass, and if you reach the end of the list before the position to inert then return.
function makeLinkedList() {
return { head: null, tail: null };
}
function makeNode(data, next = null) {
return { data, next };
}
function append(list, node) {
if (list.head) {
list.tail.next = node;
} else {
list.head = node;
}
list.tail = node;
}
function insert(list, data, position) {
if (position < 0) throw new Error("position must be nonnegative");
let newNode = makeNode(data);
let prev = null, cur = list.head;
while (cur != null && position > 0) {
prev = cur;
cur = cur.next;
position--;
}
if (cur == null && position > 0) throw new Error("position must be <= list length")
if (cur == null) {
list.tail = newNode;
} else {
newNode.next = cur;
}
if (prev == null) {
list.head = newNode;
} else {
prev.next = newNode;
}
}

You could use a version without Object.assign and work with objects as list and nodes who are created by same named function. This function could be replaced later with instanciable functions for more OOP styled approach.
This proposal takes a node and inset it, depending on the function, at the end of the list or at any place in the list. If no node exists in the list, it is inserted at the beginning of the linked list.
function linkedList() {
return { head: null, tail: null };
}
function node(data, next) {
return { data, next };
}
function insertTail(list, node) {
if (list.head) {
list.tail.next = node;
list.tail = list.tail.next;
} else {
list.head = node;
list.tail = node;
}
return list;
}
function insertPos(list, node, n) {
var temp = list.head,
previous;
if (!temp) {
return insertTail(list, node);
}
while (temp.next && n--) {
previous = temp;
temp = temp.next;
}
node.next = temp;
previous.next = node;
return list;
}
var songs = [{ id: '1', name: 'Kamikaze', artist: 'Eminem', releaseDate: '2018-08-31' }, { id: '2', name: 'despacito', artist: 'Luis Fonsi', releaseDate: '2018-08-31' }, { id: '3', name: 'La tortura', artist: 'Shakira', releaseDate: '2018-08-31' }, { id: '4', name: 'Roar', artist: 'Roar', releaseDate: '2018-08-31' }],
list = linkedList();
songs.forEach(song => insertTail(list, node(song)));
console.log(list);
insertPos(list, node({ id: '5', name: 'Havana', artist: 'who knows', releaseDate: '2018-01-01' }), 3);
console.log(list);
.as-console-wrapper { max-height: 100% !important; top: 0; }

Couple of problems. First, you're replacing a node, not strictly inserting. I think you want your .next to be the tmp node, not tmp.next.
currentPosition.next = tmp;
Second, you need to keep a reference to the node immediately before the insertion point, so that you can set its next value to the newly inserted node:
previousNode.next = node //<- node being the one you're inserting
that's why you're not seeing the difference in your linked list.
*Edit (putting it all together):
var prevNode = null;
while (!isObjectEmpty(currentPosition)) {
if (position === counter) {
var tmp = currentPosition;
currentPosition = node;
currentPosition.next = tmp;
if (prevNode === null) list.head = currentPosition;
else prevNode.next = currentPosition;
return list;
}
prevNode = currentPosition;
currentPosition = currentPosition.next;
counter++;
}
To make it safe for inserting into the initial position, we have to have the ability to set the list.head. Otherwise we set the previous node's next property.

Related

How to Delete an item from an array with specific string matching

I want to be able to match a specific string (full match not partial match) and then delete that specific item from the array if it matches.
I have some code but it doesn't seem to be deleting the item from the array. I do wish for it to change the original array and not create a new array so I am not using filter.
How can I go about accomplishing this?
Current Code:
let recentSearches = [
{ name: "Chicago, IL" },
{ name: "Orlando, FL" },
{ name: "Dallas, TX" }
];
let stringToRemove = "Dallas, TX";
recentSearches.some(recent => {
if (recent.name === stringToRemove) {
const index = recentSearches.indexOf(stringToRemove);
if (index !== -1) { //Never goes into this if
recentSearches.splice(index, 1);
console.log(recentSearches);
}
}
});
console.log(recentSearches);
JS Fiddle: enter link description here
If you don't mind the output being a different array, use filter:
const filteredSearches = recentSearches.filter((recent) => recent.name !== stringToRemove);
If you need to modify the array in-place, you should visit the elements in reverse order (in case of multiple matches, which causes indices to shift) like so:
for (let i = recentSearches.length-1; i >= 0; i--) {
if (recentSearches[i].name === stringToRemove) {
recentSearches.splice(i, 1);
}
}
The problem with your code is you use recentSearches.indexOf, but recentSearches isn't an array of strings, so nothing matches. You could modify your code as follows, but it won't work correctly in case of multiple mathces:
recentSearches.forEach((recent, index) => {
if (recent.name === stringToRemove) {
recentSearches.splice(index, 1);
}
});
Alternatively, you could use findIndex (as suggested in other comments and answers) as follows:
let index;
while (0 <= (index = recentSearches.findIndex((recent) => recent.name === stringToRemove)) {
recentSearches.splice(index, 1);
}
indexOf() is for finding exact matches. Since your array contains objects, they'll never be equal to stringToRemove.
Use findIndex() to get the index of an array element using a function that an compare the name property.
There's also no need for using some().
let recentSearches = [{
name: "Chicago, IL"
},
{
name: "Orlando, FL"
},
{
name: "Dallas, TX"
}
];
let stringToRemove = "Dallas, TX";
const index = recentSearches.findIndex(({
name
}) => name == stringToRemove);
if (index !== -1) { //Never goes into this if
recentSearches.splice(index, 1);
}
console.log(recentSearches);
Another version of the findIndex, instead of using while, you could use for, a slight advantage here is that the index is then locally scoped inside the the for, were with a while loop you have the extra scope of the index, you could close the the scope of a let by doing { let index; while() {..}} but the for loop avoids that without using {}.
let recentSearches = [
{name: "Chicago, IL"},
{name: "Orlando, FL"},
{name: "Dallas, TX"}
];
let stringToRemove = "Dallas, TX";
for (let index; index = recentSearches.findIndex(
search => search.name === stringToRemove), index > -1;)
recentSearches.splice(index, 1);
console.log(recentSearches);
The JSON search is done wrongly.
I have added the perfect code to complete your requirement. Find all instances and delete them with a while loop. This will ensure duplicate search terms are also removed if any.
let recentSearches = [
{name: "Chicago, IL"},
{name: "Orlando, FL"},
{name: "Dallas, TX"}
];
let stringToRemove = "Dallas, TX";
while (recentSearches.findIndex(search => search.name === stringToRemove) > -1) {
const index = recentSearches.findIndex(search => search.name === stringToRemove);
recentSearches.splice(index, 1);
}
console.log(recentSearches);
You can use findindex.
Store it in a variable.
And use splice
You can use this code:
Array.prototype._arraycopy = function(src, srcPos, dest, destPos, length) {
while ((--length) >= 0) {
dest[destPos++] = src[srcPos++];
}
};
Array.prototype._fastRemove = function(es, i) {
let newSize;
if ((newSize = this.length - 1) > i)
this._arraycopy(es, i + 1, es, i, newSize - i);
es[this.length = newSize] = null;
this.length = newSize;
}
Array.prototype.__removeAt = function(index) {
// Objects.checkIndex(index, size);
const es = this;
const oldValue =es[index];
this._fastRemove(es, index);
return oldValue;
}
Array.prototype.__removeAtValue = function(o) {
const es = this;
const size = this.size;
let i = 0;
(function() {
if (o == null) {
for (; i < size; i++)
if (es[i] == null)
return true;
} else {
for (; i < size; i++)
if (Object.is(o, es[i]))
return true;
}
return false;
})()
this._fastRemove(es, i);
return true;
}
Array.prototype.remove = function(index) {
return this.__removeAt(index)
}
Array.prototype.removeObj = function(obj) {
return this.__removeAtValue(obj);
}
const arr = [1, 3, 4, 5, 10];
console.log(arr);
const rem = arr.remove(1)
console.log({ arr, rem });
const objs = [{ id: 1, name: "Hello" }, { id: 2, name: "Arrow" }, { id: 3, name: "Star" }]
console.log(objs);
const deleted = objs.removeObj({ id: 2, name: "Arrow" });
console.log({ objs, deleted })

Implement the addInPos method inside the LinkedList prototype in Javascript

I need to implement the addInPos method inside the LinkedList prototype but I don't know what it is bad in my code...because the test no pass.
Implement the addInPos method inside the LinkedList prototype that must add an element in the indicated position. Both data will be provided as a parameter (pos, value). Where "pos" will be the position in which the "value" value should be added. In the event that the position in which the insertion is to be made is invalid, that is, it exceeds the size of the current list, it must return false.
If the node was added correctly return true.
Note: the zero position corresponds to the head of the LinkedList.
My code:
LinkedList.prototype.addInPos = function(pos, value) {
if(pos > this.size) {
return false;
}
const newNode = new Node(pos, value);
let current = this.head;
let previous;
if(pos === 0) {
newNode.next = current;
current.prev = newNode;
this.head = newNode;
} else {
for(let i = 0; i < pos; i++){
previous = current;
current = current.next;
}
newNode.next = current;
newNode.prev = previous;
current.prev = newNode;
previous.next = newNode;
}
}
Thanks.
With your structure ideally insertAt function would look like:
var insertAt = function(head, item, index) {
var curr = head;
var count = 0;
while (curr !== null) {
if (count === index) {
var newNode = { data: item, next: curr.next, prev: curr };
curr.next = newNode;
break;
}
count++;
curr = curr.next;
}
};
insertAt(head, 2, 3);
Let me know if this works.
Also look at this LinkedList class I have created, insertAt function is currently missing, but from this stack question, I am planning to add it in my class as well.
Class GitHub
NPM package - #dsinjs/linked-list
Complete documentation

Copy List with Random Pointer - Copy not point to -

I'm trying to solve the problem below:
A linked list is given such that each node contains an additional random pointer which could point to any node in the list or null.
Return a deep copy of the list.
The Linked List is represented in the input/output as a list of n nodes. Each node is represented as a pair of [val, random_index] where:
val: an integer representing Node.val
random_index: the index of the node (range from 0 to n-1) where random pointer points to, or null if it does not point to any node.
Input: head = [[7,null],[13,0],[11,4],[10,2],[1,0]]
Output: [[7,null],[13,0],[11,4],[10,2],[1,0]]
My idea was loop the head and create a new node with the same value and set it to my ans Node. It will be a copy because I'm creating a new one. Then, I loop again for the random pointer which looks in my ans Node the node with the value that I should point to. It works perfectly for the input above. However, if I have an input with duplicate values, the findeNode method might return an invalid Node. For example it fails for the input below:
[[3,null],[5,17],[4,null],[-9,6],[-10,3],[5,15],[0,11],[6,null],[-6,16],[3,16],[-6,11],[9,12],[-2,1],[-3,11],[-1,10],[2,11],[-3,null],[-9,7],[-2,4],[-8,null],[5,null]]
My output:
[[3,null],[5,3],[4,null],[-9,null],[-10,3],[5,15],[0,11],[6,null],[-6,13],[3,13],[-6,11],[9,12],[-2,1],[-3,11],[-1,8],[2,11],[-3,null],[-9,7],[-2,4],[-8,null],[5,null]]
Correct output:
[[3,null],[5,17],[4,null],[-9,6],[-10,3],[5,15],[0,11],[6,null],[-6,16],[3,16],[-6,11],[9,12],[-2,1],[-3,11],[-1,10],[2,11],[-3,null],[-9,7],[-2,4],[-8,null],[5,null]]
Node definition:
function Node(val, next, random) {
this.val = val;
this.next = next;
this.random = random;
};
My code:
var copyRandomList = function(head) {
if(!head) return head;
let ans = new Node(0);
let temp = ans;
let cur = head;
while(cur) {
const node = new Node(cur.val);
temp.next = node;
cur = cur.next;
temp = temp.next;
}
cur = head;
temp = ans.next;
while(cur) {
const randomNode = cur.random;
if(!randomNode) {
temp.random = null;
} else {
const node = findNode(ans, randomNode.val);
temp.random = node;
}
cur = cur.next;
temp = temp.next;
}
return ans.next;
};
const findNode = (list, val) => {
if(!val)
return null;
while(list) {
if(list.val === val)
return list;
list = list.next;
}
return null;
}

Better way to update an object's value at a variable depth

I am working on some software that reads/writes information in localStorage using a handler. You can find a working example here: http://jsbin.com/wifucugoko/edit?js,console
My problem is with the segment of code below (focusing on the switch statement):
_t.set = function(path, value) { // Update a single value or object
if (~path.indexOf(".")) {
let o = path.split(".")[0],
p = this.get(o),
q = path.split(".").slice(1);
switch (q.length) {
// There has to be a better way to do this...
case 1:
p[q[0]] = value;
break;
case 2:
p[q[0]][q[1]] = value;
break;
case 3:
p[q[0]][q[1]][q[2]] = value;
break;
case 4:
p[q[0]][q[1]][q[2]][q[3]] = value;
break;
case 5:
p[q[0]][q[1]][q[2]][q[3]][q[4]] = value;
break;
case 6:
p[q[0]][q[1]][q[2]][q[3]][q[4]][q[5]] = value;
break;
default:
return "error";
}
b.setItem(o, JSON.stringify(p));
return p;
} else {
b.setItem(path, JSON.stringify(value));
return this.get(path);
}
};
I am not going to be the only one using this codebase, and I am trying to make it easy for others to update any value that could be placed in localStorage. Right now you can update a value by using something like local.set('item.subitem.proeprty', 'value') Though the code above does that, it's ugly and doesn't scale.
How can this method be improved to (1) update a property nested at any depth automatically, instead of writing an infinitely-long switch statement, and (2) not lace a parent object with [object Object] after a value is updated?
This question has nothing to do with my use of localStorage. I originally posted this question in code review, which requires a working contextual example. They closed this question immediately, since part of my problem is the code I provided doesn't work once you start dealing with updating a value nested more than six objects deep. Though I could have continued my switch statement indefinitely, that's exactly what I'm trying to avoid.
With the three examples provided you'll see that setting a value in one place doesn't remove values in other places:
local.set('user.session.timeout', false);
local.set('user.name', {first:'john', last:'doe', mi:'c'});
local.set('user.PIN', 8675309);
All these values, though set at different times, only UPDATE or create a value, they do NOT clear any pre-existing values elsewhere.
As for me the minimal optimization would be following:
if (~path.indexOf(".")) {
let o = path.split(".")[0],
p = this.get(o),
q = path.split(".").slice(1),
dist = p;
q.forEach(function(item, index) {
if (index < q.length - 1) {
dist = dist[item];
} else {
dist[item] = value;
}
});
b.setItem(o, JSON.stringify(p));
return p;
} else {
changed parts:
dist variable is created
hardcoded switch is replaced with foreach
You could try something like this, if the path does not exists, the value is null:
function retreiveValueFromObject(path, object) {
var pathList = path.split(".");
var currentValue = object;
var brokeEarly = false;
for (var i = 0; i < pathList.length; i++) {
if (currentValue[pathList[i]]) {
currentValue = currentValue[pathList[i]];
} else {
brokeEarly = true;
break;
}
}
return {
value: currentValue,
brokeEarly: brokeEarly
};
}
function setValueInObject(path, value, object) {
var nestedObj = retreiveValueFromObject(path, object).value;
var pathList = path.split(".");
var finalKey = pathList[pathList.length - 1];
nestedObj[finalKey] = value;
}
var someObject = {
a: {
c: {
d: "value"
},
z: "c"
},
b: {
f: {
x: "world"
},
g: "hello"
},
};
console.log(retreiveValueFromObject("b.f.x", someObject));
setValueInObject("b.f.y", "newValue", someObject);
console.log(someObject);
What you are looking for is a little bit of recursion, I just implemented the update method.
let localStorageHandler = function() {
let b = window.localStorage,
_t = this;
_t.get = function(a) {
try {
return JSON.parse(b.getItem(a))
} catch (c) {
return b.getItem(a)
}
};
function descendAndUpdate(obj, path, value) {
let current = path[0],
remainingPath = path.slice(1);
// found and update
if (obj.hasOwnProperty(current) && remainingPath.length === 0) {
obj[current] = value;
// found but still not there
} else if (obj.hasOwnProperty(current)) {
return descendAndUpdate(obj[current], remainingPath, value);
}
// if you want do add new properties use:
// obj[current] = value;
// in the else clause
else {
throw('can not update unknown property');
}
}
_t.set = function(path, value) { // Update a single value or object
if (~path.indexOf(".")) {
let o = path.split(".")[0],
p = this.get(o),
q = path.split(".").slice(1);
descendAndUpdate(p, q, value);
console.log(p);
b.setItem(o, JSON.stringify(p));
return p;
} else {
b.setItem(path, JSON.stringify(value));
return this.get(path);
}
};
_t.remove = function(a) { // removes a single object from localstorage
let c = !1;
a = "number" === typeof a ? this.key(a) : a;
a in b && (c = !0, b.removeItem(a));
return c
};
};
let local = new localStorageHandler();
// Create user and session info if it doesn't exist
let blankUser = new Object({
alias: '',
dob: '',
PIN: '',
level: 0,
name: {
first: '',
last: '',
mi:'',
},
session: {
token: '',
timeout: true,
lastChange: Date.now()
}
});
local.remove('user');
// Loads user data into localstorage
if (!local.get('user')) {
local.set('user', blankUser);
}
local.set('user.session.timeout', false);
local.set('user.name', {first:'john', last:'doe', mi:'c'});
local.set('user.PIN', 8675309);
// new property
// local.set('user.sunshine', { 'like': 'always' });
console.log(local.get('user'));
A friend of mine would always prefer stacks over recursion, which would be a second option. Anyway I agree with many of the comments here. You already know your domain model. Unless you have a very good reason for this approach spend more time on serializing and unserializing those objects in the database. I have the impression you would be able to work with your data in a more natural way because the aspect of updating fields in a database would be abstracted away.
I am working on a similar project at the moment. What I am doing is storing the data in something I called a WordMatrix (https://github.com/SamM/Rephrase/blob/master/WordMatrix.js), maybe you could use something like it in your solution.
My project is a WIP but the next step is literally to add support for localStorage. The project itself is a database editor that works with key => value stores. You can view the prototype for it here: (https://samm.github.io/Rephrase/editor.html)
Once I have implemented the localStorage aspect I will update this post.
Your topic reminds me one recent another topic.
Trying to enhance the answer I provided, I propose you these functions:
// Function to get a nested element:
function obj_get_path(obj, path) {
return path.split('.').reduce((accu, val) => accu[val] || 'Not found', obj);
}
// Function to set a nested element:
function obj_set_path(obj, path, value) {
var result = obj;
var paths = path.split('.');
var len = paths.length;
for (var i = 0; i < len - 1; i++) {
result = result[paths[i]] || {};
}
result[paths[len - 1]] = value;
return obj;
}
// Example object
var obj = {
name0: 'A name',
level0: {
name1: 'An other name',
level1: {
level2: {
name3: 'Name to be changed',
text3: 'Some other text'
}
}
}
}
// Use of the function
obj = obj_set_path(obj, 'level0.level1.level2.name3', 'Takit Isy');
obj = obj_set_path(obj, 'level0.level1.level2.new3', 'I’m a new element!');
var obj_level2 = obj_get_path(obj, 'level0.level1.level2');
// Consoling
console.log('Consoling of obj_level2:\n', obj_level2);
console.log('\nConsoling of full obj:\n', obj); // To see that the object is correct
⋅
⋅
⋅
We could also adapt the 2nd function in my above snippet so that it works for both get and set, depending of if "value" is set:
// We could also adapt the second function for both uses:
function obj_path(obj, path, value = null) {
var result = obj;
var paths = path.split('.');
var len = paths.length;
for (var i = 0; i < len - 1; i++) {
result = result[paths[i]] || {};
}
// Return result if there is no set value
if (value === null) return result[paths[len - 1]];
// Set value and return
result[paths[len - 1]] = value;
return obj;
}
// Example object
var obj = {
name0: 'A name',
level0: {
name1: 'An other name',
level1: {
level2: {
name3: 'Name to be changed',
text3: 'Some other text'
}
}
}
}
// Use of the function
obj = obj_path(obj, 'level0.level1.level2.name3', 'Takit Isy');
obj = obj_path(obj, 'level0.level1.level2.new3', 'I’m a new element!');
var obj_level2 = obj_path(obj, 'level0.level1.level2');
// Consoling
console.log('Consoling of obj_level2:\n', obj_level2);
console.log('\nConsoling of full obj:\n', obj); // To see that the object is correct
Hope it helps.
How about:
function parse(str) {
var arr = str.split('.');
return function(obj) {
return arr.reduce((o, i) => o[i], obj);
}
}
let foo = {
a: {
b: {
c: {
bar: 0
}
}
}
}
let c = parse('a.b.c')(foo);
console.log(c.bar);
c['bar'] = 1;
console.log(foo);

How to convert an array into a hierarchical array

I have some data which is
var currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
The data is flat and I need to convert it into something like:
{
'ticket': 'CAP',
children : [{
'ticket' : 'CT-1',
'children' : [{
'ticket' : 'CT-1-A',
'children' : []
}, {
'ticket' : 'CT-1-B',
'children' : []
}],
[{
'ticket' : 'CT-2',
'children' : []
}]
}]
}
(I think the above is valid)?
I'm very lost as to how. I am going to show my effort but, I'm not sure if my approach is correct or not.
var currentData = [{'ticket':'cap', 'child':'CT-1'},{'ticket':'cap', 'child':'CT-2'}, {'ticket':'CT-1', 'child':'CT-1-A'},{'ticket':'CT-1', 'child':'CT-1-B'}];
var newList = [];
function convert(list){
if (newList.length <= 0){
var child = [];
var emptyChild = [];
child.push({'ticket': list[0].child, 'child': emptyChild });
newList.push({'ticket': list[0].ticket, 'children' : child});
list.splice(0,1);
} // the if statement above works fine
for(var i = 0; i < list.length; i++) {
var ticket = list[i].ticket;
for(var j = 0; j < newList.length; j++) {
if (newList[j].ticket == ticket){
var child;
var emptyChild = [];
child = {'ticket': list[i].child, 'child': emptyChild };
newList[j].children.push(child);
list.splice(i,1);
break;
} // the if above works
else{
var child2 = getFromChildren(ticket, newList, list[i]); // child2 is Always null, even if getFromChildren returns an object
newList[j].children.push(child2);
list.splice(i,1);
break;
}
}
}
if (list.length > 0){
convert(list);
}
}
function getFromChildren(ticket, list, itemToAdd){
if (list == null || list[0].children == null)
return;
for(var i = 0; i < list.length; i++) {
if (list[i] == null)
return;
if (list[i].ticket == ticket){
list[i].child.push(itemToAdd.child); // ** can't do this, javascript passes by value, not by reference :(
} else{
getFromChildren(ticket, list[i].children, itemToAdd);
}
}
}
convert(currentData);
I think I've made a mess of it. In the comments I've put a ** explaining that it isn't working due to JavaScript not passing by reference, however upon further reading I don't think that is correct as I'm passing the object which is by reference?
Edit
The data, shown with currentData will not always start at the root sadly either
function convert(arr) {
var children = {}; // this object will hold a reference to all children arrays
var res = arr.reduce(function(res, o) { // for each object o in the array arr
if(!res[o.ticket]) { // if there is no object for the element o.ticket
res[o.ticket] = {ticket: o.ticket, children: []}; // then creates an object for it
children[o.ticket] = res[o.ticket].children; // and store a reference to its children array
}
if(!res[o.child]) { // if there is no object for the element o.child
res[o.child] = {ticket: o.child, children: []}; // then creates an object for it
children[o.child] = res[o.child].children; // and store a reference to its children array
}
return res;
}, {});
arr.forEach(function(o) { // now for each object o in the array arr
children[o.ticket].push(res[o.child]); // add the object of o.child (from res) to its children array
delete res[o.child]; // and remove the child object from the object res
});
return res;
}
var currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
console.log(convert(currentData));
Explanation:
The reduce part creates an object of the form: { ticket: "...", children: [] } for each element (child or not). So right after reduce, the object res will be:
res = {
'CAP': { ticket: 'CAP', children: [] },
'CT-1': { ticket: 'CT-1', children: [] },
'CT-2': { ticket: 'CT-2', children: [] },
'CT-1-A': { ticket: 'CT-1-A', children: [] },
'CT-1-B': { ticket: 'CT-1-B', children: [] },
}
Now comes the forEach bit which loops over the array once more, and now for each object it fetches the object of .child from res above, push it into .ticket object's children (which a reference to it is stored in children object), then remove the .child object from the object res.
Below uses reduce to get the data grouped to a Map, then I convert the data to an object like you've shown above. You'll need a modern browser to run below snippet, or use a transpiler like babeljs to convert it to es5 syntax.
let currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
let children = currentData.map(e => e.child);
currentData.sort((a,b) => children.indexOf(a.ticket));
let res = currentData.reduce((a,b) => {
if (! children.includes(b.ticket)) {
return a.set(b.ticket, (a.get(b.ticket) || [])
.concat({ticket: b.child,
children: currentData
.filter(el => el.ticket === b.child)
.map(el => ({ticket: el.child, children: []}))}))
}
return a;
}, new Map);
let r = {};
for (let [key,value] of res.entries()) {
r.ticket = key;
r.children = value;
}
console.log(r);
Solution using recursion, starting node can be changed.
var currentData = [{'ticket': 'cap','child': 'CT-1'}, {'ticket': 'cap','child': 'CT-2'}, {'ticket': 'CT-1','child': 'CT-1-A'}, {'ticket': 'CT-1','child': 'CT-1-B'}];
function convert(data, start){
return {
ticket: start,
childs: data.filter(d => d.ticket == start)
.reduce((curr, next) => curr.concat([next.child]), [])
.map(c => convert(data, c))
}
}
let result = convert(currentData, 'cap');
console.log(result);
.as-console-wrapper{top: 0; max-height: none!important;}
I would go with a simple for approach, like this:
var currentData = [
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'},
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'}
];
var leafs = {};
var roots = {};
var tickets = {};
for(var i=0; i<currentData.length; i++){
var ticket = currentData[i].ticket;
var child = currentData[i].child;
if(!tickets[ticket]){
tickets[ticket] = {ticket:ticket,children:[]};
if(!leafs[ticket]){
roots[ticket] = true;
}
}
if(!tickets[child]){
tickets[child] = {ticket:child,children:[]};
}
delete roots[child];
leafs[child] = true;
tickets[ticket].children.push(tickets[child]);
}
for(var ticket in roots){
console.log(tickets[ticket]);
}
Well, if you are not familiar with reduce, map , forEach with callbacks to iterate, then here is a approach I came with, where the code is flat, storing object references in another map object and iterating exactly once the source array.
The code is much cleaner, if something is understandable add comments I will explain;
var currentData = [
{'ticket':'CT-1', 'child':'CT-1-A'},
{'ticket':'CT-1', 'child':'CT-1-B'},
{'ticket':'CAP', 'child':'CT-1'},
{'ticket':'CAP', 'child':'CT-2'}
];
function buildHierarchy(flatArr) {
let root = {},
nonRoot = {},
tempMap = {};
Object.setPrototypeOf(root, nonRoot);
for (let idx = 0; idx < flatArr.length; idx++) {
let currTicket = flatArr[idx];
let tempTicket = tempMap[currTicket.ticket] || {ticket: currTicket.ticket, children: []};
tempMap[currTicket.ticket] = tempTicket;
if (currTicket.child) {
let tempChild = tempMap[currTicket.child] || {ticket: currTicket.child, children: []};
tempTicket.children.push(tempChild);
tempMap[currTicket.child] = tempChild;
delete root[tempChild.ticket];
nonRoot[tempChild.ticket] = true;
}
root[tempTicket.ticket] = true;
}
return tempMap[Object.keys(root)[0]];
}
console.log(buildHierarchy(currentData));
I have changed the sequence of your source array in order to put the root object anywhere, and the code should work on that.

Categories

Resources