16
\$\begingroup\$

Introduction

Just on Hacker news, John Resig contemplates transforming a query “foo=1&foo=2&foo=3&blah=a&blah=b” into one that looks like this: “foo=1,2,3&blah=a,b", https://johnresig.com/blog/search-and-dont-replace/. He claims "being 10 lines shorter than Mark’s solution".

Query strings consist of sequences of name-value pairs. Name-value pairs consist of a name and value, separated by =. names and values are possibly empty sequences of alphanumeric characters. Name-value pairs are separated by the & character in the sequence. Values are unique for each name.

Challenge

10 lines shorter than Mark's solution is not enough.

  • Read a query string.
  • Combine name value pairs with equal name into a single name value pair, with values concatenated and separated by comma.
  • Output the combined query string, with name-value pairs in the order the names are found from left to right in the query string.

This is code golf, the standard loopholes are closed.

Example Input and Output

Input:

foo=1&foo=&blah=a&foo=3&bar=x&blah=b&=1&=2

Output:

foo=1,,3&blah=a,b&bar=x&=1,2

Input:

foo=bar&foo=foo

Output:

foo=bar,foo

\$\endgroup\$
12
  • 1
    \$\begingroup\$ Can a name appear multiple time non-consecutively? \$\endgroup\$ – xnor Sep 29 '20 at 20:47
  • 4
    \$\begingroup\$ Requiring that the combined query string be in the original order seems unnecessarily restrictive. \$\endgroup\$ – Chris Sep 29 '20 at 22:05
  • 1
    \$\begingroup\$ In the linked program, empty values are ignored (i.e. &=1,2 is not included). \$\endgroup\$ – Arnauld Sep 29 '20 at 22:13
  • 1
    \$\begingroup\$ @Arnauld Yeah, that is a difference. \$\endgroup\$ – mschauer Sep 29 '20 at 22:16
  • 2
    \$\begingroup\$ @Sisyphus it's not that I want to defend PHP (we like it for its flaws too, good old chap has its ways), but according to Wikipedia "The exact structure of the query string is not standardized. Methods used to parse the query string may differ between websites." \$\endgroup\$ – Kaddath Sep 30 '20 at 7:42

27 Answers 27

4
\$\begingroup\$

05AB1E, 23 bytes

'&¡'=δ¡.¡н}εø€Ù}…,=&vyý

Try it online or verify all test cases.

Explanation:

'&¡                     '# Split the (implicit) input-string on "&"
     δ                   # For each inner string:
   '= ¡                 '#  Split it on "="
       .¡ }              # Group all pairs by:
         н               #  Their first value
           ε             # Map over each group of pairs:
            ø            #  Zip/transpose, swapping rows/columns
             €           #  For both inner lists:
              Ù          #   Uniquify it
               }…,=&     # After the map: push string ",=&"
                    v    # Pop and loop over each character `y`:
                     yý  #  Join the inner-most list of strings with `y` as delimiter
                         # (after the loop, the result is output implicitly)

Try it online with a step-by-step output.

\$\endgroup\$
5
  • 2
    \$\begingroup\$ Wow. Really impressive! Brilliant \$\endgroup\$ – SomoKRoceS Oct 1 '20 at 13:58
  • \$\begingroup\$ Really wished Japt had a method just for grouping while working on this challenge, the group & sort one cost me 5 bytes :( \$\endgroup\$ – Shaggy Oct 1 '20 at 14:35
  • 1
    \$\begingroup\$ Although, you planted the idea of transposing in my head which lead to me saving 2 bytes so thanks for that :) \$\endgroup\$ – Shaggy Oct 1 '20 at 14:50
  • \$\begingroup\$ @Shaggy Yeah, I actually know the feeling. The is actually rather new. Before we had to first sort everything, and then could use a consecutive group by builtin. Usually took almost halve the program for a group by before the was added. And glad my answer inspired you to golf some bytes. :) \$\endgroup\$ – Kevin Cruijssen Oct 1 '20 at 15:06
  • \$\begingroup\$ @SomoKRoceS Absolutely! \$\endgroup\$ – mschauer Oct 1 '20 at 15:26
12
\$\begingroup\$

JavaScript (ES6),  103 99 87  86 bytes

Saved 1 byte thanks to @MatthewJensen

s=>Object.values(o=/(\w*=)(\w*)/g,s.replace(o,(s,k,v)=>o[k]=o[k]?o[k]+[,v]:s)).join`&`

Try it online!

Commented

