Repository: inetaf/netaddr (go)

Issue:
proposal: add an IP address classification mechanism
I make pretty frequent use of a few of the stdlib's IP address classification methods:

```
    func (ip IP) IsGlobalUnicast() bool
    func (ip IP) IsInterfaceLocalMulticast() bool
    func (ip IP) IsLinkLocalMulticast() bool
    func (ip IP) IsLinkLocalUnicast() bool
    func (ip IP) IsLoopback() bool
    func (ip IP) IsMulticast() bool
    func (ip IP) IsUnspecified() bool
```

I'm not sure all of these are necessary in something like netaddr, but I do think there should be a mechanism for determing the properties of an address.

I came up with a few ideas:

1) add methods like the stdlib does today

The unfortunate thing here is that you end up with a potentially more bloated API, and it's difficult to draw the line on where to stop adding methods. What about IPv6 unique local addresses for example?

2) expose a bit flags API and a set of constants, akin to net.Flags

This method is pretty concise but may require a bit of processing to return the bit flags set. Once the flags are returned, you have to check them with bit ops which @danderson and I both tend to find unappealing.

```go
ip := mustIP("fe80::1")
if ip.Properties()&netaddr.LinkLocalUnicast != 0 {
    fmt.Println("is link-local unicast")
}
```

3) return a struct with boolean fields for a more concise API and easier use

The disadvantage here is the need to allocate a structure and do some work every time the method would be called.

```go
ip := mustIP("fe80::1")
if ip.Properties().LinkLocalUnicast {
    fmt.Println("is link-local unicast")
}
```

---

Any other ideas?

I am leaning toward option 1 (do what stdlib does) as it seems to be the most reasonable tradeoff between readability, performance, and allocations, even if it means sacrificing some conciseness in the exported API.

Pull request description:
netaddr: add IP.IsMulticast method
Signed-off-by: Matt Layher <mdlayher@gmail.com>

This is all I need at the moment. The test can be expanded with more conditions later, or broken into smaller tests. Either way is fine by me.

Fixes #5.

Patch:
diff --git a/netaddr.go b/netaddr.go
index 0eceddc..39317ff 100644
--- a/netaddr.go
+++ b/netaddr.go
@@ -138,6 +138,24 @@ func (ip IP) Is6() bool {
 	return ip.ipImpl.is6()
 }
 
+// IsMulticast reports whether ip is a multicast address. If ip is the zero
+// value, it will return false.
+func (ip IP) IsMulticast() bool {
+	// See: https://en.wikipedia.org/wiki/Multicast_address.
+	switch ip := ip.ipImpl.(type) {
+	case nil:
+		return false
+	case v4Addr:
+		return ip[0]&0xf0 == 0xe0
+	case v6Addr:
+		return ip[0] == 0xff
+	case v6AddrZone:
+		return ip.v6Addr[0] == 0xff
+	default:
+		panic("netaddr: unhandled ipImpl representation")
+	}
+}
+
 // String returns the string representation of ip.
 func (ip IP) String() string {
 	if ip.ipImpl == nil {


Test patch:
diff --git a/netaddr_test.go b/netaddr_test.go
index 95b9342..13de3fe 100644
--- a/netaddr_test.go
+++ b/netaddr_test.go
@@ -83,6 +83,69 @@ func TestIPIPAddr(t *testing.T) {
 	}
 }
 
+func TestIPProperties(t *testing.T) {
+	// TODO: expand test with more Is* property checks.
+
+	var (
+		nilIP IP
+
+		unicast4     = mustIP("192.0.2.1")
+		unicast6     = mustIP("2001:db::1")
+		unicastZone6 = mustIP("fe80::1%eth0")
+
+		multicast4     = mustIP("224.0.0.1")
+		multicast6     = mustIP("ff02::1")
+		multicastZone6 = mustIP("ff02::1%eth0")
+	)
+
+	tests := []struct {
+		name      string
+		ip        IP
+		multicast bool
+	}{
+		{
+			name: "nil",
+			ip:   nilIP,
+		},
+		{
+			name: "unicast v4Addr",
+			ip:   unicast4,
+		},
+		{
+			name: "unicast v6Addr",
+			ip:   unicast6,
+		},
+		{
+			name: "unicast v6AddrZone",
+			ip:   unicastZone6,
+		},
+		{
+			name:      "multicast v4Addr",
+			ip:        multicast4,
+			multicast: true,
+		},
+		{
+			name:      "multicast v6Addr",
+			ip:        multicast6,
+			multicast: true,
+		},
+		{
+			name:      "multicast v6AddrZone",
+			ip:        multicastZone6,
+			multicast: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			multicast := tt.ip.IsMulticast()
+			if multicast != tt.multicast {
+				t.Errorf("IsMulticast = %v; want %v", multicast, tt.multicast)
+			}
+		})
+	}
+}
+
 func mustIP(s string) IP {
 	ip, err := ParseIP(s)
 	if err != nil {