Milos
Milos

Reputation: 1263

Recursive methods using Javascript

I'm trying to replicate the json stringify method but instead use recursion. I've been able to pass alot of test cases but when it comes to nested arrays I seem to be having issues. if there's any empty array inside an array ('[]'), i get something like [,7,9] instead of [[],7,9]. Also if I pass in:

stringifyJSON([[["test","mike",4,["jake"]],3,4]])
"[[test,mike,4,jake,3,4]]"

I thought I was close in getting this to work, but I might have to start over. Do you guys have any ideas on what I might be able to change to make this work for nested examples? Here's the code I have now:

var testarray = [9,[[],2,3]] //should return '[9,[[],2,3]]'
var count = 0
var stringifyJSON = function(obj,stack) {
	var typecheck = typeof obj;
	var resarray = stack;
	if(resarray == null){	//does resarray exist? Is this the first time through?
		var resarray = [];
	}
	if(typeof obj === "string"){	//Is obj a string?
		return '"' + String(obj) + '"';
	}

	if((Array.isArray(obj)) && (obj.length > 0)){  //If not a string, is it an object?
		for(var i = 0; i<obj.length;i++){
			if(Array.isArray(obj[i])){
				var arraytemp = []
				stringifyJSON(arraytemp.push(obj[i]),resarray)   // this is probably incorrect, this is how i handle a nested array situation
			}
			if(typeof obj[i] === 'number'){		//if the number is inside of the array, don't quote it
				resarray.push(obj[i]);
			}
			else if(typecheck === 'object' && Array.isArray(obj[0])){
				resarray.push('[' + obj[i] + ']');
			}
			else{
				resarray.push('"' + obj[i] + '"');
			}
			
			obj.shift()		//delete the first object in the array and get ready to recurse to get to the second object.
			stringifyJSON(obj,resarray);  //remember the new array when recursing by passing it into the next recursive instance
		}
	}

	if(obj !== null && typeof obj === 'object'){	//is obj an object?
		for(var key in obj){
			stringifyJSON(resarray.push(key + '"' + ':' + obj[key]),resarray)
		}
		

	}
	if(typeof obj === "number" || obj == null || obj === true || obj === false){	//special cases and if it's a number
		return '' + obj + ''
	}
	if(typecheck === 'object'){	//a special case where you have an empty array that needs to be quoted.
		return '['+resarray+']'
	}
	return '' + resarray.join('') + '';

	

};

//JSON values cannot be a function, a date, or undefined

Upvotes: 1

Views: 794

Answers (2)

Mulan
Mulan

Reputation: 135277

Do you guys have any ideas on what I might be able to change to make this work for nested examples?

Sure, but it's going to scrap you entire function, so I hope you don't mind. I'll provide a bullet-list of points why this approach is essential and yours is essentially flawed from the get-go :(


Corner cases

This function does a simple case analysis on a non-null data's constructor property and encodes accordingly. It manages to cover a lot of corner cases that you're unlikely to consider, such as

  • JSON.stringify(undefined) returns undefined
  • JSON.stringify(null) returns 'null'
  • JSON.stringify(true) returns 'true'
  • JSON.stringify([1,2,undefined,4]) returns '[1,2,null,4]'
  • JSON.stringify({a: undefined, b: 2}) returns '{ "b": 2 }'
  • JSON.stringify({a: /foo/}) returns { "a": {} }

So to verify that our stringifyJSON function actually works properly, I'm not going to test the output of it directly. Instead, I'm going to write a little test method that ensures the JSON.parse of our encoded JSON actually returns our original input value

// we really only care that JSON.parse can work with our result
// the output value should match the input value
// if it doesn't, we did something wrong in our stringifier
const test = data => {
  return console.log(JSON.parse(stringifyJSON(data)))
}

test([1,2,3])     // should return [1,2,3]
test({a:[1,2,3]}) // should return {a:[1,2,3]}

Disclaimer: it should be obvious that the code I'm about to share is not meant to be used as an actual replacement for JSON.stringify – there's countless corner cases we probably didn't address. Instead, this code is shared to provide a demonstration for how we could go about such a task. Additional corner cases could easily be added to this function.


Runnable demo

Without further ado, here is stringifyJSON in a runnable demo that verifies excellent compatibility for several common cases

const stringifyJSON = data => {
  if (data === undefined)
    return undefined
  else if (data === null)
    return 'null'
  else if (data.constructor === String)
    return '"' + data.replace(/"/g, '\\"') + '"'
  else if (data.constructor === Number)
    return String(data)
  else if (data.constructor === Boolean)
    return data ? 'true' : 'false'
  else if (data.constructor === Array)
    return '[ ' + data.reduce((acc, v) => {
      if (v === undefined)
        return [...acc, 'null']
      else
        return [...acc, stringifyJSON(v)]
    }, []).join(', ') + ' ]'
  else if (data.constructor === Object)
    return '{ ' + Object.keys(data).reduce((acc, k) => {
      if (data[k] === undefined)
        return acc
      else
        return [...acc, stringifyJSON(k) + ':' + stringifyJSON(data[k])]
    }, []).join(', ') + ' }'
  else
    return '{}'
}

// round-trip test and log to console
const test = data => {
  return console.log(JSON.parse(stringifyJSON(data)))
}

test(null)                               // null
test('he said "hello"')                  // 'he said "hello"'
test(5)                                  // 5
test([1,2,true,false])                   // [ 1, 2, true, false ]
test({a:1, b:2})                         // { a: 1, b: 2 }
test([{a:1},{b:2},{c:3}])                // [ { a: 1 }, { b: 2 }, { c: 3 } ]
test({a:[1,2,3], c:[4,5,6]})             // { a: [ 1, 2, 3 ], c: [ 4, 5, 6 ] }
test({a:undefined, b:2})                 // { b: 2 }
test([[["test","mike",4,["jake"]],3,4]]) // [ [ [ 'test', 'mike', 4, [ 'jake' ] ], 3, 4 ] ]


"So why is this better?"

  • this works for more than just Array types – we can stringify Strings, Numbers, Arrays, Objects, Array of Numbers, Arrays of Objects, Objects containing Arrays of Strings, even nulls and undefineds, and so on – you get the idea
  • each case of our stringifyJSON object is like a little program that tells us exactly how to encode each type (eg String, Number, Array, Object etc)
  • no whacked out typeof type checking – after we check for the undefined and null cases, we know we can try to read the constructor property.
  • no manual looping where we have to mentally keep track of counter variables, how/when to increment them
  • no complex if conditions using &&, ||, !, or checking things like x > y.length etc
  • no use of obj[0] or obj[i] that stresses our brain out
  • no assumptions about Arrays/Objects being empty – and without having to check the length property
  • no other mutations for that matter – that means we don't have to think about some master return value resarray or what state it's in after push calls happen at various stages in the program

Custom objects

JSON.stringify allows us to set a toJSON property on our custom objects so that when we stringify them, we will get the result we want.

const Foo = x => ({
  toJSON: () => ({ type: 'Foo', value: x })
})

console.log(JSON.stringify(Foo(5)))
// {"type":"Foo","value":5}

We could easily add this kind of functionality to our code above – changes in bold

const stringifyJSON = data => {
  if (data === undefined)
    return undefined
  else if (data === null)
    return 'null'
  else if (data.toJSON instanceof Function)
    return stringifyJSON(data.toJSON())
  ...
  else
    return '{}'
}

test({toJSON: () => ({a:1, b:2})})  // { a: 1, b: 2 }

Upvotes: 2

coderface
coderface

Reputation: 1

So after playing around with your code, I found that if you replace:

resarray.push('[' + obj[i] + ']');

with:

resarray.push(stringifyJSON(obj[i])) it works well with the arrays and still satisfies your recursive process.

Also I found a few quirks with running it through objects like { hello: 5, arr: testarray, arr2: [1, 2,3, "hello"], num: 789, str: '4444', str2: "67494" }; and found that by changing the stringifying of objects from:

stringifyJSON(resarray.push(key + '"' + ':' + obj[key]),resarray)

to something more like:

stringifyJSON(resarray.push('"' + key + '"' + ':' + stringifyJSON(obj[key])),resarray);

it should workout more of how you want. This is really cool though and I had fun playing around with it!

Upvotes: 0

Related Questions