Skip to content

Instantly share code, notes, and snippets.

@Xananax
Last active March 10, 2016 14:52
Show Gist options
  • Save Xananax/a700a263bf83f0207540 to your computer and use it in GitHub Desktop.
Save Xananax/a700a263bf83f0207540 to your computer and use it in GitHub Desktop.
Looks up deep values in a javascript object or array
/**
// Super simple version of the below; if you just want to look up deep values, that's all you need:
function lookup_simple(obj,path,delimiter='.'){
if(obj==null || !path || !path.length){return obj;}
path = Array.isArray(path) ? path : path.split(delimiter);
const rest = path.slice(1);
const current = obj[path[0]];
return (rest.length ? lookup_simple(current,rest) : current)
}
// ...And that's all that's needed for the simple lookup. For a more advanced version, read on
**/
/**
* Get deep values inside objects or arrays. Accepts wildcards ('*')
* and slice ('[x-y]').
* This function only slices the path if it is not an array, then passes
* control to the actual 'lookup' function below
* @param {Object|Array} obj the object to look into
* @param {Array|String} path the path
* @param {String} delimiter delimiter to cut the path with. '.' by default
* @return {any} returned value(s)
*/
function _lookup(obj,path,delimiter='.'){
const parts = isArray(path) ? path : path.split(delimiter);
return lookup(obj,parts);
}
/**
* Actual lookup function. Recursive function that will
* use the next key in an array to find a deep value.
* If the key matches certain parameters, it will pass control
* to other functions.
* @param {Object|Array} obj the object to look into
* @param {Array} path the path
* @return {any}
*/
function lookup(obj,path){
if(obj==null || !path || !path.length){return obj;}
const key = path[0];
const rest = path.slice(1);
// if you simply want a deep value retrieval with no special handling
// of specific characters, just remove everything below
// until 'const current = obj[key]'
// checking if the key matches conditions...
const nextFn = map(key,obj,path);
if(nextFn){ //if it does, pass control to that function
return nextFn(key,obj,rest);
}
// if it doesn't, simply get the next key and stroll along.
const current = obj[key];
return (rest.length ? lookup(current,rest) : current)
}
/**
* Returns a function to apply to an object, if the key
* matches specific conditions. If no filter function is found, the
* function returns undefined.
* Currently, the function checks if the key matches '*', in which case
* it will return the 'gather' function, or if the key matches '[x-y]',
* in which case it will return the 'slice' function. If you want more handling
* of special characters in your path, here is the place to add them.
* @param {String} key the current key
* @param {any} obj An object or an array
* @param {Array} path a path
* @return {Function|undefined} Either a suitable filtering function, or undefined
*/
function map(key,obj,path){
if(key==='*'){
return gather;
}
if(slice_regex.test(key)){
return slice;
}
}
/**
* Retrieves every key in an array that matches the given path.
* If no value is retrieved, the function returns undefined.
* @param {Array} obj the array to look into
* @param {Array} path a path
* @return {Array|undefined} A new array made of the gathered sub-object, or undefined
*/
function gatherArray(obj,path){
const result = []
, length = obj.length
;
let i = 0
, found = false
;
while(i<length){
let ret = lookup(obj[i++],path);
if(!isUndefined(ret)){
found = true;
result.push(ret);
}
}
if(found){return result;}
return;
}
/**
* Retrieves every key in an object that matches the given path.
* If no value is retrieved, the function returns undefined.
* @param {Object} obj the object to look into
* @param {Array} path a path
* @return {Object|undefined} an object with keys matching the path, or undefined
*/
function gatherObject(obj,path){
const result = {}
, keys = Object.keys(obj)
, length = keys.length
;
let i = 0
, found = false;
;
while(i<length){
let k = keys[i++];
let ret = lookup(obj[k],path);
if(!isUndefined(ret)){
found = true;
result[k] = ret;
}
}
if(found){return result;}
return;
}
/**
* Retrieves every key matching a given path in an object or array.
* If the given object is neither an object or an array, the function returns undefined.
* @param {String} key has no use in this function
* @param {Array|Object} obj the object to look into
* @param {Array} path the path
* @return {Array|Object|undefined} A new object or array, or undefined
*/
function gather(key,obj,path){
if(isArray(obj)){
return gatherArray(obj,path);
}
if(isObject(obj)){
return gatherObject(obj,path);
}
return;
}
const slice_regex = /\[(\d+)-(\d+)\]/;
/**
* Slices an array with a key '[x-y]' where x is the start, and y the end
* @param {String} key A key matching the pattern '[x-y]'
* @param {Array} obj An Array
* @param {Array} path A path to further deep-get values
* @return {Array|undefined} An array of results
*/
function slice(key,obj,path){
if(isArray(obj)){
const limits = (key.match(slice_regex) || []);
const [,start,end] = limits;
const sliced = obj.slice(start,end);
if(!path.length){
return sliced;
}
return gatherArray(sliced,path);
}
}
/** Utilities functions **/
function isUndefined(obj){return typeof obj === 'undefined';}
function isArray(obj){return Array.isArray(obj);}
function isObject(obj){return (obj && typeof obj === 'object' && obj.constructor == Object);}
/**
* Quick and dirty testing of the lookup function
* /
function test(){
var test1 = {
a:'A'
, b:'B'
, c:{
cA:'CA'
, cB:'CB'
}
, d:[
{
d0a:'D0A'
, d0b:'D0B'
}
, {
'd1A':'D1A'
, 'd0a':'D1B'
}
]
, e:[
'e0','e1','e2','e3','e4','e5'
]
, f:[
{
a:{
'aa':'FAA'
, 'ab':'FAB'
}
}
, {
a:{
aa:'FBA'
}
}
, {
c:{
aa:'FCA'
}
}
, {
a:{
aa:'FDA'
, ab:'FDB'
}
}
]
}
console.log(_lookup(test1,'d.*.d0a'))
console.log(_lookup(test1,'e.[1-3]'))
console.log(_lookup(test1,'f.*.*.aa'))
console.log(_lookup(test1,'f.*.*.ab'))
}
test();
/**/
exports = module.exports = _lookup;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment