Repository: sindresorhus/query-string
Pull request #167 (2019-03-01 14:15:47)

Add `comma` format to the `arrayFormat` option
This pull requests adds fourth `arrayFormat` option, named `comma`. We needed this for our project, as we needed to support arrays in query in below shape...

`foo=1,2,3,4,5,6,7,8`

This format is mainly used to shorten URLs which include things as identifiers, where there is expected to be a lot of them.

 On the way to add this feature I had to slightly reimplement `encoderForArrayFormat` to use `Array.prototype.reduce` instead of previous `for .. in`

tomaass: Great work @zepod !
Fix tests please I really want to use it on my project 🥇 
sindresorhus: Why did you have to switch to Array#reduce?
sindresorhus: You have to include TypeScript types for the changes too, since a TypeScript declaration file was recently added to `master`.
zepod: @sindresorhus 
the way it was implemented with `for` `encoderForArrayFormat` was a unary function mapping **single element** of provided array, to **single key** in the query sequence.

Using comma format, we wanted to map the **multiple elements** of array to a **single key** in the query. 
 
zepod: Typescript types added

Patch:
diff --git a/index.d.ts b/index.d.ts
index 8d34e21..617f4a3 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -29,11 +29,11 @@ export interface ParseOptions {
 	 *    queryString.parse('foo=1&foo=2&foo=3');
 	 *    //=> foo: [1,2,3]
 	 */
-	readonly arrayFormat?: 'bracket' | 'index' | 'none';
+	readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'none';
 }
 
 export interface ParsedQuery {
-	readonly [key: string]: string | string[] | undefined;
+	readonly [key: string]: string | string[] | null | undefined;
 }
 
 /**
@@ -100,7 +100,7 @@ export interface StringifyOptions {
 	 *    queryString.stringify({foo: [1,2,3]});
 	 *    // => foo=1&foo=2&foo=3
 	 */
