Home > Blockchain >  Recursion function that converts an object of any nesting into a string
Recursion function that converts an object of any nesting into a string

Time:10-03

I have a simple object:

   let obj = {
      season: 'winter',
      data: {
          month: ['December', 'January', 'February']
         }
    };

I need to get a string like:

`season=winter&data[month][0]=December&data[month][1]=January&data[month][2]=February`

I tried but I didn't completely succeed

function convertToText(obj) {
  let string = [];
  if (typeof(obj) == "object") {
    for (prop in obj) {
      if (obj.hasOwnProperty(prop))
        string.push(prop   "="   convertToText(obj[prop]));
    };
    return string.join("&");
  } else {
    string.push(JSON.stringify(obj))
  }

  return string.join();
}

let obj = {
  season: 'winter',
  data: {
    month: ['December', 'January', 'February']
  }
};

console.log(convertToText(obj));

and gets this:

"season='winter'&data=month=0='December'&1='January'&2='February'"

CodePudding user response:

It's often helpful to keep around a collection of utility functions. I have one, pathEntries, that creates something like what Object.entries does, but instead of a string key, it has an array of string/integer keys giving the whole path in a nested object. The version used here includes only paths to root nodes. It would convert your input into this format:

[
  [["season"], "winter"],
  [["data", "month", 0], "December"],
  [["data", "month", 1], "January"],
  [["data", "month", 2], "February"]
]

Using that, it's quite simple:

