Skip to content

Subnet / CIDR Calculator — Ruby source

Compute IPv4 subnet details from a CIDR block - network, broadcast, host range, netmask, wildcard, host count, IP class, and private-range detection. Split a block into smaller subnets, all in your browser.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# subnet-cidr — Ruby port: IPv4 subnet/CIDR math (dotted-quad ⇄ uint32, subnet details, CIDR splitting). Language: Ruby (3.1+, stdlib only). Port of src/lib/subnet-cidr.ts — same logic as this dir's python.py / go.go; invalid input yields nil rather than raising.

Details = Struct.new(:network, :broadcast, :first_host, :last_host, :host_count,
                     :prefix, :ip_class, :is_private)

# The TS /^\d+$/ check — rejects signs, hex, whitespace, and the empty string.
def all_digits?(s) = !s.empty? && s.match?(/\A\d+\z/)

# "a.b.c.d" → uint32. nil for a wrong octet count, junk octets, or an octet > 255.
def ip_to_int(ip)
  parts = ip.split('.', -1)
  return nil unless parts.length == 4
  result = 0
  parts.each do |part|
    return nil unless all_digits?(part)
    octet = part.to_i # decimal on purpose — no octal autodetect
    return nil if octet > 255
    result = result * 256 + octet
  end
  result & 0xFFFFFFFF
end

def int_to_ip(v)
  "#{(v >> 24) & 255}.#{(v >> 16) & 255}.#{(v >> 8) & 255}.#{v & 255}"
end

def mask_from_prefix(p) = p.zero? ? 0 : (0xFFFFFFFF << (32 - p)) & 0xFFFFFFFF

# "ip/prefix" → full block details; nil for an invalid CIDR.
def subnet_details(cidr)
  ip_part, prefix_part = cidr.split('/', 2)
  return nil if prefix_part.nil? || prefix_part.include?('/') || !all_digits?(prefix_part)
  prefix = prefix_part.to_i
  ip_int = ip_to_int(ip_part)
  return nil if prefix > 32 || ip_int.nil?

  mask = mask_from_prefix(prefix)
  wildcard = (~mask) & 0xFFFFFFFF
  network = ip_int & mask
  broadcast = network | wildcard
  # /31 (point-to-point) and /32 (single host) have no host/broadcast split.
  first_host = prefix >= 31 ? network : network + 1
  last_host = prefix >= 31 ? broadcast : broadcast - 1
  host_count = prefix >= 32 ? 1 : prefix == 31 ? 2 : (1 << (32 - prefix)) - 2

  o1 = network >> 24
  o2 = (network >> 16) & 255
  ip_class = o1 < 128 ? 'A' : o1 < 192 ? 'B' : o1 < 224 ? 'C' : '-' # D/E → '-'
  is_private = o1 == 10 || (o1 == 172 && o2.between?(16, 31)) || (o1 == 192 && o2 == 168)
  Details.new(int_to_ip(network), int_to_ip(broadcast), int_to_ip(first_host),
              int_to_ip(last_host), host_count, prefix, ip_class, is_private)
end

# Split into 2^(new_prefix-prefix) children; [] for invalid input or range.
def split_subnet(cidr, new_prefix)
  details = subnet_details(cidr)
  return [] if details.nil? || new_prefix < details.prefix || new_prefix > 32
  network = ip_to_int(details.network) & mask_from_prefix(details.prefix)
  count = 1 << (new_prefix - details.prefix)
  block = 1 << (32 - new_prefix)
  (0...count).map { |i| "#{int_to_ip(network + i * block)}/#{new_prefix}" }
end

if $PROGRAM_NAME == __FILE__
  d = subnet_details('192.168.1.100/24')
  puts "192.168.1.100/24 net=#{d.network} bcast=#{d.broadcast} first=#{d.first_host} " \
       "last=#{d.last_host} hosts=#{d.host_count} class=#{d.ip_class} private=#{d.is_private}"
  split_subnet('192.168.1.0/24', 26).each { |kid| puts "  split /26 → #{kid}" }
end

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →