From a1b7066be92af91ed82cb2f9a1e37dbd19914659 Mon Sep 17 00:00:00 2001 From: M-Scott-Lassiter Date: Sat, 30 Apr 2022 19:55:33 -0700 Subject: [PATCH] feat: add ability to use lower case letters in the dictionary This adds a boolean property, `allowLowerCaseDictionary`, that the user can set to true. Any subsequent change to the dictionary will allow a mix of both upper and lower case letters, and they will act as separate characters. Resolves: #31 --- API.md | 80 ++++++++++++++++++++++++++++++++++--------------- CHANGELOG.md | 14 ++++----- index.js | 63 ++++++++++++++++++++++++++++++++------- index.test.js | 82 +++++++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 192 insertions(+), 47 deletions(-) diff --git a/API.md b/API.md index 777819a..bc68784 100644 --- a/API.md +++ b/API.md @@ -7,14 +7,17 @@ - [dictionary][3] - [Parameters][4] - [Examples][5] - - [resetDefaultDictionary][6] - - [Examples][7] - - [encode][8] - - [Parameters][9] + - [allowLowerCaseDictionary][6] + - [Parameters][7] + - [Examples][8] + - [resetDefaultDictionary][9] - [Examples][10] - - [decode][11] + - [encode][11] - [Parameters][12] - [Examples][13] + - [decode][14] + - [Parameters][15] + - [Examples][16] ## AlphanumericEncoder @@ -34,7 +37,7 @@ Returns or sets the current dictionary. #### Parameters -- `newDictionary` **[string][14]** (If setting) String of unique letters and numbers, in order, for the new dictionary +- `newDictionary` **[string][17]** (If setting) String of unique letters and numbers, in order, for the new dictionary #### Examples @@ -49,11 +52,36 @@ console.log(encoder.dictionary) // 'ABCD' encoder.dictionary = 'ABCDA' // Throws error because the letter 'A' is repeated ``` -- Throws **[RangeError][15]** if setting dictionary to `null`, `undefined` or empty string (i.e. `''`) -- Throws **[RangeError][15]** if `newDictionary` contains a non-alphanumeric character -- Throws **[RangeError][15]** if `newDictionary` has a repeating character +- Throws **[RangeError][18]** if setting dictionary to `null`, `undefined` or empty string (i.e. `''`) +- Throws **[RangeError][18]** if `newDictionary` contains a non-alphanumeric character +- Throws **[RangeError][18]** if `newDictionary` has a repeating character -Returns **[string][14]** (If used as getter) The current dictionary in use +Returns **[string][17]** (If used as getter) The current dictionary in use + +### allowLowerCaseDictionary + +Returns or sets a boolean value that determines whether the dictionary will allow lower case letters or not. + +#### Parameters + +- `isAllowed` **[boolean][19]** (If setting). Accept truthy or falsy statements. + +#### Examples + +```javascript +const encoder = new AlphanumericEncoder() +encoder.dictionary = 'abcdefg' // Default for `allowLowerCaseDictionary` is false +console.log(encoder.dictionary) // 'ABCDEFG' +``` + +```javascript +const encoder = new AlphanumericEncoder() +encoder.allowLowerCaseDictionary = true +encoder.dictionary = 'ABCDefg' +console.log(encoder.dictionary) // 'ABCDefg' +``` + +Returns **[boolean][19]** (If used as getter) ### resetDefaultDictionary @@ -78,7 +106,7 @@ Takes any number and converts it into a base (dictionary length) letter combo. #### Parameters -- `integerToEncode` **[number][16]** Base 10 integer. If passed a non-integer number, decimal values are truncated. +- `integerToEncode` **[number][20]** Base 10 integer. If passed a non-integer number, decimal values are truncated. Passing zero, negative numbers, or non-numbers will return `undefined`. #### Examples @@ -117,9 +145,9 @@ console.log(encoder.encode(null)) // undefined console.log(encoder.encode(undefined)) // undefined ``` -- Throws **[RangeError][15]** if `integerToEncode` exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). +- Throws **[RangeError][18]** if `integerToEncode` exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). -Returns **[string][14]** Dictionary encoded value +Returns **[string][17]** Dictionary encoded value ### decode @@ -127,7 +155,7 @@ Takes any string and converts it into a base 10 integer based on the defined dic #### Parameters -- `stringToDecode` **[string][14]** If passed a non-integer number, decimal values are truncated. +- `stringToDecode` **[string][17]** If passed a non-integer number, decimal values are truncated. Passing an empty string, `null`, or `undefined` will return `undefined`. #### Examples @@ -155,23 +183,27 @@ console.log(encoder.decode('ADBAC')) // 551 console.log(encoder.decode('ANE')) // undefined ``` -- Throws **[RangeError][15]** if the decoded integer exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). +- Throws **[RangeError][18]** if the decoded integer exceeds the maximum safe integer for Javascript (`2^53 - 1 = 9007199254740991`). -Returns **[number][16]** Positive integer representation. If one of the characters is not present in the dictionary, it will return `undefined`. +Returns **[number][20]** Positive integer representation. If one of the characters is not present in the dictionary, it will return `undefined`. [1]: #alphanumericencoder [2]: #examples [3]: #dictionary [4]: #parameters [5]: #examples-1 -[6]: #resetdefaultdictionary -[7]: #examples-2 -[8]: #encode -[9]: #parameters-1 +[6]: #allowlowercasedictionary +[7]: #parameters-1 +[8]: #examples-2 +[9]: #resetdefaultdictionary [10]: #examples-3 -[11]: #decode +[11]: #encode [12]: #parameters-2 [13]: #examples-4 -[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String -[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RangeError -[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number +[14]: #decode +[15]: #parameters-3 +[16]: #examples-5 +[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String +[18]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/RangeError +[19]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean +[20]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number diff --git a/CHANGELOG.md b/CHANGELOG.md index f72cbc4..2487ea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,21 @@ ## [1.2.0](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/compare/v1.1.1...v1.2.0) (2022-04-30) - ### :lady_beetle: Bug Fixes -* add error handling for excessively large integers ([bc725f1](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/bc725f17e423910a0d64a20e56f2fa4c5064bce9)), closes [#28](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/28) - +- add error handling for excessively large integers ([bc725f1](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/bc725f17e423910a0d64a20e56f2fa4c5064bce9)), closes [#28](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/28) ### :building_construction: Build Changes -* update dev-dependency subdependencies (routine update) ([d918311](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/d9183113ef2512895bfdf2a9c5d22fbcde5547d8)), closes [#28](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/28) - +- update dev-dependency subdependencies (routine update) ([d918311](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/d9183113ef2512895bfdf2a9c5d22fbcde5547d8)), closes [#28](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/28) ### :gift: Feature Changes -* add the `resetDefaultDictionary` method ([9166bd8](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/9166bd87ae07874b5bef3b60764e93984c326e1d)), closes [#29](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/29) - +- add the `resetDefaultDictionary` method ([9166bd8](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/9166bd87ae07874b5bef3b60764e93984c326e1d)), closes [#29](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/29) ### :dart: Test Changes -* add applicable test for the `resetDefaultDictionary` method ([a67ef00](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/a67ef002b45d98fc9a6fb85b492f5e1eb0459517)), closes [#29](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/29) -* add tests to verify errors thrown for excessively large integers ([a9f39d6](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/a9f39d64bb21c5a44a572eb6c449f3acd21a37c6)), closes [#28](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/28) +- add applicable test for the `resetDefaultDictionary` method ([a67ef00](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/a67ef002b45d98fc9a6fb85b492f5e1eb0459517)), closes [#29](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/29) +- add tests to verify errors thrown for excessively large integers ([a9f39d6](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/commit/a9f39d64bb21c5a44a572eb6c449f3acd21a37c6)), closes [#28](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/issues/28) ### [1.1.1](https://github.com/M-Scott-Lassiter/Alphanumeric-Encoder/compare/v1.1.0...v1.1.1) (2022-04-28) diff --git a/index.js b/index.js index 0aa0d08..fa16cfb 100644 --- a/index.js +++ b/index.js @@ -9,11 +9,23 @@ */ class AlphanumericEncoder { constructor() { - /** @private */ - this._defaultDictionary = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' // Default dictionary is the English alphabet, all capitalized, in order - /** @private */ + /** + * @private + * @type {string} Internal value used to initialize and reset the dictionary + * @default The English alphabet, all capitalized, in order + * */ + this._defaultDictionary = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + /** + * @private + * @type {string} Internal value of the dictionary to encode/decode against + */ this._dictionaryInUse = '' - /** @private */ + /** + * @private + * @type {boolean} Internal value that tracks whether or not the dictionary setter should allow lower case letters or not + * */ + this._allowLowerCaseDictionary = false + this.resetDefaultDictionary() } @@ -49,28 +61,57 @@ class AlphanumericEncoder { throw new RangeError('All characters in the dictionary must be alphanumeric.') } - // Convert to upper case only. Verify each character is only used one time within the dictionary. - const uppercaseDictionary = newDictionary.toUpperCase() + // Convert to upper case only unless allowing for lower case letters + let formattedDictionary = newDictionary + if (!this._allowLowerCaseDictionary) { + formattedDictionary = newDictionary.toUpperCase() + } - for (let i = 0; i < uppercaseDictionary.length; i++) { + // Verify each character is only used one time within the dictionary. + for (let i = 0; i < formattedDictionary.length; i++) { if ( - uppercaseDictionary.indexOf(uppercaseDictionary[i]) !== - uppercaseDictionary.lastIndexOf(uppercaseDictionary[i]) + formattedDictionary.indexOf(formattedDictionary[i]) !== + formattedDictionary.lastIndexOf(formattedDictionary[i]) ) { throw new RangeError( - `The dictionary in use has at least one repeating symbol: ${uppercaseDictionary[i]}` + `The dictionary in use has at least one repeating symbol: ${formattedDictionary[i]}` ) } } // Validation is complete. Update the internal property. - this._dictionaryInUse = uppercaseDictionary + this._dictionaryInUse = formattedDictionary } get dictionary() { return this._dictionaryInUse } + /** + * Returns or sets a boolean value that determines whether the dictionary will allow lower case letters or not. + * + * @param {boolean} isAllowed (If setting). Accept truthy or falsy statements. + * @returns {boolean} (If used as getter) + * @default false + * @example + * const encoder = new AlphanumericEncoder() + * encoder.dictionary = 'abcdefg' // Default for `allowLowerCaseDictionary` is false + * console.log(encoder.dictionary) // 'ABCDEFG' + * + * @example + * const encoder = new AlphanumericEncoder() + * encoder.allowLowerCaseDictionary = true + * encoder.dictionary = 'ABCDefg' + * console.log(encoder.dictionary) // 'ABCDefg' + */ + set allowLowerCaseDictionary(isAllowed) { + this._allowLowerCaseDictionary = !!isAllowed // The double !! converts truthy or falsy values to true or false, respectively + } + + get allowLowerCaseDictionary() { + return this._allowLowerCaseDictionary + } + /** * Reset the dictionary in use to the default. * @default `ABCDEFGHIJKLMNOPQRSTUVWXYZ` diff --git a/index.test.js b/index.test.js index ca24180..a810faf 100644 --- a/index.test.js +++ b/index.test.js @@ -1,3 +1,5 @@ +// @ts-check + const AlphanumericEncoder = require('./index') let encoder = new AlphanumericEncoder() @@ -31,6 +33,14 @@ const numberWithDictionaryToEncodedLetters = [ ['EDCBA', 31, 'EEE'] ] +const numberWithLowerCaseDictionaryToEncodedLetters = [ + ['ABCDabcd', 4, 'D'], + ['ABCDabcd', 6, 'b'], + ['ABCDabcd', 9, 'AA'], + ['ABCDabcd', 15, 'Ac'], + ['ABCDabcd', 2984, 'abDd'] +] + const expectedValidDictionaryValues = ['ABCD', 'ABcd', 'aBcD', 'ABC123'] const expectedInvalidDictionaryValuesWithRepeatingSymbols = ['ABCDA', 'ABCDa', 'ABCD1231'] @@ -47,6 +57,30 @@ function setupNewEncoderForTesting() { }) } +describe('Allow Lower Case Dictionaries', () => { + setupNewEncoderForTesting() + + test('Default should not allow lower case dictionaries', () => { + expect(encoder.allowLowerCaseDictionary).toBeFalsy() + }) + + test.each([true, 1, [123], { value: 1 }])( + 'allowLowerCaseDictionary with truthy value %p', + (truthyValue) => { + encoder.allowLowerCaseDictionary = truthyValue + expect(encoder.allowLowerCaseDictionary).toBeTruthy() + } + ) + + test.each([false, 0, null, undefined])( + 'allowLowerCaseDictionary with falsy value %p', + (truthyValue) => { + encoder.allowLowerCaseDictionary = truthyValue + expect(encoder.allowLowerCaseDictionary).toBeFalsy() + } + ) +}) + describe('Dictionary Validation', () => { setupNewEncoderForTesting() test('Default dictionary should be capital alphabet in order', () => { @@ -77,7 +111,7 @@ describe('Dictionary Validation', () => { }).toThrow(/undefined/) }) - describe('Valid Dictionaries', () => { + describe('Valid Dictionaries (no lower case)', () => { setupNewEncoderForTesting() test.each(expectedValidDictionaryValues)( @@ -94,6 +128,26 @@ describe('Dictionary Validation', () => { }) }) + describe('Valid Dictionaries (allow lower case)', () => { + setupNewEncoderForTesting() + + test.each(expectedValidDictionaryValues)( + 'Expect %p to be a valid dictionary', + (validDictionaryString) => { + encoder.allowLowerCaseDictionary = true + encoder.dictionary = validDictionaryString + expect(encoder.dictionary).toBe(validDictionaryString) + } + ) + + const complexDictionary = 'ABCD123abcd' + test(`Expect ${complexDictionary} to be a valid dictionary`, () => { + encoder.allowLowerCaseDictionary = true + encoder.dictionary = complexDictionary + expect(encoder.dictionary).toBe('ABCD123abcd') + }) + }) + describe('Invalid Dictionaries', () => { setupNewEncoderForTesting() @@ -149,7 +203,7 @@ describe('Test Encoding', () => { ) }) - describe('Encode with Custom Dictionary', () => { + describe('Encode with Custom Upper Case Dictionary', () => { test.each(numberWithDictionaryToEncodedLetters)( 'Under dictionary %p, expect %p to encode to %p', (dictionary, number, letter) => { @@ -158,6 +212,17 @@ describe('Test Encoding', () => { } ) }) + + describe('Encode with Custom Lower Case Dictionary', () => { + test.each(numberWithLowerCaseDictionaryToEncodedLetters)( + 'Under dictionary %p, expect %p to encode to %p', + (dictionary, number, letter) => { + encoder.allowLowerCaseDictionary = true + encoder.dictionary = dictionary + expect(encoder.encode(number)).toBe(letter) + } + ) + }) }) describe('Test Decoding', () => { @@ -192,7 +257,7 @@ describe('Test Decoding', () => { ) }) - describe('Decode with Custom Dictionary', () => { + describe('Decode with Custom Upper Case Dictionary', () => { test.each(numberWithDictionaryToEncodedLetters)( 'Under dictionary %p, expect %p to be decoded from %p', (dictionary, number, letter) => { @@ -201,4 +266,15 @@ describe('Test Decoding', () => { } ) }) + + describe('Decode with Custom Lower Case Dictionary', () => { + test.each(numberWithLowerCaseDictionaryToEncodedLetters)( + 'Under dictionary %p, expect %p to be decoded from %p', + (dictionary, number, letter) => { + encoder.allowLowerCaseDictionary = true + encoder.dictionary = dictionary + expect(encoder.decode(letter)).toBe(number) + } + ) + }) })