There are at least 2×5×4×3×3×1=360 different things you might unambiguously mean by "parsing" a query string in Javascript:

  • What data type do you want returned? (Below code provides both these)
    • Map
    • Object
  • How do you want duplicate keys handled? (Below code provides all these)
    • Just the first item (like built-in URLSearchParams)
    • Just the last item (like PHP's parse_str; below code prefers this)
    • All the items, as an Array (like Python's parse_qs)
    • All the items, as a Set
    • Throw an error
  • How do you want instances of …&=val&… (empty key name) handled?
    • Take the key to be the empty string (like built-in URLSearchParams; like Python's parse_qs; below code chooses this)
    • Ignore them entirely (like PHP's parse_str)
    • Throw an error
  • How do you want instances of …&key=&… (empty value) handled?
    • Take the value to be the empty string (like built-in URLSearchParams; below code chooses this)
    • Ignore them entirely (like Python's parse_qs)
    • Reset the key; treat them as a delete directive
    • Throw an error
  • How do you want instances of …&key&… (missing an equals-sign) handled?
    • Take the key to be present but the value absent, by a constant such as null (below code chooses this)
    • Pretend there's an equals-sign; treat them identically to an empty value (like built-in URLSearchParams; like Python's parse_qs)
    • Ignore them entirely, despite empty values being treated as the empty string
    • Reset the key; treat them as a deletedirective
    • Throw an error
  • Do you want further parsing done on the values within this function?

The below code demos some of the most important and useful combinations of these:

function _parse_qs_array(search) {
	if(search === undefined) search = window.location.search;
	let m = search.match(/^\?(.+)$/s);
	let params = m ? m[1].split('&') : new Array();
	return params.map(s => {
		let [key, value] = s.match(/^(.*?)(?:=(.*))?$/s).slice(1);
		key = decodeURIComponent(key.replaceAll('+', '%20'));
		value = (value !== undefined ) ? decodeURIComponent(value.replaceAll('+', '%20')) : null;
		return [key, value];
	});
}


function parse_qs_map(search) {
	// Parses the given query string, returning the results as a Map.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys take on the LAST value provided (like PHP's parse_str).
	return new Map(_parse_qs_array(search));
}

function parse_qs_obj(search) {
	// Parses the given query string, returning the results as an Object.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys take on the LAST value provided (like PHP's parse_str).
	return Object.fromEntries(_parse_qs_array(search));
}


function parse_qs_map_first(search) {
	// Parses the given query string, returning the results as a Map.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys take on the FIRST value provided (like URLSearchParams).
	return new Map(_parse_qs_array(search).reverse());
}

function parse_qs_obj_first(search) {
	// Parses the given query string, returning the results as an Object.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys take on the FIRST value provided (like URLSearchParams).
	return Object.fromEntries(_parse_qs_array(search).reverse());
}


function parse_qs_map_all(search) {
	// Parses the given query string, returning the results as a Map with Array values.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys are all returned, in-order.
	const map = new Map();
	_parse_qs_array(search).forEach(([key, value]) => {
		if(! map.has(key) ) map.set(key, new Array());
		map.get(key).push(value);
	});
	return map;
}

function parse_qs_obj_all(search) {
	// Parses the given query string, returning the results as an Object with Array values.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys are all returned, in-order.
	const obj = new Object();
	_parse_qs_array(search).forEach(([key, value]) => {
		if( obj[key] === undefined ) obj[key] = new Array();
		obj[key].push(value);
	});
	return obj;
}


function parse_qs_map_all_unique(search) {
	// Parses the given query string, returning the results as a Map with Set values.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys are all returned. (Duplicate key-value pairs have no effect.)
	const map = new Map();
	_parse_qs_array(search).forEach(([key, value]) => {
		if(! map.has(key) ) map.set(key, new Set());
		map.get(key).add(value);
	});
	return map;
}

function parse_qs_obj_all_unique(search) {
	// Parses the given query string, returning the results as an Object with Set values.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys are all returned. (Duplicate key-value pairs have no effect.)
	const obj = new Object();
	_parse_qs_array(search).forEach(([key, value]) => {
		if( obj[key] === undefined ) obj[key] = new Set();
		obj[key].add(value);
	});
	return obj;
}


function parse_qs_map_1(search) {
	// Parses the given query string, returning the results as a Map.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys will cause this function to throw a SyntaxError.
	const map = new Map();
	_parse_qs_array(search).forEach(([key, value]) => {
		if( map.has(key) ) throw new SyntaxError(`${arguments.callee.name}: duplicate key`);
		map.set(key, value);
	});
	return map;
}

function parse_qs_obj_1(search) {
	// Parses the given query string, returning the results as an Object.
	// Naked keys (without an equals-sign) are taken to have null values.
	// Duplicate keys will cause this function to throw a SyntaxError.
	const obj = new Object();
	_parse_qs_array(search).forEach(([key, value]) => {
		if( obj[key] !== undefined ) throw new SyntaxError(`${arguments.callee.name}: duplicate key`);
		obj[key] = value;
	});
	return obj;
}

Leave a Reply

Your email address will not be published. Required fields are marked *

Warning: This site uses Akismet to filter spam. Until or unless I can find a suitable replacement anti-spam solution, this means that (per their indemnification document) all commenters' IP addresses will be sent to Automattic, Inc., who may choose to share such with 3rd parties.
If this is unacceptable to you, I highly recommend using an anonymous proxy or public Wi-Fi connection when commenting.