s =>                  // s = query string
  Object.values(      // get the values of ...
    o =               //   ... the object of this regular expression, which is
      /(\w*=)(\w*)/g, //   re-used to store the keys and values of the query
    s.replace(        //   match each key with the '=' sign and the corresponding
      o,              //   value, using the regular expression defined above
      (s, k, v) =>    //   for each matched string s, key k and value v:
        o[k] =        //     update o[k]:
          o[k] ?      //       if it's already defined:
            o[k] +    //         get the current value
            [, v]     //         append a comma, followed by v
          :           //       else:
            s         //         set it to the entire matched string
                      //         (key, '=', first value)
    )                 //   end of replace()
  ).join`&`           // end of Object.values(); join with '&'
\$\endgroup\$
2
  • 1
    \$\begingroup\$ You can save one byte by using s.replace(...) as the second parameter to Object.values() Try It Online \$\endgroup\$ – Matthew Jensen Sep 29 '20 at 23:19
  • \$\begingroup\$ @MatthewJensen Nice one! \$\endgroup\$ – Arnauld Sep 29 '20 at 23:28
10
\$\begingroup\$

JavaScript, 263 201 196 194 190 189 188 173 161 bytes

q=>{w={};q.split`&`.map(x=>{y=x.split`=`;if(!w[y[0]])w[y[0]]=[];w[y[0]].push(y[1])});z=`${Object.entries(w).map(a=>a=[a[0]+'='+a[1].join`,`]).join`&`}`;return z}

Try it online

\$\endgroup\$
5
  • 5
    \$\begingroup\$ Welcome to the site! The winning criteria of this challenge is code golf, meaning you should attempt to minimise your program as much as possible (e.g. remove whitespace, unnecessary brackets etc.). Furthermore, be sure to check out our Tips for golfing in Javascript for ways of golfing that may not be as obvious \$\endgroup\$ – caird coinheringaahing Sep 29 '20 at 20:52
  • 2
    \$\begingroup\$ @cairdcoinheringaahing thanks for the warm welcome and the link to the tips! I was a bit shy about posting my first golf and knew that I'd probably miss something... Are the "usual loopholes" referenced by OP listed somewhere? \$\endgroup\$ – lucasreta Sep 29 '20 at 21:03
  • 5
    \$\begingroup\$ The "usual loopholes", more commonly referred to as "standard loopholes", are listed on meta \$\endgroup\$ – pppery Sep 29 '20 at 21:11
  • \$\begingroup\$ Thanks, @pppery! I'll be sure to check them out \$\endgroup\$ – lucasreta Sep 29 '20 at 21:29
  • 2
    \$\begingroup\$ @lucasreta The loopholes are basically un-sportsmanlike "cheats" and jokes that stopped being funny after the first time. Don't worry about them too much, you won't accidentally run into one! \$\endgroup\$ – Zgarb Sep 30 '20 at 12:00
7
\$\begingroup\$

Julia 1.0, 106 bytes

f(a,z=split.(split(a,'&'),'='),u=first.(z))=join((i*'='*join(last.(z)[u.==i],',') for i in unique(u)),"&")

Try it online!

\$\endgroup\$
7
\$\begingroup\$

Python 3.8 (pre-release), 116 bytes

lambda s:(a:=[k.split("=")for k in s.split("&")])and"&".join(b+"="+",".join(d for c,d in a if c==b)for b in dict(a))

Try it online!

-46 bytes thanks to ovs

-1 byte thanks to Jonathan Allan (in Py 3.8 PR, with walrus)

\$\endgroup\$
7
  • 3
    \$\begingroup\$ Two small golfs: You don't need to convert the split result into a tuple, so the (*,) is not necessary. And k=[x[0]for x in a] can be shortened to k,*_=zip(*a). \$\endgroup\$ – ovs Sep 29 '20 at 21:07
  • 3
    \$\begingroup\$ And you can use dictionaries for uniqifying a list in the original order: tio.run/##NYzBCoMwEER/… \$\endgroup\$ – ovs Sep 29 '20 at 21:17
  • \$\begingroup\$ @ovs Oh that's clever. Also dictionaries keep the original order? TIL, thank you! \$\endgroup\$ – hyper-neutrino Sep 29 '20 at 21:20
  • 1
    \$\begingroup\$ Yes they do. Since version 3.7 this is part of the specification, before that it was just the way it happened to work in CPython. From the docs: Changed in version 3.7: Dictionary order is guaranteed to be insertion order. This behavior was an implementation detail of CPython from 3.6. \$\endgroup\$ – ovs Sep 29 '20 at 21:31
  • \$\begingroup\$ @ovs Ooh, that's very nice. I was considering deduping with sets but I know those don't keep order, so I wasn't sure about dicts. Thanks for the info! \$\endgroup\$ – hyper-neutrino Sep 29 '20 at 21:32
