JavaScript Essentials

Numbers & the Math Object

45 min Lesson 6 of 60

Introduction to Numbers in JavaScript

Numbers are one of the two primary primitive data types you will work with constantly in JavaScript, alongside strings. Unlike many other programming languages that distinguish between integers, floats, doubles, and other numeric types, JavaScript has a single number type. Every number in JavaScript, whether it is 42, 3.14, or -100, is represented as a 64-bit double-precision floating-point value following the IEEE 754 standard. This simplicity makes JavaScript easy to get started with, but it also introduces some quirks and precision issues that every developer must understand. In this lesson, you will learn how numbers work in JavaScript, how to parse and format them, and how to use the powerful Math object for calculations.

Number Types: Integer and Float

Although JavaScript treats all numbers as floating-point values internally, you can work with them as if they were integers or decimals. There is no separate integer type -- the distinction is purely in how you use the numbers.

Example: Integers and Floats

// Integers (whole numbers)
let count = 42;
let negative = -17;
let zero = 0;

// Floating-point numbers (decimals)
let pi = 3.14159;
let temperature = -40.5;
let tiny = 0.001;

// Both are the same type
console.log(typeof count);       // number
console.log(typeof pi);          // number
console.log(typeof NaN);         // number (yes, NaN is a number type!)

// JavaScript does not distinguish between int and float
console.log(10 === 10.0);        // true
console.log(1 === 1.0000);       // true

// Numeric separators (ES2021) -- for readability
let billion = 1_000_000_000;
let bytes = 0xFF_FF;
let fraction = 0.000_001;
console.log(billion);            // 1000000000
console.log(bytes);              // 65535

// Scientific notation
let large = 3e8;                 // 3 x 10^8 = 300000000
let small = 1.5e-6;             // 1.5 x 10^-6 = 0.0000015
console.log(large);              // 300000000
console.log(small);              // 0.0000015

// Other number bases
let binary = 0b1010;            // Binary (base 2) = 10
let octal = 0o755;              // Octal (base 8) = 493
let hex = 0xFF;                 // Hexadecimal (base 16) = 255
console.log(binary);            // 10
console.log(octal);             // 493
console.log(hex);               // 255
Note: The numeric separator underscore (_) was introduced in ES2021 and has no effect on the value. It is purely for readability, similar to how you might write 1,000,000 on paper. Use it freely to make large numbers easier to read in your code.

Special Numeric Values

JavaScript has several special numeric values that you will encounter. Understanding them is essential for writing robust code that handles edge cases correctly.

Example: Special Numeric Values

// Infinity
console.log(Infinity);           // Infinity
console.log(-Infinity);          // -Infinity
console.log(1 / 0);             // Infinity
console.log(-1 / 0);            // -Infinity
console.log(Infinity + 1);      // Infinity
console.log(Infinity * Infinity); // Infinity

// NaN (Not a Number)
console.log(NaN);                // NaN
console.log(0 / 0);             // NaN
console.log('hello' * 2);       // NaN
console.log(Math.sqrt(-1));      // NaN

// NaN is not equal to anything, including itself!
console.log(NaN === NaN);        // false
console.log(NaN == NaN);         // false

// Use Number.isNaN() to check for NaN (NOT the global isNaN)
console.log(Number.isNaN(NaN));          // true
console.log(Number.isNaN('hello'));      // false (correct!)
console.log(isNaN('hello'));             // true (incorrect! global isNaN coerces)

// Positive and negative zero
console.log(0 === -0);           // true (they are considered equal)
console.log(Object.is(0, -0));   // false (Object.is distinguishes them)
console.log(1 / 0);             // Infinity
console.log(1 / -0);            // -Infinity
Common Mistake: Never use the global isNaN() function for checking NaN. It coerces its argument to a number first, so isNaN('hello') returns true even though the string 'hello' is not NaN -- it is a string. Always use Number.isNaN() instead, which only returns true for the actual NaN value.

Safe Integer Range: Number.MAX_SAFE_INTEGER

Because JavaScript uses 64-bit floating-point representation, there is a limit to how precisely it can represent integers. Beyond a certain range, integer arithmetic becomes unreliable. JavaScript provides constants to help you understand these limits.

Example: Integer Safety Limits

// Maximum safe integer: 2^53 - 1
console.log(Number.MAX_SAFE_INTEGER);   // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER);   // -9007199254740991

// Beyond safe range, precision is lost
console.log(9007199254740991);          // 9007199254740991 (correct)
console.log(9007199254740992);          // 9007199254740992 (still looks ok)
console.log(9007199254740993);          // 9007199254740992 (WRONG! same as above)

// Checking if a number is a safe integer
console.log(Number.isSafeInteger(42));                  // true
console.log(Number.isSafeInteger(9007199254740991));    // true
console.log(Number.isSafeInteger(9007199254740992));    // false
console.log(Number.isSafeInteger(3.14));                // false (not an integer)

// Other numeric limits
console.log(Number.MAX_VALUE);          // 1.7976931348623157e+308
console.log(Number.MIN_VALUE);          // 5e-324 (smallest positive number)
console.log(Number.EPSILON);            // 2.220446049250313e-16

// BigInt for numbers beyond safe integer range (ES2020)
let huge = 9007199254740993n;           // Note the 'n' suffix
console.log(huge);                       // 9007199254740993n (correct!)
let hugeCalc = BigInt(9007199254740991) + 2n;
console.log(hugeCalc);                   // 9007199254740993n

// BigInt cannot be mixed with regular numbers
// console.log(huge + 1);  // TypeError!
console.log(huge + 1n);                  // 9007199254740994n (must use BigInt)
Pro Tip: If your application works with IDs from a database or API that might exceed 2^53, consider using BigInt or keeping those values as strings. This is a common issue when working with systems that use 64-bit integer IDs, such as Twitter/X post IDs or Snowflake IDs.

Parsing Numbers: parseInt and parseFloat

When you receive data from user input, URL parameters, or APIs, it often arrives as a string. JavaScript provides two primary functions for converting strings to numbers: parseInt() for integers and parseFloat() for decimal numbers.

Example: parseInt and parseFloat

// parseInt - parses a string and returns an integer
console.log(parseInt('42'));          // 42
console.log(parseInt('42.9'));        // 42 (truncates, does not round)
console.log(parseInt('  100  '));     // 100 (trims whitespace)
console.log(parseInt('10px'));        // 10 (stops at non-numeric character)
console.log(parseInt('width: 200'));  // NaN (cannot start with non-numeric)
console.log(parseInt(''));            // NaN

// parseInt with radix (base) -- ALWAYS specify the radix!
console.log(parseInt('0xFF', 16));    // 255 (hexadecimal)
console.log(parseInt('111', 2));      // 7 (binary)
console.log(parseInt('777', 8));      // 511 (octal)
console.log(parseInt('10', 10));      // 10 (decimal, explicit)

// parseFloat - parses a string and returns a floating-point number
console.log(parseFloat('3.14'));      // 3.14
console.log(parseFloat('3.14.15'));   // 3.14 (stops at second dot)
console.log(parseFloat('.5'));        // 0.5
console.log(parseFloat('100px'));     // 100
console.log(parseFloat('abc'));       // NaN

// Number() constructor -- stricter conversion
console.log(Number('42'));            // 42
console.log(Number('42.9'));          // 42.9
console.log(Number('10px'));          // NaN (stricter than parseInt!)
console.log(Number(''));              // 0 (empty string becomes 0)
console.log(Number(true));            // 1
console.log(Number(false));           // 0
console.log(Number(null));            // 0
console.log(Number(undefined));       // NaN

// Unary plus operator -- shorthand for Number()
console.log(+'42');                   // 42
console.log(+'3.14');                 // 3.14
console.log(+'hello');                // NaN
console.log(+true);                   // 1
console.log(+'');                     // 0

// Practical example: safely parse user input
function parseUserAge(input) {
    let age = parseInt(input, 10);
    if (Number.isNaN(age) || age < 0 || age > 150) {
        return null;
    }
    return age;
}

console.log(parseUserAge('25'));      // 25
console.log(parseUserAge('abc'));     // null
console.log(parseUserAge('-5'));      // null
console.log(parseUserAge('200'));     // null
Common Mistake: Always pass the radix (base) argument to parseInt(). Without it, parseInt('08') used to be interpreted as octal in older browsers, returning 0 instead of 8. Modern browsers default to base 10, but specifying parseInt(value, 10) makes your intent explicit and prevents subtle bugs.