const pathEntries = (obj) => Object (obj) === obj
  ? Object .entries (obj) .flatMap (([k, x]) => pathEntries (x) .map (
      ([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v] 
    )) 
  : [[[], obj]]

const toQuery = (obj) => pathEntries (obj) .map (
  ([[p, ...ps], v]) => `${p}${ ps .map (n => `[${ n }]`) .join ('') }=${ v }`
) .join ('&')


const obj = {season: 'winter', data: {month: ['December', 'January', 'February']}}

console .log (toQuery (obj))

Because we know that the paths cannot be empty, we can comfortably extract the first element from them and treat it differently. (It's not wrapped in [ - ]), and we can just build one part of our response string directly using the first node in the path, p, the remaining nodes, ps, and the value, v. Then we join these parts together with &, and we're done.

pathEntries is more interesting. If we're not working on an object, we simply return an array containing a single pair: an empty array for the path, and our input value. If it is an Object, then we use Object .entries to break it down into key-value pairs, recur on the values, and prepend the key to the path of each result.

For this problem we could simplify by replacing this line:

      ([p, v]) => [[Array .isArray (obj) ? Number (k) : k, ... p], v] 

with

      ([p, v]) => [[k, ... p], v] 

The original version yields enough information to reconstitute objects including arrays nested at any depth. With this change, we would turn them into plain objects with keys like "0", "1", etc. That's fine for this problem. But I usually choose the more powerful version, because I might want to reuse it in multiple places in an application.

Notes

User Mulan suggested that continually checking to see whether our key is numeric while we recur is sub-optimal. There are many ways to fix that, including storing it in an IIFE, using a call/bind helper, or using (abusing?) the fact that Object .entries returns a collection of pairs, two-element arrays, to default a third parameter, as demonstrated here:

const pathEntries = (obj) => Object (obj) === obj
  ? Object .entries (obj) .flatMap (
      ([k, x, k1 = Array .isArray (obj) ? Number (k) : k]) => 
        pathEntries (x) .map (([p, v]) => [[k1, ... p], v])
    ) 
  : [[[], obj]]

Mulan also mentioned that the URLSearchParams API is a much better way to build robust URLs. This is entirely correct. We can do so with something like:

const pathEntries = (obj) => Object (obj) === obj
  ? Object .entries (obj) .flatMap (
      ([k, x, k1 = Array .isArray (obj) ? Number (k) : k]) => 
        pathEntries (x) .map (([p, v]) => [[k1, ... p], v])
    ) 
  : [[[], obj]]

const toQuery = (obj) => pathEntries (obj) .map (
  ([[p, ...ps], v]) => [`${p}${ ps .map (n => `[${ n }]`) .join ('') }`, v]
) .reduce ((usp, [k, v]) => ((usp .append (k, v)), usp), new URLSearchParams (''))


const obj = {season: 'winter', data: {month: ['December', 'January', 'February']}}

console .log (toQuery (obj) .toString()) 

Note that the brackets are now correctly encoded. However this is not the exact output that was requested. You will have to decide if that's appropriate for your needs.

CodePudding user response:

Recursively walk through the entire object hierarchy, keeping track of the path which will be used as prefix for the "key=value" pairs.

Arrays can be handled specially - the key for an array value, as well as any of its indexes need to be wrapped in square brackets.

Finally, the base case would be to convert the prefix to the left side of the equal and a plain value as the right side.

const isPlainObject = data =>
  typeof data === "object" 
    && data !== null 
    && !Array.isArray(data);

const arrayKey = (prefix, index) =>
  prefix
    //take every element except the last one:
    .slice(0, -1)
    //append last element and index enclosed in square brackets:
    .concat(`[${prefix.at(-1)}]`, `[${index}]`); 

function convertToText(data, prefix = []) {
  if (isPlainObject(data))
      return Object.entries(data)
        .map(([key, value]) => convertToText(value, prefix.concat(key)))
        .join("&");
  
  if (Array.isArray(data))
    return data
        .map((x, index) => convertToText(x, arrayKey(prefix, index)))
        .join("&")
    
    return `${prefix.join("")}=${data}`;
};

let obj = {
  season: 'winter',
  data: {
    month: ['December', 'January', 'February']
  }
};

console.log(convertToText(obj));

CodePudding user response:

Given obj -

const obj = {
  season: 'winter',
  data: {
    month: ['December', 'January', 'February']
  }
}

Start with a generic flat function to flatten the data -

function *flat(t){
  switch (t?.constructor) {
    case Object:
    case Array:
      for (const [k,v] of Object.entries(t))
        for (const [path, value] of flat(v))
          yield [[k, ...path], value]
      break
    default:
      yield [[], t]
  } 
}
for (const [path, value] of flat(obj))
  console.log(path.join("/"), value)
season winter
data/month/0 December
data/month/1 January
data/month/2 February

Use URLSearchParams to encode the params -

function toSearchParams(t) {
  const r = new URLSearchParams()
  for (const [path, value] of flat(t))
    r.append(
      [path[0], ...path.slice(1).map(v => `[${v}]`)].join(""),
      value
    )
  return r
}
console.log(toSearchParams(obj))
URLSearchParams {
  'season' => 'winter',
  'data[month][0]' => 'December',
  'data[month][1]' => 'January',
  'data[month][2]' => 'February'
}

URLSearchParams can be converted to a string -

console.log(String(toSearchParams(obj)))
console.log(decodeURIComponent(String(toSearchParams(obj))))
season=winter&data[month][0]=December&data[month][1]=January&data[month][2]=February
season=winter&data[month][0]=December&data[month][1]=January&data[month][2]=February

See url.searchParams for use with the URL module that you should be leveraging as well.


As a shortcut above, I treat both object keys and array keys as strings. If we want flat to preserve numeric keys for array values, we can write a different outer loop for each type -

function *flat(t){
  switch (t?.constructor) {
    case Object:
      for (const k of Object.keys(t))
        for (const [path, value] of flat(t[k]))
          yield [[k, ...path], value]   // k: string
      break
    case Array:
      for (let k = 0; k < t.length; k  )
        for (const [path, value] of flat(t[k]))
          yield [[k, ...path], value]   // k: number
      break
    default:
      yield [[], t]
  } 
}

We could collapse the inner loop using an optional path parameter -

function *flat(t, path = []){
  switch (t?.constructor) {
    case Object:
      for (const k of Object.keys(t))
        yield *flat(t[k], [...path, k])
      break
    case Array:
      for (let k = 0; k < t.length; k  )
        yield *flat(t[k], [...path, k])
      break
    default:
      yield [path, t]
  } 
}
  • Related