7
\$\begingroup\$

Ruby, 88 80 76 64 bytes

->s{s.scan(/(\w*=)(\w*)/).group_by(&:shift).map{|k,v|k+v*?,}*?&}

Try it online!

-8 bytes thanks to ovs for pointing I could assign a lambda to a variable

-12 bytes thanks to Dingus!

\$\endgroup\$
2
  • \$\begingroup\$ It is a shame tio doesn't use 2.7+ or we could save 3 more bytes by using .map{_1+_2*?,} \$\endgroup\$ – engineersmnky Oct 1 '20 at 16:22
  • \$\begingroup\$ Oh very cool, I was not even aware of that feature. \$\endgroup\$ – Jonah Oct 1 '20 at 16:24
5
\$\begingroup\$

Python 3, 94 bytes

lambda s:'&'.join(k+'='+",".join(v)for k,v in parse_qs(s,1).items())
from urllib.parse import*

Try it online!

A few things:

  • This only works because Python 3.6+ maintains dictionary order based upon time of insertion. We would love to use Python 2 for a shorter import (urlparse vs urllib.parse) but dictionaries are not ordered properly.
  • As much as I love f-strings, k+'='+",".join(v) is shorter than f'{k}={",".join(v)}'.
  • The double .join feels like golf smell, but I can't find a shorter way.

Python 3, 95 Bytes

from urllib.parse import*
d=parse_qs(input(),1)
print('&'.join(k+'='+','.join(d[k])for k in d))

Try it online!

\$\endgroup\$
1
  • \$\begingroup\$ I think I know what you mean with code smell. The problem has also a solution of the form f(x) = find two equal names and merge values and g(x, y=f(x)) = x == y ? x : g(y) \$\endgroup\$ – mschauer Oct 1 '20 at 7:17
5
\$\begingroup\$

Jelly, 28 bytes

-2 Thanks to Zgarb!

ṣ”&ṣ€”=Ṗ€ĠṢịƲµZQ€j€”,j”=)j”&

A monadic Link accepting and yielding a list of characters.

Try it online!

How?

ṣ”&ṣ€”=Ṗ€ĠṢịƲµZQ€j€”,j”=)j”& - Link: list of characters, S
ṣ”&                          - split at '&'s
   ṣ€”=                      - split each at '='s
                               call that A
            Ʋ                - last four links as a monad - f(A):
       Ṗ€                    -   all but last of each
         Ġ                   -   group indices by their values
          Ṣ                  -   sort (since Ġ orders by the values, not the indices)
           ị                 -   index into (A) (vectorises)
             µ          )    - for each:
              Z              -   transpose
               Q€            -   deduplicate each
                 j€”,        -   join each with ','s
                     j”=     -   join with '='s
                         j”& - join with '&'s
\$\endgroup\$
2
  • \$\begingroup\$ I don't think the is necessary. You can safely join the left half too since it's a singleton list. \$\endgroup\$ – Zgarb Sep 30 '20 at 6:05
  • \$\begingroup\$ You are correct, thanks! \$\endgroup\$ – Jonathan Allan Sep 30 '20 at 10:49
5
\$\begingroup\$

05AB1E, 62 51 bytes

(-11 from @kevin)

'&¡'=δ¡D€нÙÐV_UsvYyнk©Xsèyθ',««Xs®ǝU}X妨}Yζí'=ý'&ý

Try it online!


My 62 approach:

'&¡ε'=¡}D€нÙ©DgÅ0Usvy¬®skDVXsèyθ',««XsYǝU}X妨}®ζεć'=s««˜}˜'&ý

Explanation:

'&¡ε'=¡}D€нÙ©DgÅ0Usvy¬®skDVXsèyθ',««XsYǝU}X妨}®ζεć'=s««˜}˜'&ý
'&¡                                                              split by &
   ε'=¡}                                                         foreach: split by =
        D                                                        duplicate
         €н                                                      foreach: push header (get the keys list)
           Ù                                                     uniquify
            ©                                                    save in register c
             Dg                                                  suplicate and get the length of that list of keys
               Å0                                                create a list of 0's with the length above
                 U                                               save in variable X
                  svy                    }                       for each set of key-value
                     ¨sk                                        find the index of that key in the keys list
                         DV                                      save the index in variable y
                           Xsè                                   get the current value of the element of X at index Y (in X we are keeping the concatenation of the values for key i)
                              yθ                                 extract the tail of the element in this iteration (a value to concatenate)
                                ',««                             concatenate with , in between
                                    XsYǝU                        update X with the new value of the element representing the key
                                          X妨}                  remove tail and head from each element of X (removing the trailing , and leading 0)
                                               ®                 push back the list of keys
                                                ζ                zip (list of keys and list of merged values)
                                                 εć'=s««˜}       foreach element in the zipped list, join with = in between such that the result is "key=values"
                                                          ˜      flat
                                                           '&ý   join with &

