Jump to content

Canonicalize CIDR

From Rosetta Code
Task
Canonicalize CIDR
You are encouraged to solve this task according to the task description, using any language you may know.
Task

Implement a function or program that, given a range of IPv4 addresses in CIDR notation (dotted-decimal/network-bits), will return/output the same range in canonical form.

That is, the IP address portion of the output CIDR block must not contain any set (1) bits in the host part of the address.


Example

Given   87.70.141.1/22,   your code should output   87.70.140.0/22


Explanation

An Internet Protocol version 4 address is a 32-bit value, conventionally represented as a number in base 256 using dotted-decimal notation, where each base-256 digit is given in decimal and the digits are separated by periods. Logically, this 32-bit value represents two components: the leftmost (most-significant) bits determine the network portion of the address, while the rightmost (least-significant) bits determine the host portion. Classless Internet Domain Routing block notation indicates where the boundary between these two components is for a given address by adding a slash followed by the number of bits in the network portion.

In general, CIDR blocks stand in for the entire set of IP addresses sharing the same network component, so it's common to see access control lists that specify individual IP addresses using /32 to indicate that only the one address is included. Software accepting this notation as input often expects it to be entered in canonical form, in which the host bits are all zeroes. But network admins sometimes skip this step and just enter the address of a specific host on the subnet with the network size, resulting in a non-canonical entry.

The example address, 87.70.141.1/22, represents binary 0101011101000110100011 / 0100000001, with the / indicating the network/host division. To canonicalize, clear all the bits to the right of the / and convert back to dotted decimal: 0101011101000110100011 / 0000000000 → 87.70.140.0.


More examples for testing
        36.18.154.103/12    →  36.16.0.0/12
        62.62.197.11/29     →  62.62.197.8/29
        67.137.119.181/4    →  64.0.0.0/4
        161.214.74.21/24    →  161.214.74.0/24
        184.232.176.184/18  →  184.232.128.0/18



11l

Translation of: C
F cidr_parse(str)
   V (addr_str, m_str) = str.split(‘/’)
   V (a, b, c, d) = addr_str.split(‘.’).map(Int)
   V m = Int(m_str)
   I m < 1 | m > 32
     | a < 0 | a > 255
     | b < 0 | b > 255
     | c < 0 | c > 255
     | d < 0 | d > 255
      R (0, 0)
   V mask = ~((1 << (32 - m)) - 1)
   V address = (a << 24) + (b << 16) + (c << 8) + d
   address [&]= mask
   R (address, m)

F cidr_format(=address, mask_length)
   V d = address [&] F'F
   address >>= 8
   V c = address [&] F'F
   address >>= 8
   V b = address [&] F'F
   address >>= 8
   V a = address [&] F'F
   R a‘.’b‘.’c‘.’d‘/’mask_length