Formatting Numbers: toFixed and toPrecision

When displaying numbers to users, you often need to control how many decimal places or significant digits are shown. JavaScript provides two methods for this purpose: toFixed() for decimal places and toPrecision() for significant digits.

Example: toFixed and toPrecision

// toFixed(digits) -- formats to fixed decimal places (returns a STRING)
let price = 19.5;
console.log(price.toFixed(2));      // "19.50"
console.log(price.toFixed(0));      // "20" (rounds!)
console.log(price.toFixed(4));      // "19.5000"

let pi = 3.14159;
console.log(pi.toFixed(2));         // "3.14"
console.log(pi.toFixed(3));         // "3.142" (rounds up)

// IMPORTANT: toFixed returns a STRING, not a number
let formatted = (19.99).toFixed(2);
console.log(typeof formatted);      // string
console.log(formatted + 1);         // "19.991" (string concatenation!)
console.log(parseFloat(formatted) + 1); // 20.99 (correct!)

// toPrecision(digits) -- formats to N significant digits (returns a STRING)
let num = 123.456;
console.log(num.toPrecision(6));    // "123.456"
console.log(num.toPrecision(5));    // "123.46"
console.log(num.toPrecision(4));    // "123.5"
console.log(num.toPrecision(3));    // "123"
console.log(num.toPrecision(2));    // "1.2e+2"
console.log(num.toPrecision(1));    // "1e+2"

// toString with radix -- convert to different bases
let decimal = 255;
console.log(decimal.toString(16));  // "ff" (hexadecimal)
console.log(decimal.toString(2));   // "11111111" (binary)
console.log(decimal.toString(8));   // "377" (octal)

// Practical example: format currency
function formatCurrency(amount, currency = 'USD') {
    return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: currency
    }).format(amount);
}

console.log(formatCurrency(1234.5));       // $1,234.50
console.log(formatCurrency(1234.5, 'EUR')); // (euro)1,234.50
console.log(formatCurrency(1234.5, 'JPY')); // (yen)1,235

// Format large numbers with commas
function formatNumber(num) {
    return num.toLocaleString('en-US');
}

console.log(formatNumber(1234567.89));  // 1,234,567.89
console.log(formatNumber(1000000));     // 1,000,000
Note: Both toFixed() and toPrecision() return strings, not numbers. This is a frequent source of bugs. If you need to do further arithmetic, convert the result back to a number using parseFloat() or the unary plus operator +.

Checking Number Types: isFinite and isInteger

JavaScript provides utility methods on the Number object for checking the nature of numeric values. These are essential for input validation and defensive programming.

Example: Number Checking Methods

// Number.isFinite -- checks if value is a finite number
console.log(Number.isFinite(42));          // true
console.log(Number.isFinite(3.14));        // true
console.log(Number.isFinite(Infinity));    // false
console.log(Number.isFinite(-Infinity));   // false
console.log(Number.isFinite(NaN));         // false
console.log(Number.isFinite('42'));        // false (no coercion!)

// Global isFinite vs Number.isFinite
console.log(isFinite('42'));               // true (coerces string to number!)
console.log(Number.isFinite('42'));        // false (no coercion, correct!)

// Number.isInteger -- checks if value is an integer
console.log(Number.isInteger(42));         // true
console.log(Number.isInteger(42.0));       // true (42.0 is the same as 42)
console.log(Number.isInteger(42.5));       // false
console.log(Number.isInteger('42'));       // false (must be a number type)
console.log(Number.isInteger(Infinity));   // false
console.log(Number.isInteger(NaN));        // false

// Number.isNaN -- checks if value is NaN
console.log(Number.isNaN(NaN));            // true
console.log(Number.isNaN(42));             // false
console.log(Number.isNaN('NaN'));          // false (it is a string, not NaN)
console.log(Number.isNaN(undefined));      // false

// Practical example: validate numeric input
function isValidNumber(value) {
    let num = Number(value);
    return Number.isFinite(num);
}

console.log(isValidNumber('42'));          // true
console.log(isValidNumber('3.14'));        // true
console.log(isValidNumber('hello'));       // false
console.log(isValidNumber(''));            // true (be careful! Number('') is 0)

// Better validation
function isValidNumericInput(value) {
    if (typeof value === 'string' && value.trim() === '') return false;
    let num = Number(value);
    return Number.isFinite(num);
}

console.log(isValidNumericInput(''));       // false (correct!)
console.log(isValidNumericInput('  '));    // false (correct!)
console.log(isValidNumericInput('42'));    // true

The Math Object: Rounding Methods

The Math object is a built-in JavaScript object that provides mathematical constants and functions. It is not a constructor -- you cannot create instances of it. Instead, you use its static methods and properties directly. Let us start with the rounding methods, which are among the most commonly used.

Example: Math.round, Math.ceil, Math.floor, and Math.trunc

// Math.round -- rounds to the nearest integer
console.log(Math.round(4.5));    // 5
console.log(Math.round(4.4));    // 4
console.log(Math.round(4.6));    // 5
console.log(Math.round(-4.5));   // -4 (rounds toward +Infinity)
console.log(Math.round(-4.6));   // -5

// Math.ceil -- always rounds UP (toward +Infinity)
console.log(Math.ceil(4.1));     // 5
console.log(Math.ceil(4.9));     // 5
console.log(Math.ceil(4.0));     // 4 (already an integer)
console.log(Math.ceil(-4.1));    // -4 (toward +Infinity!)
console.log(Math.ceil(-4.9));    // -4

// Math.floor -- always rounds DOWN (toward -Infinity)
console.log(Math.floor(4.1));    // 4
console.log(Math.floor(4.9));    // 4
console.log(Math.floor(4.0));    // 4
console.log(Math.floor(-4.1));   // -5 (toward -Infinity!)
console.log(Math.floor(-4.9));   // -5

// Math.trunc -- removes decimal part (truncates toward zero)
console.log(Math.trunc(4.9));    // 4
console.log(Math.trunc(4.1));    // 4
console.log(Math.trunc(-4.9));   // -4 (toward zero, NOT toward -Infinity)
console.log(Math.trunc(-4.1));   // -4

// Comparison of negative number rounding
let n = -4.3;
console.log(Math.round(n));     // -4
console.log(Math.ceil(n));      // -4
console.log(Math.floor(n));     // -5
console.log(Math.trunc(n));     // -4

// Practical example: round to N decimal places
function roundTo(num, decimals) {
    let factor = Math.pow(10, decimals);
    return Math.round(num * factor) / factor;
}

console.log(roundTo(3.14159, 2));   // 3.14
console.log(roundTo(3.14159, 3));   // 3.142
console.log(roundTo(3.14159, 0));   // 3

// Practical example: calculate number of pages
function getPageCount(totalItems, itemsPerPage) {
    return Math.ceil(totalItems / itemsPerPage);
}

console.log(getPageCount(101, 10));  // 11
console.log(getPageCount(100, 10));  // 10
console.log(getPageCount(1, 10));    // 1
Pro Tip: Remember the distinction between Math.floor() and Math.trunc() for negative numbers. Math.floor(-4.3) gives -5 (rounds toward negative infinity), while Math.trunc(-4.3) gives -4 (removes the decimal part). For positive numbers, they behave identically.

Math.max and Math.min

The Math.max() and Math.min() methods return the largest and smallest of the given numbers. They accept any number of arguments and are frequently used for clamping values within a range.

Example: Math.max and Math.min

// Basic usage
console.log(Math.max(1, 5, 3, 9, 2));    // 9
console.log(Math.min(1, 5, 3, 9, 2));    // 1

// With negative numbers
console.log(Math.max(-10, -5, -1));       // -1
console.log(Math.min(-10, -5, -1));       // -10

// Edge cases
console.log(Math.max());                  // -Infinity (no arguments)
console.log(Math.min());                  // Infinity (no arguments)
console.log(Math.max(5, NaN, 3));         // NaN (any NaN argument returns NaN)

// Using with arrays via spread operator
let scores = [85, 92, 78, 96, 88];
console.log(Math.max(...scores));          // 96
console.log(Math.min(...scores));          // 78

// Clamping a value to a range
function clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
}

console.log(clamp(150, 0, 100));  // 100
console.log(clamp(-20, 0, 100));  // 0
console.log(clamp(50, 0, 100));   // 50

// Practical example: limit scroll position
function limitScroll(scrollPos, maxScroll) {
    return clamp(scrollPos, 0, maxScroll);
}

// Practical example: find range of values
function getRange(numbers) {
    let min = Math.min(...numbers);
    let max = Math.max(...numbers);
    return { min, max, range: max - min };
}

let temps = [72, 68, 75, 80, 65, 78];
console.log(getRange(temps));
// { min: 65, max: 80, range: 15 }

Random Numbers: Math.random

The Math.random() method returns a pseudo-random floating-point number between 0 (inclusive) and 1 (exclusive). By combining it with multiplication, addition, and rounding, you can generate random numbers in any range.

Example: Generating Random Numbers

// Basic usage -- returns [0, 1)
console.log(Math.random());  // e.g., 0.7234567891234567

// Random integer between 0 and max (exclusive)
function randomInt(max) {
    return Math.floor(Math.random() * max);
}
console.log(randomInt(10));   // 0-9
console.log(randomInt(100));  // 0-99

// Random integer between min and max (inclusive)
function randomRange(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}
console.log(randomRange(1, 6));    // 1-6 (dice roll)
console.log(randomRange(10, 20));  // 10-20
console.log(randomRange(-5, 5));   // -5 to 5

// Random floating point in a range
function randomFloat(min, max) {
    return Math.random() * (max - min) + min;
}
console.log(randomFloat(1.5, 3.5));  // e.g., 2.345678...

// Practical example: shuffle an array (Fisher-Yates algorithm)
function shuffle(arr) {
    let array = [...arr];  // Create a copy
    for (let i = array.length - 1; i > 0; i--) {
        let j = Math.floor(Math.random() * (i + 1));
        [array[i], array[j]] = [array[j], array[i]];
    }
    return array;
}

let deck = ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10'];
console.log(shuffle(deck));
// e.g., ['7', '3', 'A', '10', '5', '8', '2', '9', '4', '6']

// Practical example: generate a random color
function randomColor() {
    let r = randomRange(0, 255);
    let g = randomRange(0, 255);
    let b = randomRange(0, 255);
    return `rgb(${r}, ${g}, ${b})`;
}

console.log(randomColor());  // e.g., rgb(142, 87, 203)

// Practical example: generate a random hex color
function randomHexColor() {
    return '#' + Math.floor(Math.random() * 0xFFFFFF)
                     .toString(16)
                     .padStart(6, '0');
}

console.log(randomHexColor());  // e.g., #8e57cb

// Practical example: pick a random element from an array
function randomElement(arr) {
    return arr[Math.floor(Math.random() * arr.length)];
}

let greetings = ['Hello', 'Hi', 'Hey', 'Greetings', 'Welcome'];
console.log(randomElement(greetings));  // e.g., "Hey"
Common Mistake: Do not use Math.round(Math.random() * max) to generate random integers. This gives the endpoints (0 and max) only half the probability of other values. Always use Math.floor(Math.random() * (max + 1)) for uniform distribution. Also, Math.random() is not cryptographically secure -- for security-sensitive applications like generating tokens, use crypto.getRandomValues() instead.

Math.abs: Absolute Value

The Math.abs() method returns the absolute value of a number -- its distance from zero, always positive. This is useful for calculating differences, distances, and magnitudes regardless of direction.

Example: Math.abs

console.log(Math.abs(5));      // 5
console.log(Math.abs(-5));     // 5
console.log(Math.abs(0));      // 0
console.log(Math.abs(-3.14));  // 3.14

// Practical example: calculate the difference between two values
function difference(a, b) {
    return Math.abs(a - b);
}

console.log(difference(10, 7));   // 3
console.log(difference(7, 10));   // 3 (same result regardless of order)

// Practical example: check if two numbers are approximately equal
function approximately(a, b, tolerance = 0.001) {
    return Math.abs(a - b) < tolerance;
}

console.log(approximately(0.1 + 0.2, 0.3));     // true
console.log(approximately(3.14159, 3.14));        // false
console.log(approximately(3.14159, 3.14, 0.01));  // true

// Practical example: calculate distance between two points
function distance(x1, y1, x2, y2) {
    return Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
}

