string-natural-compare
Advanced tools
Comparing version 2.0.3 to 3.0.0
'use strict'; | ||
var alphabet; | ||
var alphabetIndexMap; | ||
var alphabetIndexMapLength = 0; | ||
const defaultAlphabetIndexMap = []; | ||
function isNumberCode(code) { | ||
return code >= 48 && code <= 57; | ||
return code >= 48/* '0' */ && code <= 57/* '9' */; | ||
} | ||
function naturalCompare(a, b) { | ||
var lengthA = (a += '').length; | ||
var lengthB = (b += '').length; | ||
var aIndex = 0; | ||
var bIndex = 0; | ||
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}'`); | ||
} | ||
while (aIndex < lengthA && bIndex < lengthB) { | ||
var charCodeA = a.charCodeAt(aIndex); | ||
var charCodeB = b.charCodeAt(bIndex); | ||
const lengthA = a.length; | ||
const lengthB = b.length; | ||
let indexA = 0; | ||
let indexB = 0; | ||
let alphabetIndexMap = defaultAlphabetIndexMap; | ||
if (opts) { | ||
if (opts.caseInsensitive) { | ||
a = a.toLowerCase(); | ||
b = b.toLowerCase(); | ||
} | ||
if (opts.alphabet) { | ||
alphabetIndexMap = buildAlphabetIndexMap(opts.alphabet); | ||
} | ||
} | ||
while (indexA < lengthA && indexB < lengthB) { | ||
let charCodeA = a.charCodeAt(indexA); | ||
let charCodeB = b.charCodeAt(indexB); | ||
if (isNumberCode(charCodeA)) { | ||
@@ -26,14 +43,14 @@ if (!isNumberCode(charCodeB)) { | ||
var numStartA = aIndex; | ||
var numStartB = bIndex; | ||
let numStartA = indexA; | ||
let numStartB = indexB; | ||
while (charCodeA === 48 && ++numStartA < lengthA) { | ||
while (charCodeA === 48/* '0' */ && ++numStartA < lengthA) { | ||
charCodeA = a.charCodeAt(numStartA); | ||
} | ||
while (charCodeB === 48 && ++numStartB < lengthB) { | ||
while (charCodeB === 48/* '0' */ && ++numStartB < lengthB) { | ||
charCodeB = b.charCodeAt(numStartB); | ||
} | ||
var numEndA = numStartA; | ||
var numEndB = numStartB; | ||
let numEndA = numStartA; | ||
let numEndB = numStartB; | ||
@@ -47,4 +64,4 @@ while (numEndA < lengthA && isNumberCode(a.charCodeAt(numEndA))) { | ||
var difference = numEndA - numStartA - numEndB + numStartB; // numA length - numB length | ||
if (difference) { | ||
let difference = numEndA - numStartA - numEndB + numStartB; // numA length - numB length | ||
if (difference !== 0) { | ||
return difference; | ||
@@ -55,3 +72,3 @@ } | ||
difference = a.charCodeAt(numStartA++) - b.charCodeAt(numStartB++); | ||
if (difference) { | ||
if (difference !== 0) { | ||
return difference; | ||
@@ -61,4 +78,4 @@ } | ||
aIndex = numEndA; | ||
bIndex = numEndB; | ||
indexA = numEndA; | ||
indexB = numEndB; | ||
continue; | ||
@@ -69,4 +86,4 @@ } | ||
if ( | ||
charCodeA < alphabetIndexMapLength && | ||
charCodeB < alphabetIndexMapLength && | ||
charCodeA < alphabetIndexMap.length && | ||
charCodeB < alphabetIndexMap.length && | ||
alphabetIndexMap[charCodeA] !== -1 && | ||
@@ -81,12 +98,12 @@ alphabetIndexMap[charCodeB] !== -1 | ||
++aIndex; | ||
++bIndex; | ||
++indexA; | ||
++indexB; | ||
} | ||
if (aIndex >= lengthA && bIndex < lengthB && lengthA >= lengthB) { | ||
return -1; | ||
if (indexA < lengthA) { // `b` is a substring of `a` | ||
return 1; | ||
} | ||
if (bIndex >= lengthB && aIndex < lengthA && lengthB >= lengthA) { | ||
return 1; | ||
if (indexB < lengthB) { // `a` is a substring of `b` | ||
return -1; | ||
} | ||
@@ -97,35 +114,28 @@ | ||
naturalCompare.caseInsensitive = naturalCompare.i = function(a, b) { | ||
return naturalCompare(('' + a).toLowerCase(), ('' + b).toLowerCase()); | ||
}; | ||
const alphabetIndexMapCache = {}; | ||
Object.defineProperties(naturalCompare, { | ||
alphabet: { | ||
get: function() { | ||
return alphabet; | ||
}, | ||
function buildAlphabetIndexMap(alphabet) { | ||
const existingMap = alphabetIndexMapCache[alphabet]; | ||
if (existingMap !== undefined) { | ||
return existingMap; | ||
} | ||
set: function(value) { | ||
alphabet = value; | ||
alphabetIndexMap = []; | ||
const indexMap = []; | ||
const maxCharCode = alphabet.split('').reduce((maxCode, char) => { | ||
return Math.max(maxCode, char.charCodeAt(0)); | ||
}, 0); | ||
var i = 0; | ||
for (let i = 0; i <= maxCharCode; i++) { | ||
indexMap.push(-1); | ||
} | ||
if (alphabet) { | ||
for (; i < alphabet.length; i++) { | ||
alphabetIndexMap[alphabet.charCodeAt(i)] = i; | ||
} | ||
} | ||
for (let i = 0; i < alphabet.length; i++) { | ||
indexMap[alphabet.charCodeAt(i)] = i; | ||
} | ||
alphabetIndexMapLength = alphabetIndexMap.length; | ||
alphabetIndexMapCache[alphabet] = indexMap; | ||
for (i = 0; i < alphabetIndexMapLength; i++) { | ||
if (alphabetIndexMap[i] === undefined) { | ||
alphabetIndexMap[i] = -1; | ||
} | ||
} | ||
}, | ||
}, | ||
}); | ||
return indexMap; | ||
} | ||
module.exports = naturalCompare; |
{ | ||
"name": "string-natural-compare", | ||
"version": "2.0.3", | ||
"version": "3.0.0", | ||
"description": "Compare alphanumeric strings the same way a human would, using a natural order algorithm", | ||
@@ -11,24 +11,5 @@ "author": "Nathan Woltman <nwoltman@outlook.com>", | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/nwoltman/string-natural-compare" | ||
}, | ||
"bugs": { | ||
"url": "https://github.com/nwoltman/string-natural-compare/issues" | ||
}, | ||
"repository": "github:nwoltman/string-natural-compare", | ||
"homepage": "https://github.com/nwoltman/string-natural-compare", | ||
"devDependencies": { | ||
"coveralls": "^3.0.2", | ||
"grunt": "~1.0.3", | ||
"grunt-eslint": "^21.0.0", | ||
"grunt-mocha-istanbul": "^5.0.2", | ||
"grunt-mocha-test": "^0.13.3", | ||
"istanbul": "^0.4.5", | ||
"jit-grunt": "^0.10.0", | ||
"mocha": "^5.2.0", | ||
"should": "^13.2.1" | ||
}, | ||
"scripts": { | ||
"test": "grunt" | ||
}, | ||
"bugs": "https://github.com/nwoltman/string-natural-compare/issues", | ||
"keywords": [ | ||
@@ -46,3 +27,30 @@ "string", | ||
"alphanumeric" | ||
] | ||
], | ||
"eslintIgnore": [ | ||
"benchmark/node_modules/", | ||
"coverage/" | ||
], | ||
"nyc": { | ||
"reporter": [ | ||
"html", | ||
"text-summary" | ||
], | ||
"check-coverage": true, | ||
"branches": 100, | ||
"lines": 100, | ||
"statements": 100 | ||
}, | ||
"devDependencies": { | ||
"@nwoltman/eslint-config": "^0.5.0", | ||
"coveralls": "^3.0.7", | ||
"eslint": "^6.6.0", | ||
"mocha": "^6.2.2", | ||
"nyc": "^14.1.1", | ||
"should": "^13.2.3" | ||
}, | ||
"scripts": { | ||
"lint": "eslint .", | ||
"test": "eslint . && nyc mocha", | ||
"coveralls": "nyc report --reporter=text-lcov | coveralls" | ||
} | ||
} |
@@ -8,3 +8,3 @@ # 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) | ||
@@ -19,13 +19,8 @@ ``` | ||
This module provides two functions: | ||
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. | ||
+ `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. | ||
### Fast and Robust | ||
This module uses a performant and robust algorithm to compare alphanumeric strings. It can compare strings containing any size of number and is heavily tested with a custom [benchmark suite](https://github.com/nwoltman/string-natural-compare/tree/master/benchmark) to make sure that it is as fast as possible, even when a [custom alphabet](#custom-alphabet) has been configured. | ||
This module can compare strings containing any size of number and is heavily tested with a custom [benchmark suite](https://github.com/nwoltman/string-natural-compare/tree/master/benchmark) to make sure that it is as fast as possible. | ||
@@ -36,6 +31,4 @@ | ||
```sh | ||
# npm | ||
npm install string-natural-compare --save | ||
# yarn | ||
# or | ||
yarn add string-natural-compare | ||
@@ -47,19 +40,28 @@ ``` | ||
#### `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`. | ||
+ `alphabet` (_string_) - A string of characters that define a custom character ordering. Default: `undefined`. | ||
```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'] | ||
@@ -73,51 +75,37 @@ | ||
// -> 1 | ||
// Other inputs with the same ordering as this may yield a different number > 0 | ||
// (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 = [ | ||
// large array of objects, it is best for performance to pre-compute the | ||
// transformed text and store it on the object. This way, the text will not need | ||
// to be transformed 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 | ||
// Estonian alphabet | ||
naturalCompare.alphabet = 'ABDEFGHIJKLMNOPRSŠZŽTUVÕÄÖÜXYabdefghijklmnoprsšzžtuvõäöüxy'; | ||
['t', 'z', 'x', 'õ'].sort(naturalCompare); | ||
// -> ['z', 't', 'õ', 'x'] | ||
// Russian alphabet | ||
naturalCompare.alphabet = 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя'; | ||
['Ё', 'А', 'б', 'Б'].sort(naturalCompare); | ||
// Using a custom alphabet (Russian alphabet) | ||
const russianOpts = { | ||
alphabet: 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюя', | ||
}; | ||
['Ё', 'А', 'б', 'Б'].sort((a, b) => naturalCompare(a, b, russianOpts)); | ||
// -> ['А', 'Б', 'Ё', 'б'] | ||
@@ -124,0 +112,0 @@ ``` |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
9718
6
105
1
110
1