Try it online!

\$\endgroup\$
4
  • 1
    \$\begingroup\$ Can probably be golfed some more, but here is 51 bytes for now based on your current implementation. The main changes are ε'=¡} to '=δ¡ and εć'=s««˜}˜'&ý to í'=ý'&ý, and I've swapped the ©® and VY variables, by changing the ©D to ÐV, so you can change the DV to © inside the loop. \$\endgroup\$ – Kevin Cruijssen Sep 30 '20 at 8:03
  • \$\begingroup\$ Thanks! I'll add your improvements. \$\endgroup\$ – SomoKRoceS Sep 30 '20 at 17:48
  • 1
    \$\begingroup\$ I had a bit more time for this challenge and was able to find a shorter approach (23 bytes). Since it's completely different than your current approach I've posted it as a separated answer, but I figured I'd let you know in case you want to learn from it. :) \$\endgroup\$ – Kevin Cruijssen Oct 1 '20 at 13:44
  • 1
    \$\begingroup\$ That's awesome! I'll take a look \$\endgroup\$ – SomoKRoceS Oct 1 '20 at 13:54
5
\$\begingroup\$

JavaScript, 79 bytes

f=
s=>(s=new URLSearchParams(s)).forEach((v,k)=>s.set(k,s.getAll(k)))||unescape(s)
<input oninput=o.textContent=f(this.value)><pre id=o>

If I/O could be actual query strings according to the WHATWG spec, rather than invalid strings that looks like query strings but aren't correctly URLencoded, then 7 bytes could be saved by stringifying the result instead of unescaping it.

\$\endgroup\$
10
  • \$\begingroup\$ @Shaggy You don't really need more test cases, do you? \$\endgroup\$ – mschauer Sep 30 '20 at 14:57
  • 1
    \$\begingroup\$ Yes, @mschauer, more (than one) is always better. See, for example, the comment I just left on the question. \$\endgroup\$ – Shaggy Sep 30 '20 at 16:39
  • \$\begingroup\$ @Shaggy OK, so how about this version? \$\endgroup\$ – Neil Sep 30 '20 at 19:04
  • \$\begingroup\$ A port of your Retina solution comes in at 76 bytes ;) \$\endgroup\$ – Shaggy Oct 1 '20 at 9:32
  • \$\begingroup\$ @Shaggy That needs a newer version of JS than mine does though. \$\endgroup\$ – Neil Oct 1 '20 at 9:36
4
\$\begingroup\$

JavaScript, 106 104 76 bytes

Port of Neil's Retina solution, posted with permission.

f=q=>q==(q=q.replace(/(?<=^|&)((\w*=)[^&]*)(.*?)&\2(\w*)/,`$1,$4$3`))?q:f(q)

Try it online!

Original

A little bit of drunk golfing that I came back to work on sober but in the process spotted Arnauld's solution and realised I was on the path to something almost identical so I left this as-was.

q=>Object.keys(o={},q.split`&`.map(p=>o[[k,v]=p.split`=`,k]=[...o[k]||[],v])).map(x=>x+`=`+o[x]).join`&`

Try it online!

\$\endgroup\$
3
\$\begingroup\$

Retina 0.8.2, 45 bytes

