Skip to content

Commit

Permalink
refactor!: Throw on non-string arguments and replace the caseInsensit…
Browse files Browse the repository at this point in the history
…ive function with an option

BREAKING CHANGE: `naturalCompare()` now throws if either of the first two arguments are not a string. Also, the `naturalCompare.caseInsensitive()` function has been removed. Pass `{caseInsensitive: true}` as the third parameter to `naturalCompare()` instead.
  • Loading branch information
nwoltman committed Oct 29, 2019
1 parent 6d9d8c0 commit 65d397b
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 97 deletions.
71 changes: 34 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Compare alphanumeric strings the same way a human would, using a natural order a
[![NPM Version](https://img.shields.io/npm/v/string-natural-compare.svg)](https://www.npmjs.com/package/string-natural-compare)
[![Build Status](https://travis-ci.org/nwoltman/string-natural-compare.svg?branch=master)](https://travis-ci.org/nwoltman/string-natural-compare)
[![Coverage Status](https://coveralls.io/repos/nwoltman/string-natural-compare/badge.svg?branch=master)](https://coveralls.io/r/nwoltman/string-natural-compare?branch=master)
[![devDependencies Status](https://david-dm.org/nwoltman/string-natural-compare/dev-status.svg)](https://david-dm.org/nwoltman/string-natural-compare?type=dev)
[![Dependencies Status](https://img.shields.io/david/nwoltman/string-natural-compare)](https://david-dm.org/nwoltman/string-natural-compare)

```
Standard sorting: Natural order sorting:
Expand All @@ -15,13 +15,8 @@ Standard sorting: Natural order sorting:
img2.png img12.png
```

This module provides two functions:

+ `naturalCompare`
+ `naturalCompare.caseInsensitive` (alias: `naturalCompare.i`)

These functions return a number indicating whether one string should come before, after, or is the same as another string.
They can be easily used with the native [`.sort()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) array method.
This module exports a function that returns a number indicating whether one string should come before, after, or is the same as another string.
It can be used directly with the native [`.sort()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort) array method.

### Fast and Robust

Expand All @@ -31,33 +26,39 @@ This module can compare strings containing any size of number and is heavily tes
## Installation

```sh
# npm
npm install string-natural-compare --save

# yarn
# or
yarn add string-natural-compare
```


## Usage

#### `naturalCompare(strA, strB[, options])`

+ `strA` (_string_)
+ `strB` (_string_)
+ `options` (_object_) - Optional options object with the following options:
+ `caseInsensitive` (_boolean_) - Set to `true` to compare strings case-insensitively. Default: `false`.

```js
var naturalCompare = require('string-natural-compare');
const naturalCompare = require('string-natural-compare');

// Simple case-sensitive sorting
var a = ['z1.doc', 'z10.doc', 'z17.doc', 'z2.doc', 'z23.doc', 'z3.doc'];
a.sort(naturalCompare);
// Simple, case-sensitive sorting
const files = ['z1.doc', 'z10.doc', 'z17.doc', 'z2.doc', 'z23.doc', 'z3.doc'];
files.sort(naturalCompare);
// -> ['z1.doc', 'z2.doc', 'z3.doc', 'z10.doc', 'z17.doc', 'z23.doc']


// Simple case-insensitive sorting
var a = ['B', 'C', 'a', 'd'];
a.sort(naturalCompare.caseInsensitive); // alias: a.sort(naturalCompare.i);
// Case-insensitive sorting
const chars = ['B', 'C', 'a', 'd'];
const naturalCompareCI = (a, b) => naturalCompare(a, b, {caseInsensitive: true});
chars.sort(naturalCompareCI);
// -> ['a', 'B', 'C', 'd']

// Note:
['a', 'A'].sort(naturalCompare.caseInsensitive); // -> ['a', 'A']
['A', 'a'].sort(naturalCompare.caseInsensitive); // -> ['A', 'a']
['a', 'A'].sort(naturalCompareCI); // -> ['a', 'A']
['A', 'a'].sort(naturalCompareCI); // -> ['A', 'a']


// Compare strings containing large numbers
Expand All @@ -69,44 +70,40 @@ naturalCompare(
// (Other inputs with the same ordering as this example may yield a different number > 0)


// In most cases we want to sort an array of objects
var a = [
// Sorting an array of objects
const hotelRooms = [
{street: '350 5th Ave', room: 'A-1021'},
{street: '350 5th Ave', room: 'A-21046-b'}
];

// Sort by street (case-insensitive), then by room (case-sensitive)
a.sort(function(a, b) {
return (
naturalCompare.caseInsensitive(a.street, b.street) ||
naturalCompare(a.room, b.room)
);
});
hotelRooms.sort((a, b) => (
naturalCompare(a.street, b.street, {caseInsensitive: true}) ||
naturalCompare(a.room, b.room)
));


// When text transformation is needed or when doing a case-insensitive sort on a
// large array, it is best for performance to pre-compute the transformed text
// and store it in that object. This way, the text transformation will not be
// needed for every comparison when sorting.
var a = [
// needed for every comparison while sorting.
const cars = [
{make: 'Audi', model: 'R8'},
{make: 'Porsche', model: '911 Turbo S'}
];

// Sort by make, then by model (both case-insensitive)
a.forEach(function(car) {
for (const car of cars) {
car.sortKey = (car.make + ' ' + car.model).toLowerCase();
});
a.sort(function(a, b) {
return naturalCompare(a.sortKey, b.sortKey);
});
}
cars.sort((a, b) => naturalCompare(a.sortKey, b.sortKey));
```

### Custom Alphabet

It is possible to configure a custom alphabet to achieve a desired character ordering.

```js
const naturalCompare = require('string-natural-compare');

// Estonian alphabet
naturalCompare.alphabet = 'ABDEFGHIJKLMNOPRSŠZŽTUVÕÄÖÜXYabdefghijklmnoprsšzžtuvõäöüxy';
['t', 'z', 'x', 'õ'].sort(naturalCompare);
Expand Down
22 changes: 15 additions & 7 deletions natural-compare.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,21 @@ function isNumberCode(code) {
return code >= 48 && code <= 57;
}

function naturalCompare(a, b) {
var lengthA = (a += '').length;
var lengthB = (b += '').length;
function naturalCompare(a, b, opts) {
if (typeof a !== 'string') {
throw new TypeError(`The first argument must be a string. Received type '${typeof a}'`);
}
if (typeof b !== 'string') {
throw new TypeError(`The second argument must be a string. Received type '${typeof b}'`);
}

if (opts && opts.caseInsensitive) {
a = a.toLowerCase();
b = b.toLowerCase();
}

var lengthA = a.length;
var lengthB = b.length;
var aIndex = 0;
var bIndex = 0;

Expand Down Expand Up @@ -88,10 +100,6 @@ function naturalCompare(a, b) {
return lengthA - lengthB;
}

naturalCompare.caseInsensitive = naturalCompare.i = function(a, b) {
return naturalCompare(('' + a).toLowerCase(), ('' + b).toLowerCase());
};

Object.defineProperties(naturalCompare, {
alphabet: {
get() {
Expand Down
137 changes: 84 additions & 53 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,95 @@

require('should');

const assert = require('assert').strict;
const naturalCompare = require('../');

function verify(testData) {
const a = testData[0];
const b = testData[2];
const failMessage = 'failure on input: [' + testData.join(' ') + ']';
const failMessage = `failure on input: [${testData.join(' ')}]`;

switch (testData[1]) {
case '=':
naturalCompare(a, b).should.equal(0, failMessage);
naturalCompare.caseInsensitive(a, b).should.equal(0, failMessage);
naturalCompare(a, b, {caseInsensitive: true}).should.equal(0, failMessage);
break;
case '>':
naturalCompare(a, b).should.be.greaterThan(0, failMessage);
naturalCompare.caseInsensitive(a, b).should.be.greaterThan(0, failMessage);
naturalCompare(a, b, {caseInsensitive: true}).should.be.greaterThan(0, failMessage);
break;
case '<':
naturalCompare(a, b).should.be.lessThan(0, failMessage);
naturalCompare.caseInsensitive(a, b).should.be.lessThan(0, failMessage);
naturalCompare(a, b, {caseInsensitive: true}).should.be.lessThan(0, failMessage);
break;
default:
throw new Error('Unknown comparison operator: ' + testData[1]);
}
}

describe('naturalCompare() and naturalCompare.caseInsensitive()', () => {
describe('naturalCompare()', () => {

it('should throw if the first argument is not a string', () => {
assert.throws(
() => naturalCompare(undefined),
new TypeError("The first argument must be a string. Received type 'undefined'")
);
assert.throws(
() => naturalCompare(null),
new TypeError("The first argument must be a string. Received type 'object'")
);
assert.throws(
() => naturalCompare(1),
new TypeError("The first argument must be a string. Received type 'number'")
);
assert.throws(
() => naturalCompare(false),
new TypeError("The first argument must be a string. Received type 'boolean'")
);
assert.throws(
() => naturalCompare({}),
new TypeError("The first argument must be a string. Received type 'object'")
);
assert.throws(
() => naturalCompare([]),
new TypeError("The first argument must be a string. Received type 'object'")
);
assert.throws(
() => naturalCompare(Symbol('sym')),
new TypeError("The first argument must be a string. Received type 'symbol'")
);
});

it('should throw if the second argument is not a string', () => {
assert.throws(
() => naturalCompare('', undefined),
new TypeError("The second argument must be a string. Received type 'undefined'")
);
assert.throws(
() => naturalCompare('', null),
new TypeError("The second argument must be a string. Received type 'object'")
);
assert.throws(
() => naturalCompare('', 0),
new TypeError("The second argument must be a string. Received type 'number'")
);
assert.throws(
() => naturalCompare('', true),
new TypeError("The second argument must be a string. Received type 'boolean'")
);
assert.throws(
() => naturalCompare('', {}),
new TypeError("The second argument must be a string. Received type 'object'")
);
assert.throws(
() => naturalCompare('', []),
new TypeError("The second argument must be a string. Received type 'object'")
);
assert.throws(
() => naturalCompare('', Symbol('sym')),
new TypeError("The second argument must be a string. Received type 'symbol'")
);
});

it('should compare strings that do not contain numbers', () => {
[
Expand Down Expand Up @@ -118,22 +181,6 @@ describe('naturalCompare() and naturalCompare.caseInsensitive()', () => {
].forEach(verify);
});

it('should compare non-string inputs as strings', () => {
[
[1, '<', 2],
[2, '>', 1],
[20, '>', 3],
[true, '>', false],
[null, '<', undefined],
[{}, '=', {}],
[
{toString: () => 'a'},
'<',
{toString: () => 'b'},
],
].forEach(verify);
});

it('should correctly compare strings containing very large numbers', () => {
[
[
Expand All @@ -154,12 +201,7 @@ describe('naturalCompare() and naturalCompare.caseInsensitive()', () => {
].forEach(verify);
});

});


describe('naturalCompare()', () => {

it('should perform case-sensitive comparisons', () => {
it('should perform case-sensitive comparisons by default', () => {
naturalCompare('a', 'A').should.be.greaterThan(0);
naturalCompare('b', 'C').should.be.greaterThan(0);
});
Expand Down Expand Up @@ -209,40 +251,29 @@ describe('naturalCompare()', () => {
.sort(naturalCompare)
.should.deepEqual(['1', '2', '9', '10', 'A', 'B', 'Š', 'X', 'a', 'z', 'u', 'õ', 'ä', 'Д']);

naturalCompare.alphabet = ''; // Don't mess up other tests
});

});


describe('naturalCompare.caseInsensitive()', () => {

it('should perform case-insensitive comparisons', () => {
naturalCompare.caseInsensitive('a', 'A').should.equal(0);
naturalCompare.caseInsensitive('b', 'C').should.be.lessThan(0);
naturalCompare.alphabet = null; // Reset alphabet for other tests
});

it('should function correctly as the callback to array.sort()', () => {
['C', 'B', 'a', 'd']
.sort(naturalCompare.caseInsensitive)
.should.deepEqual(['a', 'B', 'C', 'd']);
});

it('should compare strings using the provided alphabet', () => {
naturalCompare.alphabet = 'ABDEFGHIJKLMNOPRSŠZŽTUVÕÄÖÜXYabdefghijklmnoprsšzžtuvõäöüxy';
describe('with {caseInsensitive: true}', () => {

['Д', 'a', 'ä', 'B', 'Š', 'X', 'Ü', 'õ', 'u', 'z', '1', '2', '9', '10']
.sort(naturalCompare.caseInsensitive)
.should.deepEqual(['1', '2', '9', '10', 'a', 'B', 'Š', 'z', 'u', 'õ', 'ä', 'Ü', 'X', 'Д']);
});
it('should perform case-insensitive comparisons', () => {
naturalCompare('a', 'A', {caseInsensitive: true}).should.equal(0);
naturalCompare('b', 'C', {caseInsensitive: true}).should.be.lessThan(0);

});
['C', 'B', 'a', 'd']
.sort((a, b) => naturalCompare(a, b, {caseInsensitive: true}))
.should.deepEqual(['a', 'B', 'C', 'd']);
});

it('should compare strings using the provided alphabet', () => {
naturalCompare.alphabet = 'ABDEFGHIJKLMNOPRSŠZŽTUVÕÄÖÜXYabdefghijklmnoprsšzžtuvõäöüxy';

describe('naturalCompare.i', () => {
['Д', 'a', 'ä', 'B', 'Š', 'X', 'Ü', 'õ', 'u', 'z', '1', '2', '9', '10']
.sort((a, b) => naturalCompare(a, b, {caseInsensitive: true}))
.should.deepEqual(['1', '2', '9', '10', 'a', 'B', 'Š', 'z', 'u', 'õ', 'ä', 'Ü', 'X', 'Д']);
});

it('is an alias for naturalCompare.caseInsensitive', () => {
naturalCompare.i.should.equal(naturalCompare.caseInsensitive);
});

});
Expand Down

0 comments on commit 65d397b

Please sign in to comment.