There are at least 2×5×7×3×3×1=630 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 URLSearchParams)
    • Just the last item (like PHP's parse_str; below code prefers this)
    • All the items, as an Array
    • All the items, as a Set
    • Throw an error
  • How do you want instances of …&key&… (missing an equals-sign) handled?
    • Take the value to be true
    • Take the value to be null (below code chooses this)
    • Take the value to be false
    • Take the value to be undefined
    • Pretend there's an equals-sign; take the value to be the empty string (like URLSearchParams)
    • Ignore them entirely
    • Reset the key; treat them as a deletedirective
    • Throw an error
  • How do you want instances of …&key=&… (empty value) handled?
    • Take the value to be the empty string (like URLSearchParams; below code obviously chooses this)
    • Reset the key; treat them as a delete directive
    • Throw an error
  • How do you want instances of …&=val&… (empty key name) handled?
    • Take the key to be the empty string (like URLSearchParams; below code chooses this)
    • Ignore them entirely (like PHP's parse_str)
    • 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.

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.