diff --git a/src/natural_sort.ts b/src/natural_sort.ts index 8bc5e2e..d9f6c79 100644 --- a/src/natural_sort.ts +++ b/src/natural_sort.ts @@ -7,9 +7,13 @@ * file that was distributed with this source code. */ +let collator: Intl.Collator | undefined + /** * Perform natural sorting with "Array.sort()" method */ export function naturalSort(current: string, next: string) { - return current.localeCompare(next, undefined, { numeric: true, sensitivity: 'base' }) + // Create the collator only when natural sorting is used, not on package import. + collator ??= new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }) + return collator.compare(current, next) } diff --git a/tests/fs_read_all.spec.ts b/tests/fs_read_all.spec.ts index 30c7a21..a63ab06 100644 --- a/tests/fs_read_all.spec.ts +++ b/tests/fs_read_all.spec.ts @@ -45,6 +45,17 @@ test.group('FS read all | relative paths', (group) => { assert.deepEqual(files, ['app.ts', 'config.js', 'main.json', 'server.ts'].map(normalize)) }) + test('naturally sort filenames and nested directories', async ({ assert }) => { + for (const file of ['item10.js', 'item2.js', 'nested10/item1.js', 'nested2/item1.js']) { + await outputFile(join(BASE_PATH, file), '') + } + + assert.deepEqual( + await fsReadAll(BASE_PATH), + ['item2.js', 'item10.js', 'nested2/item1.js', 'nested10/item1.js'].map(normalize) + ) + }) + test('recursively get a list of all files from a directory', async ({ assert, expectTypeOf }) => { await outputFile(join(BASE_PATH, 'app.ts'), '') await outputFile(join(BASE_PATH, 'app/server.ts'), '') diff --git a/tests/natural_sort.spec.ts b/tests/natural_sort.spec.ts new file mode 100644 index 0000000..72e8986 --- /dev/null +++ b/tests/natural_sort.spec.ts @@ -0,0 +1,60 @@ +/* + * @poppinss/utils + * + * (c) Poppinss + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { naturalSort } from '../src/natural_sort.js' + +test.group('natural sort', () => { + test('sort numbers naturally and preserve the order of equivalent strings', ({ assert }) => { + assert.deepEqual(['file10', 'file02', 'File2', 'filé2', 'file1'].sort(naturalSort), [ + 'file1', + 'file02', + 'File2', + 'filé2', + 'file10', + ]) + }) + + test('match localeCompare ordering for the default locale', ({ assert }) => { + const values = [ + '', + 'a', + 'A', + 'ä', + 'å', + 'z', + 'é', + 'e\u0301', + 'ß', + 'ss', + 'ı', + 'i', + 'İ', + 'I', + '配置', + '😀', + '\ud800', + '\udfff', + '\u0000', + ...['file', 'nested/item', 'with space/'].flatMap((prefix) => + ['0', '01', '1', '2', '10', '100', '١', '2'].map((number) => `${prefix}${number}.js`) + ), + ] + const previous = (current: string, next: string) => + current.localeCompare(next, undefined, { numeric: true, sensitivity: 'base' }) + + for (const current of values) { + for (const next of values) { + assert.equal(Math.sign(naturalSort(current, next)), Math.sign(previous(current, next))) + } + } + assert.deepEqual([...values].sort(naturalSort), [...values].sort(previous)) + assert.deepEqual([...values].reverse().sort(naturalSort), [...values].reverse().sort(previous)) + }) +})