+1`(?<=^|&)((\w*=)[^&]*)(.*?)&\2(\w*)
$1,$4$3

Try it online! Explanation: Repeatedly matches the first duplicate key and its first duplicate and joins the value to that of the original key.

\$\endgroup\$
3
\$\begingroup\$

Japt, 29 28 27 26 24 bytes

q& móÈk¶
ü@bøXÎîÕvÎqÃq&

Try it

Saved 2 bytes thanks to some inspiration from Kevin.

(Oh, if only Japt had a method just for grouping, rather than also sorting, this could be 19 bytes.)

Explnataion

q& móÈk¥\nü@bøXÎîÕvÎqÃq&     :Implicit input of string U
q&                            :Split on "&"
   m                          :Map
    ó                         :  Partition after each character that returns falsey (i.e., an empty string)
     È                        :  When passed through the following function
      k                       :    Remove all characters that appear in
       ¥                      :    Literal "=="
        \n                    :Reassign to U
          ü                   :Group & sort by
           @                  :Passing each X through the following function
            b                 :  First index in U
             ø                :  That contains
              XÎ              :    First element of X
                Ã             :End grouping
                 ®            :Map each Z
                  Õ           :  Transpose
                   v          :  Map first element to
                    Î         :    Get first element
                     q        :  Join resulting array
                      Ã       :End map
                       q&     :Join with "&"

Or, to provide a step-by-step walkthrough:

Input

"foo=1&foo=&blah=a&foo=3&bar=x&blah=b&=1&=2"

Split

["foo=1","foo=","blah=a","foo=3","bar=x","blah=b","=1","=2"]

Map & Partition

[["foo=","1"],["foo="],["blah=","a"],["foo=","3"],["bar=","x"],["blah=","b"],["=","1"],["=","2"]]

Group & sort

[[["foo=","1"],["foo="],["foo=","3"]],[["blah=","a"],["blah=","b"]],[["bar=","x"]],[["=","1"],["=","2"]]]

Map and ...

Transpose

[[["foo=","foo=","foo="],["1",null,"3"]],[["blah=","blah="],["a","b"]],[["bar="],["x"]],[["=","="],["1","2"]]]

Map first element to its first element

[["foo=",["1",null,"3"]],["blah=",["a","b"]],["bar=",["x"]],["=",["1","2"]]]

Join

["foo=1,,3","blah=a,b","bar=x","=1,2"]

Join

"foo=1,,3&blah=a,b&bar=x&=1,2"
\$\endgroup\$
2
\$\begingroup\$

Awk, 144 \$\cdots\$144 138 byte

Added 6 bytes to fix a bug.
Added 4 bytes to fix a bug kindly pointed out and solved by Dominic van Essen.
Saved 6 bytes thanks to Dominic van Essen!!!

BEGIN{RS="&"}{if(!(j=i[$1]++))n[m++]=$1;a[$1][j]=$2}END{for(l in n){k=n[l];o=k"=";for(j in a[k])o=o (j>0?",":"")a[k][j];printf z o;z="&"}}

Try it online!

\$\endgroup\$
5
  • \$\begingroup\$ "in the order the names are found from left to right in the query string" \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 14:05
  • \$\begingroup\$ ...but I think you could solve it in pure AWK -F= for 144 bytes \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 14:06
  • \$\begingroup\$ @DominicvanEssen Oops, missed that. Great save - thanks! :D \$\endgroup\$ – Noodle9 Sep 30 '20 at 14:10
  • \$\begingroup\$ printf lets you get rid of ORS="" for 138 bytes \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 14:18
  • \$\begingroup\$ @DominicvanEssen Nice one - thanks! :D \$\endgroup\$ – Noodle9 Sep 30 '20 at 14:33
2
\$\begingroup\$

R, 195 bytes

{Z=pryr::f
`/`=Z(a,b,el(regmatches(a,gregexpr(b,a))))
`-`=Z(a,b,paste(a,collapse=b))
Z(S,{L=S/'\\w*='
L=factor(L,unique(L))
U=tapply(S/'=\\w*',L,Z(a,trimws(a,,'=')-','))
paste0(names(U),U)-'&'})}

Try it online!

\$\endgroup\$
6
  • \$\begingroup\$ You can fix it by including the trimws source in the TIO header like this \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 14:23
  • \$\begingroup\$ But: "in the order the names are found from left to right in the query string" \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 14:24
  • \$\begingroup\$ Well spotted - fixed (at the cost of 26 bytes) by using tapply instead of by and ordering the factor levels first. \$\endgroup\$ – Cong Chen Sep 30 '20 at 14:45
  • \$\begingroup\$ Ouch! 26 bytes! \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 14:53
  • \$\begingroup\$ You can just use factor(L,unique(L)) to save 7... \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 14:59
2
\$\begingroup\$

Lua, 162 bytes

l,t={},{}(...):gsub('(%w-)=(%w-)',load"k,v=...o=t[k]l[#l+1]=not o and k or _ t[k]=o and o..','..v or v")for i=1,#l do io.write(i>1 and'&'or'',l[i],'=',t[l[i]])end

Try it online!

This is a long one, huh. It could have been made much shorter if not for ordering requiment.

Explanation:

l,t={},{} -- list (by first inclusion), table (keys to string)
-- (ab)use string replacement function to callback over matches in input string
(...):gsub(
    -- Match key-value pairs
    '(%w-)=(%w-)',
    -- For each pair, run callback (braces are replaced for multiline)
    load[[
        k,v=... -- Assign key, value
        o=t[k] -- Look for already stored string if any
        l[#l+1]=not o and k or _ -- If key is new, store it in list
        t[k]=o and o..','..v or v -- Append to string if it is not new, store it if it is
    ]]
)
-- For every record in list
for i=1,#l do
    -- Write with no newlines
    io.write(
        i>1 and'&'or'', -- Output & before all values but first
        l[i],'=',t[l[i]] -- Print key-value pair
    )
end
\$\endgroup\$
1
  • \$\begingroup\$ I don't think I should wave the ordering requirement at this time, but you can submit the unordered solution clearly marked in parallel competition if you want. \$\endgroup\$ – mschauer Sep 30 '20 at 18:41
2
\$\begingroup\$

Wolfram Language (Mathematica), 86 bytes

StringRiffle[Last@Reap[Sow@@@StringExtract[#,"&"->;;,"="->{2,1}],_,List],"&","=",","]&

Try it online! Pure function, takes a string as input and returns another string as output. This is inspired by and very similar to att's answer, but it uses an algorithm similar to the one in the blog post (here utilizing Sow/Reap). Here's an example of how the subexpressions evaluate on an input of "foo=1&bar=a&foo=":

StringExtract[#,"&"->;;,"="->{2,1}] == {{"1", "foo"}, {"a", "bar"}, {"", "foo"}}
Sow@@@...                           == {"1", "a", ""}; {"1", ""} sown w/ tag "foo"; {"a"} sown w/ tag "bar"
Reap[...,_,List]                    == {{"1", "a", ""}, {{"foo", {"1", ""}}, {"bar", {"a"}}}}
Last@...                            == {{"foo", {"1", ""}}, {"bar", {"a"}}}
StringRiffle[...,"&","=",","]       == "foo=1,&bar=a"
\$\endgroup\$
2
\$\begingroup\$

Factor, 229 bytes

: c ( s -- s s ) 1 <hashtable> swap "&"split [ "="split ] map
[ [ dup [ last ] dip first pick push-at ] each ]
[ [ first ] map dup union ] bi dup [ [ over at ","join ] map ] dip
[ "="append ] map swap zip [ concat ] map "&"join ;

Try it online!

It's long but I'm somewhat content with it :)

\$\endgroup\$
2
\$\begingroup\$

SNOBOL4 (CSNOBOL4), 243 221 212 bytes

	Q =INPUT
	T =TABLE()
N	Q (ARB '=') . L ARB . V ('&' | RPOS(0)) REM . Q	:F(O)
	T<L> =T<L> ',' V 	:(N)
O	R =CONVERT(T,'ARRAY')
I	X =X + 1
	R<X,2> ',' REM . V 	:F(P)
	O =O '&' R<X,1> V	:(I)
P	O '&' REM . OUTPUT
END

Try it online!

TABLE in SNOBOL is weird. It's perfectly fine accepting a PATTERN like ARB as a key, but not the empty string ''. However, using <label>= as the label instead of <label> neatly solves this problem.

Explanation for a previous iteration:

	E =RPOS(0)					;* alias for end of string 
	A =ARB						;* alias for ARBitrary match (as short as possible)
	Q =INPUT					;* read input
	T =TABLE()					;* create a TABLE (a dictionary)
N	Q A . L '=' A . V ('&' | E) REM . Q	:F(O)	;* in regex land, this is something like
	;* '(.*=)(.*)(&|$)' where you save \1 and \2 as L and V, respectively. If there's no match, goto O
	T<L> =T<L> V ','	:(N)		;* update the values list, then goto N
O	R =CONVERT(T,'ARRAY')				;* convert T to a 2D array of [Label,Value]
I	X =X + 1					;* increment array index
	R<X,2> A . V ',' E	:F(P)			;* remove the trailing ',' from the value list. If X is out of bounds, goto P
	O =O R<X,1> V '&'	:(I)			;* Append L and V to O with an '=' and '&', then goto I
P	O A . OUTPUT '&' E				;* Print everything except for the trailing '&'
END
\$\endgroup\$
2
\$\begingroup\$

PHP, 146 bytes

<?=parse_str(str_replace('=','_[]=',$argv[1]),$a)??join('&',array_map(function($b,$c){return rtrim($b,'_').'='.join(',',$c);},array_keys($a),$a));

Try it online! Explanation: parse_str wasn't designed to handle repeated values, but you can persuade it to by naming each value with a trailing []. It also wasn't designed to handle empty names, but since I'm appending [] anyway I can also add a _ to satisfy that case. Having parsed the query string it then remains to join everything back together.

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

Red, 221 bytes

func[s][m: copy #()s: split s"&"forall s[s/1: split s/1"="append s/1/1"="put m s/1/1
copy""]foreach v s[m/(v/1): append m/(v/1) rejoin[v/2","]]t: copy""foreach k keys-of
m[take/last m/:k repend t[k m/:k"&"]]take/last t t]

Try it online!

This is awfully long and I'll try to golf it at least a bit. Too bad Red doesn't have a handy join function...

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

Charcoal, 42 bytes

≔E⪪S&⪪ι=θW⁻Eθ§κ⁰υ⊞υ§ι⁰⪫Eυ⁺⁺ι=⪫EΦθ¬⌕λι⊟λ,¦&

Try it online! Link is to verbose version of code. Explanation:

≔E⪪S&⪪ι=θ

Split the input on &s and split each token on =s.

W⁻Eθ§κ⁰υ⊞υ§ι⁰

Create a list of unique keys in order of their first appearance.

⪫Eυ⁺⁺ι=⪫EΦθ¬⌕λι⊟λ,¦&

For each key, extract and join the values with ,, concatenate with the key and separator, and join the overall result with &.

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

C (gcc), 319 304 bytes

Thanks to ceilingcat for the suggestions.

This function scans each tokenized (name,value) pair and adds a list entry for each new name encountered, then appends a list entry for each value. After constructing the lists, it then iterates through each list and prints the values. To save space, I flattened the structures into arrays of void *.

f(s,t,v,i)char*s,*t;{void*d[3]={t=0},**f,**w;for(;t=strtok(t?0:s,"&");*w=calloc(8,2),w[1]=v){t[i=strcspn(t,"=")]=0;v=t-~i;for(f=&d;strcmp(f[1]?:t,t);f=*f);for(w=f[2]=f[1]?f[2]:(f[1]=t,*f=calloc(8,5))+24;w[1];w=*w);}for(f=&d;i=*f&&printf("&%s="+!!s,f[1]);f=*f)for(w=f[2];s=*w;w=s)i=!printf(",%s"+i,w[1]);}

Try it online!

Non-golfed version of original submission:

struct list {
  struct list *next;
  char *name;
  struct list *value;
};

void f(char *s) {
  char *tok=NULL, *value;
  struct list d={}, *e, *v;
  int i;

  for(; tok=strtok(tok?NULL:s, "&"); ) {
    tok[i=strcspn(tok, "=")]=0;
    value=tok+i+1;
    for(e=&d; e->name && strcmp(e->name, tok); e=e->next);
    if(!e->name) {
      e->next=calloc(sizeof(struct list), 2);
      e->name=tok;
      e->value=e->next+1;
    }
    for(v=e->value; v->name; v=v->next);
    v->next=calloc(sizeof(struct list), 1);
    v->name=value;
  }
  for(e=&d; e->next; e=e->next, s=0) {
    printf("&%s="+!!s, e->name);
    for(v=e->value, i=1; v->next; v=v->next, i=0)
      printf(",%s"+i, v->name);
  }
}

Try it online!

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

Wolfram Language (Mathematica), 90 88 bytes

StringRiffle[S=StringSplit;List@@@Normal@Merge[Rule@@@S[#~S~"&","=",2],#&],"&","=",","]&

Try it online!

-2 thanks to LegionMammal978

S=StringSplit;
Rule@@@S[#~S~"&","=",2]         (* Convert to a list of Rules *)
Merge[ % ,#&]                   (* Combine rules into an Association, leaving values unchanged *)
Normal@ %                       (* Convert Association back into a list of Rules, *)
List@@@ %                       (* and turn Rules into Lists *)
StringRiffle[ % ,"&","=",","]   (* Concatenate, using "&", "=", and "," as separators *)
\$\endgroup\$
2
  • \$\begingroup\$ You can replace All with 2 to save 2 bytes. \$\endgroup\$ – LegionMammal978 Sep 30 '20 at 20:40
  • \$\begingroup\$ @LegionMammal978 You should post it. I didn't even know about StringExtract. \$\endgroup\$ – att Oct 1 '20 at 2:28
1
\$\begingroup\$

Perl 5, -pF\& flags, 73 57 bytes

Uses -pF\& to loop over inputs and autosplit on &.

Unordered results, in parallel competition.

/=/,$z{$`}.=$z{$`}?",$'":"$`=$'"for@F;$_=join'&',values%z

Try it online!

Uses a hash %z to keep track of values for individual names, and then prints them all out at the end. -16 bytes thanks to NahuelFouilleul.

\$\endgroup\$
2
  • \$\begingroup\$ great answer, some bytes may be saved Try it online! \$\endgroup\$ – Nahuel Fouilleul Sep 30 '20 at 20:11
  • \$\begingroup\$ @NahuelFouilleul Nice, thanks :) \$\endgroup\$ – Chris Sep 30 '20 at 20:14
