← Math in Practice
Concept Number representation and masks

Binary, hexadecimal, and bit operations

Follow the bit positions before trusting the compact notation.

A service has address 192.168.34.77/20. A teammate says it belongs to the 192.168.32.0 network; another reads the third octet as a mismatch. The same release also carries compact flags such as 0x25. To resolve either question, make the bit positions visible.

The judgment to keep

A mask selects positions; it does not explain the surrounding system. Confirm the arithmetic, then check the route, protocol definition, and runtime representation that give those bits meaning.

TypeScriptGo Place value · masks · hexadecimal · fixed-width arithmetic
01 / Read the address

The disagreement is about which address bits define the network.

An IPv4 address contains four decimal octets, each from 0 to 255. The suffix /20 means the first 20 of its 32 bits identify the prefix. It does not mean “the first 20 decimal digits” or “20 addresses.” The subnet mask writes those first 20 positions as ones and the remainder as zeroes.

Case file / Deployment networkDoes the API host share the configured prefix?
Host
192.168.34.77
Prefix
/20 means 20 network bits in a 32-bit IPv4 address.
Competing claim
The configured network is 192.168.32.0/20.
Question
Can the host address be reduced to that network address by applying the mask?
02 / Read the bits

Each position contributes a power of two.

Binary is base two. Moving one place left doubles the place value: … 16, 8, 4, 2, 1. The bit 00100010₂ has a one in the 32 place and the 2 place, so its value is 32 + 2 = 34. A bit position is a yes/no contribution to the total, not a decimal digit with value zero through nine.

For a four-bit group, the values are 8, 4, 2, 1. This is why a hexadecimal digit can stand for exactly four bits: 0010₂ = 2₁₆, and 1110₂ = E₁₆. Binary makes individual decisions visible; decimal is usually easier for people to read quantities; hexadecimal is a compact way to preserve bit grouping.

One octet of the /20 mask · bitwise AND keeps positions where both bits are 1
ValueBinaryMeaning
Host 340010001032 + 2
Mask 24011110000Keep the upper four bits of this octet
AND result 320010000032 + 0
Trace: the first two octets are fully masked, the third keeps its upper four bits, and the fourth is zeroed. So 192.168.34.77 & 255.255.240.0 = 192.168.32.0.
03 / Apply the mask

Each operator answers a different bit-level question.

  • AND (&) keeps a bit only when both inputs have 1. Use it to extract fields or apply a network mask.
  • OR (|) sets a bit when either input has 1. Use flags | READ to turn a permission flag on without changing other bits.
  • XOR (^) sets a bit when inputs differ. XOR with one flag toggles that flag; XOR with a comparison value can identify changed bits.
  • Shift left/right (<<, >>) moves bit positions. For small nonnegative integers, shifting left by one doubles the value; shifts also help position a field or construct a mask.

A flag should be tested with (value & FLAG) !== 0, not equality with the flag: the value can contain other flags at the same time. Clear a one-bit flag using value & ~FLAG, while preserving the rest.

VALUE0x25

0010 0101₂ · flags at bits 5, 2, and 0

TEST0x04

0000 0100₂ · test bit 2

AND0x04

0010 0101 & 0000 0100 = 0000 0100 · set

CLEAR0x21

0x25 & ~0x04 clears bit 2, retaining bits 5 and 0

04 / Use hexadecimal

Hexadecimal shortens the writing without changing the bits.

Hexadecimal is base 16, so each digit represents one four-bit nibble. For example, 0xF0 is 1111 0000₂; 0x04 is 0000 0100₂. The 0x prefix is a convention that labels the number as hexadecimal. It does not indicate a different kind of storage.

The `/20` mask is 255.255.240.0 in dotted decimal and 0xFFFFF000 as a 32-bit word. Four consecutive one bits become one F. Hex is useful in packet dumps and flag fields because changes align visually with groups of four bits; when a particular bit is unclear, expand it to binary.

0xFFFFF000 = 11111111 11111111 11110000 00000000₂
192.168.34.77 = 11000000 10101000 00100010 01001101₂
AND = 11000000 10101000 00100000 00000000₂ = 192.168.32.0
05 / Practice in code

Represent the same 32 bits explicitly in each language.

The TypeScript version parses the four octets into network byte order with DataView, applies a prefix mask, then converts the result back to unsigned with >>> 0. For 192.168.34.77/20, both programs print 192.168.32.0. The Go version uses net/netip, which parses and masks an IPv4 prefix without relying on architecture-sized int arithmetic.

JavaScript’s ordinary Number bitwise operators convert operands to signed 32-bit integers. IPv4 values with the high bit set (addresses from 128.0.0.0 onward) can therefore appear negative as intermediates. >>> 0 reinterprets the 32 result bits as an unsigned number; DataView makes byte order explicit. The sample validates its input and handles /0 separately because JavaScript shift counts wrap modulo 32: shifting by 32 behaves like shifting by zero.

Compare the same prefix calculation in TypeScript and Go.

Both versions validate the IPv4 prefix and produce the same network address.

