3
\$\begingroup\$

In a moment of boredom, I decided to write two methods, one of which would create a query string, while the other would parse it, the constraint I imposed on myself being that each method could not exceed one statement. The result is by no means something I'd ever consider using at work or in my personal projects, for that matter; my reason for posting this question is that I'd like to know how, given the constraint I set for myself, the code could be improved on? Like, what's the best code you could write to solve the problem at hand - creating and parsing a query string - without having the two methods constitute more than 2 statements? I'm sure you could do it in a much more elegant manner, and I'm looking forward to any feedback coming my way!

const createQueryString = input => encodeURI(`?${Object.keys(input).map(name 
=> `${name}=${input[name]}`).join('&')}`);

const queryString = createQueryString({
  name: 'Darrell',
  age: '3     2',
  nationality: 'Swedish'
});

console.log(queryString);

const parseQueryString = (input, obj = Object.create(null)) => 
decodeURI(input.slice(input.indexOf('?') + 1, 
input.length)).split('&').map(entry => [
  [entry.split('=')[0]],
  [entry.split('=')[1]]
]).reduce((obj, query) => Object.assign(obj, {
  [query[0][0]]: query[1][0]
}), {});

const parsedQueryString = parseQueryString(queryString);

console.log(parsedQueryString);
\$\endgroup\$
2
\$\begingroup\$

Well this is a fascinating question!

I don't see much you could do in createQueryString besides potentially using Object.entries instead of Object.keys and a slight rearrangement to get rid of the outer template string.

const createQueryString = input => 
  '?' + encodeURI(Object.entries(input)
    .map(([name, value]) => `${name}=${value}`).join('&'));

Do note that there is a problem here if your name or value contains &. To fix this you would need to call encodeURIComponent on each part and then drop the outer enocdeURI call.

const createQueryString = input => 
  '?' + Object.entries(input)
    .map(([name, value]) => 
      `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
    ).join('&');

parseQueryString can be improved in several ways.

  1. You don't use obj = Object.create(null), so get rid of it.
  2. There is a (mostly) a built in way to do this which I will pretend doesn't exist for the rest of this post. (One disadvantage with this method is that it requires a full URI, not just the query string)

    [...new URL(uri).searchParams.entries()]
        .reduce((obj, entry) => Object.assign(obj, { [entry[0]]: entry[1] }), {})
    
  3. You can use regular expressions to simplify the code slightly.

    const parseQueryString = (input) => 
      input.slice(input.indexOf('?') + 1)
      .match(/[\w\d%\-!.~'()\*]+=[\w\d%\-!.~'()\*]+/g)
      .map(s => s.split('=').map(decodeURIComponent))
      .reduce((obj, [key, value]) => Object.assign(obj, { [key]: value }), {})
    

Full demo:

const createQueryString = input => 
  '?' + Object.entries(input)
    .map(([name, value]) => 
      `${encodeURIComponent(name)}=${encodeURIComponent(value)}`
    ).join('&');

const queryString = createQueryString({
  name: 'Darrell',
  age: '3     2',
  nationality: 'Swedish',
  messy: '&hi=bye'
});

const parseQueryString = input => 
  input.slice(input.indexOf('?') + 1)
  .match(/[\w\d%\-!.~'()\*]+=[\w\d%\-!.~'()\*]+/g)
  .map(s => s.split('=').map(decodeURIComponent))
  .reduce((obj, [key, value]) => Object.assign(obj, { [key]: value }), {})

const parsedQueryString = parseQueryString(queryString);

console.log(parsedQueryString)

| improve this answer | |
\$\endgroup\$
2
\$\begingroup\$

Everyone went through the unnecessary effort of building a parser/generator when modern browsers like Firefox, Chrome and Safari have URLSearchParams. Here it is, wrapped in the functions mentioned:

const createQueryString = i => (new URLSearchParams(i)).toString()
const parseQueryString = i => Array.from(new URLSearchParams(i).entries()).reduce((c, p) => (c[p[0]] = p[1], c), {})

const queryString = createQueryString({
  name: 'Darrell',
  age: '3     2',
  nationality: 'Swedish'
});

console.log(queryString);

const parsedQueryString = parseQueryString(queryString);

console.log(parsedQueryString);

Of course, your mileage may vary. If you want to support legacy browsers, you can just polyfill. That way, your code is forward-looking, and removing legacy support would only mean removing the polyfill, not updating the code.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Not sure how I missed that you could just use URLSearchParams. Good catch! \$\endgroup\$ – Gerrit0 Feb 9 '18 at 20:30
0
\$\begingroup\$

A good problem for regExp and string methods. Though I would not consider that a single expression can have a comma. (a,b) is two expressions in my book, which would mean only one argument per function call, one entry per array. Without the comma its near impossible?

The followings is an example of how you should not write code.

e encoding function. p parsing function.

e=i=>encodeURI("?"+Object.entries(i).join('&').replace(/((^|&).*?),/g,"$1="))
p=(i,o={})=>((decodeURI(i)+"&").replace(/([^?].*?)=(.*?)&/g,(p,a,b)=>o[a]=b),o)

const queryString = e({name: "Blind",age: "5 0",nationality: 'Australian'})
const parsedString = p(queryString);

AS pointed out by Joseph answer URLSearchParams is a perfect, but his answer was a little bloated and can be simplified

The is no need for the toString() as it can be coerced with an empty string

const createQueryString =i=>""+new URLSearchParams(i)

And the parse can be simplified removing the redundant entries call and creating an array literal populated via a spread operator.

const parseQStr = (i,o={})=>([...new URLSearchParams(i)].forEach(v=>o[v[0]]=v[1]),o)

// or the slightly slower
const parseQStr = i=>[...new URLSearchParams(i)].reduce((o,v)=>(o[v[0]]=v[1]),o),{})
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Your queryString output is not correct. You shouldn't encode the leading ?, or the separating & and =. Since you encode everything there is no difference as far as your parser can tell between e({fails: '1&test=1'}) and e({fails: '1', test: '1'}). \$\endgroup\$ – Gerrit0 Feb 9 '18 at 17:42

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.