hashids.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. export default class Hashids {
  2. constructor(salt = '', minLength = 0, alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890') {
  3. const minAlphabetLength = 16;
  4. const sepDiv = 3.5;
  5. const guardDiv = 12;
  6. const errorAlphabetLength = 'error: alphabet must contain at least X unique characters';
  7. const errorAlphabetSpace = 'error: alphabet cannot contain spaces';
  8. let uniqueAlphabet = '', sepsLength, diff;
  9. /* funcs */
  10. this.escapeRegExp = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
  11. this.parseInt = (v, radix) => (/^(-|\+)?([0-9]+|Infinity)$/.test(v)) ? parseInt(v, radix) : NaN;
  12. /* alphabet vars */
  13. this.seps = 'cfhistuCFHISTU';
  14. this.minLength = parseInt(minLength, 10) > 0 ? minLength : 0;
  15. this.salt = (typeof salt === 'string') ? salt : '';
  16. if (typeof alphabet === 'string') {
  17. this.alphabet = alphabet;
  18. }
  19. for (let i = 0; i !== this.alphabet.length; i++) {
  20. if (uniqueAlphabet.indexOf(this.alphabet.charAt(i)) === -1) {
  21. uniqueAlphabet += this.alphabet.charAt(i);
  22. }
  23. }
  24. this.alphabet = uniqueAlphabet;
  25. if (this.alphabet.length < minAlphabetLength) {
  26. throw errorAlphabetLength.replace('X', minAlphabetLength);
  27. }
  28. if (this.alphabet.search(' ') !== -1) {
  29. throw errorAlphabetSpace;
  30. }
  31. /*
  32. `this.seps` should contain only characters present in `this.alphabet`
  33. `this.alphabet` should not contains `this.seps`
  34. */
  35. for (let i = 0; i !== this.seps.length; i++) {
  36. const j = this.alphabet.indexOf(this.seps.charAt(i));
  37. if (j === -1) {
  38. this.seps = this.seps.substr(0, i) + ' ' + this.seps.substr(i + 1);
  39. } else {
  40. this.alphabet = this.alphabet.substr(0, j) + ' ' + this.alphabet.substr(j + 1);
  41. }
  42. }
  43. this.alphabet = this.alphabet.replace(/ /g, '');
  44. this.seps = this.seps.replace(/ /g, '');
  45. this.seps = this._shuffle(this.seps, this.salt);
  46. if (!this.seps.length || (this.alphabet.length / this.seps.length) > sepDiv) {
  47. sepsLength = Math.ceil(this.alphabet.length / sepDiv);
  48. if (sepsLength > this.seps.length) {
  49. diff = sepsLength - this.seps.length;
  50. this.seps += this.alphabet.substr(0, diff);
  51. this.alphabet = this.alphabet.substr(diff);
  52. }
  53. }
  54. this.alphabet = this._shuffle(this.alphabet, this.salt);
  55. const guardCount = Math.ceil(this.alphabet.length / guardDiv);
  56. if (this.alphabet.length < 3) {
  57. this.guards = this.seps.substr(0, guardCount);
  58. this.seps = this.seps.substr(guardCount);
  59. } else {
  60. this.guards = this.alphabet.substr(0, guardCount);
  61. this.alphabet = this.alphabet.substr(guardCount);
  62. }
  63. }
  64. encode(...numbers) {
  65. const ret = '';
  66. if (!numbers.length) {
  67. return ret;
  68. }
  69. if (numbers[0] && numbers[0].constructor === Array) {
  70. numbers = numbers[0];
  71. if (!numbers.length) {
  72. return ret;
  73. }
  74. }
  75. for (let i = 0; i !== numbers.length; i++) {
  76. numbers[i] = this.parseInt(numbers[i], 10);
  77. if (numbers[i] >= 0) {
  78. continue;
  79. } else {
  80. return ret;
  81. }
  82. }
  83. return this._encode(numbers);
  84. }
  85. decode(id) {
  86. const ret = [];
  87. if (!id || !id.length || typeof id !== 'string') {
  88. return ret;
  89. }
  90. return this._decode(id, this.alphabet);
  91. }
  92. encodeHex(hex) {
  93. hex = hex.toString();
  94. if (!/^[0-9a-fA-F]+$/.test(hex)) {
  95. return '';
  96. }
  97. const numbers = hex.match(/[\w\W]{1,12}/g);
  98. for (let i = 0; i !== numbers.length; i++) {
  99. numbers[i] = parseInt('1' + numbers[i], 16);
  100. }
  101. return this.encode.apply(this, numbers);
  102. }
  103. decodeHex(id) {
  104. let ret = [];
  105. const numbers = this.decode(id);
  106. for (let i = 0; i !== numbers.length; i++) {
  107. ret += (numbers[i]).toString(16).substr(1);
  108. }
  109. return ret;
  110. }
  111. _encode(numbers) {
  112. let ret,
  113. alphabet = this.alphabet,
  114. numbersIdInt = 0;
  115. for (let i = 0; i !== numbers.length; i++) {
  116. numbersIdInt += (numbers[i] % (i + 100));
  117. }
  118. ret = alphabet.charAt(numbersIdInt % alphabet.length);
  119. const lottery = ret;
  120. for (let i = 0; i !== numbers.length; i++) {
  121. let number = numbers[i];
  122. const buffer = lottery + this.salt + alphabet;
  123. alphabet = this._shuffle(alphabet, buffer.substr(0, alphabet.length));
  124. const last = this._toAlphabet(number, alphabet);
  125. ret += last;
  126. if (i + 1 < numbers.length) {
  127. number %= (last.charCodeAt(0) + i);
  128. const sepsIndex = number % this.seps.length;
  129. ret += this.seps.charAt(sepsIndex);
  130. }
  131. }
  132. if (ret.length < this.minLength) {
  133. let guardIndex = (numbersIdInt + ret[0].charCodeAt(0)) % this.guards.length;
  134. let guard = this.guards[guardIndex];
  135. ret = guard + ret;
  136. if (ret.length < this.minLength) {
  137. guardIndex = (numbersIdInt + ret[2].charCodeAt(0)) % this.guards.length;
  138. guard = this.guards[guardIndex];
  139. ret += guard;
  140. }
  141. }
  142. const halfLength = parseInt(alphabet.length / 2, 10);
  143. while (ret.length < this.minLength) {
  144. alphabet = this._shuffle(alphabet, alphabet);
  145. ret = alphabet.substr(halfLength) + ret + alphabet.substr(0, halfLength);
  146. const excess = ret.length - this.minLength;
  147. if (excess > 0) {
  148. ret = ret.substr(excess / 2, this.minLength);
  149. }
  150. }
  151. return ret;
  152. }
  153. _decode(id, alphabet) {
  154. let ret = [], i = 0,
  155. r = new RegExp(`[${this.escapeRegExp(this.guards)}]`, 'g'),
  156. idBreakdown = id.replace(r, ' '),
  157. idArray = idBreakdown.split(' ');
  158. if (idArray.length === 3 || idArray.length === 2) {
  159. i = 1;
  160. }
  161. idBreakdown = idArray[i];
  162. if (typeof idBreakdown[0] !== 'undefined') {
  163. const lottery = idBreakdown[0];
  164. idBreakdown = idBreakdown.substr(1);
  165. r = new RegExp(`[${this.escapeRegExp(this.seps)}]`, 'g');
  166. idBreakdown = idBreakdown.replace(r, ' ');
  167. idArray = idBreakdown.split(' ');
  168. for (let j = 0; j !== idArray.length; j++) {
  169. const subId = idArray[j];
  170. const buffer = lottery + this.salt + alphabet;
  171. alphabet = this._shuffle(alphabet, buffer.substr(0, alphabet.length));
  172. ret.push(this._fromAlphabet(subId, alphabet));
  173. }
  174. if (this.encode(ret) !== id) {
  175. ret = [];
  176. }
  177. }
  178. return ret;
  179. }
  180. _shuffle(alphabet, salt) {
  181. let integer;
  182. if (!salt.length) {
  183. return alphabet;
  184. }
  185. alphabet = alphabet.split("");
  186. for (let i = alphabet.length - 1, v = 0, p = 0, j = 0; i > 0; i--, v++) {
  187. v %= salt.length;
  188. p += integer = salt.charCodeAt(v);
  189. j = (integer + v + p) % i;
  190. const tmp = alphabet[j];
  191. alphabet[j] = alphabet[i];
  192. alphabet[i] = tmp;
  193. }
  194. alphabet = alphabet.join("");
  195. return alphabet;
  196. }
  197. _toAlphabet(input, alphabet) {
  198. let id = '';
  199. do {
  200. id = alphabet.charAt(input % alphabet.length) + id;
  201. input = parseInt(input / alphabet.length, 10);
  202. } while (input);
  203. return id;
  204. }
  205. _fromAlphabet(input, alphabet) {
  206. return input.split("").map(
  207. (item) => alphabet.indexOf(item)
  208. ).reduce(
  209. (carry, item) => carry * alphabet.length + item,
  210. 0
  211. );
  212. }
  213. }