Canonicalize CIDR

From Rosetta Code
Revision as of 00:26, 8 August 2022 by Steenslag (talk | contribs) (→‎{{header|Ruby}}: extended test set)
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 represented by the digit value 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; it's common to see access control lists specify a single IP address using CIDR with /32 to indicate that only the one address is included. Often, the tools using this notation expect the address to be entered in canonical form, in which the "host" bits are all zeroes in the binary representation. But careless network admins may provide CIDR blocks without canonicalizing them first. This task handles the canonicalization.

The example address, 87.70.141.1, translates into 01010111010001101000110100000001 in binary notation zero-padded to 32 bits. The /22 means that the first 22 of those bits determine the match; the final 10 bits should be 0. But they instead include two 1 bits: 0100000001. So to canonicalize the address, change those 1's to 0's to yield 01010111010001101000110000000000, which in dotted-decimal is 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

<lang 11l>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)))</lang>
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

<lang algol68>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</lang>

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

<lang apl> canonicalize←{

   nums←(2⊃⎕VFI)¨(~⍵∊'./')⊆⍵
   ip len←(4↑nums)(5⊃nums)
   ip←(32/2)⊤256⊥⊃¨ip
   ip←ip∧32↑len⍴1
   ip←(4/256)⊤2⊥ip
   (1↓∊'.',¨⍕¨ip),'/',⍕len
}</lang>
Output:

<lang apl> canonicalize '87.70.141.1/22' 87.70.140.0/22</lang>

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. <lang c>#include <stdbool.h>

  1. include <stdio.h>
  2. 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;

}</lang>

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++

<lang cpp>#include <cstdint>

  1. include <iomanip>
  2. include <iostream>
  3. 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;

}</lang>

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#

<lang csharp>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}";

}</lang>

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

<lang 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))

</lang>

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

<lang 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();</lang>

Output:
Before canonicalization: 87.70.141.1/22
 After canonicalization: 87.70.140.0/22

Factor

Translation of: Ruby
Works with: Factor version 0.99 2020-07-03

<lang factor>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</lang>

Output:
$ canonicalize-cidr.factor 87.70.141.1/22
87.70.140.0/22

Go

Translation of: Ruby

<lang go>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))
   }

}</lang>

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

<lang 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); };</lang>

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. <lang haskell>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</lang>
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:

<lang J>cidr=: {{

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

}}</lang>

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:

<lang J> 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</lang>

Java

<lang 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"
   };

}</lang>

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

<lang 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)</lang>
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

Julia

Julia has a Sockets library as a builtin, which has the types IPv4 and IPv6 for single IP addresses. <lang julia>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"))

</lang>

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

Mathematica/Wolfram Language

<lang Mathematica>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"]</lang>

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

Nim

Using the IpAddress type from standard module “net”. <lang Nim>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
  1. ———————————————————————————————————————————————————————————————————————————————————————————————————

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}"</lang>
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

Perl

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

  1. 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";

}</lang>

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

<lang 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)) ) )</lang>
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

<lang python>#!/usr/bin/env python

  1. canonicalize a CIDR block specification:
  2. 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</lang>
Output:
$ canonicalize_cidr.py 87.70.141.1/22
87.70.140.0/22

Raku

String manipulation

Translation of: Perl

<lang perl6>#!/usr/bin/env raku

  1. canonicalize a CIDR block: make sure none of the host bits are set

if (!@*ARGS) {

  @*ARGS = $*IN.lines;

}

for @*ARGS -> $cidr {

 # dotted-decimal / bits in network part
 my ($dotted, $size) = $cidr.split('/');
 # get IP as binary string
 my $binary = $dotted.split('.').map(*.fmt("%08b")).join;
 # Replace the host part with all zeroes
 $binary.substr-rw($size) = 0 x (32 - $size);
 # Convert back to dotted-decimal
 my $canon = $binary.comb(8).map(*.join.parse-base(2)).join('.');
 # And output
 say "$canon/$size";

}</lang>

Output:
$ canonicalize_cidr.raku 87.70.141.1/22
87.70.140.0/22

Bit mask and shift

<lang perl6># 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;</lang>
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

<lang 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 " */</lang>

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 

Ruby

Translation of: Python
Translation of: Raku

<lang ruby>#!/usr/bin/env ruby

  1. 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</lang>

Output:
$ canonicalize_cidr.rb 87.70.141.1/22
87.70.140.0/22

Built in

<lang ruby> 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 </lang>

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

<lang 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)
   }

}

  1. [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());

}</lang>

Wren

Translation of: Ruby
Library: Wren-fmt
Library: Wren-str

<lang ecmascript>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))

}</lang>

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