Source: conversion.js

  1. /*!
  2. * Module Conversion
  3. */
  4. /**
  5. * @namespace Conversion
  6. */
  7. const MODULE_NAME = 'Conversion';
  8. //###[ IMPORTS ]########################################################################################################
  9. import {isInt, isArray, orDefault} from './basic.js';
  10. import {pad, trim} from './strings.js';
  11. //###[ DATA ]###########################################################################################################
  12. const
  13. UPPER_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
  14. LOWER_CHARS = 'abcdefghijklmnopqrstuvwxyz',
  15. NUMBERS = '0123456789',
  16. BASE_ALPHABETS = {
  17. '64' : `${UPPER_CHARS}${LOWER_CHARS}${NUMBERS}+/`,
  18. '63' : `${NUMBERS}${UPPER_CHARS}${LOWER_CHARS}_`,
  19. '36' : `${NUMBERS}${UPPER_CHARS}`,
  20. '26' : UPPER_CHARS,
  21. '16' : `${NUMBERS}${UPPER_CHARS.slice(0, 6)}`,
  22. '10' : NUMBERS,
  23. '8' : NUMBERS.slice(0, 8),
  24. '2' : NUMBERS.slice(0, 2)
  25. },
  26. BASE64_ALPHABET = BASE_ALPHABETS['64']
  27. ;
  28. //###[ HELPERS ]########################################################################################################
  29. /**
  30. * Builds an alphabet string, based on an integer, an alphabet string or an array of strings containing the alphabet's
  31. * chars. An integer uses BASE_ALPHABETS to select a base alphabet to slice the alphabet from. The first base alphabet
  32. * having enough chars is going be used. The configurations of the base alphabets are according to current base
  33. * practices.
  34. *
  35. * Characters in custom alphabets are sorted according to base64 definition, with additional chars appended at the end,
  36. * sorted ascending based on char value.
  37. *
  38. * @private
  39. */
  40. function buildAlphabet(__methodName__='buildAlphabet', baseOrAlphabet=64, useChunks=false){
  41. baseOrAlphabet = orDefault(baseOrAlphabet, 64);
  42. let alphabet;
  43. if( isInt(baseOrAlphabet) ){
  44. if( (baseOrAlphabet < 2) || (baseOrAlphabet > 64) ){
  45. throw new Error(`${MODULE_NAME}:${__methodName__} | base not usable, smaller than 2 or larger than 64`);
  46. }
  47. if( useChunks && (baseOrAlphabet < 3) ){
  48. throw new Error(`${MODULE_NAME}:${__methodName__} | base not usable for chunks, smaller than 3`);
  49. }
  50. for( let baseAlphabetKey of Object.keys(BASE_ALPHABETS).sort() ){
  51. if( Number(baseAlphabetKey) >= baseOrAlphabet ){
  52. alphabet = BASE_ALPHABETS[baseAlphabetKey].slice(0, baseOrAlphabet);
  53. break;
  54. }
  55. }
  56. } else {
  57. alphabet = [];
  58. if( !isArray(baseOrAlphabet) ){
  59. baseOrAlphabet = `${baseOrAlphabet}`.split('');
  60. }
  61. baseOrAlphabet.forEach(char => {
  62. alphabet = alphabet.concat(`${char}`.split(''));
  63. });
  64. alphabet = Array.from(new Set(alphabet));
  65. alphabet.sort((a, b) => {
  66. const
  67. aBase64Index = BASE64_ALPHABET.indexOf(a),
  68. bBase64Index = BASE64_ALPHABET.indexOf(b)
  69. ;
  70. if( (aBase64Index < 0) && (bBase64Index < 0) ){
  71. return (a === b) ? 0 : ((a < b) ? -1 : 1);
  72. } else if( aBase64Index < 0 ){
  73. return 1;
  74. } else if( bBase64Index < 0 ){
  75. return -1;
  76. } else {
  77. return (aBase64Index === bBase64Index) ? 0 : ((aBase64Index < bBase64Index) ? -1 : 1);
  78. }
  79. });
  80. alphabet = alphabet.join('');
  81. }
  82. if( (alphabet.length < 2) || (alphabet.length > 64) ){
  83. throw new Error(`${MODULE_NAME}:${__methodName__} | alphabet not usable, must have between two and 64 chars`);
  84. }
  85. if( useChunks && (alphabet.length < 3) ){
  86. throw new Error(`${MODULE_NAME}:${__methodName__} | alphabet not usable for chunks, less than 3 chars`);
  87. }
  88. return alphabet;
  89. }
  90. /**
  91. * Calculates how many character mapping pages/page characters we need for a specific alphabet,
  92. * defined by its length/base.
  93. *
  94. * @private
  95. */
  96. function calculateNeededPages(base){
  97. const availablePages = Math.floor(base / 2);
  98. let
  99. neededPages = 0,
  100. charPoolSize = base,
  101. combinations = charPoolSize
  102. ;
  103. while( combinations < 64 ){
  104. neededPages++;
  105. if( neededPages <= availablePages ){
  106. charPoolSize--;
  107. }
  108. combinations = (neededPages + 1) * charPoolSize;
  109. }
  110. return neededPages;
  111. }
  112. /**
  113. * Returns an array of value prefixes used to map characters to different code pages, in cases where we need to encode
  114. * a base64 character above the base of our target alphabet, which means, that we have to repeat character usage, but
  115. * with a page prefix to multiply the value set by reducing the base alphabet for that purpose.
  116. *
  117. * @private
  118. */
  119. function buildPageMap(alphabet){
  120. const
  121. base = alphabet.length,
  122. neededPages = calculateNeededPages(base),
  123. availablePages = Math.floor(base / 2),
  124. pageMap = ['']
  125. ;
  126. let remainder, quotient;
  127. for( let i = 1; i <= neededPages; i++ ){
  128. remainder = i % availablePages;
  129. quotient = Math.ceil(i / availablePages);
  130. pageMap.push(pad('', alphabet[(remainder > 0) ? (remainder - 1) : (availablePages - 1)], quotient));
  131. }
  132. return pageMap;
  133. }
  134. /**
  135. * Returns a dictionary, mapping each base64 character to one or more characters of the target alphabet.
  136. * In cases, where the character to encode is beyond the target alphabet, page prefixes are prepended to
  137. * cover all characters by increasing length.
  138. *
  139. * @private
  140. */
  141. function buildCharMap(pageMap, alphabet){
  142. const
  143. base = alphabet.length,
  144. neededPages = calculateNeededPages(base),
  145. availablePages = Math.floor(base / 2),
  146. pagedAlphabet = alphabet.slice(Math.min(neededPages, availablePages)),
  147. pagedBase = pagedAlphabet.length,
  148. charMap = {}
  149. ;
  150. let remainder, quotient;
  151. for( let i in BASE64_ALPHABET.split('') ){
  152. remainder = i % pagedBase;
  153. quotient = Math.floor(i / pagedBase);
  154. charMap[BASE64_ALPHABET[i]] = `${pageMap[quotient]}${pagedAlphabet[remainder]}`
  155. }
  156. return charMap;
  157. }
  158. /**
  159. * Converts a string to base64, while handling unicode characters correctly.
  160. * Be advised, that the result needs to be decoded with base64ToString() again, since
  161. * we also need to correctly handle unicode on the way back.
  162. *
  163. * @private
  164. */
  165. function stringToBase64(value){
  166. return btoa(String.fromCodePoint(...((new TextEncoder()).encode(`${value}`)))).replaceAll('=', '');
  167. }
  168. /**
  169. * Decodes a base64-encoded string to its original value.
  170. * Be advised, that the base64 value has to be encoded using stringToBase64(), since unicode characters need
  171. * special handling during en/decoding.
  172. *
  173. * This function will fail with an error, if the given value is not actually decodable with base64.
  174. *
  175. * @private
  176. */
  177. function base64ToString(value, __methodName__='base64ToString'){
  178. let res = null;
  179. try {
  180. res = (new TextDecoder()).decode(Uint8Array.from(atob(`${value}`), char => char.codePointAt(0)))
  181. } catch(ex){
  182. throw new Error(`${MODULE_NAME}:${__methodName__} | cannot decode "${value}"`);
  183. }
  184. return res;
  185. }
  186. /**
  187. * Converts a decimal/base10 value to a different base numerically.
  188. * Be aware that this needs the value to be a safe integer.
  189. * This function does not deal with negative numbers by itself.
  190. *
  191. * @private
  192. */
  193. function base10toBaseX(value, alphabet){
  194. const base = alphabet.length;
  195. let
  196. baseXValue = '',
  197. quotient = value,
  198. remainder
  199. ;
  200. if( quotient !== 0){
  201. while( quotient !== 0 ){
  202. remainder = quotient % base;
  203. quotient = Math.floor(quotient / base);
  204. baseXValue = `${alphabet[remainder]}${baseXValue}`;
  205. }
  206. } else {
  207. baseXValue = `${alphabet[0]}`;
  208. }
  209. return baseXValue;
  210. }
  211. /**
  212. * Converts a value, based on a defined alphabet, to its decimal/base10 representation.
  213. * Be aware that this needs the result to be a safe integer.
  214. * This function does not deal with negative numbers by itself.
  215. *
  216. * @private
  217. */
  218. function baseXToBase10(value, alphabet){
  219. value = `${value}`.split('').reverse().join('');
  220. const base = alphabet.length;
  221. let base10Value = 0;
  222. for( let i = 0; i < value.length; i++ ){
  223. base10Value += Math.pow(base, i) * alphabet.indexOf(value[i]);
  224. }
  225. return base10Value;
  226. }
  227. //###[ EXPORTS ]########################################################################################################
  228. /**
  229. * @namespace Conversion:toBaseX
  230. */
  231. /**
  232. * This function converts a value to a representation in a defined base between 2 and 64.
  233. * So this covers common use cases like binary, octal, hexadecimal, alphabetical, alphanumeric and of course base64.
  234. *
  235. * The result of this function is always either a decimal number or a string, just as the input value. All numbers
  236. * apart from decimal ones are returned as strings without prefix. So, decimal 5 will be the number 5, but the binary
  237. * version will be the string "101". Positive and negative decimal integers are valid numbers here, but this
  238. * implementation does not support floats (multiply and divide if needed). Only numerical bases above 36 contain
  239. * lower case characters, so decimal 255 is "FF" in base 16 and not "ff".
  240. *
  241. * This function is unicode safe, by using byte conversion
  242. * (see: https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem).
  243. * Be aware, that this also means, that results of `btoa/atob()` and `toBaseX/fromBaseX()` are _not_ interchangeable,
  244. * since they work with different values internally.
  245. *
  246. * There are three approaches to changing the base of a value in JavaScript:
  247. *
  248. * 1. Either you are taking the numerical/mathematical road, treating a value as a number in its alphabet being
  249. * interpreted as a number, where each character, counting from the back is the base to the power of the
  250. * character index. This is the approach you'd expect, when, for instance, you'd want to convert the decimal number 5
  251. * to binary 101. The downside of this approach is, that the relatively small max safe integer in JavaScript makes
  252. * converting large numbers, such as longer strings, impossible.
  253. *
  254. * 2. Therefore, the second approach takes the numeric approach, but combines it with chunking, splitting the value into
  255. * pieces, which are, by themselves, safely convertible. The downside is, that we need an extra character to delimit
  256. * chunks in the result, since values have non-uniform lengths. This means, that this does not work with the basic
  257. * binary base, and we need at least 3 alphabet characters.
  258. *
  259. * 3. The last approach uses the native base64 string encoding with `btoa()` as a safe translation layer, mapping the
  260. * resulting string to the target base, using a generated (and possibly paged) character map. This way treats all
  261. * values as strings and is not compatible to numerical conversion anymore, but uses the same characters. The result
  262. * of this approach can encode every string of every length without structural tricks, but has the longest results.
  263. *
  264. * This function is capable of all three approaches, which are equally safe for unicode values. The numerical
  265. * approach is the default. If you want to encode large numbers or strings longer than ~6 characters, select
  266. * a different approach using the `useCharacterMap` or `useChunks` parameters. Character mapping has preference, while
  267. * chunks have no effect in character mapping.
  268. *
  269. * Each encoding process ends with a self-test, checking if the result is actually decodable using
  270. * `fromBaseX()`, using the same settings again. This ensures, that every result is valid and retrievable in the future,
  271. * preventing any undiscovered errors, which would make it impossible to work with the original value again.
  272. *
  273. * You may define the base as an integer between 2 and 64 or as a custom alphabet in the same range. Integer based
  274. * alphabets are generated using defined base alphabets, which are sliced if necessary. Custom alphabets are
  275. * automatically sorted to match base64 are far as possible, pushing additional characters to the end, which are then
  276. * sorted ascending by character value.
  277. *
  278. * "{" and "}" are the only forbidden characters in a custom alphabet, since we need these to mark number values in
  279. * `fromBaseX()`.
  280. *
  281. * Numerical conversion keeps negative numbers negative and marks the result with a preceding "-".
  282. *
  283. * Hint: if you want to genrate codes to be presented to the user, see `Random:randomUserCode`.
  284. *
  285. * @param {Number|String} value - value to be encoded
  286. * @param {?Number|String|Array<String>} [baseOrAlphabet=64] - either the numerical base to convert to (64, 36, ...) or the alphabet of characters to use in encoding; numerical bases must be between 2 and 64 (if the result is chunked, we need a base 3)
  287. * @param {?Boolean} [useCharacterMap=true] - set to true, to use a character map, based on btoa(), instead of numerical conversion
  288. * @param {?Boolean} [useChunks=false] - set to true, to add chunking to the numerical approach, converting the value in groups separated by a delimiter, which is the first letter of the base's alphabet
  289. * @param {?Number} [chunkSize=6] - define a different chunks size; only change this, if 6 seems too big in your context, going higher is not advisable
  290. * @throws error if baseOrAlphabet is not usable
  291. * @throws error if result is not decodable again using the same settings
  292. * @returns {String} the encoded value
  293. *
  294. * @memberof Conversion:toBaseX
  295. * @alias toBaseX
  296. * @see fromBaseX
  297. * @see https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
  298. * @see Random:randomUserCode
  299. * @example
  300. * toBaseX('foobar')
  301. * => 'Zm9vYmFy'
  302. * toBaseX(-5, 2)
  303. * => '-101'
  304. * toBaseX(42, 'abcdefghij')
  305. * => 'ec'
  306. * toBaseX('too-long-for-number-conversion', 36, true)
  307. * => 'U70R0DCN0F0DS04T0BQ040R0GCN0N0JSNA03TZ0J01S0K0N0KQOA0HRN0R0C'
  308. * toBaseX('too-long-for-number-conversion', 16, false, true)
  309. * => 'D3EF5D81F026D9DFDA970BBF17222402A47D5AD650CF6C2FE2102A494BCBDD0A2864C'
  310. */
  311. export function toBaseX(value, baseOrAlphabet=64, useCharacterMap=false, useChunks=false, chunkSize=6){
  312. const __methodName__ = 'toBaseX';
  313. const
  314. valueIsNumber = isInt(value),
  315. valueIsNegativeNumber = valueIsNumber && (value < 0)
  316. ;
  317. value = valueIsNumber ? `${Math.abs(value)}` : `${value}`;
  318. useCharacterMap = orDefault(useCharacterMap, false, 'bool');
  319. useChunks = orDefault(useChunks, false, 'bool');
  320. chunkSize = orDefault(chunkSize, 10, 'int');
  321. const alphabet = buildAlphabet(__methodName__, baseOrAlphabet, useChunks);
  322. if( alphabet.includes('{') || alphabet.includes('}') ){
  323. throw new Error(`${MODULE_NAME}:${__methodName__} | invalid alphabet, must not contain "{" or "}"`)
  324. }
  325. let
  326. base64Value = '',
  327. base10Value = 0,
  328. baseXValue = ''
  329. ;
  330. if( useCharacterMap ){
  331. base64Value = stringToBase64(value);
  332. if( baseOrAlphabet === 64 ) return base64Value;
  333. const
  334. pageMap = buildPageMap(alphabet),
  335. charMap = buildCharMap(pageMap, alphabet)
  336. ;
  337. for( let char of base64Value ){
  338. baseXValue += charMap[char];
  339. }
  340. } else {
  341. if( valueIsNumber ){
  342. base64Value = base10toBaseX(value, BASE64_ALPHABET);
  343. } else {
  344. base64Value = stringToBase64(value);
  345. }
  346. const
  347. chunks = [],
  348. chunkAlphabet = useChunks ? alphabet.slice(1) : alphabet,
  349. chunkSeparator = useChunks ? alphabet[0] : ''
  350. ;
  351. if( useChunks ){
  352. let chunkStart = 0;
  353. while( chunkStart < base64Value.length ){
  354. chunks.push(base64Value.slice(chunkStart, chunkStart + chunkSize));
  355. chunkStart += chunkSize;
  356. }
  357. } else {
  358. chunks.push(base64Value);
  359. }
  360. for( let chunk of chunks ){
  361. base10Value = baseXToBase10(chunk, BASE64_ALPHABET);
  362. if( !useChunks && (baseOrAlphabet === 10) ){
  363. baseXValue += base10Value;
  364. break;
  365. }
  366. baseXValue += `${chunkSeparator}${base10toBaseX(base10Value, chunkAlphabet)}`;
  367. }
  368. if( chunkSeparator !== '' ){
  369. baseXValue = baseXValue.slice(1);
  370. }
  371. }
  372. baseXValue = `${valueIsNegativeNumber ? '-' : ''}${baseXValue}`;
  373. const decodedValue = `${fromBaseX(baseXValue, baseOrAlphabet, useCharacterMap, useChunks, valueIsNumber)}`;
  374. if( decodedValue !== `${valueIsNegativeNumber ? '-' : ''}${value}` ){
  375. throw new Error(
  376. `${MODULE_NAME}:${__methodName__} | critical error, encoded value "${baseXValue}" `
  377. +`not decodable to "${value}", is "${decodedValue}" instead; `
  378. +`if this looks "cut off", this may be a problem with JS max safe integer size `
  379. +`(safe value length for number-based conversion is just ~8 chars), `
  380. +`try using character mapping or chunks to circumvent this problem`
  381. );
  382. }
  383. return baseXValue;
  384. }
  385. /**
  386. * @namespace Conversion:fromBaseX
  387. */
  388. /**
  389. * This function converts a based representation back to its original number or string value.
  390. * This is the mirror function to `toBaseX()` and expects a value encoded with that function. See that function
  391. * for implementation details, modes and restrictions.
  392. *
  393. * The result of this function is always either a decimal number or a string, just as the input value. All numbers
  394. * apart from decimal ones are returned as strings without prefix. So, decimal 5 will be the number 5, but the binary
  395. * version will be the string "101".
  396. *
  397. * You may define the base as an integer between 2 and 64 or as a custom alphabet in the same range. Integer based
  398. * alphabets are generated using defined base alphabets, which are sliced if necessary. Custom alphabets are
  399. * automatically sorted to match base64 are far as possible, pushing additional characters to the end, which are then
  400. * sorted ascending by character value.
  401. *
  402. * "{" and "}" are the only forbidden characters in a custom alphabet, since we need these to mark number values in
  403. * `fromBaseX()`.
  404. *
  405. * Numerical conversion keeps negative numbers negative and marks the result with a preceding "-".
  406. *
  407. * @param {Number|String} value - value to be decoded
  408. * @param {?Number|String|Array<String>} [baseOrAlphabet=64] - either the numerical base to convert to (64, 36, ...) or the alphabet of characters to use in encoding; numerical bases must be between 2 and 64 (if the result is chunked, we need a base 3)
  409. * @param {?Boolean} [useCharacterMap=true] - set to true, to use a character map, based on btoa(), instead of numerical conversion
  410. * @param {?Boolean} [useChunks=false] - set to true, to add chunking to the numerical approach, converting the value in groups separated by a delimiter, which is the first letter of the base's alphabet
  411. * @param {?Boolean} [valueIsNumber=false] - if true, the given value is treated as a number for numerical conversion; this is necessary, since numbers such as binaries are defined as strings and are therefore not auto-detectable
  412. * @throws error if baseOrAlphabet is not usable
  413. * @throws error character mapped decoding fails, due to missing token/unmatched alphabet
  414. * @returns {String} the decoded value
  415. *
  416. * @memberof Conversion:fromBaseX
  417. * @alias fromBaseX
  418. * @see toBaseX
  419. * @example
  420. * fromBaseX('Zm9vYmFy')
  421. * => 'foobar'
  422. * fromBaseX('16W33YPUS', 36)
  423. * => 'äす'
  424. * fromBaseX('{-3C3}', 13)
  425. * => -666
  426. * fromBaseX('q', 64, false, false, true)
  427. * => 42
  428. * fromBaseX('U70R0DCN0F0DS04T0BQ040R0GCN0N0JSNA03TZ0J01S0K0N0KQOA0HRN0R0C', 36, true)
  429. * => 'too-long-for-number-conversion'
  430. * fromBaseX('D3EF5D81F026D9DFDA970BBF17222402A47D5AD650CF6C2FE2102A494BCBDD0A2864C', 16, false, true)
  431. * => 'too-long-for-number-conversion'
  432. */
  433. export function fromBaseX(value, baseOrAlphabet=64, useCharacterMap=false, useChunks=false, valueIsNumber=false){
  434. const __methodName__ = 'fromBaseX';
  435. valueIsNumber = !!valueIsNumber
  436. || isInt(value)
  437. || (`${value}`.startsWith('{') && `${value}`.endsWith('}'))
  438. ;
  439. value = `${value}`;
  440. if( valueIsNumber ){
  441. value = trim(value, ['{', '}']);
  442. }
  443. const valueIsNegativeNumber = valueIsNumber && value.startsWith('-');
  444. if( valueIsNegativeNumber ){
  445. value = value.slice(1);
  446. }
  447. useCharacterMap = orDefault(useCharacterMap, false, 'bool');
  448. useChunks = orDefault(useChunks, false, 'bool');
  449. if(
  450. (baseOrAlphabet === 64)
  451. && !useCharacterMap
  452. && !useChunks
  453. && !valueIsNumber
  454. ) return base64ToString(value, __methodName__);
  455. const alphabet = buildAlphabet(__methodName__, baseOrAlphabet, useChunks);
  456. if( alphabet.includes('{') || alphabet.includes('}') ){
  457. throw new Error(`${MODULE_NAME}:${__methodName__} | invalid alphabet, must not contain "{" or "}"`)
  458. }
  459. let
  460. base64Value = '',
  461. base10Value = 0,
  462. decodedValue
  463. ;
  464. if( useCharacterMap ){
  465. const
  466. pageMap = buildPageMap(alphabet),
  467. charMap = buildCharMap(pageMap, alphabet),
  468. inverseCharMap = Object.fromEntries(
  469. Object
  470. .entries(charMap)
  471. .map(([key, value]) => [value, key])
  472. ),
  473. tokensByLength = Object.keys(inverseCharMap).sort((a, b) => {
  474. return (a.length === b.length) ? 0 : ((a.length > b.length) ? -1 : 1);
  475. })
  476. ;
  477. let tokenFound = false;
  478. while( value !== '' ){
  479. for( let token of tokensByLength ){
  480. tokenFound = false;
  481. if( value.startsWith(token) ){
  482. tokenFound = true;
  483. base64Value += inverseCharMap[token];
  484. value = value.slice(token.length);
  485. break;
  486. }
  487. }
  488. if( !tokenFound ){
  489. throw new Error(
  490. `${MODULE_NAME}:${__methodName__} | unknown token at start of "${value}", likely due to non-matching alphabet`
  491. );
  492. }
  493. }
  494. decodedValue = base64ToString(base64Value, __methodName__);
  495. if( valueIsNegativeNumber ){
  496. decodedValue = `-${decodedValue}`;
  497. }
  498. return decodedValue;
  499. } else {
  500. decodedValue = '';
  501. const
  502. chunkAlphabet = useChunks ? alphabet.slice(1) : alphabet,
  503. chunkSeparator = useChunks ? alphabet[0] : '',
  504. chunks = useChunks ? value.split(chunkSeparator) : [value]
  505. ;
  506. for( let chunk of chunks ){
  507. base10Value = baseXToBase10(chunk, chunkAlphabet);
  508. if( valueIsNumber ){
  509. decodedValue += `${base10Value}`;
  510. } else {
  511. base64Value += base10toBaseX(base10Value, BASE64_ALPHABET);
  512. }
  513. }
  514. if( decodedValue === '' ){
  515. decodedValue = base64ToString(base64Value, __methodName__)
  516. if( valueIsNegativeNumber ){
  517. decodedValue = `-${decodedValue}`;
  518. }
  519. }
  520. if( !useChunks && valueIsNumber ){
  521. decodedValue = Number(decodedValue);
  522. if( valueIsNegativeNumber && (decodedValue >= 0) ){
  523. decodedValue = -decodedValue;
  524. }
  525. }
  526. return decodedValue;
  527. }
  528. }