From 03964d2ff5271a782b968242dbdad764bdd37bd7 Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 2 Jun 2025 20:23:37 +0200 Subject: [PATCH] =?UTF-8?q?Actualizaci=C3=B3n=20de=20la=20clase=20IP4Mask?= =?UTF-8?q?=20y=20Class=5FIP4=20con=20nuevos=20m=C3=A9todos=20para=20conve?= =?UTF-8?q?rsiones,=20an=C3=A1lisis=20de=20red=20y=20utilidades.=20Se=20a?= =?UTF-8?q?=C3=B1aden=20funcionalidades=20para=20manejar=20direcciones=20I?= =?UTF-8?q?P,=20incluyendo=20operaciones=20aritm=C3=A9ticas=20y=20an=C3=A1?= =?UTF-8?q?lisis=20de=20relaciones=20entre=20redes.=20Se=20mejora=20la=20d?= =?UTF-8?q?ocumentaci=C3=B3n=20y=20se=20ajustan=20los=20m=C3=A9todos=20de?= =?UTF-8?q?=20autocompletado.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ip4_type.py | 603 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 590 insertions(+), 13 deletions(-) diff --git a/ip4_type.py b/ip4_type.py index 0abe240..6ada040 100644 --- a/ip4_type.py +++ b/ip4_type.py @@ -3,7 +3,7 @@ Clase híbrida para direcciones IPv4 """ from class_base import ClassBase from sympy_Base import SympyClassBase -from typing import Optional, Union +from typing import Optional, Union, List, Tuple import re import sympy @@ -99,26 +99,187 @@ class IP4Mask(ClassBase): @staticmethod def Helper(input_str): - if re.match(r"^\s*IP4Mask\\b", input_str, re.IGNORECASE): - return 'Máscara: IP4Mask[24], IP4Mask[255.255.255.0]. Funciones: get_prefix_int(), get_netmask_str(), hosts_count()' + if re.match(r"^\s*IP4Mask\b", input_str, re.IGNORECASE): + return 'Máscara: IP4Mask[24], IP4Mask[255.255.255.0]. Funciones: get_prefix_int(), get_mask_str(), hosts_count(), to_hex(), to_binary()' return None @staticmethod def PopupFunctionList(): return [ ("get_prefix_int", "Obtiene prefijo CIDR (ej: 24)"), - ("get_netmask_str", "Obtiene máscara como string (ej: 255.255.255.0)"), + ("get_mask_str", "Obtiene máscara como string (ej: 255.255.255.0)"), + ("to_dotted", "Alias para get_mask_str()"), + ("to_prefix", "Alias para get_prefix_int()"), ("hosts_count", "Número de hosts disponibles"), + ("total_addresses", "Total de direcciones (incluyendo red/broadcast)"), + ("to_decimal", "Convierte a entero decimal"), + ("to_hex", "Convierte a hexadecimal"), + ("to_binary", "Representación binaria"), + ("to_wildcard", "Máscara wildcard (inversa)"), + ("bit_representation", "Representación visual con bits"), + ("octet_breakdown", "Desglose por octetos"), + ("is_more_specific", "¿Es más específica que otra máscara?"), + ("is_subnet_of", "¿Es subred de otra máscara?"), + ("common_supernet", "Máscara común que contiene ambas"), + ("next_smaller", "Siguiente máscara más pequeña"), + ("next_larger", "Siguiente máscara más grande"), + ("split", "Dividir en dos subredes"), + ("is_valid", "¿Es una máscara válida?"), + ("is_host_mask", "¿Es máscara de host (/32)?"), + ("is_network_mask", "¿Es máscara de red (/0)?"), + ("efficiency", "Eficiencia de uso de direcciones"), ("to_sympy", "Convierte a expresión SymPy para álgebra"), ] - def hosts_count(self): - """Número de hosts disponibles""" - return 2**(32 - self._prefix) - 2 if self._prefix < 31 else 0 + # ========== CONVERSIONES Y REPRESENTACIONES ========== + + def to_decimal(self) -> int: + """Convierte a entero decimal""" + return self._mask_int + + def to_hex(self) -> str: + """Convierte a hexadecimal""" + return f"0x{self._mask_int:08X}" + + def to_binary(self) -> str: + """Representación binaria de 32 bits""" + return f"{self._mask_int:032b}" + + def to_dotted(self) -> str: + """Alias para get_mask_str()""" + return self.get_mask_str() + + def to_prefix(self) -> int: + """Alias para get_prefix_int()""" + return self.get_prefix_int() + + def to_wildcard(self) -> str: + """Máscara wildcard (inversa)""" + wildcard_int = self._mask_int ^ 0xFFFFFFFF + return f"{(wildcard_int >> 24) & 0xFF}.{(wildcard_int >> 16) & 0xFF}.{(wildcard_int >> 8) & 0xFF}.{wildcard_int & 0xFF}" + + def bit_representation(self) -> str: + """Representación visual con bits""" + binary = self.to_binary() + return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}".replace('1', '█').replace('0', '.') + + def octet_breakdown(self) -> dict: + """Desglose por octetos""" + return { + "oct1": (self._mask_int >> 24) & 0xFF, + "oct2": (self._mask_int >> 16) & 0xFF, + "oct3": (self._mask_int >> 8) & 0xFF, + "oct4": self._mask_int & 0xFF + } + + # ========== CÁLCULOS DE RED ========== + + def hosts_count(self) -> int: + """Número de hosts disponibles (excluyendo red/broadcast)""" + if self._prefix >= 31: + return 0 + return (2 ** (32 - self._prefix)) - 2 + + def total_addresses(self) -> int: + """Total de direcciones (incluyendo red/broadcast)""" + return 2 ** (32 - self._prefix) + + def subnet_count(self, base_mask: 'IP4Mask') -> int: + """¿Cuántas subredes de este tamaño caben en la máscara base?""" + if not isinstance(base_mask, IP4Mask): + raise TypeError("base_mask must be IP4Mask instance") + if self._prefix < base_mask._prefix: + raise ValueError("Target mask must be more specific than base mask") + return 2 ** (self._prefix - base_mask._prefix) + + def efficiency(self) -> float: + """Eficiencia de uso de direcciones (0-1)""" + if self._prefix >= 31: + return 1.0 if self._prefix == 32 else 0.0 + return self.hosts_count() / self.total_addresses() + + # ========== COMPARACIONES Y ANÁLISIS ========== + + def is_more_specific(self, other_mask: 'IP4Mask') -> bool: + """¿Es más específica (mayor prefijo) que otra máscara?""" + if not isinstance(other_mask, IP4Mask): + raise TypeError("other_mask must be IP4Mask instance") + return self._prefix > other_mask._prefix + + def is_subnet_of(self, other_mask: 'IP4Mask') -> bool: + """¿Es subred (más específica) de otra máscara?""" + return self.is_more_specific(other_mask) + + def common_supernet(self, other_mask: 'IP4Mask') -> 'IP4Mask': + """Máscara común que contiene ambas""" + if not isinstance(other_mask, IP4Mask): + raise TypeError("other_mask must be IP4Mask instance") + common_prefix = min(self._prefix, other_mask._prefix) + return IP4Mask(common_prefix) + + # ========== UTILIDADES DE PROGRAMADOR ========== + + def is_valid(self) -> bool: + """¿Es una máscara válida?""" + return 0 <= self._prefix <= 32 + + def is_host_mask(self) -> bool: + """¿Es máscara de host (/32)?""" + return self._prefix == 32 + + def is_network_mask(self) -> bool: + """¿Es máscara de red (/0)?""" + return self._prefix == 0 + + def next_smaller(self) -> 'IP4Mask': + """Siguiente máscara más pequeña (más específica)""" + if self._prefix >= 32: + raise ValueError("Cannot make /32 more specific") + return IP4Mask(self._prefix + 1) + + def next_larger(self) -> 'IP4Mask': + """Siguiente máscara más grande (menos específica)""" + if self._prefix <= 0: + raise ValueError("Cannot make /0 less specific") + return IP4Mask(self._prefix - 1) + + def split(self) -> Tuple['IP4Mask', 'IP4Mask']: + """Divide en dos subredes del siguiente tamaño""" + if self._prefix >= 32: + raise ValueError("Cannot split /32 mask") + smaller_mask = self.next_smaller() + return (smaller_mask, smaller_mask) def to_sympy(self): """Convierte a SymPy cuando se necesite álgebra""" return sympy.sympify(self._prefix) + + def hosts_expression(self): + """Expresión SymPy para número de hosts""" + return 2**(32 - self._prefix) + + # ========== MÉTODOS ESTÁTICOS ========== + + @staticmethod + def optimal_for_hosts(host_count: int) -> 'IP4Mask': + """¿Qué máscara para N hosts?""" + if host_count <= 0: + return IP4Mask(32) + if host_count == 1: + return IP4Mask(31) + + # Encontrar el menor prefijo que acomode host_count + 2 (red y broadcast) + total_needed = host_count + 2 + prefix = 32 + while 2**(32 - prefix) < total_needed and prefix > 0: + prefix -= 1 + return IP4Mask(prefix) + + @staticmethod + def compare_efficiency(mask_list: List['IP4Mask']) -> List[Tuple['IP4Mask', float]]: + """Eficiencia de uso de cada máscara""" + return [(mask, mask.efficiency()) for mask in mask_list] + class Class_IP4(SympyClassBase): """Clase híbrida para direcciones IPv4""" @@ -229,9 +390,8 @@ class Class_IP4(SympyClassBase): @staticmethod def Helper(input_str): - if re.match(r"^\s*IP4\\b", input_str, re.IGNORECASE): - return ('Ej: IP4[192.168.1.1/24], IP4[10.0.0.1;8], IP4[172.16.0.5;255.255.0.0]\\n' - 'Funciones: NetworkAddress(), BroadcastAddress(), Nodes(), get_netmask_str(), get_prefix_length(), mask()') + if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE): + return ('Ej: IP4[192.168.1.1/24], IP4[10.0.0.1;8]. Funciones: NetworkAddress(), BroadcastAddress(), add_hosts(), next_ip(), mask()') return None @staticmethod @@ -241,10 +401,42 @@ class Class_IP4(SympyClassBase): ("BroadcastAddress", "Dirección de broadcast"), ("Nodes", "Hosts disponibles"), ("mask", "Objeto máscara con sus métodos"), - ("get_netmask_str", "Obtiene la máscara de red (ej: 255.255.255.0)"), - ("get_prefix_length", "Obtiene la longitud del prefijo CIDR (ej: 24)"), + ("add_hosts", "Sumar N hosts a la dirección"), + ("subtract_hosts", "Restar N hosts a la dirección"), + ("next_ip", "Siguiente dirección IP"), + ("previous_ip", "Dirección IP anterior"), + ("first_host", "Primera IP asignable"), + ("last_host", "Última IP asignable"), + ("is_network_address", "¿Es dirección de red?"), + ("is_broadcast_address", "¿Es dirección de broadcast?"), + ("is_host_address", "¿Es dirección de host válida?"), + ("is_first_host", "¿Es la primera IP asignable?"), + ("is_last_host", "¿Es la última IP asignable?"), + ("to_integer", "Convierte a entero"), + ("to_hex", "Convierte a hexadecimal"), + ("to_binary", "Convierte a binario"), + ("to_octets", "Convierte a lista de octetos"), + ("bit_representation", "Representación visual de bits"), + ("in_same_network", "¿Está en la misma red?"), + ("is_subnet_of", "¿Está en esa subred?"), + ("distance_to", "Distancia en direcciones"), + ("hamming_distance", "Distancia de Hamming"), + ("parent_network", "Red padre"), + ("child_networks", "Subredes hijas"), + ("sibling_networks", "Redes hermanas"), + ("is_private", "¿Es IP privada?"), + ("is_loopback", "¿Es loopback?"), + ("is_multicast", "¿Es multicast?"), + ("is_linklocal", "¿Es link-local?"), + ("ip_class", "Clase de IP (A/B/C/D/E)"), + ("reverse_dns_zone", "Zona DNS reversa"), + ("ptr_record", "Registro PTR"), + ("get_netmask_str", "Máscara como string"), + ("get_prefix_length", "Longitud del prefijo CIDR"), ] + # ========== MÉTODOS BÁSICOS EXISTENTES ========== + def get_netmask_str(self) -> Optional[str]: return self._mask_obj.get_mask_str() if self._mask_obj else None @@ -298,4 +490,389 @@ class Class_IP4(SympyClassBase): """Retorna objeto máscara para autocompletado""" if not self._mask_obj: raise ValueError("No mask defined for this IP4 object.") - return self._mask_obj \ No newline at end of file + return self._mask_obj + + # ========== ARITMÉTICA DE DIRECCIONES ========== + + def add_hosts(self, n: int) -> 'Class_IP4': + """Suma N hosts a la dirección""" + new_ip_int = (self._ip_int + n) & 0xFFFFFFFF + new_parts = [ + (new_ip_int >> 24) & 0xFF, + (new_ip_int >> 16) & 0xFF, + (new_ip_int >> 8) & 0xFF, + new_ip_int & 0xFF + ] + new_ip_str = '.'.join(str(x) for x in new_parts) + return Class_IP4(new_ip_str, self._mask_obj) + + def subtract_hosts(self, n: int) -> 'Class_IP4': + """Resta N hosts a la dirección""" + return self.add_hosts(-n) + + def next_ip(self) -> 'Class_IP4': + """Siguiente dirección IP""" + return self.add_hosts(1) + + def previous_ip(self) -> 'Class_IP4': + """Dirección IP anterior""" + return self.add_hosts(-1) + + def increment_octet(self, octet_num: int, value: int) -> 'Class_IP4': + """Incrementa un octeto específico (1-4)""" + if not 1 <= octet_num <= 4: + raise ValueError("octet_num must be between 1 and 4") + + octets = self.to_octets() + octets[octet_num - 1] = (octets[octet_num - 1] + value) % 256 + new_ip_str = '.'.join(str(x) for x in octets) + return Class_IP4(new_ip_str, self._mask_obj) + + def next_network(self) -> 'Class_IP4': + """Próxima red del mismo tamaño""" + if not self._mask_obj: + raise ValueError("No mask defined for network calculation") + + network = self.NetworkAddress() + network_size = 2 ** (32 - self._mask_obj.get_prefix_int()) + next_net = network.add_hosts(network_size) + return next_net.NetworkAddress() + + def previous_network(self) -> 'Class_IP4': + """Red anterior del mismo tamaño""" + if not self._mask_obj: + raise ValueError("No mask defined for network calculation") + + network = self.NetworkAddress() + network_size = 2 ** (32 - self._mask_obj.get_prefix_int()) + prev_net = network.subtract_hosts(network_size) + return prev_net.NetworkAddress() + + # ========== ANÁLISIS DE RED ========== + + def is_network_address(self) -> bool: + """¿Es la dirección de red?""" + if not self._mask_obj: + return False + return self._ip_int == self.NetworkAddress()._ip_int + + def is_broadcast_address(self) -> bool: + """¿Es la dirección de broadcast?""" + if not self._mask_obj: + return False + return self._ip_int == self.BroadcastAddress()._ip_int + + def is_host_address(self) -> bool: + """¿Es una dirección de host válida?""" + if not self._mask_obj: + return True # Sin máscara, asumimos que es host + return not (self.is_network_address() or self.is_broadcast_address()) + + def is_first_host(self) -> bool: + """¿Es la primera IP asignable?""" + if not self._mask_obj: + return False + return self._ip_int == self.first_host()._ip_int + + def is_last_host(self) -> bool: + """¿Es la última IP asignable?""" + if not self._mask_obj: + return False + return self._ip_int == self.last_host()._ip_int + + def first_host(self) -> 'Class_IP4': + """Primera IP asignable""" + if not self._mask_obj: + raise ValueError("No mask defined") + if self._mask_obj.get_prefix_int() >= 31: + return self # En /31 y /32, la IP es el host + return self.NetworkAddress().add_hosts(1) + + def last_host(self) -> 'Class_IP4': + """Última IP asignable""" + if not self._mask_obj: + raise ValueError("No mask defined") + if self._mask_obj.get_prefix_int() >= 31: + return self # En /31 y /32, la IP es el host + return self.BroadcastAddress().subtract_hosts(1) + + def network_range(self) -> Tuple['Class_IP4', 'Class_IP4']: + """Rango de red (primera, última) como tupla""" + return (self.first_host(), self.last_host()) + + def host_position(self) -> int: + """¿En qué posición está dentro de la red?""" + if not self._mask_obj: + raise ValueError("No mask defined") + network = self.NetworkAddress() + return self._ip_int - network._ip_int + + # ========== CONVERSIONES AVANZADAS ========== + + def to_integer(self) -> int: + """Convierte a entero""" + return self._ip_int + + def to_hex(self) -> str: + """Convierte a hexadecimal""" + return f"0x{self._ip_int:08X}" + + def to_binary(self) -> str: + """Convierte a binario""" + return f"{self._ip_int:032b}" + + def to_octets(self) -> List[int]: + """Convierte a lista de octetos""" + return [ + (self._ip_int >> 24) & 0xFF, + (self._ip_int >> 16) & 0xFF, + (self._ip_int >> 8) & 0xFF, + self._ip_int & 0xFF + ] + + def bit_representation(self) -> str: + """Representación visual de bits""" + binary = self.to_binary() + return f"{binary[:8]}.{binary[8:16]}.{binary[16:24]}.{binary[24:32]}".replace('1', '█').replace('0', '.') + + def hamming_distance(self, other_ip: 'Class_IP4') -> int: + """Distancia de Hamming (diferencia en bits)""" + if not isinstance(other_ip, Class_IP4): + raise TypeError("other_ip must be Class_IP4 instance") + xor_result = self._ip_int ^ other_ip._ip_int + return bin(xor_result).count('1') + + # ========== ANÁLISIS DE RELACIONES ========== + + def in_same_network(self, other_ip: 'Class_IP4') -> bool: + """¿Están en la misma red?""" + if not isinstance(other_ip, Class_IP4): + raise TypeError("other_ip must be Class_IP4 instance") + if not self._mask_obj: + raise ValueError("No mask defined for network comparison") + + mask_int = IP4Mask._prefix_to_mask_int(self._mask_obj.get_prefix_int()) + return (self._ip_int & mask_int) == (other_ip._ip_int & mask_int) + + def is_subnet_of(self, network_ip: 'Class_IP4') -> bool: + """¿Esta IP está en esa red?""" + if not isinstance(network_ip, Class_IP4): + raise TypeError("network_ip must be Class_IP4 instance") + if not network_ip._mask_obj: + raise ValueError("network_ip must have a mask defined") + + mask_int = IP4Mask._prefix_to_mask_int(network_ip._mask_obj.get_prefix_int()) + network_addr = network_ip._ip_int & mask_int + this_network = self._ip_int & mask_int + return network_addr == this_network + + def distance_to(self, other_ip: 'Class_IP4') -> int: + """Distancia en direcciones""" + if not isinstance(other_ip, Class_IP4): + raise TypeError("other_ip must be Class_IP4 instance") + return abs(self._ip_int - other_ip._ip_int) + + def parent_network(self, larger_mask: IP4Mask) -> 'Class_IP4': + """Red padre con máscara mayor""" + if not isinstance(larger_mask, IP4Mask): + raise TypeError("larger_mask must be IP4Mask instance") + if self._mask_obj and larger_mask.get_prefix_int() >= self._mask_obj.get_prefix_int(): + raise ValueError("larger_mask must be less specific") + + mask_int = IP4Mask._prefix_to_mask_int(larger_mask.get_prefix_int()) + parent_int = self._ip_int & mask_int + + parts = [ + (parent_int >> 24) & 0xFF, + (parent_int >> 16) & 0xFF, + (parent_int >> 8) & 0xFF, + parent_int & 0xFF + ] + parent_str = '.'.join(str(x) for x in parts) + return Class_IP4(parent_str, larger_mask) + + def child_networks(self, smaller_mask: IP4Mask) -> List['Class_IP4']: + """Subredes hijas""" + if not isinstance(smaller_mask, IP4Mask): + raise TypeError("smaller_mask must be IP4Mask instance") + if not self._mask_obj: + raise ValueError("IP must have a mask defined") + if smaller_mask.get_prefix_int() <= self._mask_obj.get_prefix_int(): + raise ValueError("smaller_mask must be more specific") + + network = self.NetworkAddress() + subnet_count = 2 ** (smaller_mask.get_prefix_int() - self._mask_obj.get_prefix_int()) + subnet_size = 2 ** (32 - smaller_mask.get_prefix_int()) + + subnets = [] + current_ip = network._ip_int + for i in range(subnet_count): + parts = [ + (current_ip >> 24) & 0xFF, + (current_ip >> 16) & 0xFF, + (current_ip >> 8) & 0xFF, + current_ip & 0xFF + ] + subnet_str = '.'.join(str(x) for x in parts) + subnets.append(Class_IP4(subnet_str, smaller_mask)) + current_ip += subnet_size + + return subnets + + def sibling_networks(self) -> List['Class_IP4']: + """Redes hermanas del mismo tamaño""" + if not self._mask_obj: + raise ValueError("IP must have a mask defined") + if self._mask_obj.get_prefix_int() == 0: + return [self] # /0 no tiene hermanas + + parent_mask = IP4Mask(self._mask_obj.get_prefix_int() - 1) + parent = self.parent_network(parent_mask) + return parent.child_networks(self._mask_obj) + + # ========== CLASIFICACIÓN DE IPS ========== + + def is_private(self) -> bool: + """¿Es IP privada? (RFC 1918)""" + # 10.0.0.0/8 + if (self._ip_int & 0xFF000000) == 0x0A000000: + return True + # 172.16.0.0/12 + if (self._ip_int & 0xFFF00000) == 0xAC100000: + return True + # 192.168.0.0/16 + if (self._ip_int & 0xFFFF0000) == 0xC0A80000: + return True + return False + + def is_loopback(self) -> bool: + """¿Es loopback? (127.0.0.0/8)""" + return (self._ip_int & 0xFF000000) == 0x7F000000 + + def is_multicast(self) -> bool: + """¿Es multicast? (224.0.0.0/4)""" + return (self._ip_int & 0xF0000000) == 0xE0000000 + + def is_linklocal(self) -> bool: + """¿Es link-local? (169.254.0.0/16)""" + return (self._ip_int & 0xFFFF0000) == 0xA9FE0000 + + def ip_class(self) -> str: + """Clase de IP (A/B/C/D/E) - legacy pero útil""" + first_octet = (self._ip_int >> 24) & 0xFF + if first_octet < 128: + return "A" + elif first_octet < 192: + return "B" + elif first_octet < 224: + return "C" + elif first_octet < 240: + return "D" # Multicast + else: + return "E" # Experimental + + # ========== UTILIDADES DNS ========== + + def reverse_dns_zone(self) -> str: + """Zona DNS reversa""" + if not self._mask_obj: + raise ValueError("No mask defined for reverse DNS zone") + + prefix = self._mask_obj.get_prefix_int() + octets = self.to_octets() + + if prefix >= 24: + return f"{octets[2]}.{octets[1]}.{octets[0]}.in-addr.arpa" + elif prefix >= 16: + return f"{octets[1]}.{octets[0]}.in-addr.arpa" + elif prefix >= 8: + return f"{octets[0]}.in-addr.arpa" + else: + return "in-addr.arpa" + + def ptr_record(self) -> str: + """Registro PTR""" + octets = self.to_octets() + return f"{octets[3]}.{octets[2]}.{octets[1]}.{octets[0]}.in-addr.arpa" + + def generate_host_list(self, count: int) -> List['Class_IP4']: + """Genera lista de IPs consecutivas""" + if count <= 0: + return [] + + result = [] + current = self + for i in range(count): + result.append(current) + current = current.next_ip() + return result + + # ========== MÉTODOS ESTÁTICOS ========== + + @staticmethod + def find_common_network(ip_list: List['Class_IP4']) -> 'Class_IP4': + """Menor red que contiene todas las IPs""" + if not ip_list: + raise ValueError("ip_list cannot be empty") + + if len(ip_list) == 1: + return ip_list[0] + + # Encontrar el prefijo común más largo + min_ip = min(ip._ip_int for ip in ip_list) + max_ip = max(ip._ip_int for ip in ip_list) + + # XOR para encontrar bits diferentes + xor_result = min_ip ^ max_ip + + # Contar bits comunes desde la izquierda + common_bits = 0 + for i in range(32): + if (xor_result >> (31 - i)) & 1: + break + common_bits += 1 + + # Crear la red común + mask = IP4Mask(common_bits) + network_int = min_ip & IP4Mask._prefix_to_mask_int(common_bits) + + parts = [ + (network_int >> 24) & 0xFF, + (network_int >> 16) & 0xFF, + (network_int >> 8) & 0xFF, + network_int & 0xFF + ] + network_str = '.'.join(str(x) for x in parts) + return Class_IP4(network_str, mask) + + @staticmethod + def detect_overlaps(network_list: List['Class_IP4']) -> List[Tuple['Class_IP4', 'Class_IP4']]: + """Encontrar redes solapadas""" + overlaps = [] + for i, net1 in enumerate(network_list): + for net2 in network_list[i+1:]: + if net1.is_subnet_of(net2) or net2.is_subnet_of(net1): + overlaps.append((net1, net2)) + return overlaps + + @staticmethod + def analyze_distribution(ip_list: List['Class_IP4']) -> dict: + """Análisis estadístico de distribución""" + if not ip_list: + return {} + + classes = {"A": 0, "B": 0, "C": 0, "D": 0, "E": 0} + private_count = 0 + + for ip in ip_list: + classes[ip.ip_class()] += 1 + if ip.is_private(): + private_count += 1 + + return { + "total": len(ip_list), + "classes": classes, + "private_count": private_count, + "public_count": len(ip_list) - private_count, + "common_network": Class_IP4.find_common_network(ip_list) + } \ No newline at end of file