Last active
February 9, 2021 23:28
-
-
Save jketcham/e3ad473a4d4dff92deb4e1fd8824be46 to your computer and use it in GitHub Desktop.
Convert a string to a number
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ASCII character codes | |
const ZERO_CHAR = 48; | |
const ONE_CHAR = 49; | |
const TWO_CHAR = 50; | |
const THREE_CHAR = 51; | |
const FOUR_CHAR = 52; | |
const FIVE_CHAR = 53; | |
const SIX_CHAR = 54; | |
const SEVEN_CHAR = 55; | |
const EIGHT_CHAR = 56; | |
const NINE_CHAR = 57; | |
const HYPHEN_CHAR = 45; | |
const PERIOD_CHAR = 46; | |
// Mapping of ASCII character code to int | |
const NUMBER_MAP = { | |
[ZERO_CHAR]: 0, | |
[ONE_CHAR]: 1, | |
[TWO_CHAR]: 2, | |
[THREE_CHAR]: 3, | |
[FOUR_CHAR]: 4, | |
[FIVE_CHAR]: 5, | |
[SIX_CHAR]: 6, | |
[SEVEN_CHAR]: 7, | |
[EIGHT_CHAR]: 8, | |
[NINE_CHAR]: 9, | |
}; | |
// Characters that should only occur in number once | |
const CHARS_LIMIT_ONE = [ | |
HYPHEN_CHAR, | |
PERIOD_CHAR, | |
]; | |
const VALID_CHARS = [ | |
ZERO_CHAR, | |
ONE_CHAR, | |
TWO_CHAR, | |
THREE_CHAR, | |
FOUR_CHAR, | |
FIVE_CHAR, | |
SIX_CHAR, | |
SEVEN_CHAR, | |
EIGHT_CHAR, | |
NINE_CHAR, | |
HYPHEN_CHAR, | |
PERIOD_CHAR, | |
]; | |
/** | |
* Verifies these conditions for conversion to a number | |
* | |
* - Only contains valid characters | |
* - No more than 1 occurence of a hyphen and period character | |
* - Hyphen can only occur at the beginning of the input | |
*/ | |
function isValidInput(input) { | |
const charOccurences = {}; | |
for (let i = 0; i < input.length; i++) { | |
const char = input[i]; | |
if (char === HYPHEN_CHAR && i > 0) { | |
return false; | |
} | |
if (!VALID_CHARS.includes(char)) { | |
return false; | |
} | |
const occurenceCount = charOccurences[char]; | |
// Only one of these characters can exist in input | |
if (occurenceCount && CHARS_LIMIT_ONE.includes(char)) { | |
return false; | |
} | |
charOccurences[char] = 1; | |
} | |
return true; | |
} | |
function isNegative(input) { | |
return input[0] === HYPHEN_CHAR; | |
} | |
// Split input between digits before and after a decimal place | |
function splitDigits(input) { | |
const pivotIndex = input.findIndex((char) => char === PERIOD_CHAR); | |
if (pivotIndex === -1) { | |
return [input]; | |
} | |
return [ | |
input.slice(0, pivotIndex), // Digits before period | |
input.slice(pivotIndex + 1), // Digits after period | |
]; | |
} | |
// Return the input as an array of ASCII codes | |
function convertToCharCodes(input) { | |
return input | |
.split('') // Seperate string into array of its characters | |
.map((char, index) => input.charCodeAt(index)); | |
} | |
function convertToInt(input) { | |
if (!input) { | |
return 0; | |
} | |
let number = 0; | |
// Start at beginning of the input and move to the right | |
for (let i = 0; i < input.length; i++) { | |
const digit = NUMBER_MAP[input[i]]; | |
const place = (input.length - i) - 1; | |
number += digit * Math.pow(10, place); | |
} | |
return number; | |
} | |
function convertToDecimal(input) { | |
if (!input) { | |
return 0.0; | |
} | |
let number = 0.0; | |
// Start from the end of the input and move to the left | |
for (let i = input.length - 1; i >= 0; i--) { | |
const digit = NUMBER_MAP[input[i]]; | |
number += 0.1 * digit * Math.pow(0.1, i); | |
} | |
return number; | |
} | |
function stringToNumber(input) { | |
input = convertToCharCodes(input); | |
if (!isValidInput(input)) { | |
throw new Error('Input is invalid'); | |
} | |
const negativeInput = isNegative(input); | |
// If number is negative, remove the hyphen from the input | |
input = negativeInput ? input.slice(1) : input; | |
const [intChars, decimalChars] = splitDigits(input); | |
const intNumber = convertToInt(intChars); | |
const decimalNumber = convertToDecimal(decimalChars); | |
return (intNumber + decimalNumber) * (negativeInput ? -1 : 1); | |
} | |
module.exports = stringToNumber; | |
const TEST_CASES = [ | |
{ input: '123', output: 123 }, | |
{ input: '-123', output: -123 }, | |
{ input: '-123.456', output: -123.456 }, | |
{ input: '123abc', output: null }, | |
{ input: '--1234', output: null }, | |
{ input: '123-', output: null }, | |
{ input: '12.123.123', output: null }, | |
]; | |
TEST_CASES.forEach(({ input, output }) => { | |
try { | |
const result = stringToNumber(input); | |
console.log({ input, expectedResult: output, result, passes: output === result }); | |
} catch (error) { | |
if (error.message === 'Input is invalid') { | |
console.log({ input, expectedResult: output, result: 'Invalid', passes: output === null }); | |
} else { | |
console.log(`Unexpected error: ${error.message} while processing "${input}"`); | |
} | |
} | |
}); | |
// Expected output: | |
// $ node stringToNumber.js | |
// { input: '123', expectedResult: 123, result: 123, passes: true } | |
// { input: '-123', expectedResult: -123, result: -123, passes: true } | |
// { | |
// input: '-123.456', | |
// expectedResult: -123.456, | |
// result: -123.456, | |
// passes: true | |
// } | |
// { | |
// input: '123abc', | |
// expectedResult: null, | |
// result: null, | |
// passes: true | |
// } | |
// { | |
// input: '--1234', | |
// expectedResult: null, | |
// result: null, | |
// passes: true | |
// } | |
// { | |
// input: '123-', | |
// expectedResult: null, | |
// result: null, | |
// passes: true | |
// } | |
// { | |
// input: '12.123.123', | |
// expectedResult: null, | |
// result: null, | |
// passes: true | |
// } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment