17
\$\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

asked Sep 29, 2020 at 20:26
\$\endgroup\$
12
  • 1
    \$\begingroup\$ Can a name appear multiple time non-consecutively? \$\endgroup\$ Commented Sep 29, 2020 at 20:47
  • 5
    \$\begingroup\$ Requiring that the combined query string be in the original order seems unnecessarily restrictive. \$\endgroup\$ Commented Sep 29, 2020 at 22:05
  • 1
    \$\begingroup\$ In the linked program, empty values are ignored (i.e. &=1,2 is not included). \$\endgroup\$ Commented Sep 29, 2020 at 22:13
  • 1
    \$\begingroup\$ @Arnauld Yeah, that is a difference. \$\endgroup\$ Commented Sep 29, 2020 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\$ Commented Sep 30, 2020 at 7:42

27 Answers 27

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 '&'
answered Sep 29, 2020 at 21:32
\$\endgroup\$
2
  • 1
    \$\begingroup\$ You can save one byte by using s.replace(...) as the second parameter to Object.values() Try It Online \$\endgroup\$ Commented Sep 29, 2020 at 23:19
  • \$\begingroup\$ @MatthewJensen Nice one! \$\endgroup\$ Commented Sep 29, 2020 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

answered Sep 29, 2020 at 20:43
\$\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\$ Commented Sep 29, 2020 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\$ Commented Sep 29, 2020 at 21:03
  • 5
    \$\begingroup\$ The "usual loopholes", more commonly referred to as "standard loopholes", are listed on meta \$\endgroup\$ Commented Sep 29, 2020 at 21:11
  • \$\begingroup\$ Thanks, @pppery! I'll be sure to check them out \$\endgroup\$ Commented Sep 29, 2020 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\$ Commented Sep 30, 2020 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!

answered Sep 29, 2020 at 21:46
\$\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)

answered Sep 29, 2020 at 20:59
\$\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\$ Commented Sep 29, 2020 at 21:07
  • 3
    \$\begingroup\$ And you can use dictionaries for uniqifying a list in the original order: tio.run/##NYzBCoMwEER/… \$\endgroup\$ Commented Sep 29, 2020 at 21:17
  • \$\begingroup\$ @ovs Oh that's clever. Also dictionaries keep the original order? TIL, thank you! \$\endgroup\$ Commented Sep 29, 2020 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\$ Commented Sep 29, 2020 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\$ Commented Sep 29, 2020 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!

answered Sep 29, 2020 at 22:11
\$\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\$ Commented Oct 1, 2020 at 16:22
  • \$\begingroup\$ Oh very cool, I was not even aware of that feature. \$\endgroup\$ Commented Oct 1, 2020 at 16:24
6
\$\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!

answered Sep 30, 2020 at 1:38
\$\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\$ Commented Oct 1, 2020 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
Zgarb
43.2k4 gold badges84 silver badges265 bronze badges
answered Sep 29, 2020 at 21:54
\$\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\$ Commented Sep 30, 2020 at 6:05
  • \$\begingroup\$ You are correct, thanks! \$\endgroup\$ Commented Sep 30, 2020 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!

answered Sep 29, 2020 at 22:13
\$\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\$ Commented Sep 30, 2020 at 8:03
  • \$\begingroup\$ Thanks! I'll add your improvements. \$\endgroup\$ Commented Sep 30, 2020 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\$ Commented Oct 1, 2020 at 13:44
  • 1
    \$\begingroup\$ That's awesome! I'll take a look \$\endgroup\$ Commented Oct 1, 2020 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.

answered Sep 29, 2020 at 22:41
\$\endgroup\$
10
  • \$\begingroup\$ @Shaggy You don't really need more test cases, do you? \$\endgroup\$ Commented Sep 30, 2020 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\$ Commented Sep 30, 2020 at 16:39
  • \$\begingroup\$ @Shaggy OK, so how about this version? \$\endgroup\$ Commented Sep 30, 2020 at 19:04
  • \$\begingroup\$ A port of your Retina solution comes in at 76 bytes ;) \$\endgroup\$ Commented Oct 1, 2020 at 9:32
  • \$\begingroup\$ @Shaggy That needs a newer version of JS than mine does though. \$\endgroup\$ Commented Oct 1, 2020 at 9:36
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.

answered Oct 1, 2020 at 13:34
\$\endgroup\$
5
  • 2
    \$\begingroup\$ Wow. Really impressive! Brilliant \$\endgroup\$ Commented Oct 1, 2020 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\$ Commented Oct 1, 2020 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\$ Commented Oct 1, 2020 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\$ Commented Oct 1, 2020 at 15:06
  • \$\begingroup\$ @SomoKRoceS Absolutely! \$\endgroup\$ Commented Oct 1, 2020 at 15:26
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!

answered Sep 29, 2020 at 21:17
\$\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.

answered Sep 30, 2020 at 15:23
\$\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"
answered Sep 30, 2020 at 15:52
\$\endgroup\$
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!

answered Sep 30, 2020 at 13:48
\$\endgroup\$
6
  • \$\begingroup\$ You can fix it by including the trimws source in the TIO header like this \$\endgroup\$ Commented Sep 30, 2020 at 14:23
  • \$\begingroup\$ But: "in the order the names are found from left to right in the query string" \$\endgroup\$ Commented Sep 30, 2020 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\$ Commented Sep 30, 2020 at 14:45
  • \$\begingroup\$ Ouch! 26 bytes! \$\endgroup\$ Commented Sep 30, 2020 at 14:53
  • \$\begingroup\$ You can just use factor(L,unique(L)) to save 7... \$\endgroup\$ Commented Sep 30, 2020 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
answered Sep 30, 2020 at 18:15
\$\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\$ Commented Sep 30, 2020 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"
answered Oct 1, 2020 at 13:02
\$\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 :)

answered Oct 1, 2020 at 18:37
\$\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
answered Sep 30, 2020 at 16:00
\$\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.

answered Oct 9, 2020 at 12:50
\$\endgroup\$
2
\$\begingroup\$

Awk, (削除) 144 (削除ここまで) \$\cdots\$(削除) 138 (削除ここまで) 135 bytes

BEGIN{RS="&"}{a[1ドル][j=i[1ドル]++]=2ドル;j||(n[m++]=1ドル)}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="&"}}

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!!!
Saved 3 bytes thanks to ceilingcat!!!

Try it online!

answered Sep 30, 2020 at 11:38
\$\endgroup\$
6
  • \$\begingroup\$ "in the order the names are found from left to right in the query string" \$\endgroup\$ Commented Sep 30, 2020 at 14:05
  • \$\begingroup\$ ...but I think you could solve it in pure AWK -F= for 144 bytes \$\endgroup\$ Commented Sep 30, 2020 at 14:06
  • \$\begingroup\$ @DominicvanEssen Oops, missed that. Great save - thanks! :D \$\endgroup\$ Commented Sep 30, 2020 at 14:10
  • \$\begingroup\$ printf lets you get rid of ORS="" for 138 bytes \$\endgroup\$ Commented Sep 30, 2020 at 14:18
  • \$\begingroup\$ @DominicvanEssen Nice one - thanks! :D \$\endgroup\$ Commented Sep 30, 2020 at 14:33
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...

answered Sep 30, 2020 at 11:34
\$\endgroup\$
1
\$\begingroup\$

Charcoal, 42 bytes

≔E⪪S&⪪ι=θW−Eθ§κ0υ⊞υ§ι0⪫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θ§κ0υ⊞υ§ι0

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 &.

answered Sep 30, 2020 at 19:18
\$\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!

answered Sep 30, 2020 at 5:36
\$\endgroup\$
0
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.

answered Sep 29, 2020 at 22:03
\$\endgroup\$
2
  • \$\begingroup\$ great answer, some bytes may be saved Try it online! \$\endgroup\$ Commented Sep 30, 2020 at 20:11
  • \$\begingroup\$ @NahuelFouilleul Nice, thanks :) \$\endgroup\$ Commented Sep 30, 2020 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
answered Sep 30, 2020 at 9:26
\$\endgroup\$
1
  • \$\begingroup\$ Well, I've found a 162 byter, taking a different approach. R still sucks at strings though :-( \$\endgroup\$ Commented Sep 30, 2020 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 '&'
}
answered Sep 30, 2020 at 19:07
\$\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\$ Commented Sep 30, 2020 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\$ Commented Sep 30, 2020 at 19:15
  • \$\begingroup\$ 151 bytes with substring, which goes to the end of the string when the end isn't specified. \$\endgroup\$ Commented Oct 1, 2020 at 8:26
  • \$\begingroup\$ ...and I suppose that paste0 is a (byte-wasting) accident... \$\endgroup\$ Commented Oct 1, 2020 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\$ Commented Oct 1, 2020 at 14:54
1
\$\begingroup\$

Wolfram Language (Mathematica), (削除) 90 (削除ここまで) (削除) 88 (削除ここまで) 87 bytes

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

Try it online!

-2 thanks to LegionMammal978

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

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.