1
\$\begingroup\$

R, 174 166 bytes

function(s,S=strsplit,A=sapply,P=paste,e=A(S(P(el(S(s,'&')),'=',sep=''),'='),c),n=unique(m<-e[1,]))P(n,A(n,function(x)P(e[2,m==x],collapse=',')),sep='=',collapse='&')

Try it online!

For some inexplicable reason I thought that this challenge wouldn't suffer from R's awfully-verbose string handling.
This didn't turn out to be the case, at least based on my attempt so far...

Commented before golfing:

compactify=
function(s,                         # s = string to compactify
S=strsplit,                         # S = alias to strsplit() function
A=sapply,                           # A = alias to sapply() function
P=paste,                            # P = alias to paste() function
a=el(                               # a = the first element of ...
  S(s,'&'))                         #  ...s split on '&'
b=S(a,,'=')                         # b = each element of a, split on '='
                                    # Now, unfortunately if there's nothing after the '=',
                                    # the strsplit() function fails to add an empty string ''
                                    # so we need to do this ourselves:
e=A(b,function(x)c(x,'')            # e = for each element of b, add a '' ...  
                        [1:2])      #  ...and then take the first two elements
                                    # This gives us a 2-row matrix, with the names in row 1,
                                    # and the values in row 2
n=unique(m<-e[1,]))                 # n = unique names, m = all names of name-value pairs
m=A(n,function(x)                   # m = for each element of n...
      P(e[2,m==x],collapse=','))    #  ...paste together the values for this name, using ',' as separator
