Navigating to previous and next item in a Javascript object - javascript

I want to navigate up and down an JavaScript Object given an item of an object. An example object:
var items = {
"5" : ["name", "link2"],
"8" : ["name 2", "link 2"],
"11" : ["name 3", "link 3"]
}
Now, given I have the item items["8"]. I want to get previous and next item respectively.
getItemBefore = function(index) {
// index holds 8
// should return "5"
}
getItemAfter = function(index) {
// index hold 8
// should return 11
}
How can I get those items?

The keys that an object holds can be retrieved as an array with Object.keys(<var>). The order of the keys in the array is arbitrary; you need to sort them before indexing. An option is the built-in sort() method for arrays, which is especially useful because custom comparison functions can be provided (see below). Default order is alphabetical.
Once you get the ordered array, you only need to look up where your item is in the array and return the next and previous elements from it:
var keys = Object.keys(items).sort();
var loc = keys.indexOf(item);
Given that loc > -1 (that is, the item exists):
Previous item: items[keys[loc-1]], but check that loc > 0 (it's not the first one).
Next item: items[keys[loc+1]], but check that loc < keys.length (it's not the last one).
Object.keys is compatible with Javascript 1.85+; here is a workaround for older browsers.
Alternative orderings
Numerical
If you want the keys to have a numerical order, use this comparison function:
var keys = Object.keys(items).sort( function(a,b) {
return b - a;
});
Creation (or Modification) Time
If you want to work with creation order instead of alphanumeric, the items must hold their creation time. Something like:
<value>.index = Date.getTime();
items['<item>'] = <value>;
Then, the sort() method needs the following comparison function:
var keys = Object.keys(items).sort( function(a,b) {
return b.index - a.index;
});
This can be easily extended to last modification ordering or similar.
Creation Order
Notice that the former solution only works if the items are created more than 1 ms apart, which would be suitable for user actions. If the items are added faster, use this instead of the timestamp:
<value>.index = Object.keys(items).length;
Or, alternatively, keep an external counter with the number of items in the object.

This answer builds upon #lemonzi's answer, see his answer for more detailed explanations.
I just wanted to add a working example for everyone struggling with this:
var items = {
"5": ["name", "link2"],
"8": ["name 2", "link 2"],
"11": ["name 3", "link 3"]
};
var getItem = function(key, i) {
var keys = Object.keys(items).sort(function(a,b){return a-b;});
var index = keys.indexOf(key);
if ((i==-1 && index>0) || (i==1 && index<keys.length-1)) {index = index+i;}
return items[keys[index]];
}
console.log(getItem("8",-1));
console.log(getItem("8",+1));
jsfiddle: https://jsfiddle.net/p09yL01f/3/
This way you only need one function to change item.
The .sort(function(a,b){return a-b;}) sorts the array numerical.
The if ((i==-1 && index>0) || (i==1 && index<keys.length-1)) { checks that the item is not the first or the last index in the array, and if it is, it returns the index itself.
If you want to display a message if the item is the first or the last, use this instead:
if ((i==-1 && index>0) || (i==1 && index<keys.length-1)) {return items[keys[index+i]];}
else {return (i==1?"last":"first")+" item";}
var items = {
"5": ["name", "link2"],
"8": ["name 2", "link 2"],
"11": ["name 3", "link 3"]
};
var getItem = function(key, i) {
var keys = Object.keys(items).sort(function(a,b){return a-b;});
var index = keys.indexOf(key);
if ((i==-1 && index>0) || (i==1 && index<keys.length-1)) {return items[keys[index+i]];}
else {return (i==1?"last":"first")+" item";}
}
console.log(getItem("5",-1));
console.log(getItem("11",+1));
jsfiddle: https://jsfiddle.net/p09yL01f/4/
The (i==1?"last":"first") is called a ternary operator, read about it.

This is a bit tricky, because technically according to the ECMAScript specifications, the order of properties in an object is implementation specific. That means there's no guarantee in the language that your properties will be in the same order each time you access them. If you're relying on that, there's a potential there could be a problem.
See this bug in Chrome. It states that Chrome doesn't promise to return your keys in any particular order, and it's been marked "WONTFIX" for 4-5 years now. That means that you can't rely on that behavior in Chrome, and here's another post indicating that now IE9 has the same "issue".
So, I would recommend that you create your own object to manage your keys, and use a JavaScript object behind the scenes to store your keys.
Here's something I threw together, it doesn't support deletes and it has no bounds checking, but it should serve your purposes. Fiddle here.
function orderedObject()
{
this.keys = [];
this.keyIndex = {};
this.store = {};
}
orderedObject.prototype.addItem = function(key,value)
{
this.keyIndex[key] = this.keys.length;
this.keys.push(key);
this.store[key] = value;
}
orderedObject.prototype.getItem = function(key)
{
return this.store[key];
}
orderedObject.prototype.getKeyBefore = function(key)
{
return this.keys[this.keyIndex[key] - 1];
}
orderedObject.prototype.getKeyAfter = function(key)
{
return this.keys[this.keyIndex[key] + 1];
}
var testObject = new orderedObject();
testObject.addItem("5" , ["name", "link2"]);
testObject.addItem("8" , ["name 2", "link 2"]);
testObject.addItem("11" , ["name 3", "link 3"]);
console.log(testObject.getKeyBefore("8"))
console.log(testObject.getKeyAfter("8"))

Would an array of objects be more appropriate here? The example below could be wrapped in an object and making the nextItem() and prevItem() methods. With this you might also need some bounds checking.
var items = [];
items[0] = {index : '5', name : 'name', link : 'link2'};
items[1] = {index : '8', name : 'name 2', link : 'link 2'};
items[2] = {index : '11', name : 'name 3', link : 'link 3'};
//then access them like this
var nextItem = function (index) {
var i = 0,
max = items.length;
for (i; i < max; i += 1) {
if (items[i].index === index) {
return items[i + 1];
}
}
return 'not found';
};
var prevItem = function (index) {
var i = 0,
max = items.length;
for(i; i < max; i += 1) {
if (items[i].index === index) {
return items[i - 1];
}
}
return 'not found';
};
//the nextItem object after 5 is shown in the console.
console.dir(nextItem('5'));

Related

Remove singular element from an object's key array

I have an object that has multiple keys and each of these keys has an array storing multiple elements. I want to be able to remove a specified element from the key's array.
I have tried using the delete keyword as well as the filter method, but I have been unsuccessful. I'm a total newbie to JS so I appreciate any assistance. Also, I want to do this using ONLY JavaScript, no libraries.
Here is the code where I am creating my object:
function add(task, weekdayDue) {
let capitalWeekday = weekdayDue.charAt(0).toUpperCase() +
weekdayDue.slice(1);
if (toDoList[capitalWeekday] === undefined) {
let subArr = [];
toDoList[capitalWeekday] = subArr.concat(task);
} else {
toDoList[capitalWeekday].push(task);
}
}
and here is the code as I have it now. Clearly it is not producing the correct result:
function remove(task, weekdayDue) {
let capitalWeekday = weekdayDue.charAt(0).toUpperCase() +
weekdayDue.slice(1);
delete toDoList.capitalWeekday[task]
//the below code is working; i want to send this to another
array
if (archivedList[capitalWeekday] === undefined) {
let subArr = [];
archivedList[capitalWeekday] = subArr.concat(task);
} else {
archivedList[capitalWeekday].push(task);
}
};
add('laundry', 'monday');
add('wash car', 'monday');
add ('vacuum', 'tuesday');
add('run errands', 'wednesday');
add('grocery shopping', 'wednesday');
// the output is: { Monday: [ 'laundry', 'wash car' ],
Tuesday: [ 'vacuum' ],
Wednesday: [ 'run errands', 'grocery shopping' ] }
Then let's say I want to remove 'wash car' from Monday I was trying:
remove('wash car', 'monday');
console.log(toDoList)
// The output is an empty object {}
I personally would refactor a bit your code, but I've worked a bit around it to fix some issues.
First of all, you shouldn't use delete for your scenario, because it will reset the item at the nth position of the array with the default value, which is undefined.
Usually, for that kind of operations, since you deal with strings, you rather take a look at the first occurrence of your item in the array, take its index, and use splice (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice) to actually remove the item from the array.
In this way, you end up with a clean array without invalid items in it.
Below is the working code (with the mentioned fixes) that does what you asked. As a side note, I would suggest you to avoid working with strings for such purposes, but I would rather tackle objects with unique ids, so that it's significantly easier to keep track of them between arrays and objects.
Additionally, there are some cases that you didn't think about, for instance I can think about calling remove by giving an invalid task, so you may work a bit around the code below to handle the case where taskIndex is -1 (meaning that no item was found with that index).
var toDoList = {}, archivedList = {};
function add(task, weekdayDue) {
let capitalWeekday = weekdayDue.charAt(0).toUpperCase() + weekdayDue.slice(1);
if (toDoList[capitalWeekday] === undefined) {
let subArr = [];
toDoList[capitalWeekday] = subArr.concat(task);
} else {
toDoList[capitalWeekday].push(task);
}
}
function remove(task, weekdayDue) {
let capitalWeekday = weekdayDue.charAt(0).toUpperCase() + weekdayDue.slice(1);
let taskIndex = toDoList[capitalWeekday].indexOf(task);
toDoList[capitalWeekday].splice(taskIndex, 1);
//delete toDoList[capitalWeekday][taskIndex];
if (archivedList[capitalWeekday] === undefined) {
let subArr = [];
archivedList[capitalWeekday] = subArr.concat(task);
} else {
archivedList[capitalWeekday].push(task);
}
};
add('test', 'monday');
add('wash car', 'monday');
remove('wash car', 'monday');
console.log(toDoList);
console.log(archivedList);
You are on the right path. Maybe the trouble you had with filter is because filter will return a new Array and not modify the current one. You could update your remove function and replace the line:
delete toDoList.capitalWeekday[task]
with
toDoList.capitalWeekday = toDoList.capitalWeekday.filter((item) => {return item !== task});
function remove(task, weekdayDue) {
let capitalWeekday = weekdayDue.charAt(0).toUpperCase() +
weekdayDue.slice(1);
// Assign new array with all elements but task
toDoList[capitalWeekday] = toDoList[capitalWeekday].filter(i => i !== task)
};
add('foo'...
add('bar'...
"{
"Baz": [
"Foo",
"Bar"
]
}"
remove('foo'...
"{
"Baz": [
"Bar"
]
}"

JavaScript: Can a nested object value reference its parent object key?

I have an object as follows:
var obj = {
parentKey : {
nestedKey1 : value,
nestedKey2 : function () {
return parentKey + nestedKey2;
}
}
};
Is there a way to use the values within the actual name of the parentKey and nestedKey1 and/or nestedKey2 in a function held in one of the nested key-value pairs as the one above?
In my actual scenario the keys are all numerical values I wish to use as criteria for a for loop.
Updated Scenario To Question
As an exercise to learn JavaScript I decided to code the logic of an elevator system. To do this I have made an object which represents the building and this object contains each floor, nested within each floor are the data of the number of people wishing to travel to another floor as follows:
var floorData = {
<floor number> : {
<people going to floor X> : <number of people>,
<people going to floor Y> : <number of people>,
peopleGoingUp : <formula that sums amount of people going up>,
peopleGoingDown : <formula that sums amount of people going down>
}
};
I want to include within each <floor number> object a property that sums the amount of peopleGoingUp and peopleGoingDown by means of a formula. For this formula to work as I intend I need to access the <floor number> name which is a numeric value from within the peopleGoingUp and peopleGoingDown formulas.
Here is my working example thus far and I have included peopleGoingUp and peopleGoingDown formulas in floor theBuilding[2]. What I wish for is to change the hand entered value of theParentKey to equal the name of the parent object.
// Total number of floors in building global object
var totalFloors = 3;
// Building object including amount of people waiting to take lift to other floors on each level of the building
var theBuilding = {
0 : {
1 : 2,
2 : 0,
3 : 1,
},
1 : {
0 : 1,
2 : 3,
3 : 2,
},
2: {
0 : 2,
1 : 1,
3 : 4,
goingUp : function () {
var sum = 0;
var theParentKey = 2; // this is supposed to be a direct reference to the parent object name to this nested object and thus equal to 2
for (i = 0; i <= totalFloors; i++) { // loop for total floors
if (i !== theParentKey && i >= theParentKey) {//sum if i isn't = this floor number and that i is greater than this floor number
sum += this[i]
}
};
return sum;
},
goingDown : function () {
var sum = 0;
var theParentKey = 2; // this is supposed to be a direct reference to the parent object name to this nested object and thus equal to 2
for (i = 0; i <= totalFloors; i++) { // loop for total floors from lowest
if (i !== theParentKey && i <= theParentKey) { //sum if i isn't = this floor number and that i is less than this floor number
sum += this[i]
}
};
return sum;
},
3 : {
0 : 0,
1 : 1,
2 : 4
}
};
console.log(theBuilding[2].goingUp()); // 4
console.log(theBuilding[2].goingDown()); // 3
Is there a way to use the values within the actual name of the
parentKey and nestedKey1 and/or nestedKey2 in a function held in one
of the nested key-value pairs as the one above?
You can do it in two ways.
Using lexical scope
The function inside nestedKey2 has access to the global scope. You can therefore reach every property inside obj using .:
obj.Parentkey
Using this
This is a bit more tricky as this inside a function in a multilevel object will point to the "same level" of the object as the function resides in. You can use this together with . to reach the lower levels of the object but there is no way to reach the higher levels.
You can, however, use a workaround implementing a circular reference:
var myObj = {
levelAccess: function() {
this.two.__ = this;
return this;
},
one: 'one',
two:
{
__: [],
myFunction: function () {return this.__.one}
}
}.levelAccess();
This solution requires that you add a __ property for each level that needs access to a higher level and then initialises all the __ properties via the levelAccess function. The solution should not cause any memory leaks, see this.
If I understood your question, you want to get names of the keys. You can use Object.keys() while passing the current object this.
var obj = {
parentKey : {
nestedKey1: 3,
nestedKey2: function () {
return Object.keys(this);
}
}
};
console.log(obj.parentKey.nestedKey2()); // ['nestedKey1', 'nestedKey2']
See this is an example
var obj = {
parentKey : {
nestedKey1 : "value",
nestedKey2 : function () {
var c = obj.parentKey;
//console.log("we get "+ c.nestedKey1 +" & "+ c.nestedKey3);
//you can update values
//obj.parentKey.nestedKey1 = "new value";
console.log("we get "+ c.nestedKey1 +" & "+ c.nestedKey3);
return "we get "+ c.nestedKey1 +" & "+ c.nestedKey3;
},
nestedKey3 : "another value"
}
};
obj.parentKey.nestedKey2();
var an = window["obj"]["parentKey"]["nestedKey3"];
console.log(an);

How to filter out Duplicates of an array of objects

I have an array of objects which look like this:
$scope.SACCodes = [
{'code':'023', 'description':'Spread FTGs', 'group':'footings'},
{'code':'024', 'description':'Mat FTGs', 'group':'footings'},
{'code':'025', 'description':'CONT. FTGs', 'group':'footings'},
{'code':'025', 'description':'CONT. FTGs', 'group':'levels'},
{'code':'023', 'description':'Trucks', 'group':'footings'}
]
I need to filter out duplicates where the code and the group are duplicates. If only one of them is the same it shouldn't filter it out.
Here is another approach based on TLindig's answer to a similar question.
Add a filter method to the scope:
$scope.onlyUnique = function(value, index, self) {
codes = self.map(function(c) {return c.code});
groups = self.map(function(c) {return c.group});
return codes.indexOf(value.code) === index || groups.indexOf(value.group) === index;
Call the filter method in your ng-repeat or wherever you want the unique values:
<div ng-repeat="c in SACCodes.filter(onlyUnique)">code: {{c.code}} desc: {{c.description}} group: {{c.group}}</div>
Output:
code: 023 desc: Spread FTGs group: footings
code: 024 desc: Mat FTGs group: footings
code: 025 desc: CONT. FTGs group: footings
code: 025 desc: CONT. FTGs group: levels
The ES6 way.
var m = new Map();
SACCodes.forEach ( function( item ) {
var key = item.code + item.group;
if ( !m.has( key ) ){
m.set( key, item );
}
});
SACCodes= [ ...m.values() ];
This uses a helper hash to note which combination of code and group have already been processed. Only if it finds a hitherto unused combination does it add it to the retVal array;
function dedup() {
var dups = {};
var retVal = [];
for (var i = 0; i < $scope.SACCodes.length; i++) {
var sCode = $scope.SACCodes[i];
var key = sCode.code +'/'+ sCode.group;
if (!dups[key]) {
retVal.push (sCode);
dups[key] = sCode;
}
}
return retVal;
}
See working example
Couple of years down the road you could use Object.values(dups); instead of retVal and thereby shorten the code.

refactor two $.each() loops to one

I have a JSON object like this...
{
"tasks":[
{
"id":"task_3",
"taskName":"Task A",
"assignee":"Barrack Obama",
"timeReqOptimisitic":"4",
"timeReqNormal":"8",
"timeReqPessimistic":"14",
"timeUnit":"Days",
"timeReq":"8.33",
"positionX":493,
"positionY":101,
"lockStatus":"unlocked"
}
],
"milestones":[
{
"id":"task_1",
"milestoneName":"Start",
"positionX":149,
"positionY":109,
"lockStatus":"unlocked",
"milestoneDate":"2015-04-07"
},
{
"id":"task_2",
"milestoneName":"Finish",
"positionX":989,
"positionY":367,
"lockStatus":"unlocked",
"milestoneDate":"2015-04-22"
}
],
"connections":[
{
"connectionId":"con_10",
"pageSourceId":"task_1",
"pageTargetId":"task_3"
},
{
"connectionId":"con_20",
"pageSourceId":"task_3",
"pageTargetId":"task_2"
}
]
}
...this is a minimal version. In practice, there are numerous items in "tasks", "milestones" and "connections".
I need to iterate through the object and determine the "id" of the "milestones" item with the lowest/earliest "milestoneDate", then identify the "connections" item that has the same value for its "pageSourceId" and return its "pageTargetId".
So in the above example:
Step 1) Iterate through the object and determine the "id" of the "milestones" item with the lowest/earliest "milestoneDate".
Answer: milestones.id = "task_1"
Step 2) Identify the "connections" item that has the same value for its "pageSourceId".
Answer: connections.pageSourceId = "task_1"
Step 3) Return its "pageTargetId".
Answer: "task_3"
I have a working example here. However, I would like to know if there is a way to accomplish this without using the extremely high start date and also in one loop.
As you are not parsing the same array on these two loops, there is no way to merge your loops.
Anyway, you can yet remove the loops to access to the arrays:
http://jsfiddle.net/gael/sruvtwre/2/
$.each(object.milestones, function( index, value ) {
if(startDate > parseDate(value.milestoneDate)) {
startDate = parseDate(value.milestoneDate);
id = value.id
}
});
$.each(object.connections, function( index, value ) {
if(id == value.pageSourceId) {
pageTargetId = value.pageTargetId;
}
});
May be also sorting, and indexing your datas. Then you would need no loops:
Elements in milestones should be sorted, so the earliest milestones element would be milestones[0].
Elements in connections should be indexed by their pageTargetId property, so the requested element should be connections[id].
Your two loops would become:
var pageTargetId= object.connections[ object.milestones[0].id ].pageTargetId;
http://jsfiddle.net/gael/sruvtwre/4/
As said in comments, sorting is not an optimal solution, even if that does not really matter for small sets.
Roughly, there is no no needs to sort all the datas, just the latest matters.
You can use array reduce method, as an comparable alternative to a simple loop:
var latestMilestone= object.milestones.reduce(function(milestone1, milestone2){
if( parseDate(milestone1.milestoneDate) > parseDate(milestone2.milestoneDate) )
return milestone1;
else
return milestone2;
//convert date to timestamp
function parseDate(date) {
var parts = date.split('-');
return Date.UTC(parts[0], parts[1]-1, parts[2]); // Note: months are 0-based
}
});
How about this:
Assuming you get the milestones.id = "task_1" in first loop; outside the loop we can have use jQuery grep. As connections will have unique pageSourceId, grep will return an array with only one object.
var filteredData = jQuery.grep('CONNECTIONS_ARRAY', function(element, index){
return element.pageSourceId == 'MILESTONES_ID'; // Which you get in the loop earlier
});
Then we can access pageTargetId like this:
if(filteredData.length){
filteredData[0].pageTargetId;
}
Try
var dates = []
, ids = []
, filtered = $.map(data.milestones, function(value, index) {
dates.push(new Date(value.milestoneDate).getTime());
ids.push(value.id);
if (dates.length === data.milestones.length) {
var id = ids[$.inArray(Math.min.apply(Math, dates), dates)]
, res = $.grep(data.connections, function(task, key) {
return task.pageSourceId === id
})[0].pageTargetId;
return res
}
})[0]; // `"task_3"`
var data = {
"tasks":[
{
"id":"task_3",
"taskName":"Task A",
"assignee":"Barrack Obama",
"timeReqOptimisitic":"4",
"timeReqNormal":"8",
"timeReqPessimistic":"14",
"timeUnit":"Days",
"timeReq":"8.33",
"positionX":493,
"positionY":101,
"lockStatus":"unlocked"
}
],
"milestones":[
{
"id":"task_1",
"milestoneName":"Start",
"positionX":149,
"positionY":109,
"lockStatus":"unlocked",
"milestoneDate":"2015-04-07"
},
{
"id":"task_2",
"milestoneName":"Finish",
"positionX":989,
"positionY":367,
"lockStatus":"unlocked",
"milestoneDate":"2015-04-22"
}
],
"connections":[
{
"connectionId":"con_10",
"pageSourceId":"task_1",
"pageTargetId":"task_3"
},
{
"connectionId":"con_20",
"pageSourceId":"task_3",
"pageTargetId":"task_2"
}
]
};
var dates = []
, ids = []
, filtered = $.map(data.milestones, function(value, index) {
dates.push(new Date(value.milestoneDate).getTime());
ids.push(value.id);
if (dates.length === data.milestones.length) {
var id = ids[$.inArray(Math.min.apply(Math, dates), dates)]
, res = $.grep(data.connections, function(task, key) {
return task.pageSourceId === id
})[0].pageTargetId;
return res
}
})[0];
document.write(filtered);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js">
</script>

accessing field value in array of objects in javascript

I need to access an element with a certain field value for the cdOption field in this array of objects of type possibleOptions:
[Object { cdOption="OPT001", description="Description 1", type="STRING"},
Object { cdOption="OPT002", description="Description 2", type="STRING"},
Object { cdOption="OPT003", description="Description 3", type="STRING"}]
The field value I'm looking for is extracted from antoher object in an array and so I'm alreay in a $.each cycle.
Can I avoid entering another cycle in order to loop the possibleOptions object and look for the specified field value?
I've tried with
possibleOptions[option.cdOpzione] but it doesn't work, is there a way to do this? I know I'm missing something.
current $.each code:
$.each(oldOptions, function(key, option) {
$.each(possibleOptions, function(key, possibleOption) {
if (option.cdOption === possibleOptions.cdOption) {
console.log(option.cdOption);
console.log(possibleOption.description);
}
});
});
In a generic way, you can't avoid the extra cycle. There may be particular solutions though, depending on your circumstances.
Solution 1
You could avoid it if you restructure your data, to have possibleOptions be an object with the values in cdOption as keys and an object with description and type as value.
Example:
var possibleOptions = {
'OPT001' : { description:"Description 1", type:"STRING" },
'OPT002' : { description:"Description 2", type:"STRING" },
'OPT003' : { description:"Description 3", type:"STRING" }
};
var val = 'OPT002';
console.log(possibleOptions[val]);
Solution 2
Another thing you could do if the cdOption is always of the form OPT-index- where -index- is 1+ the index in the array is to parse the value you're looking for, extract the -index-, parseInt and subtract one.
Example:
var val = 'OPT002';
var index = parseInt(val.substring(3))-1;
console.log(possibleOptions[index]);
Demo for both: http://jsbin.com/opojozE/1/edit
Array.filter can return an array of the elements matching a conditon. e.g. if you want to find the object (or objects) with cdOption == "OPT002", you could say:
var matches = possibleOptions.filter(
function( element ) {
return ( "OPT002" == element.cdOption );
}
);
and matches will contain:
[
{ cdOption="OPT002", description="Description 2", type="STRING"}
]
if you're just looking for one match:
var myOption = (matches.length > 0) ? matches[0] : null;
If you need to support older browsers that lack Array.filter, see Array filter method at MDN for a way to add it.

Categories