TypeScriptIPv4 prefix mask · explicit representation
ipv4.ts
/** Read an IPv4 address as an unsigned, network-byte-order 32-bit value. */
export function ipv4ToUint32(address: string): number {
	const parts = address.split('.');
	const octets = parts.map(Number);
	if (
		octets.length !== 4 ||
		octets.some((octet, index) => !/^\d+$/.test(parts[index] ?? '') || octet < 0 || octet > 255)
	) {
		throw new RangeError('address must contain four decimal octets from 0 to 255');
	}
	const bytes = new Uint8Array(octets);
	return new DataView(bytes.buffer).getUint32(0, false);
}

/** Return the canonical network address for an IPv4 address and prefix. */
export function networkAddress(address: string, prefixLength: number): string {
	if (!Number.isInteger(prefixLength) || prefixLength < 0 || prefixLength > 32) {
		throw new RangeError('prefix length must be an integer from 0 to 32');
	}

	const ip = ipv4ToUint32(address);
	// Avoid JavaScript's shift-count modulo 32 behavior for the /0 boundary.
	const mask = prefixLength === 0 ? 0 : (0xffff_ffff << (32 - prefixLength)) >>> 0;
	// Number bitwise operators coerce to signed int32; >>> 0 restores the uint32 value.
	const network = (ip & mask) >>> 0;
	const bytes = new ArrayBuffer(4);
	new DataView(bytes).setUint32(0, network, false);
	return Array.from(new Uint8Array(bytes)).join('.');
}

function assertUint32(value: number): void {
	if (!Number.isInteger(value) || value < 0 || value > 0xffff_ffff) {
		throw new RangeError('flag values must be unsigned 32-bit integers');
	}
}

function hasFlag(value: number, flag: number): boolean {
	assertUint32(value);
	assertUint32(flag);
	return (value & flag) !== 0;
}

function setFlag(value: number, flag: number): number {
	assertUint32(value);
	assertUint32(flag);
	return (value | flag) >>> 0;
}

function clearFlag(value: number, flag: number): number {
	assertUint32(value);
	assertUint32(flag);
	return (value & ~flag) >>> 0;
}

function toggleFlag(value: number, flag: number): number {
	assertUint32(value);
	assertUint32(flag);
	return (value ^ flag) >>> 0;
}

const address = '192.168.34.77';
console.log(`${address}/20 → ${networkAddress(address, 20)}`);
console.log(`0x25 has 0x04: ${hasFlag(0x25, 0x04)}`);
console.log(`set 0x08: 0x${setFlag(0x25, 0x08).toString(16)}`);
console.log(`clear 0x04: 0x${clearFlag(0x25, 0x04).toString(16)}`);
console.log(`toggle 0x04: 0x${toggleFlag(0x25, 0x04).toString(16)}`);
GoIPv4 prefix mask · explicit representation
ipv4.go
package main

import (
	"fmt"
	"net/netip"
)

func networkAddress(address string, prefixLength int) (string, error) {
	ip, err := netip.ParseAddr(address)
	if err != nil || !ip.Is4() {
		return "", fmt.Errorf("address must be a valid IPv4 address")
	}
	if prefixLength < 0 || prefixLength > 32 {
		return "", fmt.Errorf("prefix length must be from 0 to 32")
	}

	prefix := netip.PrefixFrom(ip, prefixLength).Masked()
	return prefix.Addr().String(), nil
}

func hasFlag(value, flag uint32) bool {
	return value&flag != 0
}

func setFlag(value, flag uint32) uint32 {
	return value | flag
}

func clearFlag(value, flag uint32) uint32 {
	return value &^ flag
}

func toggleFlag(value, flag uint32) uint32 {
	return value ^ flag
}

func main() {
	address := "192.168.34.77"
	network, err := networkAddress(address, 20)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%s/20 → %s\n", address, network)

	flags := uint32(0x25)
	fmt.Printf("0x25 has 0x04: %t\n", hasFlag(flags, 0x04))
	fmt.Printf("set 0x08: 0x%02x\n", setFlag(flags, 0x08))
	fmt.Printf("clear 0x04: 0x%02x\n", clearFlag(flags, 0x04))
	fmt.Printf("toggle 0x04: 0x%02x\n", toggleFlag(flags, 0x04))
}

Language references checked 2026-10-01. They specify operator behavior; they do not replace a protocol or network configuration contract.

06 / Transfer the idea

The same bit reasoning appears in flags, permissions, and protocol fields.

Practice / Read a packed fieldFind whether the checksum-present flag is set.
Field
0x25 (binary 0010 0101)
Flag definition
Checksum-present is bit 2, represented by 0x04 (0000 0100).
Task
Use AND to decide whether the flag is present; then compute the value after clearing it.
Show a worked answer

0x25 & 0x04 = 0x04, so the flag is set. Clear it with 0x25 & ~0x04, giving 0x21 in a fixed 8-bit flag field. In languages with wider or signed representations, reason about the field width and mask explicitly; an unbounded bitwise complement flips every representable bit, even when the protocol defines only eight.

Rule to carry: name the field width and meaning, show which positions the mask selects, and verify the language’s integer semantics before relying on the result.