-	readonly arrayFormat?: 'bracket' | 'index' | 'none';
+	readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'none';
 
 	/**
 	 * Supports both `Function` as a custom sorting function or `false` to disable sorting.
diff --git a/index.js b/index.js
index 58744d3..efb498c 100644
--- a/index.js
+++ b/index.js
@@ -5,37 +5,59 @@ const decodeComponent = require('decode-uri-component');
 function encoderForArrayFormat(options) {
 	switch (options.arrayFormat) {
 		case 'index':
-			return (key, value, index) => {
-				return value === null ? [
-					encode(key, options),
-					'[',
-					index,
-					']'
-				].join('') : [
-					encode(key, options),
-					'[',
-					encode(index, options),
-					']=',
-					encode(value, options)
-				].join('');
+			return key => (result, value) => {
+				const index = result.length;
+				if (value === undefined) {
+					return result;
+				}
+
+				if (value === null) {
+					return [...result, [encode(key, options), '[', index, ']'].join('')];
+				}
+
+				return [
+					...result,
+					[encode(key, options), '[', encode(index, options), ']=', encode(value, options)].join('')
+				];
 			};
 
 		case 'bracket':
-			return (key, value) => {
-				return value === null ? [encode(key, options), '[]'].join('') : [
-					encode(key, options),
-					'[]=',
-					encode(value, options)
-				].join('');
+			return key => (result, value) => {
+				if (value === undefined) {
+					return result;
+				}
+
+				if (value === null) {
+					return [...result, [encode(key, options), '[]'].join('')];
+				}
+
+				return [...result, [encode(key, options), '[]=', encode(value, options)].join('')];
+			};
+
+		case 'comma':
+			return key => (result, value, index) => {
+				if (!value) {
+					return result;
+				}
+
+				if (index === 0) {
+					return [[encode(key, options), '=', encode(value, options)].join('')];
+				}
+
+				return [[result, encode(value, options)].join(',')];
 			};
 
 		default:
-			return (key, value) => {
-				return value === null ? encode(key, options) : [
-					encode(key, options),
-					'=',
-					encode(value, options)
-				].join('');
+			return key => (result, value) => {
+				if (value === undefined) {
+					return result;
+				}
+
+				if (value === null) {
+					return [...result, encode(key, options)];
+				}
+
+				return [...result, [encode(key, options), '=', encode(value, options)].join('')];
 			};
 	}
 }
@@ -80,6 +102,13 @@ function parserForArrayFormat(options) {
 				accumulator[key] = [].concat(accumulator[key], value);
 			};
 
+		case 'comma':
+			return (key, value, accumulator) => {
+				const isArray = typeof value === 'string' && value.split('').indexOf(',') > -1;
+				const newValue = isArray ? value.split(',') : value;
+				accumulator[key] = newValue;
+			};
+
 		default:
 			return (key, value, accumulator) => {
 				if (accumulator[key] === undefined) {
@@ -205,17 +234,10 @@ exports.stringify = (obj, options) => {
 		}
 
 		if (Array.isArray(value)) {
-			const result = [];
-
-			for (const value2 of value.slice()) {
-				if (value2 === undefined) {
-					continue;
-				}
-
-				result.push(formatter(key, value2, result.length));
-			}
-
-			return result.join('&');
+			return value
+				.slice()
+				.reduce(formatter(key), [])
+				.join('&');
 		}
 
 		return encode(key, options) + '=' + encode(value, options);
diff --git a/readme.md b/readme.md
index f4640cc..24f810d 100644
--- a/readme.md
+++ b/readme.md
@@ -90,6 +90,13 @@ queryString.parse('foo[0]=1&foo[1]=2&foo[3]=3', {arrayFormat: 'index'});
 //=> foo: [1,2,3]
 ```
 
+- `comma`: stands for parsing separating array elements with comma, such as:
+
+```js
+queryString.parse('foo=1,2,3', {arrayFormat: 'comma'});
+//=> foo: [1,2,3]
+```
+
 - `none`: is the **default** option and removes any bracket representation, such as:
 
 ```js
@@ -137,6 +144,14 @@ queryString.stringify({foo: [1,2,3]}, {arrayFormat: 'index'});
 // => foo[0]=1&foo[1]=2&foo[3]=3
 ```
 
+- `comma`: stands for parsing separating array elements with comma, such as:
+
+```js
+queryString.stringify({foo: [1,2,3]}, {arrayFormat: 'comma'});
+// => foo=1,2,3
+```
+
+
 - `none`: is the __default__ option and removes any bracket representation, such as:
 
 ```js


Test patch:
diff --git a/index.test-d.ts b/index.test-d.ts
index 243c241..2226bfc 100644
--- a/index.test-d.ts
+++ b/index.test-d.ts
@@ -20,6 +20,7 @@ expectType<string>(
 );
 expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'index'}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'none'}));
+expectType<string>(queryString.stringify({foo: 'bar'}, {arrayFormat: 'comma'}));
 expectType<string>(queryString.stringify({foo: 'bar'}, {sort: false}));
 const order = ['c', 'a', 'b'];
 expectType<string>(
@@ -47,6 +48,9 @@ expectType<queryString.ParsedQuery>(
 expectType<queryString.ParsedQuery>(
 	queryString.parse('?foo=bar', {arrayFormat: 'none'})
 );
+expectType<queryString.ParsedQuery>(
+	queryString.parse('?foo=bar', {arrayFormat: 'comma'})
+);
 
 // Parse URL
 expectType<queryString.ParsedUrl>(queryString.parseUrl('?foo=bar'));
@@ -63,6 +67,9 @@ expectType<queryString.ParsedUrl>(
 expectType<queryString.ParsedUrl>(
 	queryString.parseUrl('?foo=bar', {arrayFormat: 'none'})
 );
+expectType<queryString.ParsedUrl>(
+	queryString.parseUrl('?foo=bar', {arrayFormat: 'comma'})
+);
 
 // Extract
 expectType<string>(queryString.extract('http://foo.bar/?abc=def&hij=klm'));
diff --git a/test/parse.js b/test/parse.js
index 999cff8..93717cf 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -107,6 +107,12 @@ test('query strings having brackets arrays and format option as `bracket`', t =>
 	}), {foo: ['bar', 'baz']});
 });
 
+test('query strings having comma separated arrays and format option as `comma`', t => {
+	t.deepEqual(m.parse('foo=bar,baz', {
+		arrayFormat: 'comma'
+	}), {foo: ['bar', 'baz']});
+});
+
 test('query strings having brackets arrays with null and format option as `bracket`', t => {
 	t.deepEqual(m.parse('bar[]&foo[]=a&foo[]&foo[]=', {
 		arrayFormat: 'bracket'
@@ -116,6 +122,15 @@ test('query strings having brackets arrays with null and format option as `brack
 	});
 });
 
+test('query strings having comma separated arrays with null and format option as `comma`', t => {
+	t.deepEqual(m.parse('bar&foo=a,', {
+		arrayFormat: 'comma'
+	}), {
+		foo: ['a', ''],
+		bar: null
+	});
+});
+
 test('query strings having indexed arrays and format option as `index`', t => {
 	t.deepEqual(m.parse('foo[0]=bar&foo[1]=baz', {
 		arrayFormat: 'index'
diff --git a/test/stringify.js b/test/stringify.js
index 0302b50..535bba5 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -117,6 +117,24 @@ test('array stringify representation with array brackets and null value', t => {
 	}), 'bar[]&foo[]=a&foo[]&foo[]=');
 });
 
+test('array stringify representation with array commas', t => {
+	t.is(m.stringify({
+		foo: null,
+		bar: ['one', 'two']
+	}, {
+		arrayFormat: 'comma'
+	}), 'bar=one,two&foo');
+});
+
+test('array stringify representation with array commas and null value', t => {
+	t.is(m.stringify({
+		foo: ['a', null, ''],
+		bar: [null]
+	}, {
+		arrayFormat: 'comma'
+	}), 'foo=a');
+});
+
 test('array stringify representation with a bad array format', t => {
 	t.is(m.stringify({
 		foo: null,
@@ -134,7 +152,7 @@ test('array stringify representation with array indexes and sparse array', t =>
 
 test('should sort keys in given order', t => {
 	const order = ['c', 'a', 'b'];
-	const sort = (key1, key2) => order.indexOf(key1) >= order.indexOf(key2);
+	const sort = (key1, key2) => order.indexOf(key1) - order.indexOf(key2);
 
 	t.is(m.stringify({a: 'foo', b: 'bar', c: 'baz'}, {sort}), 'c=baz&a=foo&b=bar');
 });