diff --git a/API.md b/API.md index a4c1065..b0212eb 100644 --- a/API.md +++ b/API.md @@ -19,6 +19,9 @@ - [decode][15] - [Parameters][16] - [Examples][17] + - [deconstruct][18] + - [Parameters][19] + - [Examples][20] ## AlphanumericEncoder @@ -26,10 +29,10 @@ A class for encoding and decoding base 10 integers to a custom alphanumeric base ### Parameters -- `configOptions` **[object][18]?** Optional object defining initial settings for the class (optional, default `{}`) +- `configOptions` **[object][21]?** Optional object defining initial settings for the class (optional, default `{}`) - - `configOptions.allowLowerCaseDictionary` **[boolean][19]?** Whether or not to allow lower case letters in the dictionary - - `configOptions.dictionary` **[string][20]?** Starting dictionary to use + - `configOptions.allowLowerCaseDictionary` **[boolean][22]?** Whether or not to allow lower case letters in the dictionary + - `configOptions.dictionary` **[string][23]?** Starting dictionary to use ### Examples @@ -61,7 +64,7 @@ Returns or sets the current dictionary. #### Parameters -- `newDictionary` **[string][20]** (If setting) String of unique letters and numbers, in order, for the new dictionary +- `newDictionary` **[string][23]** (If setting) String of unique letters and numbers, in order, for the new dictionary #### Examples @@ -76,11 +79,11 @@ console.log(encoder.dictionary) // 'ABCD' encoder.dictionary = 'ABCDA' // Throws error because the letter 'A' is repeated ``` -- Throws **[RangeError][21]** if setting dictionary to `null`, `undefined` or empty string (i.e. `''`) -- Throws **[RangeError][21]** if `newDictionary` contains a non-alphanumeric character -- Throws **[RangeError][21]** if `newDictionary` has a repeating character +- Throws **[RangeError][24]** if setting dictionary to `null`, `undefined` or empty string (i.e. `''`) +- Throws **[RangeError][24]** if `newDictionary` contains a non-alphanumeric character +- Throws **[RangeError][24]** if `newDictionary` has a repeating character -Returns **[string][20]** (If used as getter) The current dictionary in use +Returns **[string][23]** (If used as getter) The current dictionary in use ### allowLowerCaseDictionary @@ -88,7 +91,7 @@ Returns or sets a boolean value that determines whether the dictionary will allo #### Parameters -- `isAllowed` **[boolean][19]** (If setting). Accept truthy or falsy statements. +- `isAllowed` **[boolean][22]** (If setting). Accept truthy or falsy statements. #### Examples @@ -105,7 +108,7 @@ encoder.dictionary = 'ABCDefg' console.log(encoder.dictionary) // 'ABCDefg' ``` -Returns **[boolean][19]** (If used as getter) +Returns **[boolean][22]** (If used as getter) ### resetDefaultDictionary @@ -130,7 +133,7 @@ Takes any number and converts it into a base (dictionary length) letter combo. #### Parameters -- `integerToEncode` **[number][22]** Base 10 integer. If passed a non-integer number, decimal values are truncated. +- `integerToEncode` **[number][25]** Base 10 integer. If passed a non-integer number, decimal values are truncated. Passing zero, negative numbers, or non-numbers will return `undefined`. #### Examples @@ -169,9 +172,9 @@ console.log(encoder.encode(null)) // undefined console.log(encoder.encode(undefined)) // undefined ``` -- Throws **[RangeError][21]** if `integerToEncode` exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). +- Throws **[RangeError][24]** if `integerToEncode` exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). -Returns **[string][20]** Dictionary encoded value +Returns **[string][23]** Dictionary encoded value ### decode @@ -179,7 +182,7 @@ Takes any string and converts it into a base 10 integer based on the defined dic #### Parameters -- `stringToDecode` **[string][20]** If passed a non-integer number, decimal values are truncated. +- `stringToDecode` **[string][23]** If passed a non-integer number, decimal values are truncated. Passing an empty string, `null`, or `undefined` will return `undefined`. #### Examples @@ -207,9 +210,34 @@ console.log(encoder.decode('ADBAC')) // 551 console.log(encoder.decode('ANE')) // undefined ``` -- Throws **[RangeError][21]** if the decoded integer exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). +- Throws **[RangeError][24]** if the decoded integer exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). -Returns **[number][22]** Positive integer representation. If one of the characters is not present in the dictionary, it will return `undefined`. +Returns **[number][25]** Positive integer representation. If one of the characters is not present in the dictionary, it will return `undefined`. + +### deconstruct + +Takes any string of letters and numbers and deconstructs it into an array of base 10 integers based on the defined dictionary. + +#### Parameters + +- `stringToDeconstruct` **([string][23] | [number][25])** A string of letters and numbers (e.g. `'A7'`, `'AC22'`, `'7C10F'`) + +#### Examples + +```javascript +const encoder = new AlphanumericEncoder() +console.log(encoder.deconstruct('A')) // [1] +console.log(encoder.deconstruct('AC22')) // [29, 22] +console.log(encoder.deconstruct('C3ABC123EFGH456')) // [3, 3, 731, 123, 92126, 456] +console.log(encoder.deconstruct('A1aB2B')) // [1, 1, undefined, 2, 2] +console.log(encoder.deconstruct('7AC!23A1%')) // [7, undefined, 23, 1, 1, undefined] +console.log(encoder.deconstruct('')) // undefined +``` + +- Throws **[Error][26]** if the dictionary contains a number as this function would be unable to differentiate between where a number and dictionary value. + +Returns **[Array][27]<[number][25]>** An array of numbers. Characters not present in the dictionary are treated as letters and return `undefined` for that array value. +Passing an empty string (`''`), `null`, or `undefined` will return `undefined` for the whole function. [1]: #alphanumericencoder [2]: #parameters @@ -228,8 +256,13 @@ Returns **[number][22]** Positive integer representation. If one of the characte [15]: #decode [16]: #parameters-4 [17]: #examples-5 -[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object -[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean -[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RangeError -[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[18]: #deconstruct +[19]: #parameters-5 +[20]: #examples-6 +[21]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object +[22]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[23]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[24]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RangeError +[25]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[26]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Error +[27]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array diff --git a/CHANGELOG.md b/CHANGELOG.md index 64e115a..dcb6120 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,12 @@ ## [1.4.0](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/compare/v1.3.0...v1.4.0) (2022-05-02) - ### :gift: Feature Changes -* add ability to instantiate the class using an optional config object in the constructor ([8dc230b](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/8dc230b164a2689b49d6a7f8b00f51348da8c3f8)), closes [#35](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/35) - +- add ability to instantiate the class using an optional config object in the constructor ([8dc230b](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/8dc230b164a2689b49d6a7f8b00f51348da8c3f8)), closes [#35](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/35) ### :dart: Test Changes -* add tests verifying the optional config object works as expected ([f7f7de6](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/f7f7de6681c4bb3ec1083f346d691d4d36795af3)), closes [#35](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/35) +- add tests verifying the optional config object works as expected ([f7f7de6](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/f7f7de6681c4bb3ec1083f346d691d4d36795af3)), closes [#35](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/35) ## [1.3.0](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/compare/v1.2.0...v1.3.0) (2022-05-01) diff --git a/README.md b/README.md index 49b946a..5dfb8e6 100644 --- a/README.md +++ b/README.md @@ -48,13 +48,13 @@ ### Purpose -`alphanumeric-encoder` is a lightweight library with no dependencies. It can encode an integer into a letter representation, and decode the letter back into a number. +`alphanumeric-encoder` is a lightweight library with no dependencies. It can [encode](/../../blob/main/API.md#encode) an integer into a letter representation, [decode](/../../blob/main/API.md#decode) the letter back into a number, and [deconstruct](/../../blob/main/API.md#deconstruct) combined strings of letters and numbers into an array of decoded numbers. This is useful for converting letter indexes (used by people) to numbers (used by computers). Examples include: - Spreadsheet columns (e.g. Microsoft Excel's end column is "XFD" which corresponds to 16384) - Game boards (e.g. Chess, Battleship) use letters and numbers to identify the grid -- Geographic grid reference systems +- Geodesy grid reference systems (e.g. the [United States National Grid](https://en.wikipedia.org/wiki/United_States_National_Grid)) ### Install as an NPM Package @@ -76,6 +76,10 @@ console.log(encoder.encode(733)) // 'ABE' console.log(encoder.decode('A')) // 1 console.log(encoder.decode('AC')) // 29 console.log(encoder.decode('ANE')) // 1045 + +console.log(encoder.deconstruct('C7')) // [3, 7] +console.log(encoder.deconstruct('AC22')) // [29, 22] +console.log(encoder.deconstruct('C3ABC123EFGH456')) // [3, 3, 731, 123, 92126, 456] ``` --- @@ -103,7 +107,7 @@ Other Node versions and operating systems might support the library, but the tes `alphanumeric-encoder` and all other files in this repository are distributed as free and open-source software under the [MIT License](/../../blob/main/LICENSE), © 2022. -Both [contributions](/../../blob/main/CONTRIBUTING.md) and [bug reports](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/new/choose) welcome. +Both [contributions](/../../blob/main/CONTRIBUTING.md) and [bug reports](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/new/choose) welcome. See the [change log](/../../blob/main/CHANGELOG.md) for specific details of each release. Leave a :star2: if you find this project useful! diff --git a/index.js b/index.js index 4b96e23..b34c9a5 100644 --- a/index.js +++ b/index.js @@ -287,6 +287,76 @@ class AlphanumericEncoder { return result } + + /** + * Takes any string of letters and numbers and deconstructs it into an array of base 10 integers based on the defined dictionary. + * + * @param {string|number} stringToDeconstruct A string of letters and numbers (e.g. `'A7'`, `'AC22'`, `'7C10F'`) + * @throws {Error} if the dictionary contains a number as this function would be unable to differentiate between where a number and dictionary value. + * @returns {number[]} An array of numbers. Characters not present in the dictionary are treated as letters and return `undefined` for that array value. + * Passing an empty string (`''`), `null`, or `undefined` will return `undefined` for the whole function. + * @example + * const encoder = new AlphanumericEncoder() + * console.log(encoder.deconstruct('A')) // [1] + * console.log(encoder.deconstruct('AC22')) // [29, 22] + * console.log(encoder.deconstruct('C3ABC123EFGH456')) // [3, 3, 731, 123, 92126, 456] + * console.log(encoder.deconstruct('A1aB2B')) // [1, 1, undefined, 2, 2] + * console.log(encoder.deconstruct('7AC!23A1%')) // [7, undefined, 23, 1, 1, undefined] + * console.log(encoder.deconstruct('')) // undefined + * + */ + deconstruct(stringToDeconstruct) { + // The dictionary cannot contain numbers, or else the deconstruct function cannot distinguish where + // one code begins and another ends. + if (this.dictionary.match(/[0-9]/)) { + throw new Error('Cannot deconstruct if the dictionary contains numbers.') + } + + // Passing falsy values should return undefined + if ( + stringToDeconstruct === null || + stringToDeconstruct === undefined || + String(stringToDeconstruct).length === 0 + ) { + return undefined + } + + const safeString = String(stringToDeconstruct) // Force argument to string to process number arguments and prevent slice from throwing an error + const deconstructedArray = [] + let character = '' + let componentPart = safeString.slice(0, 1) // Initialize with the first character (which has been guranteed present by above guard functions) + + // A helper function to push the final component into the array that gets returned. Numbers get added as is, strings get decoded. + const addDecodedElement = (componentString) => { + if (componentString.match(/[0-9]/)) { + deconstructedArray.push(Number.parseInt(componentString, 10)) // Numbers + } else { + deconstructedArray.push(this.decode(componentString)) // Letters + } + } + + // If more than one character in safeString, loop through each subsequent character. Once the next character is not + // the same type as the previous group (i.e. flips from letter to number, or vice versa), add the character group to + // deconstructedArray, reset, and move to the next. + for (let i = 2; i <= safeString.length; i++) { + character = safeString.slice(i - 1, i) + + // Parse using a RegExp looking for numbers. The !! converts this to either true/false. + if (!!character.match(/[0-9]/) === !!componentPart.match(/[0-9]/)) { + // Same type, concatenate and keep going + componentPart += character + } else { + // Flipped types, add to array and reset + addDecodedElement(componentPart) + componentPart = character + } + } + + // Add the final component part (for single character stringToDeconstruct, this will be the only part) + addDecodedElement(componentPart) + + return deconstructedArray + } } module.exports = AlphanumericEncoder diff --git a/index.test.js b/index.test.js index f8d7774..07b43f3 100644 --- a/index.test.js +++ b/index.test.js @@ -321,3 +321,48 @@ describe('Test Decoding', () => { ) }) }) + +describe('Test Deconstruction', () => { + setupNewEncoderForTesting() + + test.each(['', undefined, null])( + 'Trying to deconstruct %p should return "undefined"', + (badArgument) => { + expect(encoder.deconstruct(badArgument)).toBeUndefined() + } + ) + + test('Expect dictionaries containing numbers to throw an error', () => { + expect(() => { + encoder.dictionary = 'ABC123' + encoder.deconstruct('C3') + }).toThrow(/dictionary contains numbers/) + }) + + const deconstructionTestValues = [ + ['A', [1]], + ['A1', [1, 1]], + ['C7', [3, 7]], + ['7C', [7, 3]], + ['AE18', [31, 18]], + ['18AE', [18, 31]], + ['1', [1]], + [1, [1]], + [733, [733]], + [[733], [733]], + [['7C'], [7, 3]], + ['7C82AA', [7, 3, 82, 27]], + ['C3ABC123EFGH456', [3, 3, 731, 123, 92126, 456]], + ['A1aB2B', [1, 1, undefined, 2, 2]], + ['7AC!23A1%', [7, undefined, 23, 1, 1, undefined]], + ['&', [undefined]] + ] + test.each(deconstructionTestValues)( + 'Under default dictionary, deconstructing %p should return array %p', + // @ts-ignored + (deconstructArgument, resultArray) => { + // @ts-ignore + expect(encoder.deconstruct(deconstructArgument)).toEqual(resultArray) + } + ) +})