natural-sort.js (5183B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 /* 6 * Based on the Natural Sort algorithm for Javascript - Version 0.8.1 - adapted 7 * for Firefox DevTools and released under the MIT license. 8 * 9 * Author: Jim Palmer (based on chunking idea from Dave Koelle) 10 * 11 * Repository: 12 * https://github.com/overset/javascript-natural-sort/ 13 */ 14 15 "use strict"; 16 17 const tokenizeNumbersRx = 18 /(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g; 19 const hexRx = /^0x[0-9a-f]+$/i; 20 const startsWithNullRx = /^\0/; 21 const endsWithNullRx = /\0$/; 22 const whitespaceRx = /\s+/g; 23 const startsWithZeroRx = /^0/; 24 const versionRx = /^([\w-]+-)?\d+\.\d+\.\d+$/; 25 const numericDateRx = /^\d+[- /]\d+[- /]\d+$/; 26 27 // If a string contains any of these, we'll try to parse it as a Date 28 const dateKeywords = [ 29 "mon", 30 "tues", 31 "wed", 32 "thur", 33 "fri", 34 "sat", 35 "sun", 36 37 "jan", 38 "feb", 39 "mar", 40 "apr", 41 "may", 42 "jun", 43 "jul", 44 "aug", 45 "sep", 46 "oct", 47 "nov", 48 "dec", 49 ]; 50 51 /** 52 * Figures whether a given string should be considered by naturalSort to be a 53 * Date, and returns the Date's timestamp if so. Some Date formats, like 54 * single numbers and MM.DD.YYYY, are not supported due to conflicts with things 55 * like version numbers. 56 */ 57 function tryParseDate(str) { 58 const lowerCaseStr = str.toLowerCase(); 59 return ( 60 !versionRx.test(str) && 61 (numericDateRx.test(str) || 62 dateKeywords.some(s => lowerCaseStr.includes(s))) && 63 Date.parse(str) 64 ); 65 } 66 67 /** 68 * Sort numbers, strings, IP Addresses, Dates, Filenames, version numbers etc. 69 * "the way humans do." 70 * 71 * @param {object} a 72 * Passed in by Array.sort(a, b) 73 * @param {object} b 74 * Passed in by Array.sort(a, b) 75 * @param {string} sessionString 76 * Client-side value of storage-expires-session l10n string. 77 * Since this function can be called from both the client and the server, 78 * and given that client and server might have different locale, we can't compute 79 * the localized string directly from here. 80 * @param {boolean} insensitive 81 * Should the search be case insensitive? 82 */ 83 // eslint-disable-next-line complexity 84 function naturalSort(a = "", b = "", sessionString, insensitive = false) { 85 // Ensure we are working with trimmed strings 86 a = (a + "").trim(); 87 b = (b + "").trim(); 88 89 if (insensitive) { 90 a = a.toLowerCase(); 91 b = b.toLowerCase(); 92 sessionString = sessionString.toLowerCase(); 93 } 94 95 // Chunk/tokenize - Here we split the strings into arrays or strings and 96 // numbers. 97 const aChunks = a 98 .replace(tokenizeNumbersRx, "\0$1\0") 99 .replace(startsWithNullRx, "") 100 .replace(endsWithNullRx, "") 101 .split("\0"); 102 const bChunks = b 103 .replace(tokenizeNumbersRx, "\0$1\0") 104 .replace(startsWithNullRx, "") 105 .replace(endsWithNullRx, "") 106 .split("\0"); 107 108 // Hex or date detection. 109 const aHexOrDate = parseInt(a.match(hexRx), 16) || tryParseDate(a); 110 const bHexOrDate = parseInt(b.match(hexRx), 16) || tryParseDate(b); 111 112 if ( 113 (aHexOrDate || bHexOrDate) && 114 (a === sessionString || b === sessionString) 115 ) { 116 // We have a date and a session string. Move "Session" above the date 117 // (for session cookies) 118 if (a === sessionString) { 119 return -1; 120 } else if (b === sessionString) { 121 return 1; 122 } 123 } 124 125 // Try and sort Hex codes or Dates. 126 if (aHexOrDate && bHexOrDate) { 127 if (aHexOrDate < bHexOrDate) { 128 return -1; 129 } else if (aHexOrDate > bHexOrDate) { 130 return 1; 131 } 132 return 0; 133 } 134 135 // Natural sorting through split numeric strings and default strings 136 const aChunksLength = aChunks.length; 137 const bChunksLength = bChunks.length; 138 const maxLen = Math.max(aChunksLength, bChunksLength); 139 140 for (let i = 0; i < maxLen; i++) { 141 const aChunk = normalizeChunk(aChunks[i] || "", aChunksLength); 142 const bChunk = normalizeChunk(bChunks[i] || "", bChunksLength); 143 144 // Handle numeric vs string comparison - number < string 145 if (isNaN(aChunk) !== isNaN(bChunk)) { 146 return isNaN(aChunk) ? 1 : -1; 147 } 148 149 // If unicode use locale comparison 150 // eslint-disable-next-line no-control-regex 151 if (/[^\x00-\x80]/.test(aChunk + bChunk) && aChunk.localeCompare) { 152 const comp = aChunk.localeCompare(bChunk); 153 return comp / Math.abs(comp); 154 } 155 if (aChunk < bChunk) { 156 return -1; 157 } else if (aChunk > bChunk) { 158 return 1; 159 } 160 } 161 return null; 162 } 163 164 // Normalize spaces; find floats not starting with '0', string or 0 if not 165 // defined 166 const normalizeChunk = function (str, length) { 167 return ( 168 ((!str.match(startsWithZeroRx) || length == 1) && parseFloat(str)) || 169 str.replace(whitespaceRx, " ").trim() || 170 0 171 ); 172 }; 173 174 exports.naturalSortCaseSensitive = function naturalSortCaseSensitive( 175 a, 176 b, 177 sessionString 178 ) { 179 return naturalSort(a, b, sessionString, false); 180 }; 181 182 exports.naturalSortCaseInsensitive = function naturalSortCaseInsensitive( 183 a, 184 b, 185 sessionString 186 ) { 187 return naturalSort(a, b, sessionString, true); 188 };