0

I am well aware of the existence of JSON.parse and JSON.stringify and prettier the npm package. But for some reason I still have to do this by hand. Think of it as a coding interview question and please do not close this question simply because I could have used JSON.parse and JSON.stringify, as I said there is this constraint.

Here is a string "['foo', {bar:['baz',null,1.0,2]}]"

I wanted to implement a function to return a string denoting a json object with proper indentaion.

i.e. the output string should be

   [
        "foo", 
        {
            "bar":
            [
                "baz", 
                null, 
                1.0, 
                2
            ]
        }
    ]

Here is my attempt

function printJSON(str) {
    let spaces = [];
    let output = '';

    str.split('').forEach(char => {
        switch(char) {
            case '{':
            case '[':
                spaces.push('    ');
                output += char + '\n' + spaces.join('');
                break;
            case '}':
            case ']':
                spaces.pop();
                output += '\n' + spaces.join('') + char;
                break;
            case ',':
                output += char + '\n' + spaces.join('');
                break;
            default:
                output += char;
                break;
        }
    });

    console.log(output);
    return output
}

However the output format is slightly off as in

[ 
    "foo", 
     { 
        bar:[ // 🚨
            "baz", 
            null, 
            1.0, 
            2 
        ] 
    } 
] 

How can I fix this format issue? also is there a more elegant way or alternative way to achieve this?

8
  • Are you intent on rolling this function yourself? There are packages (e.g., prettier) that do this very well already Commented Jan 4, 2021 at 3:11
  • Joji, i added a section to luawtf's answer to show you how you could do this on your own. Commented Jan 4, 2021 at 5:39
  • @Thankyou thank you for updating the answer! Happy new year. Could you explain a little bit about t?.constructor here? Commented Jan 4, 2021 at 5:40
  • t.constructor allows us to check the type of any t. however because we want to allow t to be null or undefined, we use nullish coalescing ?. to prevent property lookup on a null. You can see this technique used in some of my other answers. Commented Jan 4, 2021 at 6:27
  • @Thankyou thank you for the update! However, it seems like it doesn't work when input is string. e.g. if input is "['foo', {bar:['baz',null,1.1,2]}]", the output will be an unformatted string. Commented Jan 4, 2021 at 17:53

1 Answer 1

2

Have you tried JSON.stringify's built in pretty printing?

Eg:

console.log(JSON.stringify(
  ['foo', { bar: ['baz', null, 1.0, 2 ] } ],
  null,
  4 // Indent, specify number of spaces or string
));

Outputs:

[
    "foo",
    {
        "bar": [
            "baz",
            null,
            1,
            2
        ]
    }
]

If you want to use a string as input to a function that prettifies your code:

function pretty(string) {
  var object = JSON.parse(string) /* or eval(string) */;
  return JSON.stringify(object, null, 4);
}

Rolling your own solution is not difficult however. We start by writing -

const input =
  ['foo', {bar:['baz',null,1.1,2]}]

const result =
  toJson(input)

Where toJson does a simple type analysis on input t -

function toJson (t, i = "  ", d = 0)
{ function toString(t)
  { switch (t?.constructor)
    { case Object:
        return [ "{", indent(Object.entries(t).map(([k, v]) => `${k}:\n${toString(v)}`).join(",\n"), i, d + 1), "}" ].join("\n")
      case Array:
        return [ "[", indent(t.map(v => toString(v)).join(",\n"), i, d + 1), "]" ].join("\n")
      case String:
        return `"${t}"`
      case Boolean:
        return t ? "true" : "false"
      default:
        return String(t ?? null)
    }
  }
  return indent(toString(t), i, d)
}

Where indent is -

const indent = (s, i = "  ", d = 0) =>
  s.split("\n").map(v => i.repeat(d) + v).join("\n")

Expand the snippet below to run toJson in your own browser -

function toJson (t, i = "  ", d = 0)
{ function toString(t)
  { switch (t?.constructor)
    { case Object:
        return [ "{", indent(Object.entries(t).map(([k, v]) => `${k}:\n${toString(v)}`).join(",\n"), i, d + 1), "}" ].join("\n")
      case Array:
        return [ "[", indent(t.map(v => toString(v)).join(",\n"), i, d + 1), "]" ].join("\n")
      case String:
        return `"${t}"`
      case Boolean:
        return t ? "true" : "false"
      default:
        return String(t ?? null)
    }
  }
  return indent(toString(t), i, d)
}

const indent = (s, i = "  ", d = 0) =>
  s.split("\n").map(v => i.repeat(d) + v).join("\n")

const input =
  ['foo', {bar:['baz',null,1.1,2]}]

const result =
  toJson(input)

console.log(result)

The result is as required -

[
  "foo",
  {
    bar:
    [
      "baz",
      null,
      1.1,
      2
    ]
  },
  true
]

You can customise the indentation by specifying a second argument, such as a tab character -

const result =
  toJson(input, "\t") // indent using a tab

Or use a custom amount of spaces -

const result =
  toJson(input, "    ") // indent using a four (4) spaces
Sign up to request clarification or add additional context in comments.

5 Comments

Except OP starting with a string not object
Using a string instead of an object is easy (via JSON.parse or eval), but since you mentioned it, I've edited the answer and added an example function.
eval yes but OP's string has single quotes which will fail using JSON.parse(). Click on <> in answer editor can make a runnable stack snippet here in page
That's why I included eval as an option. However, eval === evil and should not be used, especially with unknown input, but I don't think including a JSON parsing function capable of handling object properties without quotes would be helpful. If anyone is looking for a JSON parser that supports object properties without quotes, check out JSON5.
Thanks for updating the answer! I am trying to comprehend this. Could you explain a little bit about the usage of t?.constructor here? And maybe add a few comments on how you constructed this answer? I am actually having a bit of a hard time following😅 . Thank you!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.