L(test) [‘87.70.141.1/22’,
         ‘36.18.154.103/12’,
         ‘62.62.197.11/29’,
         ‘67.137.119.181/4’,
         ‘161.214.74.21/24’,
         ‘184.232.176.184/18’]
   V (address, mask_length) = cidr_parse(test)
   print(‘#<18 -> #.’.format(test, cidr_format(address, mask_length)))
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

ALGOL 68

BEGIN # show IPv4 addresses in CIDR notation in canonical form #
    # mode to hold an IPv4 address in CIDR notation #
    MODE CIDR = STRUCT( BITS   address
                      , INT    network bits
                      , BOOL   valid
                      , STRING error
                      );
    # returns a CIDR parsed from address #
    OP TOCIDR = ( STRING address text )CIDR:
         BEGIN
            STRING addr         = "." + address text + "$";
            STRING error       := "";
            BITS   address     := 16r0;
            INT    bits count  := 0;
            INT    dot count   := 0;
            INT    slash count := 0;
            BOOL   valid       := TRUE;
            INT    s pos       := LWB addr;
            INT    s max        = UPB addr;
            WHILE s pos < s max AND valid DO
                IF addr[ s pos ] = "." THEN
                    # must have an octet next #
                    dot count +:= 1;
                    INT octet  := 0;
                    INT digits := 0;
                    WHILE CHAR c = addr[ s pos +:= 1 ];
                          c >= "0" AND c <= "9"
                    DO
                        octet  *:= 10 +:= ( ABS c - ABS "0" );
                        digits +:= 1
                    OD;
                    address := ( address SHL 8 ) OR BIN ( octet MOD 256 );
                    valid := valid AND digits > 0 AND digits < 4 AND octet < 256;
                    IF NOT valid THEN error := "too many digits/octet to large" FI
                ELIF addr[ s pos ] = "/" THEN
                    # must have the network mask length next #
                    slash count +:= 1;
                    WHILE CHAR c = addr[ s pos +:= 1 ];
                          c >= "0" AND c <= "9"
                    DO
                        bits count  *:= 10 +:= ( ABS c - ABS "0" )
                    OD;
                    # should be "$" ( end of string marker ) next #
                    valid := valid AND addr[ s pos ] = "$";
                    IF NOT valid THEN error := "bit length not followed by end-of-string" FI
                ELIF addr[ s pos ] = "$" THEN
                    # end of address marker - must be the final character #
                    valid := valid AND s pos = s max;
                    IF NOT valid THEN error := "Invalid character: ""$""" FI
                ELSE
                    # invalid character #
                    valid := FALSE;
                    error := "Invalid character: """ + addr[ s pos ] + """"
                FI
            OD;
            IF valid THEN
                # address is OK so far - check it had four octets and one mask length #
                valid := dot count   =  4 # note a leading "." was added for parsing #
                     AND slash count =  1
                     AND bits count  >  0
                     AND bits count  < 33;
                IF NOT valid THEN error := "too many dots, slashes or bits" FI
            FI;
            CIDR( address, bits count, valid, error )
         END # TOCIDR # ;
    # returns address in canonical form #
    OP CANONICALISE = ( CIDR address )CIDR:
         IF NOT valid OF address THEN
            # invalid address #
            address
         ELSE
            # valid address - retain the top most bits #
            CIDR( address OF address AND (   16rffffffff
                                         SHL ( 32 - network bits OF address )
                                         )
                , network bits OF address
                , TRUE
                , ""
                )
         FI # CANONICALISE # ;
    # returns a readable form of address #
    OP TOSTRING = ( CIDR address )STRING:
         BEGIN
             [ 1 : 4 ]INT octet;
             BITS addr := address OF address;
             FOR o pos FROM UPB octet BY -1 TO LWB octet DO
                 octet[ o pos ] := ABS ( addr AND 16rff );
                 addr           := addr SHR 8
             OD;
             STRING result := whole( octet[ LWB octet ], 0 );
             FOR o pos FROM 1 + LWB octet TO UPB octet DO
                 result +:= "." + whole( octet[ o pos ], 0 )
             OD;
             result + "/" + whole( network bits OF address, 0 )
         END # TOSTRING # ;
    # task examples :          input                 expected result    #
    [,]STRING test cases = ( ( "87.70.141.1/22",     "87.70.140.0/22"   )
                           , ( "36.18.154.103/12",   "36.16.0.0/12"     )
                           , ( "62.62.197.11/29",    "62.62.197.8/29"   )
                           , ( "67.137.119.181/4",   "64.0.0.0/4"       )
                           , ( "161.214.74.21/24",   "161.214.74.0/24"  )
                           , ( "184.232.176.184/18", "184.232.128.0/18" )
                           );
    FOR t pos FROM 1 LWB test cases TO 1 UPB test cases DO
        STRING addr  = test cases[ t pos, 1 ];
        CIDR   canon = CANONICALISE TOCIDR addr;
        IF NOT valid OF canon THEN
            print( ( "Invalid address: """, addr, """: ", error OF canon, newline ) )
        ELSE
            STRING actual   = TOSTRING canon;
            STRING expected = test cases[ t pos, 2 ];
            print( ( addr
                   , " -> "
                   , actual
                   , IF expected = actual THEN "" ELSE " ** EXPECTED: """ + expected + """" FI
                   , newline
                   )
                 )
        FI
    OD 
END
Output:
87.70.141.1/22 -> 87.70.140.0/22
36.18.154.103/12 -> 36.16.0.0/12
62.62.197.11/29 -> 62.62.197.8/29
67.137.119.181/4 -> 64.0.0.0/4
161.214.74.21/24 -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

APL

Works with: Dyalog APL
 canonicalize{
    nums(2⎕VFI)¨(~'./')
    ip len(4nums)(5nums)
    ip(32/2)256⊥⊃¨ip
    ipip32len1
    ip(4/256)2ip
    (1↓∊'.',¨¨ip),'/',⍕len
 }
Output:
      canonicalize '87.70.141.1/22'
87.70.140.0/22

BASIC

Commodore BASIC

100 REM THE BINARY OPS ONLY WORK ON SIGNED 16-BIT NUMBERS
110 REM SO WE STORE THE IP ADDRESS AS AN ARRAY OF FOUR OCTETS
120 DIM IP(3)
130 REM READ DEMO DATA
140 READ CI$
150 IF CI$="" THEN END
160 REM FIND /
170 SL=0
180 FOR I=LEN(CI$) TO 1 STEP -1
190 : IF MID$(CI$,I,1)="/" THEN SL=I:I=1
200 NEXT I
210 IF SL=0 THEN PRINT "INVALID CIDR STRING: '"CI$"'":GOTO 140
220 NW=VAL(MID$(CI$,SL+1))
230 IF NW < 1 OR NW > 32 THEN PRINT "INVALID NETWORK WIDTH:"NW:GOTO 140
240 REM PARSE OCTETS INTO IP ARRAY
250 BY=0:N=0
260 FOR I=1 TO SL-1
270 : C$=MID$(CI$,I,1):IF C$<>"." THEN 300
280 : IP(N)=BY:N=N+1:BY=0:IF IP(N-1)<256 THEN 310
290 : PRINT "INVALID OCTET VALUE:"IP(N-1):GOTO 140
300 : C=VAL(C$):IF C OR (C$="0") THEN BY=BY*10+C
310 NEXT I
320 IP(N)=BY:N=N+1:IF IP(N-1)>255 THEN 290
330 REM NUMBER OF COMPLETE OCTETS IN NETWORK PART
340 NB=INT(NW/8)
350 REM NUMBER OF NETWORK BITS IN PARTIAL OCTET
360 XB=NW AND 7
370 REM ZERO OUT HOST BITS IN PARTIAL OCTET
380 IP(NB) = IP(NB) AND (255 - 2^(8-XB) + 1)
390 REM AND SET ANY ALL-HOST OCTETS TO 0
400 IF NB<3 THEN FOR I=NB+1 TO 3:IP(I)=0 :NEXT I
410 REM PRINT OUT THE RESULT
420 PRINT MID$(STR$(IP(0)),2);
430 FOR I=1 TO 3: PRINT "."MID$(STR$(IP( I)),2);:NEXT I
440 PRINT MID$(CI$,SL)
450 REM AND GO BACK FOR NEXT INPUT
460 GOTO 140
500 DATA 87.70.141.1/22,   36.18.154.103/12, 62.62.197.11/29
510 DATA 67.137.119.181/4, 161.214.74.21/24, 184.232.176.184/18
520 REM SOME INVALID INPUTS
530 DATA 127.0.0.1, 123.45.67.89/0, 98.76.54.32/100, 123.456.789.0/12
540 DATA
Output:
READY.

RUN 87.70.140.0/22 36.16.0.0/12 62.62.197.8/29 64.0.0.0/4 161.214.74.0/24 184.232.128.0/18 INVALID CIDR STRING: '127.0.0.1' INVALID NETWORK WIDTH: 0 INVALID NETWORK WIDTH: 100 INVALID OCTET VALUE: 456

READY.

FreeBASIC

Const MAX_OCTET = 255
Const MIN_NETWORK = 1
Const MAX_NETWORK = 32
Const OCTET_BITS = 8

Type IPAddress
    octets(3) As Integer
End Type

Function isDigit(Byval ch As String) As Boolean
	Return (Asc(ch) >= Asc("0")) And (Asc(ch) <= Asc("9"))
End Function

Function ParseIPAddress(cidr As String, Byref networkWidth As Integer) As IPAddress
    Dim As IPAddress ip
    Dim As Integer slashPos = Instr(cidr, "/")
    Dim As String ipPart = Left(cidr, slashPos - 1)
    
    networkWidth = Val(Mid(cidr, slashPos + 1))
    
    Dim As Integer i, currentOctet = 0, octetValue = 0
    For i = 1 To Len(ipPart)
        If Mid(ipPart, i, 1) = "." Then
            ip.octets(currentOctet) = octetValue
            currentOctet += 1
            octetValue = 0
        Elseif IsDigit(Mid(ipPart, i, 1)) Then
            octetValue = octetValue * 10 + Val(Mid(ipPart, i, 1))
        End If
    Next
    ip.octets(currentOctet) = octetValue
    
    Return ip
End Function

Function ApplyNetworkMask(ip As IPAddress, networkWidth As Integer) As IPAddress
    Dim As IPAddress result = ip
    Dim As Integer completeOctets = networkWidth \ OCTET_BITS
    Dim As Integer remainingBits = networkWidth And 7
    
    ' Apply mask to partial octet
    If completeOctets < 4 Then
        result.octets(completeOctets) And= (MAX_OCTET - 2^(OCTET_BITS-remainingBits) + 1)
        ' Zero remaining octets
        For i As Integer = completeOctets + 1 To 3
            result.octets(i) = 0
        Next
    End If
    
    Return result
End Function

Function ValidateInput(ip As IPAddress, networkWidth As Integer, cidr As String) As Boolean
    If Instr(cidr, "/") = 0 Then 
        Print "INVALID CIDR STRING: '"; cidr; "'"
        Return False
    End If
    
    If networkWidth < MIN_NETWORK Or networkWidth > MAX_NETWORK Then
        Print "INVALID NETWORK WIDTH:"; networkWidth
        Return False
    End If
    
    For i As Integer = 0 To 3
        If ip.octets(i) > MAX_OCTET Then
            Print "INVALID OCTET VALUE:"; ip.octets(i)
            Return False
        End If
    Next
    
    Return True
End Function

Sub ProcessCIDR(cidr As String)
    Dim As Integer networkWidth
    Dim As IPAddress ip = ParseIPAddress(cidr, networkWidth)
    
    If Not ValidateInput(ip, networkWidth, cidr) Then Exit Sub
    
    ip = ApplyNetworkMask(ip, networkWidth)
    
    Dim As String result = Str(ip.octets(0))
    For i As Integer = 1 To 3
        result &= "." & Str(ip.octets(i))
    Next
    Print result & "/" & networkWidth
End Sub

' Main program
Dim As String cidr(9) = { _
"87.70.141.1/22", "36.18.154.103/12", "62.62.197.11/29", _
"67.137.119.181/4", "161.214.74.21/24", "184.232.176.184/18", _
"127.0.0.1", "123.45.67.89/0", "98.76.54.32/100", "123.456.789.0/12" }

For i As Integer = 0 To 9
    ProcessCIDR(cidr(i))
Next

Sleep
Output:
87.70.140.0/22
36.16.0.0/12
62.62.197.8/29
64.0.0.0/4
161.214.74.0/24
184.232.128.0/18
INVALID CIDR STRING: '127.0.0.1'
INVALID NETWORK WIDTH: 0
INVALID NETWORK WIDTH: 100
INVALID OCTET VALUE: 456

QBasic

Translation of: Commodore BASIC
Works with: QBasic version 1.1
Works with: QuickBasic version 4.5
CLS
DIM IP(3)
DO
    REM Read DEMO Data
140 READ CI$
    IF CI$ = "" THEN EXIT DO 'Sleep: End
    REM FIND /
    SL = 0
    FOR I = LEN(CI$) TO 1 STEP -1
        IF MID$(CI$, I, 1) = "/" THEN SL = I: I = 1
    NEXT I
    IF SL = 0 THEN PRINT "INVALID CIDR STRING: '"; CI$; "'": GOTO 140
    NW = VAL(MID$(CI$, SL + 1))
    IF NW < 1 OR NW > 32 THEN PRINT "INVALID NETWORK WIDTH:"; NW: GOTO 140
    REM PARSE OCTETS INTO IP ARRAY
    BY = 0: N = 0
    FOR I = 1 TO SL - 1
        C$ = MID$(CI$, I, 1): IF C$ <> "." THEN 300
        IP(N) = BY: N = N + 1: BY = 0: IF IP(N - 1) < 256 THEN 310
290   PRINT "INVALID OCTET VALUE:"; IP(N - 1): GOTO 140
300   C = VAL(C$): IF C OR (C$ = "0") THEN BY = BY * 10 + C
310         '
    NEXT I
    IP(N) = BY: N = N + 1: IF IP(N - 1) > 255 THEN 290
    REM NUMBER OF COMPLETE OCTETS IN NETWORK PART
    NB = INT(NW / 8)
    REM NUMBER OF NETWORK BITS IN PARTIAL OCTET
    XB = NW AND 7
    REM ZERO Out HOST BITS IN PARTIAL OCTET
    IP(NB) = IP(NB) AND (255 - 2 ^ (8 - XB) + 1)
    REM And SET Any ALL-HOST OCTETS To 0
    IF NB < 3 THEN FOR I = NB + 1 TO 3: IP(I) = 0: NEXT I
    REM Print Out THE RESULT
    PRINT MID$(STR$(IP(0)), 2);
    FOR I = 1 TO 3
        PRINT "."; MID$(STR$(IP(I)), 2);
    NEXT I
    PRINT MID$(CI$, SL)
LOOP
DATA 87.70.141.1/22,   36.18.154.103/12, 62.62.197.11/29
DATA 67.137.119.181/4, 161.214.74.21/24, 184.232.176.184/18
REM SOME INVALID INPUTS
DATA 127.0.0.1, 123.45.67.89/0, 98.76.54.32/100, 123.456.789.0/12
DATA
Output:
Similar to Commodore BASIC entry.

uBasic/4tH

If Try (_CanonicalizeCIDR ("36.18.154.103/12")) Then Print "Illegal IP"
If Try (_CanonicalizeCIDR ("62.62.197.11/29")) Then Print "Illegal IP"
If Try (_CanonicalizeCIDR ("67.137.119.181/4")) Then Print "Illegal IP"
If Try (_CanonicalizeCIDR ("161.214.74.0/24")) Then Print "Illegal IP"
If Try (_CanonicalizeCIDR ("184.232.176.184/18")) Then Print "Illegal IP"
End

_CanonicalizeCIDR                      ' do the whole shebang
  Param (1)                            ' IP string
  Local (3)

  b@ = FUNC(_GetIP  (a@))              ' get IP address
  c@ = FUNC(_GetMask(a@))              ' get the mask
  d@ = FUNC(_Canonicalize (b@, c@))    ' canonicalize IP address
                                       ' now print the report
  Print Show(a@); Tab(20); " -> "; FUNC(_IP(d@, 24)); ".";
  Print FUNC(_IP(d@, 16)); "."; FUNC(_IP(d@,  8)); "."; AND(d@, 255); "/"; c@
Return
  
_IP Param (2) : Return (AND(255, SHL(a@, -b@)))
_Canonicalize Param (2) : Return (AND(a@, FUNC(_MakeMask (b@))))
_GetMask Param (1) : Return (Val(Chop(a@, o)))
_MakeMask Param (1) : Return (NOT(SHL(1, 32 - a@) - 1))

_GetIP                                 ' get the IP address
  Param (1)                            ' IP string
  Local (1)                            ' IP number

  o = 0
  b@ = SHL(FUNC(_Parse (a@, Ord("."))), 24)
  b@ = OR(b@, SHL(FUNC(_Parse (a@, Ord("."))), 16))
  b@ = OR(b@, SHL(FUNC(_Parse (a@, Ord("."))), 8))
  b@ = OR(b@, FUNC(_Parse (a@, Ord("/"))))
Return (b@)

_Parse                                 ' parse a token
  Param (2)                            ' string, delimiter
  Local (1)                            ' token

  c@ = Dup ("")                        ' start with an empty token

  For o = o to Len (a@) - 1 Until Peek (a@, o) = b@
    c@ = Join (c@, Char (Peek (a@, o)))
  Next

  o = o + 1
Return (Val(c@))
Output:
36.18.154.103/12     -> 36.16.0.0/12
62.62.197.11/29      -> 62.62.197.8/29
67.137.119.181/4     -> 64.0.0.0/4
161.214.74.0/24      -> 161.214.74.0/24
184.232.176.184/18   -> 184.232.128.0/18

0 OK, 0:388

C

This solution uses only the standard library. On POSIX platforms one can use the functions inet_pton/inet_ntop to parse/format IPv4 addresses.

#include <stdbool.h>
#include <stdio.h>
#include <stdint.h>

typedef struct cidr_tag {
    uint32_t address;
    unsigned int mask_length;
} cidr_t;

// Convert a string in CIDR format to an IPv4 address and netmask,
// if possible. Also performs CIDR canonicalization.
bool cidr_parse(const char* str, cidr_t* cidr) {
    int a, b, c, d, m;
    if (sscanf(str, "%d.%d.%d.%d/%d", &a, &b, &c, &d, &m) != 5)
        return false;
    if (m < 1 || m > 32
        || a < 0 || a > UINT8_MAX
        || b < 0 || b > UINT8_MAX
        || c < 0 || c > UINT8_MAX
        || d < 0 || d > UINT8_MAX)
        return false;
    uint32_t mask = ~((1 << (32 - m)) - 1);
    uint32_t address = (a << 24) + (b << 16) + (c << 8) + d;
    address &= mask;
    cidr->address = address;
    cidr->mask_length = m;
    return true;
}

// Write a string in CIDR notation into the supplied buffer.
void cidr_format(const cidr_t* cidr, char* str, size_t size) {
    uint32_t address = cidr->address;
    unsigned int d = address & UINT8_MAX;
    address >>= 8;
    unsigned int c = address & UINT8_MAX;
    address >>= 8;
    unsigned int b = address & UINT8_MAX;
    address >>= 8;
    unsigned int a = address & UINT8_MAX;
    snprintf(str, size, "%u.%u.%u.%u/%u", a, b, c, d,
             cidr->mask_length);
}

int main(int argc, char** argv) {
    const char* tests[] = {
        "87.70.141.1/22",
        "36.18.154.103/12",
        "62.62.197.11/29",
        "67.137.119.181/4",
        "161.214.74.21/24",
        "184.232.176.184/18"
    };
    for (int i = 0; i < sizeof(tests)/sizeof(tests[0]); ++i) {
        cidr_t cidr;
        if (cidr_parse(tests[i], &cidr)) {
            char out[32];
            cidr_format(&cidr, out, sizeof(out));
            printf("%-18s -> %s\n", tests[i], out);
        } else {
            fprintf(stderr, "%s: invalid CIDR\n", tests[i]);
        }
    }
    return 0;
}
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

C#

using System;
using System.Net;
using System.Linq;
                    
public class Program
{
    public static void Main()
    {
        string[] tests = {
            "87.70.141.1/22",
            "36.18.154.103/12",
            "62.62.197.11/29",
            "67.137.119.181/4",
            "161.214.74.21/24",
            "184.232.176.184/18"
        };
        
        foreach (string t in tests) Console.WriteLine($"{t}   =>   {Canonicalize(t)}");
    }
    
    static string Canonicalize(string cidr) => CIDR.Parse(cidr).Canonicalize().ToString();
}

readonly struct CIDR
{
    public readonly IPAddress ip;
    public readonly int length;
    
    public static CIDR Parse(string cidr)
    {
        string[] parts = cidr.Split('/');
        return new CIDR(IPAddress.Parse(parts[0]), int.Parse(parts[1]));
    }
    
    public CIDR(IPAddress ip, int length) => (this.ip, this.length) = (ip, length);
    
    public CIDR Canonicalize() =>
        new CIDR(
            new IPAddress(
                ToBytes(
                    ToInt(
                        ip.GetAddressBytes()
                    )
                    & ~((1 << (32 - length)) - 1)
                )
            ),
            length
        );
    
    private int ToInt(byte[] bytes) => bytes.Aggregate(0, (n, b) => (n << 8) | b);
    
    private byte[] ToBytes(int n)
    {
        byte[] bytes = new byte[4];
        for (int i = 3; i >= 0; i--) {
            bytes[i] = (byte)(n & 0xFF);
            n >>= 8;
        }
        return bytes;
    }
    
    public override string ToString() => $"{ip}/{length}";
}
Output:
87.70.141.1/22   =>   87.70.140.0/22
36.18.154.103/12   =>   36.16.0.0/12
62.62.197.11/29   =>   62.62.197.8/29
67.137.119.181/4   =>   64.0.0.0/4
161.214.74.21/24   =>   161.214.74.0/24
184.232.176.184/18   =>   184.232.128.0/18

C++

#include <cstdint>
#include <iomanip>
#include <iostream>
#include <sstream>

// Class representing an IPv4 address + netmask length
class ipv4_cidr {
public:
    ipv4_cidr() {}
    ipv4_cidr(std::uint32_t address, unsigned int mask_length)
        : address_(address), mask_length_(mask_length) {}
    std::uint32_t address() const {
        return address_;
    }
    unsigned int mask_length() const {
        return mask_length_;
    }
    friend std::istream& operator>>(std::istream&, ipv4_cidr&);
private:
    std::uint32_t address_ = 0;
    unsigned int mask_length_ = 0;
};

// Stream extraction operator, also performs canonicalization
std::istream& operator>>(std::istream& in, ipv4_cidr& cidr) {
    int a, b, c, d, m;
    char ch;
    if (!(in >> a >> ch) || a < 0 || a > UINT8_MAX || ch != '.'
        || !(in >> b >> ch) || b < 0 || b > UINT8_MAX || ch != '.'
        || !(in >> c >> ch) || c < 0 || c > UINT8_MAX || ch != '.'
        || !(in >> d >> ch) || d < 0 || d > UINT8_MAX || ch != '/'
        || !(in >> m) || m < 1 || m > 32) {
        in.setstate(std::ios_base::failbit);
        return in;
    }
    uint32_t mask = ~((1 << (32 - m)) - 1);
    uint32_t address = (a << 24) + (b << 16) + (c << 8) + d;
    address &= mask;
    cidr.address_ = address;
    cidr.mask_length_ = m;
    return in;
}

// Stream insertion operator
std::ostream& operator<<(std::ostream& out, const ipv4_cidr& cidr) {
    uint32_t address = cidr.address();
    unsigned int d = address & UINT8_MAX;
    address >>= 8;
    unsigned int c = address & UINT8_MAX;
    address >>= 8;
    unsigned int b = address & UINT8_MAX;
    address >>= 8;
    unsigned int a = address & UINT8_MAX;
    out << a << '.' << b << '.' << c << '.' << d << '/'
        << cidr.mask_length();
    return out;
}

int main(int argc, char** argv) {
    const char* tests[] = {
        "87.70.141.1/22",
        "36.18.154.103/12",
        "62.62.197.11/29",
        "67.137.119.181/4",
        "161.214.74.21/24",
        "184.232.176.184/18"
    };
    for (auto test : tests) {
        std::istringstream in(test);
        ipv4_cidr cidr;
        if (in >> cidr)
            std::cout << std::setw(18) << std::left << test << " -> "
                << cidr << '\n';
        else
            std::cerr << test << ": invalid CIDR\n";
    }
    return 0;
}
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

Common Lisp

(defun ip->bit-vector (ip)
  (flet ((int->bits (int)
           (loop :for i :below 8
                 :collect (if (logbitp i int) 1 0) :into bits
                 :finally (return (nreverse bits)))))
    (loop :repeat 4
          :with start := 0
          :for pos := (position #\. ip :start start)
          :collect (parse-integer ip :start start :end pos) :into res
          :while pos
          :do (setf start (1+ pos))
          :finally (return (apply #'concatenate 'bit-vector (mapcar #'int->bits res))))))

(defun bit-vector->ip (vec &optional n)
  (loop :repeat 4
        :for end :from 8 :by 8
        :for start := (- end 8)
        :for sub := (subseq vec start end)
        :collect (parse-integer (map 'string #'digit-char sub) :radix 2) :into res
        :finally (return (format nil "~{~D~^.~}~@[/~A~]" res n))))

(defun canonicalize-cidr (cidr)
  (let* ((n (position #\/ cidr))
         (ip (subseq cidr 0 n))
         (sn (parse-integer cidr :start (1+ n)))
         (ip* (ip->bit-vector ip))
         (canonical-ip (fill ip* 0 :start sn)))
    (bit-vector->ip canonical-ip sn)))

(loop :for cidr :in '("36.18.154.103/12" "62.62.197.11/29"
                      "67.137.119.181/4" "161.214.74.21/24"
                      "184.232.176.184/18")
      :for ccidr := (canonicalize-cidr cidr)
      :do (format t "~&~A~20,T→  ~A~%" cidr ccidr))
Output:
36.18.154.103/12    →  36.16.0.0/12
62.62.197.11/29     →  62.62.197.8/29
67.137.119.181/4    →  64.0.0.0/4
161.214.74.21/24    →  161.214.74.0/24
184.232.176.184/18  →  184.232.128.0/18

Cowgol

include "cowgol.coh";

typedef IP is uint32;

record CIDR is
    ip: IP;
    len: uint8;
end record;

sub ParseIP(buffer: [uint8]): (result: IP, ptr: [uint8]) is
    var parts: uint8 := 4;
    var part: int32;    
    result := 0;
    loop
        (part, buffer) := AToI(buffer);
        parts := parts - 1;
        result := result | ((part as IP & 0xFF) << (parts * 8));
        if parts == 0 then break;
        else buffer := @next buffer;
        end if;
    end loop;
    ptr := buffer;
end sub;

sub ParseCIDR(buffer: [uint8], cidr: [CIDR]) is
    var len: int32;
    (cidr.ip, buffer) := ParseIP(buffer);
    (len, buffer) := AToI(@next buffer);
    cidr.len := len as uint8;
end sub;

sub Canonicalize(cidr: [CIDR]) is
    var mask: IP := 0;
    var ones: uint8 := cidr.len;
    var len: uint8 := 32;
    while ones != 0 loop
        mask := (mask << 1) | 1;
        ones := ones - 1;
        len := len - 1;
    end loop;
    while len != 0 loop;
        mask := mask << 1;
        len := len - 1;
    end loop;
    cidr.ip := cidr.ip & mask;
end sub;

sub FormatIP(ip: IP, buffer: [uint8]): (ptr: [uint8]) is
    ptr := buffer;
    ptr := UIToA((ip >> 24) & 0xFF, 10, ptr);
    [ptr] := '.';
    ptr := UIToA((ip >> 16) & 0xFF, 10, @next ptr);
    [ptr] := '.';
    ptr := UIToA((ip >> 8) & 0xFF, 10, @next ptr);
    [ptr] := '.';
    ptr := UIToA(ip & 0xFF, 10, @next ptr);
    [ptr] := 0;
end sub;

sub FormatCIDR(cidr: [CIDR], buffer: [uint8]) is
    buffer := FormatIP(cidr.ip, buffer);
    [buffer] := '/';
    buffer := UIToA(cidr.len as uint32, 10, @next buffer);
end sub;

var buffer: uint8[256];
var string := &buffer[0];
var cidr: CIDR;

ParseCIDR("87.70.141.1/22", &cidr);
FormatCIDR(&cidr, string);

print("Before canonicalization: ");
print(string);
print_nl();

Canonicalize(&cidr);
FormatCIDR(&cidr, string);

print(" After canonicalization: ");
print(string);
print_nl();
Output:
Before canonicalization: 87.70.141.1/22
 After canonicalization: 87.70.140.0/22

EasyLang

func$ can_cidr s$ .
   n[] = number strsplit s$ "./"
   if len n[] <> 5
      return ""
   .
   for i to 4
      if n[i] < 0 or n[i] > 255
         return ""
      .
      ad = ad * 256 + n[i]
   .
   if n[5] > 31 or n[5] < 1
      return ""
   .
   mask = bitnot (bitshift 1 (32 - n[5]) - 1)
   ad = bitand ad mask
   for i to 4
      if r$ <> ""
         r$ = "." & r$
      .
      r$ = ad mod 256 & r$
      ad = ad div 256
   .
   return r$ & "/" & n[5]
.
repeat
   s$ = input
   until s$ = ""
   print s$ & " -> " & can_cidr s$
.
# 
input_data
87.70.141.1/22
36.18.154.103/12
62.62.197.11/29
67.137.119.181/4
161.214.74.21/24
184.232.176.184/18

Factor

Translation of: Ruby
Works with: Factor version 0.99 2020-07-03
USING: command-line formatting grouping io kernel math.parser
namespaces prettyprint sequences splitting ;
IN: rosetta-code.canonicalize-cidr

! canonicalize a CIDR block: make sure none of the host bits are set
command-line get [ lines ] when-empty
[
    ! ( CIDR-IP -- bits-in-network-part dotted-decimal )
    "/" split first2 string>number swap

    ! get IP as binary string
    "." split [ string>number "%08b" sprintf ] map "" join

    ! replace the host part with all zeros
    over cut length [ CHAR: 0 ] "" replicate-as append

    ! convert back to dotted-decimal
    8 group [ bin> number>string ] map "." join swap

    ! and output
    "%s/%d\n" printf
] each
Output:
$ canonicalize-cidr.factor 87.70.141.1/22
87.70.140.0/22

Go

Translation of: Ruby
package main

import (
    "fmt"
    "log"
    "strconv"
    "strings"
)

func check(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

// canonicalize a CIDR block: make sure none of the host bits are set
func canonicalize(cidr string) string {
    // dotted-decimal / bits in network part
    split := strings.Split(cidr, "/")
    dotted := split[0]
    size, err := strconv.Atoi(split[1])
    check(err)

    // get IP as binary string
    var bin []string
    for _, n := range strings.Split(dotted, ".") {
        i, err := strconv.Atoi(n)
        check(err)
        bin = append(bin, fmt.Sprintf("%08b", i))
    }
    binary := strings.Join(bin, "")

    // replace the host part with all zeros
    binary = binary[0:size] + strings.Repeat("0", 32-size)

    // convert back to dotted-decimal
    var canon []string
    for i := 0; i < len(binary); i += 8 {
        num, err := strconv.ParseInt(binary[i:i+8], 2, 64)
        check(err)
        canon = append(canon, fmt.Sprintf("%d", num))
    }

    // and return
    return strings.Join(canon, ".") + "/" + split[1]
}

func main() {
    tests := []string{
        "87.70.141.1/22",
        "36.18.154.103/12",
        "62.62.197.11/29",
        "67.137.119.181/4",
        "161.214.74.21/24",
        "184.232.176.184/18",
    }

    for _, test := range tests {
        fmt.Printf("%-18s -> %s\n", test, canonicalize(test))
    }
}
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

Hare

use fmt;
use net::ip;
use strings;

export fn main() void = {
	const array = ["87.70.141.1/22",
		"36.18.154.103/12",
		"62.62.197.11/29",
		"67.137.119.181/4",
		"161.214.74.21/24",
		"184.232.176.184/18"];

	for (let i = 0z; i < len(array); i += 1) {
		match (canonicalizecidr(array[i])) {
		case let s: str =>
			fmt::printfln("{:-18} => {}", array[i], s)!;
			free(s);
		case net::ip::invalid =>
			fmt::errorfln("Error: invalid item: {}", array[i])!;
		};
	};
};

fn canonicalizecidr(a: str) (str | net::ip::invalid) = {
	const sub = net::ip::parsecidr(a)?;
	match (sub.addr) {
	case let addr4: net::ip::addr4 => void;
	case net::ip::addr6 => return net::ip::invalid;
	};

	const net = sub.addr as [4]u8;
	const msk = sub.mask as [4]u8;

	const net: u32 = net[0]: u32 << 24 | net[1]: u32 << 16 | net[2]: u32 << 8 | net[3]: u32;
	const msk: u32 = msk[0]: u32 << 24 | msk[1]: u32 << 16 | msk[2]: u32 << 8 | msk[3]: u32;

	const result: u32 = net & msk;
	return fmt::asprintf("{}.{}.{}.{}/{}",
		result >> 24,
		0xff & result >> 16,
		0xff & result >> 8,
		0xff & result,
		strings::cut(a, "/").1);
};
Output:
87.70.141.1/22     => 87.70.140.0/22
36.18.154.103/12   => 36.16.0.0/12
62.62.197.11/29    => 62.62.197.8/29
67.137.119.181/4   => 64.0.0.0/4
161.214.74.21/24   => 161.214.74.0/24
184.232.176.184/18 => 184.232.128.0/18

Haskell

This is implemented using only libraries found in the base package.

import Control.Monad (guard)
import Data.Bits ((.|.), (.&.), complement, shiftL, shiftR, zeroBits)
import Data.Maybe (listToMaybe)
import Data.Word (Word32, Word8)
import Text.ParserCombinators.ReadP (ReadP, char, readP_to_S)
import Text.Printf (printf)
import Text.Read.Lex (readDecP)

-- A 32-bit IPv4 address, with a netmask applied, and the number of leading bits
-- that are in the network portion of the address.
data CIDR = CIDR Word32 Word8

-- Convert a string to a CIDR, or nothing if it's invalid.
cidrRead :: String -> Maybe CIDR
cidrRead = listToMaybe . map fst . readP_to_S cidrP

-- Convert the CIDR to a string.
cidrShow :: CIDR -> String
cidrShow (CIDR addr n) = let (a, b, c, d) = octetsFrom addr
                         in printf "%u.%u.%u.%u/%u" a b c d n

-- Parser for the string representation of a CIDR.  For a successful parse the
-- string must have the form a.b.c.d/n, where each of a, b, c and d are decimal
-- numbers in the range [0, 255] and n is a decimal number in the range [0, 32].
cidrP :: ReadP CIDR
cidrP = do a <- octetP <* char '.'
           b <- octetP <* char '.'
           c <- octetP <* char '.'
           d <- octetP <* char '/'
           n <- netBitsP
           return $ CIDR (addrFrom a b c d .&. netmask n) n
  where octetP   = wordP 255
        netBitsP = wordP  32

-- Parser for a decimal string, whose value is in the range [0, lim].
-- 
-- We want the limit argument to be an Integer, so that we can detect values
-- that are too large, rather than having them silently wrap.
wordP :: Integral a => Integer -> ReadP a
wordP lim = do n <- readDecP
               guard $ n <= lim
               return $ fi n

-- The octets of an IPv4 address.
octetsFrom :: Word32 -> (Word8, Word8, Word8, Word8)
octetsFrom addr = (oct addr 3, oct addr 2, oct addr 1, oct addr 0)
  where oct w n = fi $ w `shiftR` (8*n) .&. 0xff

-- An IPv4 address from four octets.  `ipAddr4 1 2 3 4' is the address 1.2.3.4.
addrFrom :: Word8 -> Word8 -> Word8 -> Word8 -> Word32
addrFrom a b c d = 0 <<+ a <<+ b <<+ c <<+ d
  where w <<+ o = w `shiftL` 8 .|. fi o

-- The value `netmask n' is the netmask whose leftmost n bits are 1, and the
-- remainder are 0.
netmask :: Word8 -> Word32
netmask n = complement $ complement zeroBits `shiftR` fi n

fi :: (Integral a, Num b) => a -> b
fi = fromIntegral

test :: String -> IO ()
test str = do
  let cidrStr = maybe "invalid CIDR string" cidrShow (cidrRead str)
  printf "%-18s -> %s\n" str cidrStr

main :: IO ()
main = do
  test "87.70.141.1/22"
  test "36.18.154.103/12"
  test "62.62.197.11/29"
  test "67.137.119.181/4"
  test "161.214.74.21/24"
  test "184.232.176.184/18"
  
  test "184.256.176.184/12" -- octet value is too large
  test "184.232.176.184/33" -- netmask size is too large
  test "184.232.184/18"     -- too few components
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18
184.256.176.184/12 -> invalid CIDR string
184.232.176.184/33 -> invalid CIDR string
184.232.184/18     -> invalid CIDR string

J

Implementation:

cidr=: {{
  'a e'=. 0 ".each (y rplc'. ')-.&;:'/'
  ('/',":e),~rplc&' .'":_8#.\32{.e{.,(8#2)#:a
}}

In other words, convert address part to bits, truncate to the network address, extend back to 32 bits and repack into the ascii representation.

Task examples:

   cidr '87.70.141.1/22'
87.70.140.0/22
   cidr '36.18.154.103/12'
36.16.0.0/12
   cidr '62.62.197.11/29'
62.62.197.8/29
   cidr '67.137.119.181/4'
64.0.0.0/4
   cidr '161.214.74.21/24'
161.214.74.0/24
   cidr '184.232.176.184/18'
184.232.128.0/18

Java

import java.text.MessageFormat;
import java.text.ParseException;

public class CanonicalizeCIDR {
    public static void main(String[] args) {
        for (String test : TESTS) {
            try {
                CIDR cidr = new CIDR(test);
                System.out.printf("%-18s -> %s\n", test, cidr.toString());
            } catch (Exception ex) {
                System.err.printf("Error parsing '%s': %s\n", test, ex.getLocalizedMessage());
            }
        }
    }

    private static class CIDR {
        private CIDR(int address, int maskLength) {
            this.address = address;
            this.maskLength = maskLength;
        }

        private CIDR(String str) throws Exception {
            Object[] args = new MessageFormat(FORMAT).parse(str);
            int address = 0;
            for (int i = 0; i < 4; ++i) {
                int a = ((Number)args[i]).intValue();
                if (a < 0 || a > 255)
                    throw new Exception("Invalid IP address");
                address <<= 8;
                address += a;
            }
            int maskLength = ((Number)args[4]).intValue();
            if (maskLength < 1 || maskLength > 32)
                throw new Exception("Invalid mask length");
            int mask = ~((1 << (32 - maskLength)) - 1);
            this.address = address & mask;
            this.maskLength = maskLength;
        }

        public String toString() {
            int address = this.address;
            int d = address & 0xFF;
            address >>= 8;
            int c = address & 0xFF;
            address >>= 8;
            int b = address & 0xFF;
            address >>= 8;
            int a = address & 0xFF;
            Object[] args = { a, b, c, d, maskLength };
            return new MessageFormat(FORMAT).format(args);
        }

        private int address;
        private int maskLength;
        private static final String FORMAT = "{0,number,integer}.{1,number,integer}.{2,number,integer}.{3,number,integer}/{4,number,integer}";
    };

    private static final String[] TESTS = {
        "87.70.141.1/22",
        "36.18.154.103/12",
        "62.62.197.11/29",
        "67.137.119.181/4",
        "161.214.74.21/24",
        "184.232.176.184/18"
    };
}
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

JavaScript

const canonicalize = s => {

    // Prepare a DataView over a 16 Byte Array buffer.
    // Initialised to all zeros.
    const dv = new DataView(new ArrayBuffer(16));

    // Get the ip-address and cidr components
    const [ip, cidr] = s.split('/');

    // Make sure the cidr component is a usable int, and
    // default to 32 if it does not exist.
    const cidrInt = parseInt(cidr || 32, 10);

    // Populate the buffer with uint8 ip address components.
    // Use zero as the default for shorthand pool definitions.
    ip.split('.').forEach(
        (e, i) => dv.setUint8(i, parseInt(e || 0, 10))
    );

    // Grab the whole buffer as a uint32
    const ipAsInt = dv.getUint32(0);

    // Zero out the lower bits as per the CIDR number.
    const normIpInt = (ipAsInt >> 32 - cidrInt) << 32 - cidrInt;

    // Plonk it back into the buffer
    dv.setUint32(0, normIpInt);

    // Read each of the uint8 slots in the buffer and join them with a dot.
    const canonIp = [...'0123'].map((e, i) => dv.getUint8(i)).join('.');

    // Attach the cidr number to the back of the normalised IP address.
    return [canonIp, cidrInt].join('/');
  }

  const test = s => console.log(s, '->', canonicalize(s));
  [
    '255.255.255.255/10',
    '87.70.141.1/22',
    '36.18.154.103/12',
    '62.62.197.11/29',
    '67.137.119.181/4',
    '161.214.74.21/24',
    '184.232.176.184/18',
    '10.207.219.251/32',
    '10.207.219.251',
    '110.200.21/4',
    '10..55/8',
    '10.../8'
  ].forEach(test)
Output:
87.70.141.1/22 -> 87.70.140.0/22
36.18.154.103/12 -> 36.16.0.0/12
62.62.197.11/29 -> 62.62.197.8/29
67.137.119.181/4 -> 64.0.0.0/4
161.214.74.21/24 -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18
10.207.219.251/32 -> 10.207.219.251/32
10.207.219.251 -> 10.207.219.251/32
110.200.21/4 -> 96.0.0.0/4
10..55/8 -> 10.0.0.0/8
10.../8 -> 10.0.0.0/8

jq

Adapted from Wren

Works with: jq

Also works with gojq, the Go implementation of jq, and with fq

Generic Utilities

# For gojq and fq
def _nwise($n):
  def nw: if length <= $n then . else .[0:$n] , (.[$n:] | nw) end;
  nw;

def lpad($len; $fill): tostring | ($len - length) as $l | ($fill * $l)[:$l] + .;
def lpad($len): lpad($len;" ");

# Convert the input integer to a string in the specified base (2 to 36 inclusive)
def convert(base):
  def stream:
    recurse(if . >= base then ./base|floor else empty end) | . % base ;
  [stream] | reverse
  | if   base <  10 then map(tostring) | join("")
    elif base <= 36 then map(if . < 10 then 48 + . else . + 87 end) | implode
    else error("base too large")
    end;

# input string is converted from "base" to an integer, within limits
# of the underlying arithmetic operations, and without error-checking:
def to_i(base):
  explode
  | reverse
  | map(if . > 96  then . - 87 else . - 48 end)  # "a" ~ 97 => 10 ~ 87
  | reduce .[] as $c
      # state: [power, ans]
      ([1,0]; (.[0] * base) as $b | [$b, .[1] + (.[0] * $c)])
  | .[1];

Canonicalize CIDR

# Canonicalize a CIDR block: make sure none of the host bits are set
def canonicalize:
    # dotted-decimal / bits in network part
    (. / "/") as [$dotted, $bits]
    | ($bits | tonumber) as $size

    # get IP as binary string
    | {binary: (($dotted / ".") | map( tonumber | convert(2) | lpad(8; "0") ) | join("") )}
    
    # replace the host part with all zeros
    | .binary |= .[0:$size] + "0" * (32 - $size)

    # convert back to dotted-decimal
    | [.binary | explode | _nwise(8) | implode]
    | (map( to_i(2) | tostring ) | join(".")) as $canon

    | $canon + "/" + $bits;

def tests: 
    "87.70.141.1/22",
    "36.18.154.103/12",
    "62.62.197.11/29",
    "67.137.119.181/4",
    "161.214.74.21/24",
    "184.232.176.184/18"
;

tests
| "\(lpad(18)) -> \(canonicalize)"
Output:
    87.70.141.1/22 -> 87.70.140.0/22
  36.18.154.103/12 -> 36.16.0.0/12
   62.62.197.11/29 -> 62.62.197.8/29
  67.137.119.181/4 -> 64.0.0.0/4
  161.214.74.21/24 -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

Julia

Julia has a Sockets library as a builtin, which has the types IPv4 and IPv6 for single IP addresses.

using Sockets

function canonCIDR(cidr::String)
    cidr = replace(cidr, r"\.(\.|\/)" => s".0\1") # handle ..
    cidr = replace(cidr, r"\.(\.|\/)" => s".0\1") # handle ...
    ip = split(cidr, "/")
    dig = length(ip) > 1 ? 2^(32 - parse(UInt8, ip[2])) : 1
    ip4 = IPv4(UInt64(IPv4(ip[1])) & (0xffffffff - dig + 1))
    return length(ip) == 1 ? "$ip4/32" : "$ip4/$(ip[2])"
end

println(canonCIDR("87.70.141.1/22"))
println(canonCIDR("100.68.0.18/18"))
println(canonCIDR("10.4.30.77/30"))
println(canonCIDR("10.207.219.251/32"))
println(canonCIDR("10.207.219.251"))
println(canonCIDR("110.200.21/4"))
println(canonCIDR("10..55/8"))
println(canonCIDR("10.../8"))
Output:
87.70.140.0/22
100.68.0.0/18
10.4.30.76/30
10.207.219.251/32
10.207.219.251/32
96.0.0.0/4
10.0.0.0/8
10.0.0.0/8

Lua

Library: inet
inet = require 'inet'

test_cases = {
  '87.70.141.1/22', '36.18.154.103/12', '62.62.197.11/29', '67.137.119.181/4',
  '161.214.74.21/24', '184.232.176.184/18'
}

for i, cidr in ipairs(test_cases) do
  print( inet(cidr):network() )
end
Output:
87.70.140.0/22
36.16.0.0/12
62.62.197.8/29
64.0.0.0/4
161.214.74.0/24
184.232.128.0/18

Mathematica /Wolfram Language

ClearAll[CanonicalizeCIDR]
CanonicalizeCIDR[str_String] := Module[{i, ip, chop, keep, change},
  If[StringMatchQ[str, "*.*.*.*/*"],
   i = StringSplit[str, "." | "/"];
   i = Interpreter["Integer"] /@ i;
   If[MatchQ[i, {_Integer, _Integer, _Integer, _Integer, _Integer}],
    If[AllTrue[i, Between[{0, 255}]],
     {ip, {chop}} = TakeDrop[i, 4];
     ip = Catenate[IntegerDigits[ip, 2, 8]];
     {keep, change} = TakeDrop[ip, chop];
     change = ConstantArray[0, Length[change]];
     ip = Partition[Join[keep, change], 8];
     ip = ToString[FromDigits[#, 2]] & /@ ip;
     StringRiffle[ip, "."] <> "/" <> ToString[chop]
     ,
     Failure["Invalid range of numbers", <|"Input" -> str|>]
     ]
    ,
    Failure["Invalid format", <|"Input" -> str|>]
    ]
   ]
  ]
CanonicalizeCIDR["87.70.141.1/22"]
CanonicalizeCIDR["36.18.154.103/12"]
CanonicalizeCIDR["62.62.197.11/29"]
CanonicalizeCIDR["67.137.119.181/4"]
CanonicalizeCIDR["161.214.74.21/24"]
CanonicalizeCIDR["184.232.176.184/18"]
Output:
87.70.140.0/22
36.16.0.0/12
62.62.197.8/29
64.0.0.0/4
161.214.74.0/24
184.232.128.0/18

MATLAB

Translation of: Python
clear all;close all;clc;
cidrCanonicalizer();

function cidrCanonicalizer
    % Main function to test CIDR canonicalization
    
    % Define test cases
    testCases = {
        '36.18.154.103/12', '36.16.0.0/12';
        '62.62.197.11/29', '62.62.197.8/29';
        '67.137.119.181/4', '64.0.0.0/4';
        '161.214.74.21/24', '161.214.74.0/24';
        '184.232.176.184/18', '184.232.128.0/18'
    };
    
    % Run test cases
    for i = 1:size(testCases, 1)
        ip = testCases{i, 1};
        expected = testCases{i, 2};
        result = canonicalize(ip);
        fprintf('%s -> %s\n', ip, result);
        assert(strcmp(result, expected));
    end
end

function result = dottedToInt(dotted)
    % Convert dotted IP to integer representation
    parts = str2double(strsplit(dotted, '.'));
    result = sum(parts .* (256 .^ (3:-1:0)));
end

function result = intToDotted(ip)
    % Convert integer IP to dotted representation
    result = strjoin(arrayfun(@(x) num2str(bitshift(bitand(ip, bitshift(255, x)), -x)), [24 16 8 0], 'UniformOutput', false), '.');
end

function result = networkMask(numberOfBits)
    % Create a network mask for the given number of bits
    result = bitshift((bitshift(1, numberOfBits) - 1), (32 - numberOfBits));
end

function result = canonicalize(ip)
    % Canonicalize the given CIDR IP
    [dotted, networkBits] = strtok(ip, '/');
    networkBits = str2double(strrep(networkBits, '/', ''));
    i = dottedToInt(dotted);
    mask = networkMask(networkBits);
    result = strcat(intToDotted(bitand(i, mask)), '/', num2str(networkBits));
end
Output:
36.18.154.103/12 -> 36.16.0.0/12
62.62.197.11/29 -> 62.62.197.8/29
67.137.119.181/4 -> 64.0.0.0/4
161.214.74.21/24 -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

Nim

Using the IpAddress type from standard module “net”.

import net
import strutils


proc canonicalize*(address: var IpAddress; nbits: Positive) =
  ## Canonicalize an IP address.

  var zbits = 32 - nbits    # Number of bits to reset.

  # We process byte by byte which avoids byte order issues.
  for idx in countdown(address.address_v4.high, address.address_v4.low):
    if zbits == 0:
      # No more bits to reset.
      break
    if zbits >= 8:
      # Reset the current byte and continue with the remaining bits.
      address.address_v4[idx] = 0
      dec zbits, 8
    else:
      # Use a mask to reset the bits.
      address.address_v4[idx] = address.address_v4[idx] and (0xff'u8 shl zbits)
      zbits = 0

#———————————————————————————————————————————————————————————————————————————————————————————————————

when isMainModule:

  import strformat

  var ipAddress: IpAddress
  var nbits: int

  for address in ["87.70.141.1/22", "36.18.154.103/12", "62.62.197.11/29",
                  "67.137.119.181/4", "161.214.74.21/24", "184.232.176.184/18"]:

    # Parse the address.
    let parts = address.split('/')
    try:
      ipAddress = parseIpAddress(parts[0])
      if ipAddress.family == IPV6:
        raise newException(ValueError, "")
    except ValueError:
      echo "Invalid IP V4 address: ", parts[0]
      quit(QuitFailure)

    # Check the number of bits.
    try:
      nbits = parseInt(parts[1])
      if nbits notin 1..32:
        raise newException(ValueError, "")
    except ValueError:
      echo "Invalid number of bits: ", parts[1]
      quit(QuitFailure)

    # Canonicalize the address and display the result.
    ipAddress.canonicalize(nbits)
    echo &"{address:<18}  ⇢  {ipAddress}/{nbits}"
Output:
87.70.141.1/22      ⇢  87.70.140.0/22
36.18.154.103/12    ⇢  36.16.0.0/12
62.62.197.11/29     ⇢  62.62.197.8/29
67.137.119.181/4    ⇢  64.0.0.0/4
161.214.74.21/24    ⇢  161.214.74.0/24
184.232.176.184/18  ⇢  184.232.128.0/18

OCaml

let mask = function
  | _, 0 -> 0l, 0
  | a, l -> Int32.(logand (shift_left minus_one (-l land 31)) a), l

let str_to_cidr s =
  let (<<+) b a = Int32.(add (shift_left b 8) a) in
  let recv d c b a l = d <<+ c <<+ b <<+ a, l in
  Scanf.sscanf s "%3lu.%3lu.%3lu.%3lu/%2u" recv

let cidr_to_str (a, l) =
  let addr n =
    let dgt = function
      | h :: t -> Int32.shift_right_logical h 8 :: Int32.logand h 255l :: t
      | l -> l
    in
    dgt [n] |> dgt |> dgt |> List.map Int32.to_string |> String.concat "."
  in
  Printf.sprintf "%s/%u" (addr a) l

let () =
  ["87.70.141.1/22"; "36.18.154.103/12"; "62.62.197.11/29"; "67.137.119.181/4"; "161.214.74.21/24"; "184.232.176.184/18"; "10.207.219.251/32"]
  |> List.iter (fun s -> str_to_cidr s |> mask |> cidr_to_str |> print_endline)
Output:
87.70.140.0/22
36.16.0.0/12
62.62.197.8/29
64.0.0.0/4
161.214.74.0/24
184.232.128.0/18
10.207.219.251/32

Perl

There's a CPAN module for IP address manipulation, Net::IP. Unfortunately, it requires a CIDR string to be already in canonical form; otherwise it fails with an "Invalid prefix" error. So we do it manually.

#!/usr/bin/env perl
use v5.16;
use Socket qw(inet_aton inet_ntoa);

# canonicalize a CIDR block: make sure none of the host bits are set
if (!@ARGV) {
   chomp(@ARGV = <>);
}

for (@ARGV) {

  # dotted-decimal / bits in network part
  my ($dotted, $size) = split m#/#;

  # get IP as binary string
  my $binary = sprintf "%032b", unpack('N', inet_aton $dotted);

  # Replace the host part with all zeroes
  substr($binary, $size) = 0 x (32 - $size);

  # Convert back to dotted-decimal
  $dotted = inet_ntoa(pack 'B32', $binary);

  # And output
  say "$dotted/$size";
}
Output:
$ canonicalize_cidr.pl 87.70.141.1/22
87.70.140.0/22

Phix

with javascript_semantics -- (not likely useful)
function canonicalize_cidr(string cidr)
    cidr = substitute(cidr,"."," ") -- (else %d eats 0.0 etc)
    if not find('/',cidr) then cidr &= "/32" end if
    sequence res = scanf(cidr,"%d %d %d %d/%d")
    if length(res)=1 then
        integer {a,b,c,d,m} = res[1]
        if  a>=0 and a<=255
        and b>=0 and b<=255
        and c>=0 and c<=255
        and d>=0 and d<=255
        and m>=1 and m<=32 then
            atom mask = power(2,32-m)-1,
                 addr = bytes_to_int({d,c,b,a})
            addr -= and_bits(addr,mask)
            {d,c,b,a} = int_to_bytes(addr)
            return sprintf("%d.%d.%d.%d/%d",{a,b,c,d,m})
        end if
    end if
    return "???"
end function
 
constant tests = {"87.70.141.1/22",
                  "36.18.154.103/12",
                  "62.62.197.11/29",
                  "67.137.119.181/4",
                  "161.214.74.21/24",
                  "184.232.176.184/18"}
 
for i=1 to length(tests) do
    string ti = tests[i]
    printf(1,"%-18s -> %s\n",{ti,canonicalize_cidr(ti)})
end for
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

PicoLisp

(de cidr (S)
   (let
      (L (rot (mapcar format (split (chop S) "." "/")))
         N (++ L)
         M
         (&
            (sum >> (-24 -16 -8 0) L)
            (rev 32 (dec (** 2 N))) ) )
      (pack
         (>> 24 M)
         "."
         (& 255 (>> 16 M))
         "."
         (& 255 (>> 8 M))
         "."
         (& 255 (& 255 M))
         "/"
         N ) ) )
(let Fmt (18 3 17)
   (for A
      (quote
         "87.70.141.1/22"
         "36.18.154.103/12"
         "62.62.197.11/29"
         "67.137.119.181/4"
         "161.214.74.21/24"
         "184.232.176.184/18" )
      (tab Fmt A "=>" (cidr A)) ) )
Output:
    87.70.141.1/22 =>   87.70.140.0/22
  36.18.154.103/12 =>     36.16.0.0/12
   62.62.197.11/29 =>   62.62.197.8/29
  67.137.119.181/4 =>       64.0.0.0/4
  161.214.74.21/24 =>  161.214.74.0/24
184.232.176.184/18 => 184.232.128.0/18

Python

Translation of: Perl
#!/usr/bin/env python
# canonicalize a CIDR block specification:
# make sure none of the host bits are set

import sys
from socket import inet_aton, inet_ntoa
from struct import pack, unpack

args = sys.argv[1:]
if len(args) == 0:
    args = sys.stdin.readlines()

for cidr in args:
   # IP in dotted-decimal / bits in network part
   dotted, size_str = cidr.split('/')
   size = int(size_str)

   numeric = unpack('!I', inet_aton(dotted))[0]  # IP as an integer
   binary = f'{numeric:#034b}'                   # then as a padded binary string
   prefix = binary[:size + 2]                    # just the network part
                                                 #   (34 and +2 are to account
                                                 #    for leading '0b')

   canon_binary = prefix + '0' * (32 - size)     # replace host part with all zeroes
   canon_numeric = int(canon_binary, 2)          # convert back to integer
   canon_dotted = inet_ntoa(pack('!I',
                            (canon_numeric)))    # and then to dotted-decimal
   print(f'{canon_dotted}/{size}')               # output result
Output:
$ canonicalize_cidr.py 87.70.141.1/22
87.70.140.0/22

Bit mask and shift

"""Canonicalize CIDR"""
DIGITS = (24, 16, 8, 0)


def dotted_to_int(dotted: str) -> int:
    digits = [int(digit) for digit in dotted.split(".")]
    return sum(a << b for a, b in zip(digits, DIGITS))


def int_to_dotted(ip: int) -> str:
    digits = [(ip & (255 << d)) >> d for d in DIGITS]
    return ".".join(str(d) for d in digits)


def network_mask(number_of_bits: int) -> int:
    return ((1 << number_of_bits) - 1) << (32 - number_of_bits)


def canonicalize(ip: str) -> str:
    dotted, network_bits = ip.split("/")
    i = dotted_to_int(dotted)
    mask = network_mask(int(network_bits))
    return int_to_dotted(i & mask) + "/" + network_bits


TEST_CASES = [
    ("36.18.154.103/12", "36.16.0.0/12"),
    ("62.62.197.11/29", "62.62.197.8/29"),
    ("67.137.119.181/4", "64.0.0.0/4"),
    ("161.214.74.21/24", "161.214.74.0/24"),
    ("184.232.176.184/18", "184.232.128.0/18"),
]

if __name__ == "__main__":
    for ip, expect in TEST_CASES:
        rv = canonicalize(ip)
        print(f"{ip:<18} -> {rv}")
        assert rv == expect
Output:
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

Using standard library

The ipaddress module was added in Python version 3.3.

import ipaddress

def canonicalize(address: str) -> str:
    return str(ipaddress.ip_network(address, strict=False))

TEST_CASES = [
    ("36.18.154.103/12", "36.16.0.0/12"),
    ("62.62.197.11/29", "62.62.197.8/29"),
    ("67.137.119.181/4", "64.0.0.0/4"),
    ("161.214.74.21/24", "161.214.74.0/24"),
    ("184.232.176.184/18", "184.232.128.0/18"),
]

if __name__ == "__main__":
    for ip, expect in TEST_CASES:
        rv = canonicalize(ip)
        print(f"{ip:<18} -> {rv}")
        assert rv == expect, expect
Output:
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

Raku

Using library

Library: raku-IP-Addr
use IP::Addr;
for «87.70.141.1/22 36.18.154.103/12 62.62.197.11/29 67.137.119.181/4 161.214.74.21/24 184.232.176.184/18» -> $cidr {
   say "$cidr -> $(IP::Addr.new($cidr).network)";
}

String manipulation

Translation of: Perl
#!/usr/bin/env raku
unit sub MAIN(*@cidrs);

if !@cidrs {
  # test data
  @cidrs = «87.70.141.1/22 36.18.154.103/12 62.62.197.11/29 67.137.119.181/4 161.214.74.21/24 184.232.176.184/18»;
}

for @cidrs -> $cidr {
  say "$cidr -> $(canonicalize $cidr)";
}

# canonicalize a CIDR block: make sure none of the host bits are set
sub canonicalize($cidr) {
  # dotted-decimal / bits in network part
  my ($dotted, $size) = $cidr.split: '/';

  # get network part of the IP as binary string
  my $binary = $dotted.split('.')».fmt('%08b').join.substr(0, $size);

  # Add host part with all zeroes
  $binary ~= 0 x (32 - $size);

  # Convert back to dotted-decimal
  my $canon = $binary.comb(8)».parse-base(2).join: '.';

  # And output
  say "$canon/$size";
}
Output:
87.70.141.1/22 -> 87.70.140.0/22
36.18.154.103/12 -> 36.16.0.0/12
62.62.197.11/29 -> 62.62.197.8/29
67.137.119.181/4 -> 64.0.0.0/4
161.214.74.21/24 -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

Bit mask and shift

# canonicalize a IP4 CIDR block
sub CIDR-IP4-canonicalize ($address) {
  constant @mask = 24, 16, 8, 0;
 
  # dotted-decimal / subnet size
  my ($dotted, $size) = |$address.split('/'), 32;
 
  # get IP as binary address
  my $binary = sum $dotted.comb(/\d+/) Z+< @mask;
 
  # mask off subnet
  $binary +&= (2 ** $size - 1) +< (32 - $size);
 
  # Return dotted-decimal notation
  (@mask.map($binary +> * +& 0xFF).join('.'), $size)
}
 
my @tests = <
  87.70.141.1/22
  36.18.154.103/12
  62.62.197.11/29
  67.137.119.181/4
  161.214.74.21/24
  184.232.176.184/18
  100.68.0.18/18
  10.4.30.77/30
  10.207.219.251/32
  10.207.219.251
  110.200.21/4
  10.11.12.13/8
  10.../8
>;
 
printf "CIDR: %18s  Routing prefix: %s/%s\n", $_, |.&CIDR-IP4-canonicalize
  for @*ARGS || @tests;
Output:
CIDR:     87.70.141.1/22  Routing prefix: 87.70.140.0/22
CIDR:   36.18.154.103/12  Routing prefix: 36.16.0.0/12
CIDR:    62.62.197.11/29  Routing prefix: 62.62.197.8/29
CIDR:   67.137.119.181/4  Routing prefix: 64.0.0.0/4
CIDR:   161.214.74.21/24  Routing prefix: 161.214.74.0/24
CIDR: 184.232.176.184/18  Routing prefix: 184.232.128.0/18
CIDR:     100.68.0.18/18  Routing prefix: 100.68.0.0/18
CIDR:      10.4.30.77/30  Routing prefix: 10.4.30.76/30
CIDR:  10.207.219.251/32  Routing prefix: 10.207.219.251/32
CIDR:     10.207.219.251  Routing prefix: 10.207.219.251/32
CIDR:       110.200.21/4  Routing prefix: 96.0.0.0/4
CIDR:      10.11.12.13/8  Routing prefix: 10.0.0.0/8
CIDR:            10.../8  Routing prefix: 10.0.0.0/8

REXX

/*REXX pgm canonicalizes IPv4 addresses that are in CIDR notation  (dotted─dec/network).*/
parse arg a .                                    /*obtain optional argument from the CL.*/
if a=='' | a==","  then a= '87.70.141.1/22'    , /*Not specified?  Then use the defaults*/
                           '36.18.154.103/12'  ,
                           '62.62.197.11/29'   ,
                           '67.137.119.181/4'  ,
                           '161.214.74.21/24'  ,
                           '184.232.176.184/18'

    do i=1  for words(a);  z= word(a, i)         /*process each IPv4 address in the list*/
    parse var   z    #  '/'  -0  mask            /*get the address nodes & network mask.*/
    #= subword( translate(#, , .)  0 0 0, 1, 4)  /*elide dots from addr, ensure 4 nodes.*/
    $= #                                         /*use original node address (for now). */
    hb= 32 - substr(word(mask .32, 1), 2)        /*obtain the size of the host bits.    */
    $=;                          ##=             /*crop the host bits only if mask ≤ 32.*/
            do k=1  for 4;        _= word(#, k)  /*create a 32-bit (binary) IPv4 address*/
            ##= ##  ||  right(d2b(_), 8, 0)      /*append eight bits of the   "     "   */
            end   /*k*/                          /* [↑] ... and ensure a node is 8 bits.*/
    ##= left(##, 32-hb, 0)                       /*crop bits in host part of IPv4 addr. */
    ##= left(##, 32,    0)                       /*replace cropped bits with binary '0's*/
            do j=8  by 8  for 4                  /* [↓]  parse the four nodes of address*/
            $= $ || . || b2d(substr(##, j-7, 8)) /*reconstitute the decimal nodes.      */
            end   /*j*/                          /* [↑]  and insert a dot between nodes.*/
    say                                          /*introduce a blank line between IPv4's*/
    $= substr($, 2)                              /*elid the leading decimal point in  $ */
    say '   original IPv4 address: '  z          /*display the original  IPv4  address. */
    say '   canonicalized address: '  translate( space($), ., " ")mask  /*canonicalized.*/
    end   /*i*/
exit 0                                           /*stick a fork in it,  we're all done. */
/*──────────────────────────────────────────────────────────────────────────────────────*/
b2d: return  x2d( b2x( arg(1) ) )  +  0          /*convert binary  ───►  decimal number.*/
d2b: return  x2b( d2x( arg(1) ) )  +  0          /*   "    decimal ───►   binary    "   */
output   when using the default input:
   original IPv4 address:  87.70.141.1/22
   canonicalized address:  87.70.140.0/22

   original IPv4 address:  36.18.154.103/12
   canonicalized address:  36.16.0.0/12

   original IPv4 address:  62.62.197.11/29
   canonicalized address:  62.62.197.8/29

   original IPv4 address:  67.137.119.181/4
   canonicalized address:  64.0.0.0/4

   original IPv4 address:  161.214.74.21/24
   canonicalized address:  161.214.74.0/24

   original IPv4 address:  184.232.176.184/18
   canonicalized address:  184.232.128.0/18 

RPL

RPL has hundreds of built-in functions, but strangely a few limited ones for bit handling. There is no >> operator, so shifting left an integer by n bits shall be done through a loop: 1 n START SL NEXT

RPL code Comment
≪ 
  DUP SIZE "" 1 ROT FOR j
    OVER j j SUB DUP "0" ≥ OVER "9" ≤ AND SWAP " " 
    IFTE + NEXT 
  SWAP DROP STR→ → nbits
  ≪ SWAP ROT 4 ROLL
    32 STWS 1 3 START #100h * + NEXT 
    #FFFFFFFF 1 32 nbits - START SL NEXT AND
    1 3 START DUP SRB SWAP OVER SLB - SWAP NEXT
    B→R →STR 1 3 START "." + SWAP B→R →STR + NEXT 
    "/" + nbits →STR + 
≫ ≫ ‘CANON’ STO
CANON ( "nn.nn.nn.nn/bits" → "nn.nn.nn.nn/nbits" ) 
Scan input string
  Replace non-numerical chars by spaces

Drop input string, parse addresses to stack - except # bits
Reverse stack order
Consolidate numbers into a 32-bit number
Set and apply mask
Break 32-bit number into 4 in the stack
Make a IP address with the 4 numbers
add # bits

≪ { "36.18.154.103/12" "62.62.197.11/29" "67.137.119.181/4" "161.214.74.21/24" "184.232.176.184/18" } → t
   ≪ 1 5 FOR j t j GET CANON NEXT
≫ ≫ ‘TASK’ STO
Output:
5: "36.16.0.0/12"
4: "62.62.197.8/29"
3: "64.0.0.0/4"
2: "161.214.74.0/24"
1: "184.232.128.0/18"

Ruby

Translation of: Python
Translation of: Raku
#!/usr/bin/env ruby

# canonicalize a CIDR block: make sure none of the host bits are set
if ARGV.length == 0 then
    ARGV = $stdin.readlines.map(&:chomp)
end

ARGV.each do |cidr|

  # dotted-decimal / bits in network part
  dotted, size_str = cidr.split('/')
  size = size_str.to_i

  # get IP as binary string
  binary = dotted.split('.').map { |o| "%08b" % o }.join

  # Replace the host part with all zeroes
  binary[size .. -1] = '0' * (32 - size)

  # Convert back to dotted-decimal
  canon = binary.chars.each_slice(8).map { |a| a.join.to_i(2) }.join('.')

  # And output
  puts "#{canon}/#{size}"
end
Output:
$ canonicalize_cidr.rb 87.70.141.1/22
87.70.140.0/22

Built in

require "ipaddr"
 
tests = ["87.70.141.1/22", "36.18.154.103/12", "62.62.197.11/29",
  "67.137.119.181/4", "161.214.74.21/24", "184.232.176.184/18"]
 
tests.each do |str|
  ia = IPAddr.new(str)
  puts "#{ia}/#{ia.prefix}"
end
Output:
87.70.140.0/22
36.16.0.0/12
62.62.197.8/29
64.0.0.0/4
161.214.74.0/24
184.232.128.0/18

Rust

use std::net::Ipv4Addr;

fn canonical_cidr(cidr: &str) -> Result<String, &str> {
    let mut split = cidr.splitn(2, '/');
    if let (Some(addr), Some(mask)) = (split.next(), split.next()) {
        let addr = addr.parse::<Ipv4Addr>().map(u32::from).map_err(|_| cidr)?;
        let mask = mask.parse::<u8>().map_err(|_| cidr)?;
        let bitmask = 0xff_ff_ff_ffu32 << (32 - mask);
        let addr = Ipv4Addr::from(addr & bitmask);
        Ok(format!("{}/{}", addr, mask))
    } else {
        Err(cidr)
    }
}

#[cfg(test)]
mod tests {

    #[test]
    fn valid() {
        [
            ("87.70.141.1/22", "87.70.140.0/22"),
            ("36.18.154.103/12", "36.16.0.0/12"),
            ("62.62.197.11/29", "62.62.197.8/29"),
            ("67.137.119.181/4", "64.0.0.0/4"),
            ("161.214.74.21/24", "161.214.74.0/24"),
            ("184.232.176.184/18", "184.232.128.0/18"),
        ]
        .iter()
        .cloned()
        .for_each(|(input, expected)| {
            assert_eq!(expected, super::canonical_cidr(input).unwrap());
        });
    }
}

fn main() {
    println!("{}", canonical_cidr("127.1.2.3/24").unwrap());
}

Scala

Translation of: Java
import java.text.MessageFormat

object CanonicalizeCIDR extends App {
  case class CIDR(address: Int, maskLength: Int) {
    override def toString: String = {
      val a = (address >> 24) & 0xFF
      val b = (address >> 16) & 0xFF
      val c = (address >> 8) & 0xFF
      val d = address & 0xFF
      MessageFormat.format(CIDR.format, a.asInstanceOf[AnyRef], b.asInstanceOf[AnyRef], c.asInstanceOf[AnyRef], d.asInstanceOf[AnyRef], maskLength.asInstanceOf[AnyRef])
    }
  }

  object CIDR {
    private val format = "{0,number,integer}.{1,number,integer}.{2,number,integer}.{3,number,integer}/{4,number,integer}"

    def apply(str: String): CIDR = {
      val args = new MessageFormat(format).parse(str)
      val address = args.take(4).foldLeft(0) { (acc, arg) =>
        val a = arg.asInstanceOf[Number].intValue()
        require(a >= 0 && a <= 255, "Invalid IP address")
        (acc << 8) + a
      }
      val maskLength = args(4).asInstanceOf[Number].intValue()
      require(maskLength >= 1 && maskLength <= 32, "Invalid mask length")
      val mask = ~((1 << (32 - maskLength)) - 1)
      new CIDR(address & mask, maskLength)
    }
  }

  val tests = Array(
    "87.70.141.1/22",
    "36.18.154.103/12",
    "62.62.197.11/29",
    "67.137.119.181/4",
    "161.214.74.21/24",
    "184.232.176.184/18"
  )

  tests.foreach { test =>
    try {
      val cidr = CIDR(test)
      println(f"$test%-18s -> $cidr")
    } catch {
      case ex: Exception => println(s"Error parsing '$test': ${ex.getLocalizedMessage}")
    }
  }
}
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

SNOBOL

* Pattern to match any number of digits
              D = SPAN('0123456789')

* Convert a dotted-decimal IP address to an integer
              DEFINE('INET_ATON(Str),B3,B2,B1,B0')                   :(END_INET_ATON)
INET_ATON     Str D . B3 '.' D . B2 '.' D . B1 '.' D . B0            :F(FRETURN)
              INET_ATON = B3 * 16777216 + B2 * 65536 + B1 * 256 + B0 :(RETURN)
END_INET_ATON

* Convert an integer to dotted-decimal
              DEFINE('INET_NTOA(Addr),Count,Byte')                   :(END_INET_NTOA)
INET_NTOA     Count = 0
N2ALOOP       Byte = REMDR(Addr,256)
              Addr = (Addr - Byte) / 256
              INET_NTOA = Byte '.' INET_NTOA
              Count = Count + 1
              LT(Count,4)                                            :S(N2ALOOP)
              INET_NTOA '.' RPOS(0) = ''                             :(RETURN)
END_INET_NTOA

* Convert a CIDR range to canonical form
            DEFINE('FIXCIDR(Cidr),IP,Net,In,Host,Count,Pow,Out,Bit') :(END_FIXCIDR)
FIXCIDR     Cidr Arb . IP '/' D . Net                                :F(FRETURN)
            In = INET_ATON(IP)
            Host = 32 - Net

* Compute result of treating all bits in the host part as 0
            Out = 0
            Count = 0
            Pow = 1
MASKLOOP    Bit = REMDR(In, 2)
            In = (In - Bit) / 2
            Out = GE(Count, Host) Out + Bit * Pow
            Count = Count + 1
            Pow = Pow * 2
            LT(Count, 32)                                            :S(MASKLOOP)

* Convert back to dotted-decimal
            FIXCIDR = INET_NTOA(Out) '/' Net                         :(RETURN)
END_FIXCIDR

            OUTPUT = FIXCIDR('87.70.141.1/22')
            OUTPUT = FIXCIDR('36.18.154.103/12')
            OUTPUT = FIXCIDR('62.62.197.11/29')
            OUTPUT = FIXCIDR('67.137.119.181/4')
            OUTPUT = FIXCIDR('161.214.74.21/24')
            OUTPUT = FIXCIDR('184.232.176.184/18')
END
Output:
87.70.140.0/22
36.16.0.0/12
62.62.197.8/29
64.0.0.0/4
161.214.74.0/24
184.232.128.0/18

Swift

Translation of: Python
import Foundation

func dottedToInt(_ dotted: String) -> UInt32 {
    let digits = dotted.split(separator: ".").map { UInt32($0)! }
    return digits.enumerated().reduce(0) { $0 + ($1.element << (24 - $1.offset * 8)) }
}

func intToDotted(_ ip: UInt32) -> String {
    let digits = [24, 16, 8, 0].map { (ip & (255 << $0)) >> $0 }
    return digits.map { String($0) }.joined(separator: ".")
}

func networkMask(_ numberOfBits: Int) -> UInt32 {
    // Explicitly use UInt32 for bitwise operations
    return UInt32((1 << numberOfBits) - 1) << (32 - numberOfBits)
}

func canonicalize(_ ip: String) -> String {
    let parts = ip.split(separator: "/")
    let dotted = String(parts[0])
    let networkBits = Int(parts[1])!
    
    let i = dottedToInt(dotted)
    let mask = networkMask(networkBits)
    return "\(intToDotted(i & mask))/\(networkBits)"
}

let testCases = [
    ("36.18.154.103/12", "36.16.0.0/12"),
    ("62.62.197.11/29", "62.62.197.8/29"),
    ("67.137.119.181/4", "64.0.0.0/4"),
    ("161.214.74.21/24", "161.214.74.0/24"),
    ("184.232.176.184/18", "184.232.128.0/18"),
]

for testCase in testCases {
    let (ip, expect) = testCase
    let result = canonicalize(ip)
    print("\(ip) -> \(result)")
    assert(result == expect, "Test failed for \(ip)")
}
Output:
36.18.154.103/12 -> 36.16.0.0/12
62.62.197.11/29 -> 62.62.197.8/29
67.137.119.181/4 -> 64.0.0.0/4
161.214.74.21/24 -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18


Tcl

Translation of: Python
# Canonicalize CIDR in Tcl

# Convert dotted IP address to integer
proc dotted_to_int {dotted} {
    set digits [split $dotted .]
    set result 0
    foreach digit $digits {
        set result [expr {$result * 256 + $digit}]
    }
    return $result
}

# Convert integer IP address to dotted format
proc int_to_dotted {ip} {
    set result {}
    for {set i 3} {$i >= 0} {incr i -1} {
        lappend result [expr {($ip >> ($i * 8)) & 0xFF}]
    }
    return [join $result .]
}

# Calculate network mask
proc network_mask {number_of_bits} {
    return [expr {(1 << $number_of_bits) - 1 << (32 - $number_of_bits)}]
}

# Canonicalize IP address
proc canonicalize {ip} {
    regexp {^(.*)/(.*)$} $ip -> dotted network_bits
    set i [dotted_to_int $dotted]
    set mask [network_mask $network_bits]
    return [int_to_dotted [expr {$i & $mask}]]/$network_bits
}

# Test cases
set test_cases {
    {"36.18.154.103/12" "36.16.0.0/12"}
    {"62.62.197.11/29" "62.62.197.8/29"}
    {"67.137.119.181/4" "64.0.0.0/4"}
    {"161.214.74.21/24" "161.214.74.0/24"}
    {"184.232.176.184/18" "184.232.128.0/18"}
}

# Main execution
foreach test $test_cases {
    foreach {ip expect} $test {}
    set rv [canonicalize $ip]
    puts "$ip -> $rv"
    if {$rv ne $expect} {
        error "Test failed: $rv != $expect"
    }
}
Output:
36.18.154.103/12 -> 36.16.0.0/12
62.62.197.11/29 -> 62.62.197.8/29
67.137.119.181/4 -> 64.0.0.0/4
161.214.74.21/24 -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18


TXR

The inaddr-str function in TXR Lisp parses IPv4 addresses, converting them to a sockaddr-in structure. If there is a slash notation present, it is recognized. The prefix value is validated and stored in the structure as the prefix value, and the numeric address is canonicalized to clear the irrelevant bits. Thus, the solution looks like this:

(defun cidr-canon (str)
  (let ((a (inaddr-str str)))
    `@(str-inaddr a.addr)/@{a.prefix}`))

Furthermore, the prefix notation can be condensed by removing unnecessary zeros. That is to say, 10.1.2.3/16 can be not just canonicalized to strip the irrelevant bits, but then shortened to 10.1/16.

The built-in function inaddr-str-net will produce this condensed prefix notation:

(defun cidr-canon (str)
  (let ((a (inaddr-str str)))
    (str-inaddr-net a.addr a.prefix)))

This can be written using the flow macro:

(defun cidr-canon (str)
  (flow str
    inaddr-str
    (str-inaddr-net @1.addr @1.prefix)))

UNIX Shell

Works with: Bourne Again SHell
Works with: Korn Shell
Works with: Zsh
function inet_aton {
  typeset -i addr byte
  typeset -a bytes
  if [[ -n $BASH_VERSION ]]; then
    IFS=. read -a bytes <<<"$1"
  elif [[ -n $ZSH_VERSION ]]; then
    IFS=. bytes=($=1)
  else
    IFS=. bytes=($1)
  fi
  addr=0
  for byte in "${bytes[@]}"; do
    (( addr = 256 * addr + byte ))
  done
  printf '%s\n' "$addr"
}

function inet_ntoa {
  typeset -i addr=$1
  typeset dotted i
  for (( i=0; i<4; ++i )); do
    dotted=$(( addr & 255 ))${dotted:+.$dotted}
    (( addr >>= 8 ))
  done
  printf '%s\n' "$dotted"
}

function canonicalize_cidr {
  typeset ip prefix fixed
  typeset -i netmask  addr 
  while (( $# )); do
    IFS=/ read ip prefix <<<"$1"
    netmask=$(( (-1 << (32-prefix)) & -1 ))
    addr=$(inet_aton "$ip")
    fixed=$(( addr & netmask ))
    printf '%s/%s\n' "$(inet_ntoa $fixed)" "$prefix"
    shift
  done
}

# demo code
if (( ! $# )); then
  set -- 36.18.154.103/12 62.62.197.11/29 67.137.119.181/4 161.214.74.21/24 184.232.176.184/18
fi
canonicalize_cidr "$@"
Output:
36.16.0.0/12
62.62.197.8/29
64.0.0.0/4
161.214.74.0/24
184.232.128.0/18

Wren

Translation of: Ruby
Library: Wren-fmt
Library: Wren-str
import "./fmt" for Fmt, Conv
import "./str" for Str

// canonicalize a CIDR block: make sure none of the host bits are set
var canonicalize = Fn.new { |cidr|
    // dotted-decimal / bits in network part
    var split = cidr.split("/")
    var dotted = split[0]
    var size = Num.fromString(split[1])

    // get IP as binary string
    var binary = dotted.split(".").map { |n| Fmt.swrite("$08b", Num.fromString(n)) }.join()

    // replace the host part with all zeros
    binary = binary[0...size] + "0" * (32 - size)

    // convert back to dotted-decimal
    var chunks = Str.chunks(binary, 8)
    var canon = chunks.map { |c| Conv.atoi(c, 2) }.join(".")

    // and return
    return canon + "/" + split[1]
}

var tests = [
    "87.70.141.1/22",
    "36.18.154.103/12",
    "62.62.197.11/29",
    "67.137.119.181/4",
    "161.214.74.21/24",
    "184.232.176.184/18"
]

for (test in tests) {
    Fmt.print("$-18s -> $s", test, canonicalize.call(test))
}
Output:
87.70.141.1/22     -> 87.70.140.0/22
36.18.154.103/12   -> 36.16.0.0/12
62.62.197.11/29    -> 62.62.197.8/29
67.137.119.181/4   -> 64.0.0.0/4
161.214.74.21/24   -> 161.214.74.0/24
184.232.176.184/18 -> 184.232.128.0/18

XPL0

Device 8 is a 256-byte input/output buffer that provides a convenient means for converting between ASCII and binary representations of numbers.

proc Canonicalize(IPAddr);
char IPAddr;
int  N, I, HostBits;
[Text(0, IPAddr);
Text(0, "  ^i->   ");           \^i = tab
OpenO(8);
Text(8, IPAddr);                \ASCII out
OpenI(8);
N:= 0;
for I:= 0 to 3 do
    N:= N<<8 + IntIn(8);        \binary in
HostBits:= IntIn(8);
N:= N & -1<<(32-HostBits);
for I:= 3 downto 0 do
    [IntOut(0, N>>(I*8) & $FF);
    ChOut(0, if I = 0 then ^/ else ^.);
    ];
IntOut(0, HostBits);
CrLf(0);
];

int IPAddrs, I;
[IPAddrs:= [
    "87.70.141.1/22",
    "36.18.154.103/12",
    "62.62.197.11/29",
    "67.137.119.181/4",
    "161.214.74.21/24",
    "184.232.176.184/18" ];
for I:= 0 to 6-1 do
    Canonicalize(IPAddrs(I));
]
Output:
87.70.141.1/22          ->   87.70.140.0/22
36.18.154.103/12        ->   36.16.0.0/12
62.62.197.11/29         ->   62.62.197.8/29
67.137.119.181/4        ->   64.0.0.0/4
161.214.74.21/24        ->   161.214.74.0/24
184.232.176.184/18      ->   184.232.128.0/18
Cookies help us deliver our services. By using our services, you agree to our use of cookies.