console.log(distance(0, 0, 3, 4));    // 5
console.log(distance(1, 1, 4, 5));    // 5

Powers and Roots: Math.pow, Math.sqrt, and the Exponentiation Operator

JavaScript provides several ways to calculate powers and roots. The Math.pow() method and the ** exponentiation operator handle powers, while Math.sqrt() calculates square roots.

Example: Powers and Roots

// Math.pow(base, exponent)
console.log(Math.pow(2, 3));     // 8 (2^3)
console.log(Math.pow(5, 2));     // 25 (5^2)
console.log(Math.pow(10, 6));    // 1000000 (10^6)
console.log(Math.pow(2, -1));    // 0.5 (2^-1 = 1/2)
console.log(Math.pow(8, 1/3));   // 2 (cube root of 8)

// Exponentiation operator ** (ES2016) -- preferred over Math.pow
console.log(2 ** 3);             // 8
console.log(5 ** 2);             // 25
console.log(10 ** 6);            // 1000000
console.log(2 ** -1);            // 0.5
console.log(8 ** (1/3));         // 2

// Math.sqrt -- square root
console.log(Math.sqrt(16));      // 4
console.log(Math.sqrt(2));       // 1.4142135623730951
console.log(Math.sqrt(0));       // 0
console.log(Math.sqrt(-1));      // NaN (cannot square root a negative)

// Math.cbrt -- cube root (ES6)
console.log(Math.cbrt(27));      // 3
console.log(Math.cbrt(8));       // 2
console.log(Math.cbrt(-8));      // -2 (works with negatives!)

// Math.hypot -- square root of sum of squares (ES6)
console.log(Math.hypot(3, 4));        // 5 (hypotenuse of 3-4-5 triangle)
console.log(Math.hypot(5, 12));       // 13
console.log(Math.hypot(3, 4, 5));     // 7.0710678... (3D distance)

// Practical example: compound interest
function compoundInterest(principal, rate, years, timesPerYear = 12) {
    return principal * Math.pow(1 + rate / timesPerYear, timesPerYear * years);
}

let result = compoundInterest(1000, 0.05, 10);
console.log(result.toFixed(2));  // 1647.01 ($1000 at 5% for 10 years)

// Practical example: check if a number is a power of 2
function isPowerOfTwo(n) {
    if (n <= 0) return false;
    return Math.log2(n) % 1 === 0;
}

console.log(isPowerOfTwo(64));   // true
console.log(isPowerOfTwo(100));  // false

Mathematical Constants: Math.PI and Others

The Math object provides several mathematical constants as properties. These are read-only values that you can use in your calculations.

Example: Math Constants

// Math.PI -- ratio of circumference to diameter
console.log(Math.PI);            // 3.141592653589793

// Math.E -- Euler's number (base of natural logarithm)
console.log(Math.E);             // 2.718281828459045

// Math.LN2 -- natural log of 2
console.log(Math.LN2);           // 0.6931471805599453

// Math.LN10 -- natural log of 10
console.log(Math.LN10);          // 2.302585092994046

// Math.SQRT2 -- square root of 2
console.log(Math.SQRT2);         // 1.4142135623730951

// Practical example: circle calculations
function circleArea(radius) {
    return Math.PI * radius ** 2;
}

function circleCircumference(radius) {
    return 2 * Math.PI * radius;
}

console.log(circleArea(5).toFixed(2));           // 78.54
console.log(circleCircumference(5).toFixed(2));  // 31.42

// Practical example: convert degrees to radians and back
function degreesToRadians(degrees) {
    return degrees * (Math.PI / 180);
}

function radiansToDegrees(radians) {
    return radians * (180 / Math.PI);
}

console.log(degreesToRadians(90));    // 1.5707963... (PI/2)
console.log(degreesToRadians(180));   // 3.1415926... (PI)
console.log(radiansToDegrees(Math.PI));  // 180

// Trigonometric functions
console.log(Math.sin(degreesToRadians(30)));   // 0.5 (approximately)
console.log(Math.cos(degreesToRadians(60)));   // 0.5 (approximately)
console.log(Math.tan(degreesToRadians(45)));   // 1 (approximately)

Floating-Point Precision Issues and Workarounds

