1
\$\begingroup\$

I've written a function to prettify a json object, as if it were using JSON.stringify.

I'm only focused on strings and other objects, nothing else.

Is there a better way of doing this? perhaps using a data structure?

const exampleJson = {"name":"Jon","facts":{"car":"Ford","address":{"city":"New York"},"watch":"Casio","other": {}}};

function prettyPrint(data) {
  let tabbify = n => Array(n).fill(' ').join('');

  let traverse = (data, tab = 1) => {
    let output = "",
        open   = `{\n`,
        close  = `\n${tabbify(tab - 1)}}`;

    output += open;
    
    Object.entries(data).forEach(([key, val], i, {length}) => {
      if (typeof val === 'string') {
        output += `${tabbify(tab)} "${key}": "${val}"`;
      } else if (typeof val === 'object') {
        if (Object.keys(val).length > 0) {
          output += `,\n${tabbify(tab)} "${key}": ${traverse(val, tab+2)}${i === length - 1 ? '' : ',\n'}`;
        } else {
          output += `,\n${tabbify(tab)} "${key}": {}`;
        }
      }
    })

    output += close;
    return output;
  }

  return traverse(data);
}

Output

{
    "name": "Jon",
    "facts": {
        "car": "Ford",
        "address": {
            "city": "New York"
        },
        "watch": "Casio",
        "other": {}
    }
}
\$\endgroup\$
3
\$\begingroup\$

I am prejudiced against variables named data.

You need to escape literal ", \, and \n characters in quoted strings. It's good form to do tabs too.

Using a dispatch table for types (instead of if/else) makes it easy to handle all the types.

Instead of passing tab up and down the call stack, wrap the calls in a function that indents a block of text. Nested traverse will be nested in indent too and get indented the right number of times.

Instead of checking whether you're at the end of a delimited list (to decide whether to add a delimiter), use arr.join(delimiter). It puts delimiters on inner boundaries only.

Array(n).fill(' ').join(''); is the same as ' '.repeat(n).

It would be nice to omit the quotes on property names that don't need them. Doing this perfectly happens to be really laborious—the regex is 11 kilobytes long!! It's manageable if we limit the exceptions to 7-bit ASCII, which is probably a good idea anyway.

function prettyPrint(obj) {
    const stringify = {
        "undefined": x => "undefined",
        "boolean":   x => x.toString(),
        "number":    x => x,
        "string":    x => enquote(x),
        "object":    x => traverse(x),
        "function":  x => x.toString(),
        "symbol":    x => x.toString()
    },
    indent = s => s.replace(/^/mg, "  "),
    keywords = `do if in for let new try var case else enum eval null this true 
            void with await break catch class const false super throw while 
            yield delete export import public return static switch typeof 
            default extends finally package private continue debugger 
            function arguments interface protected implements instanceof`
       .split(/\s+/)
       .reduce( (all, kw) => (all[kw]=true) && all, {} ),
    keyify = s => ( !(s in keywords) && /^[$A-Z_a-z][$\w]*$/.test(s) ? s : enquote(s) ) + ": ",
    enquote = s => s.replace(/([\\"])/g, '\\$1').replace(/\n/g,"\\n").replace(/\t/g,"\\t").replace(/^|$/g,'"'),
    traverse = obj =>  [ 
           `{`,
            indent( Object.keys(obj) 
                    .map( k => indent( keyify(k) + stringify[ typeof obj[k] ](obj[k]) ) )
                    .join(",\n")
                    ),
            `}`
        ]
        .filter( s => /\S/.test(s) )
        .join("\n")
        .replace(/^{\s*\}$/,"{}");
    return traverse(obj);
}

console.log(prettyPrint( 
    {
        "name":"Jon",
        "facts":{
            "car":"Ford",
            "address":{
               "city":"New York"
            },
            "watch":"Casio",
            "other": { 
                "true":false, 
                blargh:undefined, 
                "111number":1e5, 
                method:function(x){x++} 
            }
        }
    } 
));

\$\endgroup\$
1
\$\begingroup\$

A quick warning.

Javascript objects are referenced and that means objects can contain cyclic references. You function does not check for cyclic references. Cyclic references are very common in JavascripT so you should protect against the potential error.

Some solutions.

  • You can use a Set to track which objects have already been processed and step over repeats.
  • Add a depth argument that limits the recursion depth.

You can just let it throw a call stack overflow when the recursion gets too deep. However a warning, some browsers support tail call optimization and can recurse infinitely. Your function is not currently a tail call, but be aware as it could force the user to crash / close the page.

Example of cyclic object

 const A = {A: "Prop B be will reference Self" };
 A.B = A;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.