P(n,m,sep='=',collapse='&')         # Finally, paste together the pairs of elements in m, using '=' as separator...
                                    #  ...and collapse them all into one string using '&' as separator
\$\endgroup\$
1
  • \$\begingroup\$ Well, I've found a 162 byter, taking a different approach. R still sucks at strings though :-( \$\endgroup\$ – Giuseppe Sep 30 '20 at 19:10
1
\$\begingroup\$

R, 162 157 151 bytes

function(s,S=substring)paste0(z<-unique(k<-S(x<-el(strsplit(s,"&")),1,y<-regexpr('=',x))),sapply(split(S(x,y+1),k),paste,collapse=',')[z],collapse='&')

Try it online!

-6 bytes thanks to Dominic van Essen

A nice single line function. Ungolfed:

function(s,S=substr){
pairs <- el(strsplit(s,"&"))					# split on '&' getting list of pairs
loc <- regexpr('=',pairs)					# find location of each '=' in each pair 
keys <- substr(pairs,1,loc)					# get the key, including the '='
values <- substr(pairs,loc + 1,nchar(pairs))			# get the values (everything after '=')
unq_keys <- unique(keys)					# uniquify the keys. This retains the order.
split_vals <- split(values,keys)				# group the values into sublists by which key they are associated with
collapsed_values <- sapply(split_vals,paste,collapse=',')	# join each subgroup of values by ','
collapsed_values <- collapsed_values[unq_keys]			# and reorder them to match the order of the keys
paste0(unq_keys,collapsed_values,collapse='&')			# concatenate keys and values and join by '&'
}
\$\endgroup\$
6
  • \$\begingroup\$ There are possibly byte savings using substring in place of substr+strsplit but unpacking a gregexpr is expensive in bytes so that might be a dead end. \$\endgroup\$ – Giuseppe Sep 30 '20 at 19:12
  • \$\begingroup\$ Well done already. I never would have been able to bring myself to use 2x paste(...,collapse=) which is my most-hated R argument... \$\endgroup\$ – Dominic van Essen Sep 30 '20 at 19:15
  • \$\begingroup\$ 151 bytes with substring, which goes to the end of the string when the end isn't specified. \$\endgroup\$ – Dominic van Essen Oct 1 '20 at 8:26
  • \$\begingroup\$ ...and I suppose that paste0 is a (byte-wasting) accident... \$\endgroup\$ – Dominic van Essen Oct 1 '20 at 8:35
  • \$\begingroup\$ @DominicvanEssen nice. I guess that assumes that the strings won't be of length greater than 1M? And the paste0 is on purpose, otherwise the query string will look like <key>= <value> (with a space in between the = and the value) \$\endgroup\$ – Giuseppe Oct 1 '20 at 14:54

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.