One of the most infamous quirks in JavaScript (and all languages using IEEE 754 floating-point arithmetic) is that certain decimal calculations produce unexpected results. This is not a JavaScript bug -- it is a fundamental limitation of how computers represent decimal fractions in binary. Understanding this issue and knowing the workarounds is essential for every JavaScript developer.

Example: Floating-Point Precision Problems

// The classic example
console.log(0.1 + 0.2);          // 0.30000000000000004 (NOT 0.3!)
console.log(0.1 + 0.2 === 0.3);  // false!

// More precision issues
console.log(0.1 + 0.7);          // 0.7999999999999999
console.log(1.1 + 2.2);          // 3.3000000000000003
console.log(0.3 - 0.1);          // 0.19999999999999998
console.log(0.6 * 3);            // 1.7999999999999998
console.log(1.4 - 1.0);          // 0.39999999999999997

// Why this happens:
// 0.1 in binary is 0.0001100110011... (repeating forever)
// Just as 1/3 = 0.333... in decimal, 1/10 is infinite in binary.
// JavaScript can only store 64 bits, so it rounds, creating tiny errors.

Example: Workarounds for Floating-Point Issues

// Workaround 1: Compare with tolerance (Number.EPSILON)
function areEqual(a, b) {
    return Math.abs(a - b) < Number.EPSILON;
}

console.log(areEqual(0.1 + 0.2, 0.3));  // true

// Workaround 2: Use toFixed for display purposes
let total = 0.1 + 0.2;
console.log(total.toFixed(2));           // "0.30" (string!)
console.log(parseFloat(total.toFixed(2))); // 0.3 (number)

// Workaround 3: Work in integers (cents instead of dollars)
function addMoney(a, b) {
    // Convert to cents, add, convert back
    let centsA = Math.round(a * 100);
    let centsB = Math.round(b * 100);
    return (centsA + centsB) / 100;
}

console.log(addMoney(0.1, 0.2));    // 0.3 (correct!)
console.log(addMoney(19.99, 5.01)); // 25 (correct!)

// Workaround 4: Use a rounding function
function roundTo(num, decimals) {
    let factor = 10 ** decimals;
    return Math.round(num * factor) / factor;
}

console.log(roundTo(0.1 + 0.2, 1));    // 0.3
console.log(roundTo(1.1 + 2.2, 1));    // 3.3

// Practical example: shopping cart total
function calculateTotal(items) {
    let totalCents = items.reduce((sum, item) => {
        return sum + Math.round(item.price * 100) * item.quantity;
    }, 0);
    return totalCents / 100;
}

let cartItems = [
    { name: 'Widget', price: 9.99, quantity: 3 },
    { name: 'Gadget', price: 14.50, quantity: 2 },
    { name: 'Tool', price: 0.99, quantity: 10 }
];

console.log(calculateTotal(cartItems));  // 68.87 (correct!)

// Practical example: currency comparison
function comparePrices(price1, price2) {
    let cents1 = Math.round(price1 * 100);
    let cents2 = Math.round(price2 * 100);
    if (cents1 < cents2) return -1;
    if (cents1 > cents2) return 1;
    return 0;
}

console.log(comparePrices(0.1 + 0.2, 0.3));  // 0 (equal, correct!)
Common Mistake: Never compare floating-point numbers with === after arithmetic operations. The result 0.1 + 0.2 is NOT exactly 0.3 in JavaScript. For financial calculations, always work in the smallest currency unit (cents, pence, etc.) using integers, and only convert to decimal for display. This eliminates precision errors entirely.

Additional Math Methods

The Math object provides many more useful methods beyond what we have covered. Here are several additional methods that are worth knowing for various mathematical operations.

Example: Additional Math Methods

// Math.sign -- returns -1, 0, or 1 indicating the sign
console.log(Math.sign(42));     // 1
console.log(Math.sign(-42));    // -1
console.log(Math.sign(0));      // 0

// Math.log, Math.log2, Math.log10 -- logarithms
console.log(Math.log(Math.E));   // 1 (natural log of e)
console.log(Math.log2(8));       // 3 (log base 2 of 8)
console.log(Math.log10(1000));   // 3 (log base 10 of 1000)

// Math.clz32 -- count leading zeros in 32-bit representation
console.log(Math.clz32(1));      // 31
console.log(Math.clz32(1000));   // 22

