natural-compare.js 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. 'use strict';
  2. const defaultAlphabetIndexMap = [];
  3. function isNumberCode(code) {
  4. return code >= 48/* '0' */ && code <= 57/* '9' */;
  5. }
  6. function naturalCompare(a, b, opts) {
  7. if (typeof a !== 'string') {
  8. throw new TypeError(`The first argument must be a string. Received type '${typeof a}'`);
  9. }
  10. if (typeof b !== 'string') {
  11. throw new TypeError(`The second argument must be a string. Received type '${typeof b}'`);
  12. }
  13. const lengthA = a.length;
  14. const lengthB = b.length;
  15. let indexA = 0;
  16. let indexB = 0;
  17. let alphabetIndexMap = defaultAlphabetIndexMap;
  18. let firstDifferenceInLeadingZeros = 0;
  19. if (opts) {
  20. if (opts.caseInsensitive) {
  21. a = a.toLowerCase();
  22. b = b.toLowerCase();
  23. }
  24. if (opts.alphabet) {
  25. alphabetIndexMap = buildAlphabetIndexMap(opts.alphabet);
  26. }
  27. }
  28. while (indexA < lengthA && indexB < lengthB) {
  29. let charCodeA = a.charCodeAt(indexA);
  30. let charCodeB = b.charCodeAt(indexB);
  31. if (isNumberCode(charCodeA)) {
  32. if (!isNumberCode(charCodeB)) {
  33. return charCodeA - charCodeB;
  34. }
  35. let numStartA = indexA;
  36. let numStartB = indexB;
  37. while (charCodeA === 48/* '0' */ && ++numStartA < lengthA) {
  38. charCodeA = a.charCodeAt(numStartA);
  39. }
  40. while (charCodeB === 48/* '0' */ && ++numStartB < lengthB) {
  41. charCodeB = b.charCodeAt(numStartB);
  42. }
  43. if (numStartA !== numStartB && firstDifferenceInLeadingZeros === 0) {
  44. firstDifferenceInLeadingZeros = numStartA - numStartB;
  45. }
  46. let numEndA = numStartA;
  47. let numEndB = numStartB;
  48. while (numEndA < lengthA && isNumberCode(a.charCodeAt(numEndA))) {
  49. ++numEndA;
  50. }
  51. while (numEndB < lengthB && isNumberCode(b.charCodeAt(numEndB))) {
  52. ++numEndB;
  53. }
  54. let difference = numEndA - numStartA - numEndB + numStartB; // numA length - numB length
  55. if (difference !== 0) {
  56. return difference;
  57. }
  58. while (numStartA < numEndA) {
  59. difference = a.charCodeAt(numStartA++) - b.charCodeAt(numStartB++);
  60. if (difference !== 0) {
  61. return difference;
  62. }
  63. }
  64. indexA = numEndA;
  65. indexB = numEndB;
  66. continue;
  67. }
  68. if (charCodeA !== charCodeB) {
  69. if (
  70. charCodeA < alphabetIndexMap.length &&
  71. charCodeB < alphabetIndexMap.length &&
  72. alphabetIndexMap[charCodeA] !== -1 &&
  73. alphabetIndexMap[charCodeB] !== -1
  74. ) {
  75. return alphabetIndexMap[charCodeA] - alphabetIndexMap[charCodeB];
  76. }
  77. return charCodeA - charCodeB;
  78. }
  79. ++indexA;
  80. ++indexB;
  81. }
  82. if (indexA < lengthA) { // `b` is a substring of `a`
  83. return 1;
  84. }
  85. if (indexB < lengthB) { // `a` is a substring of `b`
  86. return -1;
  87. }
  88. return firstDifferenceInLeadingZeros;
  89. }
  90. const alphabetIndexMapCache = {};
  91. function buildAlphabetIndexMap(alphabet) {
  92. const existingMap = alphabetIndexMapCache[alphabet];
  93. if (existingMap !== undefined) {
  94. return existingMap;
  95. }
  96. const indexMap = [];
  97. const maxCharCode = alphabet.split('').reduce((maxCode, char) => {
  98. return Math.max(maxCode, char.charCodeAt(0));
  99. }, 0);
  100. for (let i = 0; i <= maxCharCode; i++) {
  101. indexMap.push(-1);
  102. }
  103. for (let i = 0; i < alphabet.length; i++) {
  104. indexMap[alphabet.charCodeAt(i)] = i;
  105. }
  106. alphabetIndexMapCache[alphabet] = indexMap;
  107. return indexMap;
  108. }
  109. module.exports = naturalCompare;