A Practical Example For HoledRange

Someone asked me a non mathematical example of using the Domain thing, so here goes.

Let's imagine I am writing a network mapper or a load balancer of some description. I have a range of IPs that are available and I want to pick a few from them, or check that an IP is in a white or black list that's handled by ranges.

First I have to define what an IP address is (I went IPV4 because it's shorter): a group of 4 numbers from 0 to 255 (0 and 255 are "special", but it only affects a tiny portion of the code). I want to build them from that representation, strings (because we love strings) and a 32 bits number because that's what it ultimately is:

struct IPAddress {
    var group1: UInt8
    var group2: UInt8
    var group3: UInt8
    var group4: UInt8
    
    init(_ c1: UInt8, _ c2: UInt8, _ c3: UInt8, _ c4: UInt8) {
        group1 = c1
        group2 = c2
        group3 = c3
        group4 = c4
    }
    
    init?(_ s: String) {
        let comps = s.components(separatedBy: ".")
        if comps.count != 4 { return nil }
        if let c1 = UInt8(comps[0]), let c2 = UInt8(comps[1]), let c3 = UInt8(comps[2]), let c4 = UInt8(comps[3]) {
            group1 = c1
            group2 = c2
            group3 = c3
            group4 = c4
        } else {
            return nil
        }
    }
    
    init(_ ip: UInt32) {
        let c1 = ip >> 24
        let c2 = (ip >> 16) & 0x00ff
        let c3 = (ip >> 8) & 0x0000ff
        let c4 = ip & 0x000000ff
        group1 = UInt8(c1)
        group2 = UInt8(c2)
        group3 = UInt8(c3)
        group4 = UInt8(c4)
    }
    
    var asString : String {
        return "\(group1).\(group2).\(group3).\(group4)"
    }
    
    var asUInt32 : UInt32 {
        let c1 = UInt32(group1) << 24
        let c2 = UInt32(group2) << 16
        let c3 = UInt32(group3) << 8
        let c4 = UInt32(group4)
        
        return c1 | c2 | c3 | c4
    }
}

I also included to string and to 32 bits because I could, and because it's generally good practice to be able to output what you accept as input.

If I want to make a domain out of that, they need to be Hashable, and Comparable:

extension IPAddress : Equatable, Comparable, Hashable {
    static func == (lhs : Self, rhs : Self) -> Bool {
        return lhs.group1 == rhs.group1 && lhs.group2 == rhs.group2 && lhs.group2 == rhs.group3 && lhs.group4 == rhs.group4
    }
    static func != (lhs : Self, rhs : Self) -> Bool {
        return !(lhs == rhs)
    }
    static func < (lhs : Self, rhs : Self) -> Bool {
        if lhs.group1 < rhs.group1 { return true }
        else if lhs.group1 == rhs.group1 && lhs.group2 < rhs.group2 { return true }
        else if lhs.group1 == rhs.group1 && lhs.group2 == rhs.group2 && lhs.group3 < rhs.group3 { return true }
        else if lhs.group1 == rhs.group1 && lhs.group2 == rhs.group2 && lhs.group3 == rhs.group3 && lhs.group4 < rhs.group4 { return true }
        else { return false }
    }
    static func <= (lhs : Self, rhs : Self) -> Bool {
        return lhs == rhs || lhs < rhs
    }
    static func > (lhs : Self, rhs : Self) -> Bool {
        if lhs.group1 > rhs.group1 { return true }
        else if lhs.group1 == rhs.group1 && lhs.group2 > rhs.group2 { return true }
        else if lhs.group1 == rhs.group1 && lhs.group2 == rhs.group2 && lhs.group3 > rhs.group3 { return true }
        else if lhs.group1 == rhs.group1 && lhs.group2 == rhs.group2 && lhs.group3 == rhs.group3 && lhs.group4 > rhs.group4 { return true }
        else { return false }
        
    }
    static func >= (lhs : Self, rhs : Self) -> Bool {
        return lhs == rhs || lhs > rhs
    }
    
    func hash(into: inout Hasher) {
        into.combine(group1)
        into.combine(group2)
        into.combine(group3)
        into.combine(group4)
    }
    
    var hashValue: Int {
        return Int(self.asUInt32)
    }
}

This stuff isn't quite boilerplate, but not very far from it. Comparing two IP addresses is about comparing the 4 groups of numbers, in order of importance.

Finally, because I want to sample my ranges, I'd like a Randomizable implementation as well, which is more interesting than it seemed at first glance:

extension IPAddress : Randomizable {
    static func randomElement() -> IPAddress? {
        
        let c1 = UInt8.random(in: 1...254)
        let c2 = UInt8.random(in: 1...254)
        let c3 = UInt8.random(in: 1...254)
        let c4 = UInt8.random(in: 1...254)
        
        return IPAddress(c1,c2,c3,c4)
        
    }
    static func randomElement(in r: ClosedRange<IPAddress>) -> IPAddress? {
        let lb = r.lowerBound
        let ub = r.upperBound
        
        if lb == ub { return lb }
        if ub < lb { return nil }
        
        let c1 = UInt8.random(in: lb.group1...ub.group1)
        let c2: UInt8
        let c3 : UInt8
        let c4 : UInt8
        
        if c1 == lb.group1 || c1 == ub.group1 { // special case , because we have to check the more minor parts of the group as well
            if c1 == lb.group1 && c1 == ub.group1 { // same group 1 for upper and lower bounds
                c2 = UInt8.random(in: lb.group2...ub.group2)
            } else if c1 == lb.group1 {
                c2 = UInt8.random(in: lb.group2...254)
            } else { //  if c1 == ub.group1
                c2 = UInt8.random(in: 1...ub.group2)
            }
            // same problem, again
            if c2 == lb.group2 || c2 == ub.group2 {
                if c2 == lb.group2 && c2 == ub.group2 {
                    c3 = UInt8.random(in: lb.group3...ub.group3)
                } else if c2 == lb.group2 {
                    c3 = UInt8.random(in: lb.group3...254)
                } else {
                    c3 = UInt8.random(in: 1...ub.group3)
                }
                // and finally
                if c3 == lb.group3 || c3 == ub.group3 {
                    if c3 == lb.group3 && c3 == ub.group3 {
                        c4 = UInt8.random(in: lb.group4...ub.group4)
                    } else if c3 == lb.group3 {
                        c4 = UInt8.random(in: lb.group4...254)
                    } else {
                        c4 = UInt8.random(in: 1...ub.group4)
                    }
                } else {
                     c4 = UInt8.random(in: 1...254)
                }
            } else {
                c3 = UInt8.random(in: 1...254)
                c4 = UInt8.random(in: 1...254)
            }
        } else {
            c2 = UInt8.random(in: 1...254)
            c3 = UInt8.random(in: 1...254)
            c4 = UInt8.random(in: 1...254)
            
        }
        
        return IPAddress(c1, c2, c3, c4)
    }
}

As for why random IPs don't contain 0 or 255, it's because they are special in some cases and I wanted to spare myself the headache. Note the hierarchy of groups as well for randomization...

Finally a test program:

       if let ip = IPAddress.randomElement(), let ip2 = IPAddress.randomElement() {
            let mi = min(ip,ip2)
            let ma = max(ip,ip2)
            let d = Domain(mi...ma)
            if let ip3 = d.randomSample(5) {
                print("["+ip.asString+";"+ip2.asString+"] => \(ip3.map({ $0.asString }))")
            }
        }

        let d = Domain(IPAddress(192,168,125,1)...IPAddress(192,168,125,10))
        if let ip3 = d.randomSample(5) {
            print("["+d.lowerBound!.asString+";"+d.upperBound!.asString+"] => \(ip3.map({ $0.asString }))")
        }
[58.47.159.20;110.200.52.86] => ["92.196.40.9", "89.184.245.202", "69.19.204.59", "101.41.128.20", "74.231.21.246"]
[192.168.125.1;192.168.125.10] => ["192.168.125.4", "192.168.125.1", "192.168.125.9", "192.168.125.3", "192.168.125.10"]

Seems legit.