Repository: sindresorhus/query-string (js)

Issue:
Add `pipe` format to the `arrayFormat` option 
Lovely package! Thanks for the great work!
However, I'd love to have the option to split multiple values with the pipe ("|") operator without being URL encoded! 

**For example:**: 
/some/url?someParamGroup=10|20|30|40?someOtherParamGrou=string|some-other-string

This format is mainly used to shorten URLs which include things as identifiers, where there is expected to be a lot of them. It's a lot more readable!

Thanks in advance!

Pull request description:
Support custom array separator
closes #226 

Patch:
diff --git a/index.d.ts b/index.d.ts
index 3fb11f0..864cea7 100644
--- a/index.d.ts
+++ b/index.d.ts
@@ -36,6 +36,15 @@ export interface ParseOptions {
 		//=> {foo: ['1', '2', '3']}
 		```
 
+	- `separator`: Parse arrays with elements separated by a custom character:
+
+		```
+		import queryString = require('query-string');
+
+		queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator: '|'});
+		//=> {foo: ['1', '2', '3']}
+		```
+
 	- `none`: Parse arrays with elements using duplicate keys:
 
 		```
@@ -45,7 +54,14 @@ export interface ParseOptions {
 		//=> {foo: ['1', '2', '3']}
 		```
 	*/
-	readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'none';
+	readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'none';
+
+	/**
+	The character used to separate array elements when using `{arrayFormat: 'separator'}`.
+
+	@default ,
+	*/
+	readonly arrayFormatSeparator?: 'string';
 
 	/**
 	Supports both `Function` as a custom sorting function or `false` to disable sorting.
@@ -188,6 +204,15 @@ export interface StringifyOptions {
 		//=> 'foo=1,2,3'
 		```
 
+  - `separator`: Serialize arrays by separating elements with character:
+
+		```
+		import queryString = require('query-string');
+
+		queryString.stringify({foo: [1, 2, 3]}, {arrayFormat: 'separator', arrayFormatSeparator: '|'});
+		//=> 'foo=1|2|3'
+		```
+
 	- `none`: Serialize arrays by using duplicate keys:
 
 		```
@@ -197,7 +222,14 @@ export interface StringifyOptions {
 		//=> 'foo=1&foo=2&foo=3'
 		```
 	*/
-	readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'none';
+	readonly arrayFormat?: 'bracket' | 'index' | 'comma' | 'separator' | 'none';
+
+	/**
+	The character used to separate array elements when using `{arrayFormat: 'separator'}`.
+
+	@default ,
+	*/
+	readonly arrayFormatSeparator?: 'string';
 
 	/**
 	Supports both `Function` as a custom sorting function or `false` to disable sorting.
diff --git a/index.js b/index.js
index d19e138..9c63162 100644
--- a/index.js
+++ b/index.js
@@ -36,6 +36,7 @@ function encoderForArrayFormat(options) {
 			};
 
 		case 'comma':
+		case 'separator':
 			return key => (result, value) => {
 				if (value === null || value === undefined || value.length === 0) {
 					return result;
@@ -45,7 +46,7 @@ function encoderForArrayFormat(options) {
 					return [[encode(key, options), '=', encode(value, options)].join('')];
 				}
 
-				return [[result, encode(value, options)].join(',')];
+				return [[result, encode(value, options)].join(options.arrayFormatSeparator)];
 			};
 
 		default:
@@ -104,9 +105,10 @@ function parserForArrayFormat(options) {
 			};
 
 		case 'comma':
+		case 'separator':
 			return (key, value, accumulator) => {
-				const isArray = typeof value === 'string' && value.split('').indexOf(',') > -1;
-				const newValue = isArray ? value.split(',').map(item => decode(item, options)) : value === null ? value : decode(value, options);
+				const isArray = typeof value === 'string' && value.split('').indexOf(options.arrayFormatSeparator) > -1;
+				const newValue = isArray ? value.split(options.arrayFormatSeparator).map(item => decode(item, options)) : value === null ? value : decode(value, options);
 				accumulator[key] = newValue;
 			};
 
@@ -122,6 +124,12 @@ function parserForArrayFormat(options) {
 	}
 }
 
+function validateArrayFormatSeparator(value) {
+	if (typeof value !== 'string' || value.length !== 1) {
+		throw new TypeError('arrayFormatSeparator must be single character string');
+	}
+}
+
 function encode(value, options) {
 	if (options.encode) {
 		return options.strict ? strictUriEncode(value) : encodeURIComponent(value);
@@ -196,10 +204,13 @@ function parse(input, options) {
 		decode: true,
 		sort: true,
 		arrayFormat: 'none',
+		arrayFormatSeparator: ',',
 		parseNumbers: false,
 		parseBooleans: false
 	}, options);
 
+	validateArrayFormatSeparator(options.arrayFormatSeparator);
+
 	const formatter = parserForArrayFormat(options);
 
 	// Create an object with no prototype
@@ -263,9 +274,12 @@ exports.stringify = (object, options) => {
 	options = Object.assign({
 		encode: true,
 		strict: true,
-		arrayFormat: 'none'
+		arrayFormat: 'none',
+		arrayFormatSeparator: ','
 	}, options);
 
+	validateArrayFormatSeparator(options.arrayFormatSeparator);
+
 	const formatter = encoderForArrayFormat(options);
 
 	const objectCopy = Object.assign({}, object);
diff --git a/readme.md b/readme.md
index 1dfacf9..3dd2ea0 100644
--- a/readme.md
+++ b/readme.md
@@ -121,6 +121,15 @@ queryString.parse('foo=1,2,3', {arrayFormat: 'comma'});
 //=> {foo: ['1', '2', '3']}
 ```
 
+- `'separator'`: Parse arrays with elements separated by a custom character:
+
+```js
+const queryString = require('query-string');
+
+queryString.parse('foo=1|2|3', {arrayFormat: 'separator', arrayFormatSeparator: '|'});
+//=> {foo: ['1', '2', '3']}
+```
+
 - `'none'`: Parse arrays with elements using duplicate keys:
 
 ```js
@@ -130,6 +139,13 @@ queryString.parse('foo=1&foo=2&foo=3');
 //=> {foo: ['1', '2', '3']}
 ```
 
+##### arrayFormatSeparator
+
+Type: `string`\
+Default: `','`
+
+The character used to separate array elements when using `{arrayFormat: 'separator'}`.
+
 ##### sort
 
 Type: `Function | boolean`\
@@ -228,6 +244,13 @@ queryString.stringify({foo: [1, 2, 3]});
 //=> 'foo=1&foo=2&foo=3'
 ```
 
+##### arrayFormatSeparator
+
+Type: `string`\
+Default: `','`
+
+The character used to separate array elements when using `{arrayFormat: 'separator'}`.
+
 ##### sort
 
 Type: `Function | boolean`


Test patch:
diff --git a/test/parse.js b/test/parse.js
index b2f4da0..3c442e7 100644
--- a/test/parse.js
+++ b/test/parse.js
@@ -143,6 +143,13 @@ test('query strings having comma separated arrays and format option as `comma`',
 	}), {foo: ['bar', 'baz']});
 });
 
+test('query strings having pipe separated arrays and format option as `separator`', t => {
+	t.deepEqual(queryString.parse('foo=bar|baz', {
+		arrayFormat: 'separator',
+		arrayFormatSeparator: '|'
+	}), {foo: ['bar', 'baz']});
+});
+
 test('query strings having brackets arrays with null and format option as `bracket`', t => {
 	t.deepEqual(queryString.parse('bar[]&foo[]=a&foo[]&foo[]=', {
 		arrayFormat: 'bracket'
@@ -253,6 +260,7 @@ test('NaN value returns as string if option is set', t => {
 test('parseNumbers works with arrayFormat', t => {
 	t.deepEqual(queryString.parse('foo[]=1&foo[]=2&foo[]=3&bar=1', {parseNumbers: true, arrayFormat: 'bracket'}), {foo: [1, 2, 3], bar: 1});
 	t.deepEqual(queryString.parse('foo=1,2,a', {parseNumbers: true, arrayFormat: 'comma'}), {foo: [1, 2, 'a']});
+	t.deepEqual(queryString.parse('foo=1|2|a', {parseNumbers: true, arrayFormat: 'separator', arrayFormatSeparator: '|'}), {foo: [1, 2, 'a']});
 	t.deepEqual(queryString.parse('foo[0]=1&foo[1]=2&foo[2]', {parseNumbers: true, arrayFormat: 'index'}), {foo: [1, 2, null]});
 	t.deepEqual(queryString.parse('foo=1&foo=2&foo=3', {parseNumbers: true}), {foo: [1, 2, 3]});
 });
@@ -285,6 +293,15 @@ test('parseNumbers and parseBooleans can work with arrayFormat at the same time'
 	t.deepEqual(queryString.parse('foo[0]=true&foo[1]=false&bar[0]=1&bar[1]=2', {parseNumbers: true, parseBooleans: true, arrayFormat: 'index'}), {foo: [true, false], bar: [1, 2]});
 });
 
+test('parse throws TypeError for invalid arrayFormatSeparator', t => {
+	t.throws(_ => queryString.parse('', {arrayFormatSeparator: ',,'}), {
+		instanceOf: TypeError
+	});
+	t.throws(_ => queryString.parse('', {arrayFormatSeparator: []}), {
+		instanceOf: TypeError
+	});
+});
+
 test('query strings having comma encoded and format option as `comma`', t => {
 	t.deepEqual(queryString.parse('foo=zero%2Cone,two%2Cthree', {arrayFormat: 'comma'}), {
 		foo: [
diff --git a/test/stringify.js b/test/stringify.js
index 09b6371..147d2e2 100644
--- a/test/stringify.js
+++ b/test/stringify.js
@@ -259,3 +259,12 @@ test('should ignore both null and undefined when skipNull is set for arrayFormat
 		arrayFormat: 'index'
 	}), 'a[0]=1&a[1]=2&c=1');
 });
+
+test('stringify throws TypeError for invalid arrayFormatSeparator', t => {
+	t.throws(_ => queryString.stringify({}, {arrayFormatSeparator: ',,'}), {
+		instanceOf: TypeError
+	});
+	t.throws(_ => queryString.stringify({}, {arrayFormatSeparator: []}), {
+		instanceOf: TypeError
+	});
+});