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?

Joji
  • 4,703
  • 7
  • 41
  • 86
  • Are you intent on rolling this function yourself? There are packages (e.g., `prettier`) that do this very well already – Nick Jan 04 '21 at 03:11
  • Joji, i added a section to luawtf's answer to show you how you could do this on your own. – Mulan Jan 04 '21 at 05:39
  • @Thankyou thank you for updating the answer! Happy new year. Could you explain a little bit about `t?.constructor` here? – Joji Jan 04 '21 at 05: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](https://stackoverflow.com/search?tab=newest&q=user%3a633183%20switch%20constructor). – Mulan Jan 04 '21 at 06: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. – Joji Jan 04 '21 at 17:53
  • @Thankyou also if the input is something like `{A:"B",C:{D:"E",F:{G:"H",I:"J"}}}` instead of an array it is an object. The output format is off. could you take a look at it? – Joji Jan 04 '21 at 17:58
  • @Joji, the string input is invalid JSON. the solution is not to make a program which supports a broken data format. instead, fix the string, and run `toJson(JSON.parse(fixedString))` to get the proposed output – Mulan Jan 04 '21 at 19:19
  • As for changing the format based on other criteria, I will leave that as an exercise for you. `toJson` is one line of code per input type, and easy to implement any pretty much any set of formatting rules. – Mulan Jan 04 '21 at 19:21

1 Answers1

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
Mulan
  • 129,518
  • 31
  • 228
  • 259
luawtf
  • 576
  • 8
  • 21
  • Except OP starting with a string not object – charlietfl Jan 04 '21 at 03:19
  • 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. – luawtf Jan 04 '21 at 03:25
  • 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 – charlietfl Jan 04 '21 at 03:26
  • 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](https://json5.org/). – luawtf Jan 04 '21 at 03:29
  • 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! – Joji Jan 04 '21 at 05:44