// Math.fround -- round to nearest 32-bit float
console.log(Math.fround(1.337)); // 1.3370000123977661

// Practical example: calculate percentage change
function percentChange(oldValue, newValue) {
    if (oldValue === 0) return newValue === 0 ? 0 : Infinity;
    return roundTo(((newValue - oldValue) / Math.abs(oldValue)) * 100, 2);
}

function roundTo(num, decimals) {
    let factor = 10 ** decimals;
    return Math.round(num * factor) / factor;
}

console.log(percentChange(100, 125));   // 25
console.log(percentChange(200, 150));   // -25
console.log(percentChange(50, 75));     // 50

// Practical example: convert between temperature scales
function celsiusToFahrenheit(c) {
    return roundTo(c * 9 / 5 + 32, 1);
}

function fahrenheitToCelsius(f) {
    return roundTo((f - 32) * 5 / 9, 1);
}

console.log(celsiusToFahrenheit(0));     // 32
console.log(celsiusToFahrenheit(100));   // 212
console.log(fahrenheitToCelsius(98.6));  // 37

// Practical example: BMI calculator
function calculateBMI(weightKg, heightM) {
    let bmi = weightKg / (heightM ** 2);
    return roundTo(bmi, 1);
}

console.log(calculateBMI(70, 1.75));     // 22.9

Number Conversion Patterns

Converting between different types and numbers is a common task. Here is a summary of the various conversion techniques and when to use each one.

Example: Comprehensive Number Conversions

// String to Number -- multiple approaches
let str = '42.5';

console.log(Number(str));          // 42.5 (strict)
console.log(parseFloat(str));      // 42.5 (lenient)
console.log(parseInt(str, 10));    // 42 (integer only)
console.log(+str);                 // 42.5 (unary plus shorthand)

// Number to String
let num = 42.5;

console.log(String(num));          // "42.5"
console.log(num.toString());       // "42.5"
console.log(num.toFixed(1));       // "42.5"
console.log(num + '');             // "42.5" (implicit, avoid this)
console.log(`${num}`);             // "42.5" (template literal)

// Boolean to Number
console.log(Number(true));         // 1
console.log(Number(false));        // 0
console.log(+true);                // 1
console.log(+false);               // 0

// Number to Boolean
console.log(Boolean(0));           // false
console.log(Boolean(42));          // true
console.log(Boolean(-1));          // true
console.log(Boolean(NaN));         // false
console.log(Boolean(Infinity));    // true

// Practical example: sum values from form inputs
function sumInputValues(values) {
    return values
        .map(v => parseFloat(v) || 0)
        .reduce((sum, n) => sum + n, 0);
}

let formValues = ['10', '20.5', 'invalid', '30', ''];
console.log(sumInputValues(formValues));  // 60.5

Practice Exercise

Build a collection of math utility functions that solve real-world problems. Test each function with at least three different inputs, including edge cases:

  1. rollDice(sides, count) -- Simulates rolling multiple dice. Takes the number of sides per die and the count of dice to roll. Returns an object with the individual rolls array and the total sum (e.g., rollDice(6, 3) might return { rolls: [4, 2, 6], total: 12 }).
  2. formatBytes(bytes) -- Converts a number of bytes into a human-readable string with appropriate units. For example, formatBytes(1024) returns "1.00 KB", formatBytes(1234567) returns "1.18 MB". Support bytes, KB, MB, GB, and TB.
  3. calculateTip(billAmount, tipPercent, splitCount) -- Calculates the tip and per-person total for a restaurant bill. Handle floating-point precision by working in cents. Return an object with tip amount, total, and per-person share.
  4. randomPassword(length) -- Generates a random password of the specified length using uppercase letters, lowercase letters, digits, and special characters. Ensure at least one character from each category is included.
  5. statisticsOf(numbers) -- Takes an array of numbers and returns an object containing: min, max, sum, average (rounded to 2 decimal places), median, and range. Handle arrays with odd and even lengths for the median calculation.

For each function, consider edge cases carefully: what happens with zero values, negative numbers, empty arrays, very large numbers, or invalid inputs? Use Number.isFinite() for validation, work in integers for currency, and always round results appropriately for display.

ES
Edrees Salih
12 hours ago

We are still cooking the magic in the way!