diff --git a/LICENSE b/LICENSE-Apache-2.0 similarity index 100% rename from LICENSE rename to LICENSE-Apache-2.0 diff --git a/LICENSE-BSD-2-Clause b/LICENSE-BSD-2-Clause new file mode 100644 index 0000000..f4a884e --- /dev/null +++ b/LICENSE-BSD-2-Clause @@ -0,0 +1,23 @@ +Copyright (c) 2015, Emir Pasic +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE-BSD-3-Clause b/LICENSE-BSD-3-Clause new file mode 100644 index 0000000..6a66aea --- /dev/null +++ b/LICENSE-BSD-3-Clause @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/LICENSE-EPL-1.0 b/LICENSE-EPL-1.0 new file mode 100644 index 0000000..5d80026 --- /dev/null +++ b/LICENSE-EPL-1.0 @@ -0,0 +1,227 @@ +Eclipse Public License - v 1.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF + THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + + 1. DEFINITIONS + + "Contribution" means: + + a) in the case of the initial Contributor, the initial code and + documentation distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + + i) changes to the Program, and + + ii) additions to the Program; + + where such changes and/or additions to the Program originate from and + are distributed by that particular Contributor. A Contribution + 'originates' from a Contributor if it was added to the Program by such + Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include additions to the Program which: (i) are + separate modules of software distributed in conjunction with the + Program under their own license agreement, and (ii) are not derivative + works of the Program. + + "Contributor" means any person or entity that distributes the Program. + + "Licensed Patents" mean patent claims licensable by a Contributor which + are necessarily infringed by the use or sale of its Contribution alone + or when combined with the Program. + + "Program" means the Contributions distributed in accordance with this + Agreement. + + "Recipient" means anyone who receives the Program under this Agreement, + including all Contributors. + + 2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare derivative works of, publicly display, + publicly perform, distribute and sublicense the Contribution of such + Contributor, if any, and such derivative works, in source code and + object code form. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, if + any, in source code and object code form. This patent license shall + apply to the combination of the Contribution and the Program if, at the + time the Contribution is added by the Contributor, such addition of the + Contribution causes such combination to be covered by the Licensed + Patents. The patent license shall not apply to any other combinations + which include the Contribution. No hardware per se is licensed + hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. Each + Contributor disclaims any liability to Recipient for claims brought by + any other entity based on infringement of intellectual property rights + or otherwise. As a condition to exercising the rights and licenses + granted hereunder, each Recipient hereby assumes sole responsibility to + secure any other intellectual property rights needed, if any. For + example, if a third party patent license is required to allow Recipient + to distribute the Program, it is Recipient's responsibility to acquire + that license before distributing the Program. + + d) Each Contributor represents that to its knowledge it has sufficient + copyright rights in its Contribution, if any, to grant the copyright + license set forth in this Agreement. + + 3. REQUIREMENTS + + A Contributor may choose to distribute the Program in object code form + under its own license agreement, provided that: + + a) it complies with the terms and conditions of this Agreement; and + + b) its license agreement: + + i) effectively disclaims on behalf of all Contributors all warranties + and conditions, express and implied, including warranties or conditions + of title and non-infringement, and implied warranties or conditions of + merchantability and fitness for a particular purpose; + + ii) effectively excludes on behalf of all Contributors all liability + for damages, including direct, indirect, special, incidental and + consequential damages, such as lost profits; + + iii) states that any provisions which differ from this Agreement are + offered by that Contributor alone and not by any other party; and + + iv) states that source code for the Program is available from such + Contributor, and informs licensees how to obtain it in a reasonable + manner on or through a medium customarily used for software exchange. + + When the Program is made available in source code form: + + a) it must be made available under this Agreement; and + + b) a copy of this Agreement must be included with each copy of the + Program. + + Contributors may not remove or alter any copyright notices contained + within the Program. + + Each Contributor must identify itself as the originator of its + Contribution, if any, in a manner that reasonably allows subsequent + Recipients to identify the originator of the Contribution. + + 4. COMMERCIAL DISTRIBUTION + + Commercial distributors of software may accept certain responsibilities + with respect to end users, business partners and the like. While this + license is intended to facilitate the commercial use of the Program, + the Contributor who includes the Program in a commercial product + offering should do so in a manner which does not create potential + liability for other Contributors. Therefore, if a Contributor includes + the Program in a commercial product offering, such Contributor + ("Commercial Contributor") hereby agrees to defend and indemnify every + other Contributor ("Indemnified Contributor") against any losses, + damages and costs (collectively "Losses") arising from claims, lawsuits + and other legal actions brought by a third party against the + Indemnified Contributor to the extent caused by the acts or omissions + of such Commercial Contributor in connection with its distribution of + the Program in a commercial product offering. The obligations in this + section do not apply to any claims or Losses relating to any actual or + alleged intellectual property infringement. In order to qualify, an + Indemnified Contributor must: a) promptly notify the Commercial + Contributor in writing of such claim, and b) allow the Commercial + Contributor to control, and cooperate with the Commercial Contributor + in, the defense and any related settlement negotiations. The + Indemnified Contributor may participate in any such claim at its own + expense. + + For example, a Contributor might include the Program in a commercial + product offering, Product X. That Contributor is then a Commercial + Contributor. If that Commercial Contributor then makes performance + claims, or offers warranties related to Product X, those performance + claims and warranties are such Commercial Contributor's responsibility + alone. Under this section, the Commercial Contributor would have to + defend claims against the other Contributors related to those + performance claims and warranties, and if a court requires any other + Contributor to pay any damages as a result, the Commercial Contributor + must pay those damages. + + 5. NO WARRANTY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS + PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY + WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR + FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible + for determining the appropriateness of using and distributing the + Program and assumes all risks associated with its exercise of rights + under this Agreement , including but not limited to the risks and costs + of program errors, compliance with applicable laws, damage to or loss + of data, programs or equipment, and unavailability or interruption of + operations. + + 6. DISCLAIMER OF LIABILITY + + EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR + ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, + INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING + WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR + DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED + HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 7. GENERAL + + If any provision of this Agreement is invalid or unenforceable under + applicable law, it shall not affect the validity or enforceability of + the remainder of the terms of this Agreement, and without further + action by the parties hereto, such provision shall be reformed to the + minimum extent necessary to make such provision valid and enforceable. + + If Recipient institutes patent litigation against any entity (including + a cross-claim or counterclaim in a lawsuit) alleging that the Program + itself (excluding combinations of the Program with other software or + hardware) infringes such Recipient's patent(s), then such Recipient's + rights granted under Section 2(b) shall terminate as of the date such + litigation is filed. + + All Recipient's rights under this Agreement shall terminate if it fails + to comply with any of the material terms or conditions of this + Agreement and does not cure such failure in a reasonable period of time + after becoming aware of such noncompliance. If all Recipient's rights + under this Agreement terminate, Recipient agrees to cease use and + distribution of the Program as soon as reasonably practicable. However, + Recipient's obligations under this Agreement and any licenses granted + by Recipient relating to the Program shall continue and survive. + + Everyone is permitted to copy and distribute copies of this Agreement, + but in order to avoid inconsistency the Agreement is copyrighted and + may only be modified in the following manner. The Agreement Steward + reserves the right to publish new versions (including revisions) of + this Agreement from time to time. No one other than the Agreement + Steward has the right to modify this Agreement. The Eclipse Foundation + is the initial Agreement Steward. The Eclipse Foundation may assign the + responsibility to serve as the Agreement Steward to a suitable separate + entity. Each new version of the Agreement will be given a + distinguishing version number. The Program (including Contributions) + may always be distributed subject to the version of the Agreement under + which it was received. In addition, after a new version of the + Agreement is published, Contributor may elect to distribute the Program + (including its Contributions) under the new version. Except as + expressly stated in Sections 2(a) and 2(b) above, Recipient receives no + rights or licenses to the intellectual property of any Contributor + under this Agreement, whether expressly, by implication, estoppel or + otherwise. All rights in the Program not expressly granted under this + Agreement are reserved. + + This Agreement is governed by the laws of the State of New York and the + intellectual property laws of the United States of America. No party to + this Agreement will bring a legal action under this Agreement more than + one year after the cause of action arose. Each party waives its rights + to a jury trial in any resulting litigation. diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..e95533a --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,41 @@ +# [Apache License 2.0](https://spdx.org/licenses/Apache-2.0.html) + +Copyright (c) Puppet, Inc. + +A copy of the full license document is included in this distribution in the file +`LICENSE-Apache-2.0`. + +# Component licenses + +Horsehead contains a number of components and subcomponents, some of which are +distributed under licenses compatible with, or complementary to, the Apache +License 2.0. Certain provisions of these component and subcomponent licenses may +place additional burden on distributing Horsehead in source code or binary form. + +## datastructure + +### `container.go`, `map.go`, `set.go` + +Copyright (c) 2015, Emir Pasic. All rights reserved. + +Licensed under a two-clause BSD-style license. A copy of the full license +document is included in this distribution in the file `LICENSE-BSD-2-Clause`. + +### `priority_queue.go` + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Licensed under a three-clause BSD-style license. A copy of the full license +document is included in this distribution in the file `LICENSE-BSD-3-Clause`. + +## graph + +The graph library is licensed under the Eclipse Public License 1.0. A copy of +the full license document is included in this distribution in the file +`LICENSE-EPL-1.0`. + +### `algo/approx_steiner_tree.go` + +Copyright 2012 University of Southern California. + +Licensed under the Apache License 2.0. diff --git a/datastructure/container.go b/datastructure/container.go new file mode 100644 index 0000000..499e76f --- /dev/null +++ b/datastructure/container.go @@ -0,0 +1,36 @@ +// Portions of this file are derived from GoDS, a data structure library for +// Go. +// +// Copyright (c) 2015, Emir Pasic. All rights reserved. +// +// https://github.com/emirpasic/gods/blob/213367f1ca932600ce530ae11c8a8cc444e3a6da/containers/containers.go + +package datastructure + +// Container represents any type with elements, such as a map, list, or set. +type Container interface { + // Empty returns true if this container is empty; false otherwise. + // + // The result of this function is equivalent to the test Size() == 0, but + // certain data structures that do not have an O(1) Size() implementation + // may optimize this function. + Empty() bool + + // Size returns the number of values in this container. + Size() int + + // Clear resets this container to its zero-value state. + Clear() + + // Values returns the values from this container as a slice of interface{}. + Values() []interface{} + + // ValuesInto inserts the values from this container into the given slice. + // The type of the into parameter must be a pointer to a slice for which + // each value must be assignable by the type of every value in this + // container. + // + // If the requirements for the into parameter are not met, this function + // will panic. + ValuesInto(into interface{}) +} diff --git a/datastructure/errors.go b/datastructure/errors.go new file mode 100644 index 0000000..b77a354 --- /dev/null +++ b/datastructure/errors.go @@ -0,0 +1,15 @@ +package datastructure + +import ( + "errors" +) + +var ( + // ErrStopIteration causes a ForEach() or ForEachInto() loop to terminate + // early. + ErrStopIteration = errors.New("datastructure: stop iteration") + + // ErrInvalidFuncSignature is raised in a panic() if a function passed to a + // ForEachInto() method does not conform to the expected interface. + ErrInvalidFuncSignature = errors.New("datastructure: invalid function signature") +) diff --git a/datastructure/hash_map.go b/datastructure/hash_map.go new file mode 100644 index 0000000..379b398 --- /dev/null +++ b/datastructure/hash_map.go @@ -0,0 +1,89 @@ +package datastructure + +// HashMap is a simple hash map implementation, backed by the built-in map type +// in Go. +type HashMap map[interface{}]interface{} + +func (m HashMap) Contains(key interface{}) (found bool) { + _, found = m[key] + return +} + +func (m HashMap) Put(key, value interface{}) (found bool) { + found = m.Contains(key) + m[key] = value + + return +} + +func (m HashMap) Get(key interface{}) (value interface{}, found bool) { + value, found = m[key] + return +} + +func (m *HashMap) GetInto(key, into interface{}) bool { + return mapGetInto(m, key, into) +} + +func (m HashMap) Remove(key interface{}) (found bool) { + found = m.Contains(key) + delete(m, key) + + return +} + +func (m HashMap) Empty() bool { + return m.Size() == 0 +} + +func (m HashMap) Size() int { + return len(m) +} + +func (m *HashMap) Clear() { + *m = make(HashMap) +} + +func (m *HashMap) Keys() []interface{} { + return mapKeys(m) +} + +func (m *HashMap) KeysInto(into interface{}) { + mapKeysInto(m, into) +} + +func (m *HashMap) Values() []interface{} { + return mapValues(m) +} + +func (m *HashMap) ValuesInto(into interface{}) { + mapValuesInto(m, into) +} + +func (m HashMap) ForEach(fn MapIterationFunc) error { + for key, value := range m { + if err := fn(key, value); err != nil { + return err + } + } + + return nil +} + +func (m *HashMap) ForEachInto(fn interface{}) error { + return mapForEachInto(m, fn) +} + +// NewHashMap creates a new hash map with the default initial capacity of this +// Go implementation. +func NewHashMap() *HashMap { + m := make(HashMap) + return &m +} + +// NewHashMapWithCapacity creates a new hash map with the specified initial +// capacity. +func NewHashMapWithCapacity(capacity int) *HashMap { + m := make(HashMap, capacity) + return &m +} diff --git a/datastructure/linked_hash_map.go b/datastructure/linked_hash_map.go new file mode 100644 index 0000000..8e10bac --- /dev/null +++ b/datastructure/linked_hash_map.go @@ -0,0 +1,123 @@ +package datastructure + +import ( + "container/list" +) + +type linkedHashMapEntry struct { + key, value interface{} +} + +// LinkedHashMap is hash map that iterates its entries in the order they were +// inserted into the map. Calls to Put() for a key that already exists in the +// map will not change its insertion order. +type LinkedHashMap struct { + accessor *list.List + storage map[interface{}]*list.Element +} + +func (m *LinkedHashMap) Contains(key interface{}) (found bool) { + _, found = m.storage[key] + return +} + +func (m *LinkedHashMap) Put(key, value interface{}) (found bool) { + if _, found = m.storage[key]; !found { + entry := &linkedHashMapEntry{key, value} + + e := m.accessor.PushBack(entry) + m.storage[key] = e + } else { + entry := m.storage[key].Value.(*linkedHashMapEntry) + entry.value = value + } + + return +} + +func (m *LinkedHashMap) Get(key interface{}) (value interface{}, found bool) { + var e *list.Element + + if e, found = m.storage[key]; found { + value = e.Value.(*linkedHashMapEntry).value + } + + return +} + +func (m *LinkedHashMap) GetInto(key, into interface{}) bool { + return mapGetInto(m, key, into) +} + +func (m *LinkedHashMap) Remove(key interface{}) (found bool) { + var e *list.Element + + if e, found = m.storage[key]; found { + m.accessor.Remove(e) + delete(m.storage, key) + } + + return +} + +func (m *LinkedHashMap) Empty() bool { + return m.Size() == 0 +} + +func (m *LinkedHashMap) Size() int { + return len(m.storage) +} + +func (m *LinkedHashMap) Clear() { + m.accessor.Init() + m.storage = make(map[interface{}]*list.Element) +} + +func (m *LinkedHashMap) Keys() []interface{} { + return mapKeys(m) +} + +func (m *LinkedHashMap) KeysInto(into interface{}) { + mapKeysInto(m, into) +} + +func (m *LinkedHashMap) Values() []interface{} { + return mapValues(m) +} + +func (m *LinkedHashMap) ValuesInto(into interface{}) { + mapValuesInto(m, into) +} + +func (m *LinkedHashMap) ForEach(fn MapIterationFunc) error { + for e := m.accessor.Front(); e != nil; e = e.Next() { + entry := e.Value.(*linkedHashMapEntry) + if err := fn(entry.key, entry.value); err != nil { + return err + } + } + + return nil +} + +func (m *LinkedHashMap) ForEachInto(fn interface{}) error { + return mapForEachInto(m, fn) +} + +// NewLinkedHashMap creates a new linked hash map with the default initial +// capacity of this Go implementation. +func NewLinkedHashMap() *LinkedHashMap { + return &LinkedHashMap{ + accessor: list.New(), + storage: make(map[interface{}]*list.Element), + } +} + +// NewLinkedHashMapWithCapacity creates a new linked hash map with the specified +// initial capacity. +func NewLinkedHashMapWithCapacity(capacity int) *LinkedHashMap { + return &LinkedHashMap{ + accessor: list.New(), + storage: make(map[interface{}]*list.Element, capacity), + } +} diff --git a/datastructure/linked_hash_map_test.go b/datastructure/linked_hash_map_test.go new file mode 100644 index 0000000..6164cc2 --- /dev/null +++ b/datastructure/linked_hash_map_test.go @@ -0,0 +1,34 @@ +package datastructure + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestLinkedHashMapIterationOrder(t *testing.T) { + m := NewLinkedHashMap() + + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + keys := []interface{}{"a", "b", "c"} + values := []interface{}{1, 2, 3} + + for ti := 0; ti < 10; ti++ { + // Make sure the order stays the same! + m.Put("b", 2) + + assert.Equal(t, keys, m.Keys()) + assert.Equal(t, values, m.Values()) + + i := 0 + m.ForEach(func(key, value interface{}) error { + assert.Equal(t, keys[i], key) + assert.Equal(t, values[i], value) + i++ + + return nil + }) + } +} diff --git a/datastructure/map.go b/datastructure/map.go new file mode 100644 index 0000000..d6d18c3 --- /dev/null +++ b/datastructure/map.go @@ -0,0 +1,75 @@ +// Portions of this file are derived from GoDS, a data structure library for +// Go. +// +// Copyright (c) 2015, Emir Pasic. All rights reserved. +// +// https://github.com/emirpasic/gods/blob/52d942a0538c185239fa3737047f297d983ac3e0/maps/maps.go + +package datastructure + +type MapIterationFunc func(key, value interface{}) error + +// Map represents a key-to-value mapping, where keys are unique. +type Map interface { + Container + + // Contains returns true if the given key exists in this map. + Contains(key interface{}) bool + + // Put adds the given key and value to the map. If the value already exists + // in the map, the old value is overwritten by the specified value. + // + // Returns true if this map already contained an entry for this key, and + // false otherwise. + Put(key, value interface{}) bool + + // Get retrieves the value associated with the given key from the map. + // + // Returns the value, or nil if the key does not exist in the map, and a + // boolean indicating whether the key exists. + Get(key interface{}) (interface{}, bool) + + // GetInto retrieves the value associated with the given key from the map + // and stores it in the into parameter passed to this function. + // + // The into parameter must be a pointer to a type assignable by the stored + // value. If the given key does not exist in the map, the into parameter is + // not modified. + // + // If the into parameter is not compatible with the stored value, this + // function will panic. + // + // Returns true if the key exists, and false otherwise. + GetInto(key interface{}, into interface{}) bool + + // Remove eliminates the given key from the map, and returns true if the key + // existed in the map. + Remove(key interface{}) bool + + // Keys returns the keys from this map as a slice of interface{}. + Keys() []interface{} + + // KeysInto inserts the keys from this map into the given slice. The type of + // the into parameter must be a pointer to a slice for which each value must + // be assignable by the type of every key in this map. + // + // If the requirements for the into parameter are not met, this function + // will panic. + KeysInto(into interface{}) + + // ForEach iterates each key-value pair in the map and executes the given + // callback function. If the callback function returns an error, this + // function will return the same error and immediately stop iteration. + // + // To stop iteration without returning an error, return ErrStopIteration. + ForEach(fn MapIterationFunc) error + + // ForEachInto iterates each key-value pair in the map and executes the + // given callback function, which must be of a type similar to + // MapIterationFunc, except that the key and value parameters may be any + // type assignable by every key and value in the map, respectively. + // + // If the requirements for the fn parameter are not met, this function will + // panic. + ForEachInto(fn interface{}) error +} diff --git a/datastructure/map_set.go b/datastructure/map_set.go new file mode 100644 index 0000000..4186e49 --- /dev/null +++ b/datastructure/map_set.go @@ -0,0 +1,123 @@ +package datastructure + +// MapBackedSet is a set that stores its elements as keys in a given map. +type MapBackedSet struct { + storage Map +} + +var mapSetValue = struct{}{} + +func (s *MapBackedSet) Contains(elements ...interface{}) bool { + for _, element := range elements { + if !s.storage.Contains(element) { + return false + } + } + + return true +} + +func (s *MapBackedSet) Add(elements ...interface{}) { + for _, element := range elements { + s.storage.Put(element, mapSetValue) + } +} + +func (s *MapBackedSet) AddAll(other Container) { + s.Add(other.Values()...) +} + +func (s *MapBackedSet) Remove(elements ...interface{}) { + for _, element := range elements { + s.storage.Remove(element) + } +} + +func (s *MapBackedSet) RemoveAll(other Set) { + var remove []interface{} + + s.ForEach(func(element interface{}) error { + if other.Contains(element) { + remove = append(remove, element) + } + + return nil + }) + + for _, element := range remove { + s.Remove(element) + } +} + +func (s *MapBackedSet) Empty() bool { + return s.storage.Empty() +} + +func (s *MapBackedSet) Size() int { + return s.storage.Size() +} + +func (s *MapBackedSet) Clear() { + s.storage.Clear() +} + +func (s *MapBackedSet) Values() []interface{} { + return s.storage.Keys() +} + +func (s *MapBackedSet) ValuesInto(into interface{}) { + setValuesInto(s, into) +} + +func (s *MapBackedSet) ForEach(fn SetIterationFunc) error { + return s.storage.ForEach(func(key, value interface{}) error { + return fn(key) + }) +} + +func (s *MapBackedSet) ForEachInto(fn interface{}) error { + return setForEachInto(s, fn) +} + +func (s *MapBackedSet) RetainAll(other Set) { + var remove []interface{} + + s.ForEach(func(element interface{}) error { + if !other.Contains(element) { + remove = append(remove, element) + } + + return nil + }) + + for _, element := range remove { + s.Remove(element) + } +} + +// NewMapBackedSet creates a new map-backed set with the given map for storage. +func NewMapBackedSet(storage Map) *MapBackedSet { + return &MapBackedSet{storage: storage} +} + +// NewHashSet creates a new set backed by a HashMap. +func NewHashSet() Set { + return NewMapBackedSet(NewHashMap()) +} + +// NewHashSetWithCapacity creates a new set backed by a HashMap with the given +// initial capacity. +func NewHashSetWithCapacity(capacity int) Set { + return NewMapBackedSet(NewHashMapWithCapacity(capacity)) +} + +// NewLinkedHashSet creates a new set backed by a LinkedHashMap. +func NewLinkedHashSet() Set { + return NewMapBackedSet(NewLinkedHashMap()) +} + +// NewLinkedHashSetWithCapacity creates a new set backed by a LinkedHashMap with +// the given initial capacity. +func NewLinkedHashSetWithCapacity(capacity int) Set { + return NewMapBackedSet(NewLinkedHashMapWithCapacity(capacity)) +} diff --git a/datastructure/map_test.go b/datastructure/map_test.go new file mode 100644 index 0000000..2141b7d --- /dev/null +++ b/datastructure/map_test.go @@ -0,0 +1,168 @@ +package datastructure + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMapInsertionAndRetrieval(t *testing.T) { + for _, m := range []Map{NewHashMap(), NewLinkedHashMap(), NewSynchronizedMap(NewHashMap())} { + assert.True(t, m.Empty()) + assert.False(t, m.Contains("a")) + + value, found := m.Get("a") + assert.Nil(t, value) + assert.False(t, found) + + found = m.Put("a", 1) + assert.False(t, m.Empty()) + assert.False(t, found) + + value, found = m.Get("a") + assert.Equal(t, 1, value) + assert.True(t, found) + } +} + +func TestMapStoresNil(t *testing.T) { + for _, m := range []Map{NewHashMap(), NewLinkedHashMap(), NewSynchronizedMap(NewHashMap())} { + m.Put("a", nil) + + assert.False(t, m.Empty()) + assert.True(t, m.Contains("a")) + + value, found := m.Get("a") + assert.Nil(t, value) + assert.True(t, found) + + var intoValue interface{} = 1 + found = m.GetInto("a", &intoValue) + assert.Nil(t, intoValue) + assert.True(t, found) + } +} + +func TestMapGetInto(t *testing.T) { + for _, m := range []Map{NewHashMap(), NewLinkedHashMap(), NewSynchronizedMap(NewHashMap())} { + m.Put("a", 1) + + var value int + found := m.GetInto("a", &value) + assert.True(t, found) + assert.Equal(t, 1, value) + } +} + +func TestMapUpdate(t *testing.T) { + for _, m := range []Map{NewHashMap(), NewLinkedHashMap(), NewSynchronizedMap(NewHashMap())} { + m.Put("a", 1) + m.Put("b", 2) + + var value int + + m.GetInto("a", &value) + assert.Equal(t, 1, value) + + m.Put("a", 2) + m.GetInto("a", &value) + assert.Equal(t, 2, value) + + m.Remove("a") + assert.False(t, m.Contains("a")) + found := m.GetInto("a", &value) + assert.Equal(t, 2, value) + assert.False(t, found) + + m.Clear() + assert.True(t, m.Empty()) + } +} + +func TestMapIteration(t *testing.T) { + for _, m := range []Map{NewHashMap(), NewLinkedHashMap(), NewHashMapWithCapacity(1), NewLinkedHashMapWithCapacity(1), NewSynchronizedMap(NewHashMap())} { + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + assert.Equal(t, 3, m.Size()) + assert.Contains(t, m.Keys(), "a") + assert.Contains(t, m.Keys(), "b") + assert.Contains(t, m.Keys(), "c") + assert.Contains(t, m.Values(), 1) + assert.Contains(t, m.Values(), 2) + assert.Contains(t, m.Values(), 3) + + err := m.ForEach(func(key, value interface{}) error { + return errors.New("something went wrong") + }) + assert.EqualError(t, err, "something went wrong") + } +} + +func TestMapIterationReflection(t *testing.T) { + for _, m := range []Map{NewHashMap(), NewLinkedHashMap(), NewSynchronizedMap(NewHashMap())} { + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + var keysInto []string + m.KeysInto(&keysInto) + assert.Len(t, keysInto, 3) + assert.Contains(t, keysInto, "a") + assert.Contains(t, keysInto, "b") + assert.Contains(t, keysInto, "c") + + var valuesInto []int + m.ValuesInto(&valuesInto) + assert.Len(t, valuesInto, 3) + assert.Contains(t, valuesInto, 1) + assert.Contains(t, valuesInto, 2) + assert.Contains(t, valuesInto, 3) + + m.ForEachInto(func(key string, value int) error { + assert.Contains(t, []string{"a", "b", "c"}, key) + assert.Contains(t, []int{1, 2, 3}, value) + + return nil + }) + + err := m.ForEachInto(func(key string, value int) error { + return errors.New("something else went wrong") + }) + assert.EqualError(t, err, "something else went wrong") + + assert.Panics(t, func() { + m.ForEachInto(func(key, value uint) error { + return nil + }) + }) + + assert.Panics(t, func() { + m.ForEachInto(func(key string, value int) (int, error) { + return 0, nil + }) + }) + } +} + +func TestMapIterationWithNilValues(t *testing.T) { + for _, m := range []Map{NewHashMap(), NewLinkedHashMap(), NewSynchronizedMap(NewHashMap())} { + m.Put("a", nil) + + assert.NotPanics(t, func() { + m.ForEachInto(func(key string, value *int) error { + assert.Equal(t, (*int)(nil), value) + return nil + }) + }) + + var valuesInto []*int + assert.NotPanics(t, func() { + m.ValuesInto(&valuesInto) + }) + assert.Len(t, valuesInto, 1) + assert.Equal(t, (*int)(nil), valuesInto[0]) + } +} diff --git a/datastructure/map_utils.go b/datastructure/map_utils.go new file mode 100644 index 0000000..d4af2ee --- /dev/null +++ b/datastructure/map_utils.go @@ -0,0 +1,96 @@ +package datastructure + +import ( + "reflect" +) + +func mapGetInto(m Map, key, into interface{}) bool { + value, found := m.Get(key) + + if found { + target := reflect.ValueOf(into).Elem() + target.Set(coalesceInvalidToZeroValueOf(reflect.ValueOf(value), target.Type())) + } + + return found +} + +func mapKeys(m Map) []interface{} { + keys := make([]interface{}, m.Size()) + + i := 0 + m.ForEach(func(key, value interface{}) error { + keys[i] = key + i++ + + return nil + }) + + return keys +} + +func mapKeysInto(m Map, into interface{}) { + p := reflect.ValueOf(into).Elem() + pt := p.Type().Elem() + + slice := p + + m.ForEach(func(key, value interface{}) error { + v := coalesceInvalidToZeroValueOf(reflect.ValueOf(key), pt) + slice = reflect.Append(slice, v) + return nil + }) + + p.Set(slice) +} + +func mapValues(m Map) []interface{} { + values := make([]interface{}, m.Size()) + + i := 0 + m.ForEach(func(key, value interface{}) error { + values[i] = value + i++ + + return nil + }) + + return values +} + +func mapValuesInto(m Map, into interface{}) { + p := reflect.ValueOf(into).Elem() + pt := p.Type().Elem() + + slice := p + + m.ForEach(func(key, value interface{}) error { + v := coalesceInvalidToZeroValueOf(reflect.ValueOf(value), pt) + slice = reflect.Append(slice, v) + return nil + }) + + p.Set(slice) +} + +func mapForEachInto(m Map, fn interface{}) error { + fnr := reflect.ValueOf(fn) + fnt := fnr.Type() + + if fnt.NumOut() != 1 { + panic(ErrInvalidFuncSignature) + } + + return m.ForEach(func(key, value interface{}) error { + p1 := coalesceInvalidToZeroValueOf(reflect.ValueOf(key), fnt.In(0)) + p2 := coalesceInvalidToZeroValueOf(reflect.ValueOf(value), fnt.In(1)) + r := fnr.Call([]reflect.Value{p1, p2}) + + err := r[0] + if err.IsNil() { + return nil + } + + return err.Interface().(error) + }) +} diff --git a/datastructure/priority_queue.go b/datastructure/priority_queue.go new file mode 100644 index 0000000..448d8c8 --- /dev/null +++ b/datastructure/priority_queue.go @@ -0,0 +1,114 @@ +// Portions of this file are derived from a priority queue implementation +// provided in the Go documentation. +// +// https://golang.org/pkg/container/heap/ + +package datastructure + +import ( + "container/heap" + "reflect" +) + +type priorityQueueItem struct { + value interface{} + priority float64 +} + +type priorityQueueImpl []*priorityQueueItem + +func (pqi priorityQueueImpl) Len() int { + return len(pqi) +} + +func (pqi priorityQueueImpl) Less(i, j int) bool { + if pqi[i].priority == pqi[j].priority { + return i < j + } + + return pqi[i].priority > pqi[j].priority +} + +func (pqi priorityQueueImpl) Swap(i, j int) { + pqi[i], pqi[j] = pqi[j], pqi[i] +} + +func (pqi *priorityQueueImpl) Push(x interface{}) { + item := x.(*priorityQueueItem) + *pqi = append(*pqi, item) +} + +func (pqi *priorityQueueImpl) Pop() interface{} { + old := *pqi + n := len(old) + item := old[n-1] + *pqi = old[0 : n-1] + return item +} + +type PriorityQueue struct { + impl priorityQueueImpl +} + +func (pq *PriorityQueue) Empty() bool { + return pq.Size() == 0 +} + +func (pq *PriorityQueue) Size() int { + return pq.impl.Len() +} + +func (pq *PriorityQueue) Clear() { + *pq = PriorityQueue{} + heap.Init(&pq.impl) +} + +// Add inserts a new item to the priority queue with the given priority. +func (pq *PriorityQueue) Add(v interface{}, priority float64) { + item := &priorityQueueItem{ + value: v, + priority: priority, + } + + heap.Push(&pq.impl, item) +} + +// Poll retrieves and removes the item with the highest priority from the queue. +// +// If no items are currently in the queue, this function returns a nil value and +// false. Otherwise, it returns the value and true. +func (pq *PriorityQueue) Poll() (interface{}, bool) { + if pq.impl.Len() == 0 { + return nil, false + } + + item := heap.Pop(&pq.impl).(*priorityQueueItem) + return item.value, true +} + +// PollInto retrieves and removes the item with the highest priority from the +// queue, and stores the item value in the into parameter. The into parameter +// must be of a type assignable by the stored value. If there are no items in +// the queue, the into parameter is not modified and the function returns false. +// Otherwise, the function returns true. +// +// If the into parameter is not compatible with the stored value, this function +// will panic. +func (pq *PriorityQueue) PollInto(into interface{}) bool { + value, found := pq.Poll() + + if found { + target := reflect.ValueOf(into).Elem() + target.Set(coalesceInvalidToZeroValueOf(reflect.ValueOf(value), target.Type())) + } + + return found +} + +// NewPriorityQueue creates a new priority queue backed by a heap. +func NewPriorityQueue() *PriorityQueue { + pq := &PriorityQueue{} + heap.Init(&pq.impl) + + return pq +} diff --git a/datastructure/priority_queue_test.go b/datastructure/priority_queue_test.go new file mode 100644 index 0000000..f8a43d9 --- /dev/null +++ b/datastructure/priority_queue_test.go @@ -0,0 +1,57 @@ +package datastructure + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestPriorityQueue(t *testing.T) { + pq := NewPriorityQueue() + assert.True(t, pq.Empty()) + + pq.Add("a", 10) + pq.Add("c", 30) + pq.Add("b", 20) + + assert.Equal(t, 3, pq.Size()) + + value, found := pq.Poll() + assert.Equal(t, "c", value) + assert.True(t, found) + + value, _ = pq.Poll() + assert.Equal(t, "b", value) + + pq.Add("x", 5) + + value, _ = pq.Poll() + assert.Equal(t, "a", value) + + pq.Clear() + assert.True(t, pq.Empty()) +} + +func TestPriorityQueueReflection(t *testing.T) { + pq := NewPriorityQueue() + + pq.Add("a", 10) + pq.Add("b", 20) + pq.Add(nil, 15) + + var value string + found := pq.PollInto(&value) + assert.Equal(t, "b", value) + assert.True(t, found) + + var intoNilValue interface{} = 1 + found = pq.PollInto(&intoNilValue) + assert.Equal(t, nil, intoNilValue) + assert.True(t, found) + + pq.PollInto(&value) + assert.Equal(t, "a", value) + + found = pq.PollInto(&value) + assert.Equal(t, "a", value) + assert.False(t, found) +} diff --git a/datastructure/set.go b/datastructure/set.go new file mode 100644 index 0000000..be71a31 --- /dev/null +++ b/datastructure/set.go @@ -0,0 +1,55 @@ +// Portions of this file are derived from GoDS, a data structure library for +// Go. +// +// Copyright (c) 2015, Emir Pasic. All rights reserved. +// +// https://github.com/emirpasic/gods/blob/213367f1ca932600ce530ae11c8a8cc444e3a6da/sets/sets.go + +package datastructure + +type SetIterationFunc func(element interface{}) error + +// A Set is a well-defined collection of distinct objects. +type Set interface { + Container + + // Contains returns true if the set contains all of the elements specified, + // and false otherwise. + Contains(elements ...interface{}) bool + + // Adds inserts all of the elements specified to the set. If an element + // already exists in the set, it will not be duplicated. + Add(elements ...interface{}) + + // AddAll inserts the values from the given container to this set. If an + // element already exists in the set, it will not be duplicated. Equivalent + // to a set union operation. + AddAll(other Container) + + // Remove eliminates all of the elements specified from the set. + Remove(elements ...interface{}) + + // RemoveAll removes the elements from the given set from this set. + // Equivalent to a set difference operation. + RemoveAll(other Set) + + // ForEach each element in the set and executes the given callback function. + // If the callback function returns an error, this function will return the + // same error and immediately stop iteration. + // + // To stop iteration without returning an error, return ErrStopIteration. + ForEach(fn SetIterationFunc) error + + // ForEachInto iterates each element in the set and executes the given + // callback function, which must be of a type similar to SetIterationFunc, + // except that the element parameter may be any type assignable by every + // element in the set. + // + // If the requirements for the fn parameter are not met, this function will + // panic. + ForEachInto(fn interface{}) error + + // RetailAll removes any elements that are not shared between this set and + // the given set. Equivalent to a set intersection operation. + RetainAll(other Set) +} diff --git a/datastructure/set_test.go b/datastructure/set_test.go new file mode 100644 index 0000000..a83df5e --- /dev/null +++ b/datastructure/set_test.go @@ -0,0 +1,143 @@ +package datastructure + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSetInsertionAndRetrieval(t *testing.T) { + for _, s := range []Set{NewHashSet(), NewLinkedHashSet(), NewSynchronizedSet(NewHashSet())} { + assert.True(t, s.Empty()) + assert.False(t, s.Contains("a", "b")) + + s.Add("a", "b", "c") + assert.Equal(t, 3, s.Size()) + assert.True(t, s.Contains("a")) + assert.True(t, s.Contains("a", "b", "c")) + assert.False(t, s.Contains("d")) + + s.Add("c") + assert.Equal(t, 3, s.Size()) + } +} + +func TestSetRemoval(t *testing.T) { + for _, s := range []Set{NewHashSet(), NewLinkedHashSet(), NewSynchronizedSet(NewHashSet())} { + s.Add("a", "b", "c") + s.Remove("a", "b") + + assert.Equal(t, 1, s.Size()) + assert.True(t, s.Contains("c")) + assert.False(t, s.Contains("a")) + + s.Clear() + assert.True(t, s.Empty()) + } +} + +func TestSetIteration(t *testing.T) { + for _, s := range []Set{NewHashSet(), NewLinkedHashSet(), NewHashSetWithCapacity(1), NewLinkedHashSetWithCapacity(1), NewSynchronizedSet(NewHashSet())} { + s.Add("a", "b", "c") + + assert.Equal(t, 3, s.Size()) + assert.Contains(t, s.Values(), "a") + assert.Contains(t, s.Values(), "b") + assert.Contains(t, s.Values(), "c") + + err := s.ForEach(func(element interface{}) error { + return errors.New("something went wrong") + }) + assert.EqualError(t, err, "something went wrong") + } +} + +func TestLinkedHashSetIteration(t *testing.T) { + values := []interface{}{"a", "b", "c"} + + s := NewLinkedHashSet() + s.Add(values...) + + for ti := 0; ti < 10; ti++ { + assert.Equal(t, values, s.Values()) + + i := 0 + s.ForEach(func(element interface{}) error { + assert.Equal(t, values[i], element) + i++ + + return nil + }) + } +} + +func TestSetIterationReflection(t *testing.T) { + for _, s := range []Set{NewHashSet(), NewLinkedHashSet(), NewSynchronizedSet(NewHashSet())} { + s.Add("a", "b", "c") + + var valuesInto []string + s.ValuesInto(&valuesInto) + assert.Len(t, valuesInto, 3) + assert.Contains(t, valuesInto, "a") + assert.Contains(t, valuesInto, "b") + assert.Contains(t, valuesInto, "c") + + s.ForEachInto(func(element string) error { + assert.Contains(t, []string{"a", "b", "c"}, element) + + return nil + }) + + err := s.ForEachInto(func(element string) error { + return errors.New("something else went wrong") + }) + assert.EqualError(t, err, "something else went wrong") + + assert.Panics(t, func() { + s.ForEachInto(func(element uint) error { + return nil + }) + }) + + assert.Panics(t, func() { + s.ForEachInto(func(element string) (int, error) { + return 0, nil + }) + }) + } +} + +func TestSetOperations(t *testing.T) { + for _, s := range []Set{NewHashSet(), NewLinkedHashSet(), NewSynchronizedSet(NewHashSet())} { + s.Add("a", "b", "c") + + u := NewHashSet() + u.Add("c", "d", "e") + + s.AddAll(u) + assert.Equal(t, 5, s.Size()) + assert.Contains(t, s.Values(), "a") + assert.Contains(t, s.Values(), "b") + assert.Contains(t, s.Values(), "c") + assert.Contains(t, s.Values(), "d") + assert.Contains(t, s.Values(), "e") + + rn := NewHashSet() + rn.Add("a", "c", "e", "f") + + s.RetainAll(rn) + assert.Equal(t, 3, s.Size()) + assert.Contains(t, s.Values(), "a") + assert.Contains(t, s.Values(), "c") + assert.Contains(t, s.Values(), "e") + + rm := NewHashSet() + rm.Add("d", "e", "f") + + s.RemoveAll(rm) + assert.Equal(t, 2, s.Size()) + assert.Contains(t, s.Values(), "a") + assert.Contains(t, s.Values(), "c") + } +} diff --git a/datastructure/set_utils.go b/datastructure/set_utils.go new file mode 100644 index 0000000..be710fc --- /dev/null +++ b/datastructure/set_utils.go @@ -0,0 +1,41 @@ +package datastructure + +import ( + "reflect" +) + +func setValuesInto(s Set, into interface{}) { + p := reflect.ValueOf(into).Elem() + pt := p.Type().Elem() + + slice := p + + s.ForEach(func(element interface{}) error { + v := coalesceInvalidToZeroValueOf(reflect.ValueOf(element), pt) + slice = reflect.Append(slice, v) + return nil + }) + + p.Set(slice) +} + +func setForEachInto(s Set, fn interface{}) error { + fnr := reflect.ValueOf(fn) + fnt := fnr.Type() + + if fnt.NumOut() != 1 { + panic(ErrInvalidFuncSignature) + } + + return s.ForEach(func(element interface{}) error { + p := coalesceInvalidToZeroValueOf(reflect.ValueOf(element), fnt.In(0)) + r := fnr.Call([]reflect.Value{p}) + + err := r[0] + if err.IsNil() { + return nil + } + + return err.Interface().(error) + }) +} diff --git a/datastructure/synchronized_map.go b/datastructure/synchronized_map.go new file mode 100644 index 0000000..8348ff2 --- /dev/null +++ b/datastructure/synchronized_map.go @@ -0,0 +1,147 @@ +package datastructure + +import ( + "sync" +) + +// SynchronizedMap is a synchronization layer for a Map. It exposes the +// complete Map interface and passes all calls through to a given delegate, +// guarding them with a mutual exclusion lock. +// +// SynchronizedMap makes no assumptions about the underlying storage. In +// particular, it does not assume that read operations are concurrently safe. +type SynchronizedMap struct { + storageMut sync.Mutex + storage Map +} + +func (sm *SynchronizedMap) Empty() bool { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Empty() +} + +func (sm *SynchronizedMap) Size() int { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Size() +} + +func (sm *SynchronizedMap) Clear() { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + sm.storage.Clear() +} + +func (sm *SynchronizedMap) Values() []interface{} { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Values() +} + +func (sm *SynchronizedMap) ValuesInto(into interface{}) { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + sm.storage.ValuesInto(into) +} + +func (sm *SynchronizedMap) Contains(key interface{}) bool { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Contains(key) +} + +func (sm *SynchronizedMap) Put(key, value interface{}) bool { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Put(key, value) +} + +func (sm *SynchronizedMap) CompareAndPut(key, value, expected interface{}) bool { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + if v, found := sm.storage.Get(key); found && v == expected { + sm.storage.Put(key, value) + return true + } + + return false +} + +func (sm *SynchronizedMap) Get(key interface{}) (interface{}, bool) { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Get(key) +} + +func (sm *SynchronizedMap) GetInto(key interface{}, into interface{}) bool { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.GetInto(key, into) +} + +func (sm *SynchronizedMap) Remove(key interface{}) bool { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Remove(key) +} + +func (sm *SynchronizedMap) CompareAndRemove(key, expected interface{}) bool { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + if v, found := sm.storage.Get(key); found && v == expected { + sm.storage.Remove(key) + return true + } + + return false +} + +func (sm *SynchronizedMap) Keys() []interface{} { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.Keys() +} + +func (sm *SynchronizedMap) KeysInto(into interface{}) { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + sm.storage.KeysInto(into) +} + +func (sm *SynchronizedMap) ForEach(fn MapIterationFunc) error { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.ForEach(fn) +} + +func (sm *SynchronizedMap) ForEachInto(fn interface{}) error { + sm.storageMut.Lock() + defer sm.storageMut.Unlock() + + return sm.storage.ForEachInto(fn) +} + +// NewSynchronizedMap creates a synchronization layer over a given map. +func NewSynchronizedMap(storage Map) *SynchronizedMap { + if sm, ok := storage.(*SynchronizedMap); ok { + return sm + } + + return &SynchronizedMap{storage: storage} +} diff --git a/datastructure/synchronized_set.go b/datastructure/synchronized_set.go new file mode 100644 index 0000000..fd6f9e3 --- /dev/null +++ b/datastructure/synchronized_set.go @@ -0,0 +1,116 @@ +package datastructure + +import ( + "sync" +) + +// SynchronizedSet is a synchronization layer for a Set. It exposes the +// complete Set interface and passes all calls through to a given delegate, +// guarding them with a mutual exclusion lock. +// +// SynchronizedSet makes no assumptions about the underlying storage. In +// particular, it does not assume that read operations are concurrently safe. +type SynchronizedSet struct { + storageMut sync.Mutex + storage Set +} + +func (ss *SynchronizedSet) Empty() bool { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + return ss.storage.Empty() +} + +func (ss *SynchronizedSet) Size() int { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + return ss.storage.Size() +} + +func (ss *SynchronizedSet) Clear() { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + ss.storage.Clear() +} + +func (ss *SynchronizedSet) Values() []interface{} { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + return ss.storage.Values() +} + +func (ss *SynchronizedSet) ValuesInto(into interface{}) { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + ss.storage.ValuesInto(into) +} + +func (ss *SynchronizedSet) Contains(elements ...interface{}) bool { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + return ss.storage.Contains(elements...) +} + +func (ss *SynchronizedSet) Add(elements ...interface{}) { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + ss.storage.Add(elements...) +} + +func (ss *SynchronizedSet) AddAll(other Container) { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + ss.storage.AddAll(other) +} + +func (ss *SynchronizedSet) Remove(elements ...interface{}) { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + ss.storage.Remove(elements...) +} + +func (ss *SynchronizedSet) RemoveAll(other Set) { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + ss.storage.RemoveAll(other) +} + +func (ss *SynchronizedSet) ForEach(fn SetIterationFunc) error { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + return ss.storage.ForEach(fn) +} + +func (ss *SynchronizedSet) ForEachInto(fn interface{}) error { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + return ss.storage.ForEachInto(fn) +} + +func (ss *SynchronizedSet) RetainAll(other Set) { + ss.storageMut.Lock() + defer ss.storageMut.Unlock() + + ss.storage.RetainAll(other) +} + +// NewSynchronizedSet creates a synchronization layer over a given set. +func NewSynchronizedSet(storage Set) *SynchronizedSet { + if ss, ok := storage.(*SynchronizedSet); ok { + return ss + } + + return &SynchronizedSet{storage: storage} +} diff --git a/datastructure/synchronized_test.go b/datastructure/synchronized_test.go new file mode 100644 index 0000000..218e4f3 --- /dev/null +++ b/datastructure/synchronized_test.go @@ -0,0 +1,48 @@ +package datastructure + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSynchronizedContainersDoNotStack(t *testing.T) { + sm1 := NewSynchronizedMap(NewHashMap()) + sm2 := NewSynchronizedMap(sm1) + + assert.Equal(t, reflect.ValueOf(sm1).Pointer(), reflect.ValueOf(sm2).Pointer()) + + ss1 := NewSynchronizedSet(NewHashSet()) + ss2 := NewSynchronizedSet(ss1) + + assert.Equal(t, reflect.ValueOf(ss1).Pointer(), reflect.ValueOf(ss2).Pointer()) +} + +func TestSynchronizedMapAtomicOperations(t *testing.T) { + sm1 := NewSynchronizedMap(NewHashMap()) + sm1.Put("a", 1) + + assert.True(t, sm1.CompareAndPut("a", 2, 1)) + + v, found := sm1.Get("a") + assert.True(t, found) + assert.Equal(t, 2, v) + + assert.False(t, sm1.CompareAndPut("a", 3, 1)) + + v, found = sm1.Get("a") + assert.True(t, found) + assert.Equal(t, 2, v) + + assert.False(t, sm1.CompareAndRemove("a", 1)) + + v, found = sm1.Get("a") + assert.True(t, found) + assert.Equal(t, 2, v) + + assert.True(t, sm1.CompareAndRemove("a", 2)) + + _, found = sm1.Get("a") + assert.False(t, found) +} diff --git a/datastructure/utils.go b/datastructure/utils.go new file mode 100644 index 0000000..58cf759 --- /dev/null +++ b/datastructure/utils.go @@ -0,0 +1,13 @@ +package datastructure + +import ( + "reflect" +) + +func coalesceInvalidToZeroValueOf(v reflect.Value, ifInvalid reflect.Type) reflect.Value { + if !v.IsValid() { + v = reflect.Zero(ifInvalid) + } + + return v +} diff --git a/graph/algo/approx_steiner_tree.go b/graph/algo/approx_steiner_tree.go new file mode 100644 index 0000000..b799e8b --- /dev/null +++ b/graph/algo/approx_steiner_tree.go @@ -0,0 +1,208 @@ +// Portions of this file are derived from a Steiner tree approximation +// algorithm written by the University of Southern California. +// +// https://github.com/usc-isi-i2/Web-Karma/blob/cef35dcb1a5042d1e8fabbbd61cb731a78c64454/karma-common/src/main/java/edu/isi/karma/modeling/alignment/SteinerTree.java +// +// Copyright 2012 University of Southern California +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// This code was developed by the Information Integration Group as part +// of the Karma project at the Information Sciences Institute of the +// University of Southern California. For more information, publications, +// and related projects, please see: http://www.isi.edu/integration + +package algo + +import ( + "github.com/puppetlabs/horsehead/v2/datastructure" + "github.com/puppetlabs/horsehead/v2/graph" +) + +// ApproximateSteinerTreeSupportedFeatures are the graph features supported by +// the Steiner tree approximation algorithm. +const ApproximateSteinerTreeSupportedFeatures = graph.DeterministicIteration + +// An ApproximateSteinerTree is an approximation of the Steiner tree of a graph +// G for a set of vertices Vr. A Steiner tree is a subset of G such that all Vr +// are present in the subset and the weights of the edges connecting Vr are +// minimized. +type ApproximateSteinerTree struct { + features graph.GraphFeature + graph graph.UndirectedGraph +} + +// Features returns the graph features being used for this algorithm. +func (ast *ApproximateSteinerTree) Features() graph.GraphFeature { + return ast.features +} + +// Edges returns the set of edges computed as the approximation of the tree. +func (ast *ApproximateSteinerTree) Edges() graph.EdgeSet { + return ast.graph.Edges() +} + +// AsGraph returns a graph representation of the subset of vertices and edges +// represented by this tree. +func (ast *ApproximateSteinerTree) AsGraph() graph.UndirectedGraph { + return ast.graph +} + +type steinerTreeCostMetric struct { + edges []graph.Edge +} + +// ApproximateSteinerTreeOf computes an approximation of the Steiner tree for a +// given graph. +// +// Steiner trees are known to be NP-complete, but an approximation can be found +// by the following algorithm: +// +// 1. Compute the metric closure of the given graph. This forms a complete +// graph with edge weights corresponding to the distances between vertices; +// this specialization is computable in polynomial time. +// 2. Remove all vertices not in the desired subset. +// 3. Compute the minimum spanning tree of the metric closure. +// 4. Expand the minimized edges into a graph. +// 5. Replace the edges of the constructed graph with their paths from the +// given graph. +// 6. Compute the minimum spanning tree of the expanded graph. +// 7. Prepare a graph with only the edges contained in the tree. +// +// If this proves too slow or inaccurate, it can be further optimized. See +// http://dl.acm.org/citation.cfm?doid=1806689.1806769 for more information. +// +// If any of the given vertices do not exist in this graph, an error of type +// VertexNotFoundError is returned. If no path exists between any two of the +// given vertices, an error of type NotConnectedError is returned. +func ApproximateSteinerTreeOf(g graph.UndirectedGraph, required []graph.Vertex) (*ApproximateSteinerTree, error) { + // Prerequisite: we deduplicate the required vertices. + vertices := datastructure.NewHashSet() + for _, vertex := range required { + vertices.Add(vertex) + } + + // If we are tasked with deterministic iteration, we need to have the + // required vertices in a specific order. Unfortunately, given that they're + // interface{}, the only thing we can depend on for the order is the source + // graph. So we have to pick them out of its vertices. + if g.Features()&graph.DeterministicIteration != 0 { + remaining := vertices + vertices = datastructure.NewLinkedHashSet() + + g.Vertices().ForEach(func(candidate graph.Vertex) error { + if remaining.Contains(candidate) { + vertices.Add(candidate) + remaining.Remove(candidate) + } + + if remaining.Empty() { + return datastructure.ErrStopIteration + } + + return nil + }) + + if !remaining.Empty() { + var first graph.Vertex + remaining.ForEachInto(func(vertex graph.Vertex) error { + first = vertex + return datastructure.ErrStopIteration + }) + + return nil, &graph.VertexNotFoundError{Vertex: first} + } + } + + // 1 & 2: Compute the metric closure. + closure := graph.NewSimpleWeightedGraphWithFeatures(g.Features()) + + vertices.ForEach(func(vertex interface{}) error { + closure.AddVertex(vertex) + return nil + }) + + err := vertices.ForEach(func(v1 interface{}) error { + paths := BellmanFordShortestPathsOf(g, v1) + + return vertices.ForEach(func(v2 interface{}) error { + if v1 == v2 || closure.ContainsEdgeBetween(v1, v2) { + return nil + } + + // Save the edges so we can recompute them later. + edges, err := paths.EdgesTo(v2) + if err != nil { + return err + } + + cost, _ := paths.CostTo(v2) + + metric := &steinerTreeCostMetric{edges} + closure.AddEdgeWithWeight(v1, v2, metric, cost) + + return nil + }) + }) + if err != nil { + return nil, err + } + + // 3: Compute the minimum spanning tree. + // + // This can be optimized as well: dense graphs can run Prim's algorithm in + // linear time. + mst := PrimMinimumSpanningTreeOf(closure) + + // 4 & 5: Expand the minimum spanning tree into a graph. + t := graph.NewUndirectedWeightedPseudographWithFeatures(g.Features()) + + vertices.ForEach(func(vertex interface{}) error { + t.AddVertex(vertex) + return nil + }) + + mst.Edges().ForEach(func(edge graph.Edge) error { + metric := edge.(*steinerTreeCostMetric) + + // Expand out the edges. + for _, step := range metric.edges { + start, _ := g.SourceVertexOf(step) + end, _ := g.TargetVertexOf(step) + weight, _ := g.WeightOf(step) + + t.AddVertex(start) + t.AddVertex(end) + t.AddEdgeWithWeight(start, end, step, weight) + } + + return nil + }) + + // 6: Compute the minimum spanning tree of our final expanded graph. + keep := PrimMinimumSpanningTreeOf(t).Edges() + + // 7: Remove all edges not in the spanning tree. + for _, edge := range t.Edges().AsSlice() { + if !keep.Contains(edge) { + t.RemoveEdge(edge) + } + } + + ast := &ApproximateSteinerTree{ + features: g.Features() & ApproximateSteinerTreeSupportedFeatures, + graph: t, + } + + return ast, nil +} diff --git a/graph/algo/approx_steiner_tree_test.go b/graph/algo/approx_steiner_tree_test.go new file mode 100644 index 0000000..a410b68 --- /dev/null +++ b/graph/algo/approx_steiner_tree_test.go @@ -0,0 +1,70 @@ +package algo + +import ( + "math/rand" + "testing" + + "github.com/puppetlabs/horsehead/v2/graph" + "github.com/stretchr/testify/assert" +) + +func TestApproximateSteinerTree(t *testing.T) { + g := graph.NewUndirectedPseudograph() + + g.AddVertex("A") + g.AddVertex("B") + g.AddVertex("C") + g.AddVertex("D") + g.AddVertex("E") + g.AddVertex("F") + g.AddVertex("G") + + g.AddEdge("A", "B", 1) + g.AddEdge("B", "C", 2) + g.AddEdge("C", "E", 3) + g.AddEdge("C", "F", 4) + g.AddEdge("D", "A", 5) + + reduced, err := ApproximateSteinerTreeOf(g, []graph.Vertex{"A", "D", "F"}) + assert.NoError(t, err) + assert.Equal(t, uint(5), reduced.AsGraph().Vertices().Count()) + + edges := reduced.Edges().AsSlice() + assert.Contains(t, edges, 1) + assert.Contains(t, edges, 2) + assert.Contains(t, edges, 4) + assert.Contains(t, edges, 5) +} + +func TestApproximateSteinerTreeOfDeterministicGraph(t *testing.T) { + g := graph.NewUndirectedPseudographWithFeatures(graph.DeterministicIteration) + + g.AddVertex("A") + g.AddVertex("B") + g.AddVertex("C") + g.AddVertex("D") + g.AddVertex("E") + g.AddVertex("F") + g.AddVertex("G") + + g.AddEdge("A", "B", 1) + g.AddEdge("B", "C", 2) + g.AddEdge("C", "E", 3) + g.AddEdge("C", "F", 4) + g.AddEdge("D", "A", 5) + g.AddEdge("C", "G", 6) + + required := []graph.Vertex{"A", "D", "F"} + + for i := 0; i < 10; i++ { + shuffled := make([]graph.Vertex, len(required)) + for in, out := range rand.Perm(len(required)) { + shuffled[out] = required[in] + } + + reduced, err := ApproximateSteinerTreeOf(g, shuffled) + assert.NoError(t, err) + assert.Equal(t, []graph.Vertex{"A", "D", "F", "B", "C"}, reduced.AsGraph().Vertices().AsSlice()) + assert.Equal(t, []graph.Edge{5, 1, 2, 4}, reduced.Edges().AsSlice()) + } +} diff --git a/graph/algo/bellman_ford.go b/graph/algo/bellman_ford.go new file mode 100644 index 0000000..89e9e70 --- /dev/null +++ b/graph/algo/bellman_ford.go @@ -0,0 +1,227 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2006-2016, by France Telecom and Contributors. + +package algo + +import ( + "math" + + "github.com/puppetlabs/horsehead/v2/graph" +) + +const ( + BellmanFordSupportedFeatures = graph.DeterministicIteration +) + +type bellmanFordPathElement struct { + vertex graph.Vertex + cost float64 + + prevEdge graph.Edge + prevPathElement *bellmanFordPathElement +} + +func (bfpe *bellmanFordPathElement) improve(prevPathElement *bellmanFordPathElement, prevEdge graph.Edge, cost float64) bool { + if cost >= (bfpe.cost - 1e-7) { + return false + } + + bfpe.cost = cost + + bfpe.prevEdge = prevEdge + bfpe.prevPathElement = prevPathElement + + return true +} + +type bellmanFordExecutor struct { + graph graph.Graph + start graph.Vertex + + // Vertices whose shortest path costs have been improved during the + // previous pass. + prevImprovedVertices []graph.Vertex + prevVertexData map[graph.Vertex]*bellmanFordPathElement + startVertexEncountered bool + + // Vertices seen so far. + vertexData map[graph.Vertex]*bellmanFordPathElement +} + +func (e *bellmanFordExecutor) calculate() { + for e.hasNext() { + e.next() + } +} + +func (e *bellmanFordExecutor) hasNext() bool { + e.encounterStartVertex() + + return len(e.prevImprovedVertices) > 0 +} + +func (e *bellmanFordExecutor) next() { + e.encounterStartVertex() + + var improvedVertices []graph.Vertex + for i := len(e.prevImprovedVertices) - 1; i >= 0; i-- { + vertex := e.prevImprovedVertices[i] + + e.forEachEdgeOf(vertex, func(edge graph.Edge) error { + otherVertex, _ := graph.OppositeVertexOf(e.graph, edge, vertex) + + if _, found := e.vertexData[otherVertex]; found { + if e.relaxVertexAgain(otherVertex, edge) { + improvedVertices = append(improvedVertices, otherVertex) + } + } else { + e.relaxVertex(otherVertex, edge) + improvedVertices = append(improvedVertices, otherVertex) + } + + return nil + }) + } + + e.savePassData(improvedVertices) +} + +func (e *bellmanFordExecutor) calculatePathCost(vertex graph.Vertex, edge graph.Edge) float64 { + other, _ := graph.OppositeVertexOf(e.graph, edge, vertex) + prev := e.prevVertexData[other] + + cost, _ := e.graph.WeightOf(edge) + if vertex != e.start { + cost += prev.cost + } + + return cost +} + +func (e *bellmanFordExecutor) forEachEdgeOf(vertex graph.Vertex, fn graph.EdgeSetIterationFunc) { + var edges graph.EdgeSet + if dg, ok := e.graph.(graph.DirectedGraph); ok { + edges, _ = dg.OutgoingEdgesOf(vertex) + } else { + edges, _ = e.graph.EdgesOf(vertex) + } + + edges.ForEach(fn) +} + +func (e *bellmanFordExecutor) createSeenData(vertex graph.Vertex, edge graph.Edge, cost float64) *bellmanFordPathElement { + other, _ := graph.OppositeVertexOf(e.graph, edge, vertex) + prev := e.prevVertexData[other] + + return &bellmanFordPathElement{ + vertex: vertex, + cost: cost, + + prevEdge: edge, + prevPathElement: prev, + } +} + +func (e *bellmanFordExecutor) encounterStartVertex() { + if e.startVertexEncountered { + return + } + + el := &bellmanFordPathElement{vertex: e.start} + + e.prevImprovedVertices = append(e.prevImprovedVertices, e.start) + e.vertexData[e.start] = el + e.prevVertexData[e.start] = el + + e.startVertexEncountered = true +} + +func (e *bellmanFordExecutor) relaxVertex(vertex graph.Vertex, edge graph.Edge) { + cost := e.calculatePathCost(vertex, edge) + + e.vertexData[vertex] = e.createSeenData(vertex, edge, cost) +} + +func (e *bellmanFordExecutor) relaxVertexAgain(vertex graph.Vertex, edge graph.Edge) bool { + cost := e.calculatePathCost(vertex, edge) + + other, _ := graph.OppositeVertexOf(e.graph, edge, vertex) + el := e.prevVertexData[other] + return e.vertexData[vertex].improve(el, edge, cost) +} + +func (e *bellmanFordExecutor) savePassData(improvedVertices []graph.Vertex) { + for _, vertex := range improvedVertices { + clone := &bellmanFordPathElement{} + *clone = *e.vertexData[vertex] + + e.prevVertexData[vertex] = clone + } + + e.prevImprovedVertices = improvedVertices +} + +type BellmanFordShortestPaths struct { + features graph.GraphFeature + executor *bellmanFordExecutor +} + +func (bfsp *BellmanFordShortestPaths) Features() graph.GraphFeature { + return bfsp.features +} + +func (bfsp *BellmanFordShortestPaths) EdgesTo(end graph.Vertex) ([]graph.Edge, error) { + if !bfsp.executor.graph.ContainsVertex(end) { + return nil, &graph.VertexNotFoundError{Vertex: end} + } + + bfsp.executor.calculate() + + el, found := bfsp.executor.vertexData[end] + if !found { + return nil, &graph.NotConnectedError{Source: bfsp.executor.start, Target: end} + } + + var edges []graph.Edge + for el.prevEdge != nil { + edges = append(edges, el.prevEdge) + el = el.prevPathElement + } + + // Reverse the list. + for l, r := 0, len(edges)-1; l < r; l, r = l+1, r-1 { + edges[l], edges[r] = edges[r], edges[l] + } + + return edges, nil +} + +func (bfsp *BellmanFordShortestPaths) CostTo(end graph.Vertex) (float64, error) { + if !bfsp.executor.graph.ContainsVertex(end) { + return math.Inf(1), &graph.VertexNotFoundError{Vertex: end} + } + + bfsp.executor.calculate() + + el, found := bfsp.executor.vertexData[end] + if !found { + return math.Inf(1), nil + } + + return el.cost, nil +} + +func BellmanFordShortestPathsOf(g graph.Graph, start graph.Vertex) *BellmanFordShortestPaths { + return &BellmanFordShortestPaths{ + features: g.Features() & BellmanFordSupportedFeatures, + executor: &bellmanFordExecutor{ + graph: g, + start: start, + + prevVertexData: make(map[graph.Vertex]*bellmanFordPathElement), + vertexData: make(map[graph.Vertex]*bellmanFordPathElement), + }, + } +} diff --git a/graph/algo/bellman_ford_test.go b/graph/algo/bellman_ford_test.go new file mode 100644 index 0000000..9a2c250 --- /dev/null +++ b/graph/algo/bellman_ford_test.go @@ -0,0 +1,45 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2006-2016, by John V Sichi and Contributors. + +package algo + +import ( + "testing" + + "github.com/puppetlabs/horsehead/v2/graph" + "github.com/stretchr/testify/assert" +) + +func CreateShortestPathsTestGraph() graph.Graph { + g := graph.NewSimpleWeightedGraph() + + g.AddVertex("A") + g.AddVertex("B") + g.AddVertex("C") + g.AddVertex("D") + g.AddVertex("E") + + g.AddEdgeWithWeight("A", "B", 1, 2.) + g.AddEdgeWithWeight("A", "C", 2, 3.) + g.AddEdgeWithWeight("B", "D", 3, 5.) + g.AddEdgeWithWeight("C", "D", 4, 20.) + g.AddEdgeWithWeight("D", "E", 5, 5.) + g.AddEdgeWithWeight("A", "E", 6, 100.) + + return g +} + +func TestBellmanFordShortestPaths(t *testing.T) { + g := CreateShortestPathsTestGraph() + bfsp := BellmanFordShortestPathsOf(g, "C") + + path, err := bfsp.EdgesTo("E") + assert.NoError(t, err) + assert.Equal(t, []graph.Edge{2, 1, 3, 5}, path) + + cost, err := bfsp.CostTo("E") + assert.NoError(t, err) + assert.Equal(t, 15., cost) +} diff --git a/graph/algo/prim.go b/graph/algo/prim.go new file mode 100644 index 0000000..19c0c3a --- /dev/null +++ b/graph/algo/prim.go @@ -0,0 +1,115 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2013-2016, by Alexey Kudinkin and Contributors. + +package algo + +import ( + "github.com/puppetlabs/horsehead/v2/datastructure" + "github.com/puppetlabs/horsehead/v2/graph" +) + +const ( + PrimMinimumSpanningTreeSupportedFeatures = graph.DeterministicIteration +) + +type PrimMinimumSpanningTree struct { + TotalWeight float64 + + features graph.GraphFeature + es graph.MutableEdgeSet +} + +func (mst *PrimMinimumSpanningTree) Features() graph.GraphFeature { + return mst.features +} + +func (mst *PrimMinimumSpanningTree) Edges() graph.EdgeSet { + return mst.es +} + +func PrimMinimumSpanningTreeOf(g graph.UndirectedGraph) *PrimMinimumSpanningTree { + vs := g.Vertices() + + var ( + es graph.MutableEdgeSet + unspanned datastructure.Set + ) + + if g.Features()&graph.DeterministicIteration != 0 { + es = graph.NewMutableEdgeSet(datastructure.NewLinkedHashSet()) + unspanned = datastructure.NewLinkedHashSetWithCapacity(int(vs.Count())) + } else { + es = graph.NewMutableEdgeSet(datastructure.NewHashSet()) + unspanned = datastructure.NewHashSetWithCapacity(int(vs.Count())) + } + + mst := &PrimMinimumSpanningTree{ + features: g.Features() & PrimMinimumSpanningTreeSupportedFeatures, + es: es, + } + + vs.ForEach(func(vertex graph.Vertex) error { + unspanned.Add(vertex) + return nil + }) + + for !unspanned.Empty() { + var root graph.Vertex + unspanned.ForEachInto(func(vertex graph.Vertex) error { + root = vertex + return datastructure.ErrStopIteration + }) + + unspanned.Remove(root) + + dangling := datastructure.NewPriorityQueue() + + edges, _ := g.EdgesOf(root) + edges.ForEach(func(edge graph.Edge) error { + weight, _ := g.WeightOf(edge) + dangling.Add(edge, -weight) + + return nil + }) + + var next graph.Edge + for dangling.PollInto(&next) { + target, _ := g.SourceVertexOf(next) + if !unspanned.Contains(target) { + target, _ = g.TargetVertexOf(next) + + if !unspanned.Contains(target) { + continue + } + } + + mst.es.Add(next) + + unspanned.Remove(target) + + edges, _ := g.EdgesOf(target) + edges.ForEach(func(edge graph.Edge) error { + candidate, _ := graph.OppositeVertexOf(g, edge, target) + if !unspanned.Contains(candidate) { + return nil + } + + weight, _ := g.WeightOf(edge) + dangling.Add(edge, -weight) + + return nil + }) + } + } + + mst.es.ForEach(func(edge graph.Edge) error { + weight, _ := g.WeightOf(edge) + mst.TotalWeight += weight + + return nil + }) + + return mst +} diff --git a/graph/algo/prim_test.go b/graph/algo/prim_test.go new file mode 100644 index 0000000..cb8b283 --- /dev/null +++ b/graph/algo/prim_test.go @@ -0,0 +1,66 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2010-2016, by Tom Conerly and Contributors. + +package algo + +import ( + "testing" + + "github.com/puppetlabs/horsehead/v2/graph" + "github.com/stretchr/testify/assert" +) + +func TestPrimWithConnectedWeightedGraph(t *testing.T) { + g := graph.NewSimpleWeightedGraph() + + for _, vertex := range []string{"A", "B", "C", "D", "E"} { + g.AddVertex(vertex) + } + + // The resulting edges should be: + // 1, 2, 3, 5 + // + // The resulting total weight should be: + // 2 + 3 + 5 + 5 = 15 + + assert.NoError(t, g.AddEdgeWithWeight("A", "B", 1, 2.)) + assert.NoError(t, g.AddEdgeWithWeight("A", "C", 2, 3.)) + assert.NoError(t, g.AddEdgeWithWeight("B", "D", 3, 5.)) + assert.NoError(t, g.AddEdgeWithWeight("C", "D", 4, 20.)) + assert.NoError(t, g.AddEdgeWithWeight("D", "E", 5, 5.)) + assert.NoError(t, g.AddEdgeWithWeight("A", "E", 6, 100.)) + + mst := PrimMinimumSpanningTreeOf(g) + assert.Equal(t, uint(4), mst.Edges().Count()) + + edges := mst.Edges().AsSlice() + assert.Contains(t, edges, 1) + assert.Contains(t, edges, 2) + assert.Contains(t, edges, 3) + assert.Contains(t, edges, 5) + assert.InDelta(t, 15., mst.TotalWeight, 1e-6) +} + +func TestPrimWithEqualWeights(t *testing.T) { + g := graph.NewSimpleGraph() + + for _, vertex := range []string{"A", "B", "C", "D", "E"} { + g.AddVertex(vertex) + } + + assert.NoError(t, g.Connect("A", "B")) + assert.NoError(t, g.Connect("A", "C")) + assert.NoError(t, g.Connect("B", "D")) + assert.NoError(t, g.Connect("C", "D")) + assert.NoError(t, g.Connect("D", "E")) + assert.NoError(t, g.Connect("A", "E")) + + mst := PrimMinimumSpanningTreeOf(g) + assert.Equal(t, uint(4), mst.Edges().Count()) + + edges := mst.Edges().AsSlice() + assert.Len(t, edges, 4) + assert.InDelta(t, graph.DefaultEdgeWeight*4, mst.TotalWeight, 1e-6) +} diff --git a/graph/algo/tiernan_simple_cycles.go b/graph/algo/tiernan_simple_cycles.go new file mode 100644 index 0000000..3ca40ab --- /dev/null +++ b/graph/algo/tiernan_simple_cycles.go @@ -0,0 +1,151 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2013-2017, by Nikolay Ognyanov and Contributors. + +package algo + +import ( + "reflect" + + "github.com/puppetlabs/horsehead/v2/datastructure" + "github.com/puppetlabs/horsehead/v2/graph" +) + +const ( + TiernanSimpleCyclesSupportedFeatures = graph.DeterministicIteration +) + +type TiernanSimpleCycles struct { + features graph.GraphFeature + g graph.DirectedGraph +} + +func (tsc *TiernanSimpleCycles) CyclesInto(into interface{}) { + p := reflect.ValueOf(into).Elem() + slice := p + + for _, cycle := range tsc.Cycles() { + size := len(cycle) + is := reflect.MakeSlice(p.Type().Elem(), size, size) + ist := is.Type().Elem() + + for i, vertex := range cycle { + v := reflect.ValueOf(vertex) + if !v.IsValid() { + v = reflect.Zero(ist) + } + + is.Index(i).Set(v) + } + + slice = reflect.Append(slice, is) + } + + p.Set(slice) +} + +func (tsc *TiernanSimpleCycles) Cycles() (cycles [][]graph.Vertex) { + if tsc.g.Vertices().Count() == 0 { + return + } + + var path []graph.Vertex + pathSet := datastructure.NewHashSet() // set[]graph.Vertex + + blocked := make(map[graph.Vertex]datastructure.Set) // map[graph.Vertex]set[]graph.Vertex + indices := make(map[graph.Vertex]int) + + i := 0 + verticesLeft := make([]graph.Vertex, tsc.g.Vertices().Count()) + tsc.g.Vertices().ForEach(func(vertex graph.Vertex) error { + verticesLeft[i] = vertex + + blocked[vertex] = datastructure.NewHashSet() + indices[vertex] = i + i++ + + return nil + }) + + var pathStart graph.Vertex + pathEnd := verticesLeft[0] + verticesLeft = verticesLeft[1:] + + path = append(path, pathEnd) + pathSet.Add(pathEnd) + + for { + // Path extension. + for { + edges, _ := tsc.g.OutgoingEdgesOf(pathEnd) + err := edges.ForEach(func(edge graph.Edge) error { + target, _ := tsc.g.TargetVertexOf(edge) + if indices[target] > indices[path[0]] && !pathSet.Contains(target) && !blocked[pathEnd].Contains(target) { + path = append(path, target) + pathSet.Add(target) + pathEnd = target + return datastructure.ErrStopIteration + } + + return nil + }) + if err == datastructure.ErrStopIteration { + // We found another extension. Repeat this search. + continue + } + + break + } + + // Circuit confirmation. + pathStart = path[0] + if tsc.g.ContainsEdgeBetween(pathEnd, pathStart) { + cycle := make([]graph.Vertex, len(path)) + copy(cycle, path) + + cycles = append(cycles, cycle) + } + + // Vertex closure. + if len(path) > 1 { + path = path[:len(path)-1] + pathSet.Remove(pathEnd) + blocked[pathEnd].Clear() + + pathLast := pathEnd + pathEnd = path[len(path)-1] + blocked[pathEnd].Add(pathLast) + + continue + } + + // Advance initial index. + if len(verticesLeft) > 0 { + pathEnd = verticesLeft[0] + verticesLeft = verticesLeft[1:] + + path = []graph.Vertex{pathEnd} + pathSet.Clear() + pathSet.Add(pathEnd) + + for _, blockedPath := range blocked { + blockedPath.Clear() + } + + continue + } + + // Terminate. + break + } + + return +} + +func TiernanSimpleCyclesOf(g graph.DirectedGraph) *TiernanSimpleCycles { + return &TiernanSimpleCycles{ + features: g.Features() & TiernanSimpleCyclesSupportedFeatures, + g: g, + } +} diff --git a/graph/algo/tiernan_simple_cycles_test.go b/graph/algo/tiernan_simple_cycles_test.go new file mode 100644 index 0000000..1758687 --- /dev/null +++ b/graph/algo/tiernan_simple_cycles_test.go @@ -0,0 +1,84 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2013-2017, by Nikolay Ognyanov and Contributors. + +package algo + +import ( + "testing" + + "github.com/puppetlabs/horsehead/v2/graph" + "github.com/stretchr/testify/assert" +) + +func TestTiernanSimpleCycles(t *testing.T) { + g := graph.NewDirectedPseudographWithFeatures(graph.DeterministicIteration) + for i := 0; i < 7; i++ { + g.AddVertex(i) + } + + g.Connect(0, 0) + assert.Equal(t, [][]graph.Vertex{{0}}, TiernanSimpleCyclesOf(g).Cycles()) + + g.Connect(1, 1) + assert.Equal(t, [][]graph.Vertex{{0}, {1}}, TiernanSimpleCyclesOf(g).Cycles()) + + g.Connect(0, 1) + g.Connect(1, 0) + assert.Equal(t, [][]graph.Vertex{{0, 1}, {0}, {1}}, TiernanSimpleCyclesOf(g).Cycles()) + + g.Connect(1, 2) + g.Connect(2, 3) + g.Connect(3, 0) + assert.Equal(t, [][]graph.Vertex{{0, 1, 2, 3}, {0, 1}, {0}, {1}}, TiernanSimpleCyclesOf(g).Cycles()) + + g.Connect(6, 6) + assert.Equal(t, [][]graph.Vertex{{0, 1, 2, 3}, {0, 1}, {0}, {1}, {6}}, TiernanSimpleCyclesOf(g).Cycles()) + + conditions := []struct { + ConnectedVertices, Cycles int + }{ + {1, 1}, + {2, 3}, + {3, 8}, + {4, 24}, + {5, 89}, + {6, 415}, + {7, 2372}, + {8, 16072}, + {9, 125673}, + } + + for _, cond := range conditions { + g = graph.NewDirectedPseudograph() + for i := 0; i < cond.ConnectedVertices; i++ { + g.AddVertex(i) + } + for i := 0; i < cond.ConnectedVertices; i++ { + for j := 0; j < cond.ConnectedVertices; j++ { + g.Connect(i, j) + } + } + + assert.Len(t, TiernanSimpleCyclesOf(g).Cycles(), cond.Cycles) + } +} + +func TestTiernanSimpleCyclesInto(t *testing.T) { + g := graph.NewSimpleDirectedGraphWithFeatures(graph.DeterministicIteration) + g.AddVertex("a") + g.AddVertex("b") + g.AddVertex("c") + + g.Connect("a", "b") + g.Connect("b", "c") + g.Connect("c", "a") + + var cycles [][]string + assert.NotPanics(t, func() { + TiernanSimpleCyclesOf(g).CyclesInto(&cycles) + }) + + assert.Equal(t, [][]string{{"a", "b", "c"}}, cycles) +} diff --git a/graph/base.go b/graph/base.go new file mode 100644 index 0000000..963ce6c --- /dev/null +++ b/graph/base.go @@ -0,0 +1,331 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2003-2016, by Barak Naveh and Contributors. + +package graph + +import ( + "github.com/puppetlabs/horsehead/v2/datastructure" +) + +type intrusiveEdge struct { + Source, Target Vertex + Edge Edge + Weight float64 +} + +type baseGraphOps interface { + EdgesBetween(source, target Vertex) EdgeSet + EdgeBetween(source, target Vertex) (Edge, error) + EdgesOf(vertex Vertex) EdgeSet + AddEdge(edge Edge) + RemoveEdge(edge Edge) + Vertices() MutableVertexSet +} + +type baseEdgesView struct { + g *baseGraph +} + +func (sev *baseEdgesView) Contains(edge Edge) bool { + return sev.g.edges.Contains(edge) +} + +func (sev *baseEdgesView) Count() uint { + return uint(sev.g.edges.Size()) +} + +func (sev *baseEdgesView) AsSlice() []Edge { + s := make([]Edge, sev.g.edges.Size()) + + i := 0 + sev.ForEach(func(edge Edge) error { + s[i] = edge + i++ + + return nil + }) + + return s +} + +func (sev *baseEdgesView) ForEach(fn EdgeSetIterationFunc) error { + return sev.g.edges.ForEachInto(func(key Edge, value *intrusiveEdge) error { + return fn(key) + }) +} + +type baseGraph struct { + AllowsLoops, AllowsMultipleEdges bool + Ops baseGraphOps + + features GraphFeature + edges datastructure.Map // map[Edge]*intrusiveEdge + edgesView EdgeSet +} + +func (g *baseGraph) Features() GraphFeature { + return g.features +} + +func (g *baseGraph) EdgesBetween(source, target Vertex) EdgeSet { + return g.Ops.EdgesBetween(source, target) +} + +func (g *baseGraph) EdgeBetween(source, target Vertex) (Edge, error) { + return g.Ops.EdgeBetween(source, target) +} + +func (g *baseGraph) Connect(source, target Vertex) error { + return g.AddEdge(source, target, NewEdge()) +} + +func (g *baseGraph) AddEdge(source, target Vertex, edge Edge) error { + return g.AddEdgeWithWeight(source, target, edge, DefaultEdgeWeight) +} + +func (g *baseGraph) ConnectWithWeight(source, target Vertex, weight float64) error { + return g.AddEdgeWithWeight(source, target, NewEdge(), weight) +} + +func (g *baseGraph) AddEdgeWithWeight(source, target Vertex, edge Edge, weight float64) error { + if g.ContainsEdge(edge) { + return ErrEdgeAlreadyInGraph + } + + if !g.ContainsVertex(source) { + return &VertexNotFoundError{source} + } + if !g.ContainsVertex(target) { + return &VertexNotFoundError{target} + } + + if !g.AllowsMultipleEdges && g.ContainsEdgeBetween(source, target) { + return ErrEdgeAlreadyInGraph + } + + if !g.AllowsLoops && source == target { + return ErrWouldCreateLoop + } + + ie := &intrusiveEdge{ + Source: source, + Target: target, + Edge: edge, + Weight: weight, + } + + g.edges.Put(edge, ie) + g.Ops.AddEdge(edge) + + return nil +} + +func (g *baseGraph) AddVertex(vertex Vertex) { + g.Ops.Vertices().Add(vertex) +} + +func (g *baseGraph) ContainsEdgeBetween(source, target Vertex) bool { + _, err := g.EdgeBetween(source, target) + return err == nil +} + +func (g *baseGraph) ContainsEdge(edge Edge) bool { + return g.edges.Contains(edge) +} + +func (g *baseGraph) ContainsVertex(vertex Vertex) bool { + return g.Vertices().Contains(vertex) +} + +func (g *baseGraph) Edges() EdgeSet { + if g.edgesView == nil { + g.edgesView = &baseEdgesView{g} + } + + return g.edgesView +} + +func (g *baseGraph) EdgesOf(vertex Vertex) (EdgeSet, error) { + if !g.ContainsVertex(vertex) { + return nil, &VertexNotFoundError{vertex} + } + + return g.Ops.EdgesOf(vertex), nil +} + +func (g *baseGraph) RemoveEdges(edges []Edge) (modified bool) { + for _, edge := range edges { + modified = modified || g.RemoveEdge(edge) + } + + return +} + +func (g *baseGraph) RemoveEdgesBetween(source, target Vertex) EdgeSet { + edges := g.EdgesBetween(source, target) + g.RemoveEdges(edges.AsSlice()) + + return edges +} + +func (g *baseGraph) RemoveEdge(edge Edge) bool { + if !g.ContainsEdge(edge) { + return false + } + + g.Ops.RemoveEdge(edge) + g.edges.Remove(edge) + + return true +} + +func (g *baseGraph) RemoveEdgeBetween(source, target Vertex) (Edge, error) { + edge, err := g.EdgeBetween(source, target) + if err != nil { + return nil, err + } + + g.RemoveEdge(edge) + return edge, nil +} + +func (g *baseGraph) RemoveVertices(vertices []Vertex) (modified bool) { + for _, vertex := range vertices { + modified = modified || g.RemoveVertex(vertex) + } + + return +} + +func (g *baseGraph) RemoveVertex(vertex Vertex) bool { + if !g.ContainsVertex(vertex) { + return false + } + + g.RemoveEdges(g.Ops.EdgesOf(vertex).AsSlice()) + g.Ops.Vertices().Remove(vertex) + + return true +} + +func (g *baseGraph) Vertices() VertexSet { + return g.Ops.Vertices() +} + +func (g *baseGraph) SourceVertexOf(edge Edge) (Vertex, error) { + ie, found := g.edges.Get(edge) + if !found { + return nil, ErrEdgeNotFound + } + + return ie.(*intrusiveEdge).Source, nil +} + +func (g *baseGraph) TargetVertexOf(edge Edge) (Vertex, error) { + ie, found := g.edges.Get(edge) + if !found { + return nil, ErrEdgeNotFound + } + + return ie.(*intrusiveEdge).Target, nil +} + +func (g *baseGraph) WeightOf(edge Edge) (float64, error) { + ie, found := g.edges.Get(edge) + if !found { + return DefaultEdgeWeight, ErrEdgeNotFound + } + + return ie.(*intrusiveEdge).Weight, nil +} + +func newBaseGraph(features GraphFeature, allowsLoops, allowsMultipleEdges bool, ops baseGraphOps) *baseGraph { + var edges datastructure.Map + if features&DeterministicIteration != 0 { + edges = datastructure.NewLinkedHashMap() + } else { + edges = datastructure.NewHashMap() + } + + return &baseGraph{ + AllowsLoops: allowsLoops, + AllowsMultipleEdges: allowsMultipleEdges, + Ops: ops, + + features: features, + edges: edges, + } +} + +type baseUndirectedGraph struct { + *baseGraph + Ops baseUndirectedGraphOps +} + +type baseUndirectedGraphOps interface { + baseGraphOps + DegreeOf(vertex Vertex) uint +} + +func (ug *baseUndirectedGraph) DegreeOf(vertex Vertex) (uint, error) { + if !ug.ContainsVertex(vertex) { + return 0, &VertexNotFoundError{vertex} + } + + return ug.Ops.DegreeOf(vertex), nil +} + +func newBaseUndirectedGraph(features GraphFeature, allowsLoops, allowsMultipleEdges bool, ops baseUndirectedGraphOps) *baseUndirectedGraph { + return &baseUndirectedGraph{newBaseGraph(features, allowsLoops, allowsMultipleEdges, ops), ops} +} + +type baseDirectedGraph struct { + *baseGraph + Ops baseDirectedGraphOps +} + +type baseDirectedGraphOps interface { + baseGraphOps + InDegreeOf(vertex Vertex) uint + IncomingEdgesOf(vertex Vertex) EdgeSet + OutDegreeOf(vertex Vertex) uint + OutgoingEdgesOf(vertex Vertex) EdgeSet +} + +func (dg *baseDirectedGraph) InDegreeOf(vertex Vertex) (uint, error) { + if !dg.ContainsVertex(vertex) { + return 0, &VertexNotFoundError{vertex} + } + + return dg.Ops.InDegreeOf(vertex), nil +} + +func (dg *baseDirectedGraph) IncomingEdgesOf(vertex Vertex) (EdgeSet, error) { + if !dg.ContainsVertex(vertex) { + return nil, &VertexNotFoundError{vertex} + } + + return dg.Ops.IncomingEdgesOf(vertex), nil +} + +func (dg *baseDirectedGraph) OutDegreeOf(vertex Vertex) (uint, error) { + if !dg.ContainsVertex(vertex) { + return 0, &VertexNotFoundError{vertex} + } + + return dg.Ops.OutDegreeOf(vertex), nil +} + +func (dg *baseDirectedGraph) OutgoingEdgesOf(vertex Vertex) (EdgeSet, error) { + if !dg.ContainsVertex(vertex) { + return nil, &VertexNotFoundError{vertex} + } + + return dg.Ops.OutgoingEdgesOf(vertex), nil +} + +func newBaseDirectedGraph(features GraphFeature, allowsLoops, allowsMultipleEdges bool, ops baseDirectedGraphOps) *baseDirectedGraph { + return &baseDirectedGraph{newBaseGraph(features, allowsLoops, allowsMultipleEdges, ops), ops} +} diff --git a/graph/directed.go b/graph/directed.go new file mode 100644 index 0000000..48b1e44 --- /dev/null +++ b/graph/directed.go @@ -0,0 +1,361 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2015-2017, by Barak Naveh and Contributors. + +package graph + +import ( + "github.com/puppetlabs/horsehead/v2/datastructure" +) + +// DirectedGraphSupportedFeatures are the features supported by all directed +// graphs. +const DirectedGraphSupportedFeatures = DeterministicIteration + +type directedEdgeContainer struct { + incoming MutableEdgeSet + outgoing MutableEdgeSet +} + +type directedVertexSet struct { + features GraphFeature + storage datastructure.Map // map[Vertex]*directedEdgeContainer +} + +func (vs *directedVertexSet) Contains(vertex Vertex) bool { + return vs.storage.Contains(vertex) +} + +func (vs *directedVertexSet) Count() uint { + return uint(vs.storage.Size()) +} + +func (vs *directedVertexSet) AsSlice() []Vertex { + s := make([]Vertex, 0, vs.Count()) + vs.storage.KeysInto(&s) + return s +} + +func (vs *directedVertexSet) ForEach(fn VertexSetIterationFunc) error { + return vs.storage.ForEachInto(func(key Vertex, value *directedEdgeContainer) error { + return fn(key) + }) +} + +func (vs *directedVertexSet) Add(vertex Vertex) { + if vs.storage.Contains(vertex) { + return + } + + vs.storage.Put(vertex, nil) +} + +func (vs *directedVertexSet) Remove(vertex Vertex) { + vs.storage.Remove(vertex) +} + +func (vs *directedVertexSet) initVertex(vertex Vertex) *directedEdgeContainer { + if !vs.storage.Contains(vertex) { + return nil + } + + var container *directedEdgeContainer + vs.storage.GetInto(vertex, &container) + + if container == nil { + container = &directedEdgeContainer{} + vs.storage.Put(vertex, container) + } + + return container +} + +func (vs *directedVertexSet) incomingEdgesOf(vertex Vertex) MutableEdgeSet { + container := vs.initVertex(vertex) + if container == nil { + return nil + } + + if container.incoming == nil { + if vs.features&DeterministicIteration != 0 { + container.incoming = NewMutableEdgeSet(datastructure.NewLinkedHashSet()) + } else { + container.incoming = NewMutableEdgeSet(datastructure.NewHashSet()) + } + } + + return container.incoming +} + +func (vs *directedVertexSet) outgoingEdgesOf(vertex Vertex) MutableEdgeSet { + container := vs.initVertex(vertex) + if container == nil { + return nil + } + + if container.outgoing == nil { + if vs.features&DeterministicIteration != 0 { + container.outgoing = NewMutableEdgeSet(datastructure.NewLinkedHashSet()) + } else { + container.outgoing = NewMutableEdgeSet(datastructure.NewHashSet()) + } + } + + return container.outgoing +} + +type directedGraphOps struct { + g *baseDirectedGraph + vertices *directedVertexSet +} + +func (o *directedGraphOps) EdgesBetween(source, target Vertex) EdgeSet { + if !o.g.ContainsVertex(source) || !o.g.ContainsVertex(target) { + return nil + } + + es := &unenforcedSliceEdgeSet{} + + o.vertices.outgoingEdgesOf(source).ForEach(func(edge Edge) error { + tt, _ := o.g.TargetVertexOf(edge) + if tt == target { + es.Add(edge) + } + + return nil + }) + + return es +} + +func (o *directedGraphOps) EdgeBetween(source, target Vertex) (Edge, error) { + if !o.g.ContainsVertex(source) || !o.g.ContainsVertex(target) { + return nil, &NotConnectedError{Source: source, Target: target} + } + + var found Edge + err := o.vertices.outgoingEdgesOf(source).ForEach(func(edge Edge) error { + tt, _ := o.g.TargetVertexOf(edge) + if tt == target { + found = edge + return datastructure.ErrStopIteration + } + + return nil + }) + + if err == datastructure.ErrStopIteration { + return found, nil + } + + return nil, &NotConnectedError{Source: source, Target: target} +} + +func (o *directedGraphOps) EdgesOf(vertex Vertex) EdgeSet { + if !o.g.ContainsVertex(vertex) { + return nil + } + + var set MutableEdgeSet + if o.g.Features()&DeterministicIteration != 0 { + set = NewMutableEdgeSet(datastructure.NewLinkedHashSet()) + } else { + set = NewMutableEdgeSet(datastructure.NewHashSet()) + } + + o.IncomingEdgesOf(vertex).ForEach(func(edge Edge) error { + set.Add(edge) + return nil + }) + o.OutgoingEdgesOf(vertex).ForEach(func(edge Edge) error { + set.Add(edge) + return nil + }) + + return set +} + +func (o *directedGraphOps) AddEdge(edge Edge) { + source, _ := o.g.SourceVertexOf(edge) + target, _ := o.g.TargetVertexOf(edge) + + o.vertices.outgoingEdgesOf(source).Add(edge) + o.vertices.incomingEdgesOf(target).Add(edge) +} + +func (o *directedGraphOps) RemoveEdge(edge Edge) { + source, _ := o.g.SourceVertexOf(edge) + target, _ := o.g.TargetVertexOf(edge) + + o.vertices.outgoingEdgesOf(source).Remove(edge) + o.vertices.incomingEdgesOf(target).Remove(edge) +} + +func (o *directedGraphOps) Vertices() MutableVertexSet { + return o.vertices +} + +func (o *directedGraphOps) InDegreeOf(vertex Vertex) uint { + if !o.g.ContainsVertex(vertex) { + return 0 + } + + return o.vertices.incomingEdgesOf(vertex).Count() +} + +func (o *directedGraphOps) IncomingEdgesOf(vertex Vertex) EdgeSet { + if !o.g.ContainsVertex(vertex) { + return nil + } + + return o.vertices.incomingEdgesOf(vertex) +} + +func (o *directedGraphOps) OutDegreeOf(vertex Vertex) uint { + if !o.g.ContainsVertex(vertex) { + return 0 + } + + return o.vertices.outgoingEdgesOf(vertex).Count() +} + +func (o *directedGraphOps) OutgoingEdgesOf(vertex Vertex) EdgeSet { + if !o.g.ContainsVertex(vertex) { + return nil + } + + return o.vertices.outgoingEdgesOf(vertex) +} + +func newDirectedGraph(features GraphFeature, allowLoops, allowMultipleEdges bool) *baseDirectedGraph { + var vertexStorage datastructure.Map + if features&DeterministicIteration != 0 { + vertexStorage = datastructure.NewLinkedHashMap() + } else { + vertexStorage = datastructure.NewHashMap() + } + + ops := &directedGraphOps{ + vertices: &directedVertexSet{features: features, storage: vertexStorage}, + } + + g := newBaseDirectedGraph(features, allowLoops, allowMultipleEdges, ops) + ops.g = g + + return g +} + +// +// Simple graphs +// + +// A SimpleDirectedGraph is a directed graph that does not permit loops or +// multiple edges between vertices. +type SimpleDirectedGraph struct { + MutableDirectedGraph +} + +// NewSimpleDirectedGraph creates a new simple directed graph. +func NewSimpleDirectedGraph() *SimpleDirectedGraph { + return NewSimpleDirectedGraphWithFeatures(0) +} + +// NewSimpleDirectedGraphWithFeatures creates a new simple directed graph with +// the specified graph features. +func NewSimpleDirectedGraphWithFeatures(features GraphFeature) *SimpleDirectedGraph { + return &SimpleDirectedGraph{newDirectedGraph(features&DirectedGraphSupportedFeatures, false, false)} +} + +// A SimpleDirectedWeightedGraph is a simple directed graph for which edges are +// assigned weights. +type SimpleDirectedWeightedGraph struct { + MutableDirectedWeightedGraph +} + +// NewSimpleDirectedWeightedGraph creates a new simple directed weighted graph. +func NewSimpleDirectedWeightedGraph() *SimpleDirectedWeightedGraph { + return NewSimpleDirectedWeightedGraphWithFeatures(0) +} + +// NewSimpleDirectedWeightedGraphWithFeatures creates a new simple directed +// weighted graph with the specified graph features. +func NewSimpleDirectedWeightedGraphWithFeatures(features GraphFeature) *SimpleDirectedWeightedGraph { + return &SimpleDirectedWeightedGraph{newDirectedGraph(features&DirectedGraphSupportedFeatures, false, false)} +} + +// +// Multigraphs +// + +// A DirectedMultigraph is a directed graph that does not permit loops, but does +// permit multiple edges between any two vertices. +type DirectedMultigraph struct { + MutableDirectedGraph +} + +// NewDirectedMultigraph creates a new directed multigraph. +func NewDirectedMultigraph() *DirectedMultigraph { + return NewDirectedMultigraphWithFeatures(0) +} + +// NewDirectedMultigraphWithFeatures creates a new directed multigraph with the +// specified graph features. +func NewDirectedMultigraphWithFeatures(features GraphFeature) *DirectedMultigraph { + return &DirectedMultigraph{newDirectedGraph(features&DirectedGraphSupportedFeatures, false, true)} +} + +// A DirectedWeightedMultigraph is a directed multigraph for which edges are +// assigned weights. +type DirectedWeightedMultigraph struct { + MutableDirectedWeightedGraph +} + +// NewDirectedWeightedMultigraph creates a new directed weighted multigraph. +func NewDirectedWeightedMultigraph() *DirectedWeightedMultigraph { + return NewDirectedWeightedMultigraphWithFeatures(0) +} + +// NewDirectedWeightedMultigraphWithFeatures creates a new directed weighted +// multigraph with the specified graph features. +func NewDirectedWeightedMultigraphWithFeatures(features GraphFeature) *DirectedWeightedMultigraph { + return &DirectedWeightedMultigraph{newDirectedGraph(features&DirectedGraphSupportedFeatures, false, true)} +} + +// +// Pseudographs +// + +// A DirectedPseudograph is a directed graph that permits both loops and +// multiple edges between vertices. +type DirectedPseudograph struct { + MutableDirectedGraph +} + +// NewDirectedPseudograph creates a new directed pseudograph. +func NewDirectedPseudograph() *DirectedPseudograph { + return NewDirectedPseudographWithFeatures(0) +} + +// NewDirectedPseudographWithFeatures a new directed pseudograph with the given +// graph features. +func NewDirectedPseudographWithFeatures(features GraphFeature) *DirectedPseudograph { + return &DirectedPseudograph{newDirectedGraph(features&DirectedGraphSupportedFeatures, true, true)} +} + +// A DirectedWeightedPseudograph is a directed pseudograph for which the edges +// are assigned weights. +type DirectedWeightedPseudograph struct { + MutableDirectedWeightedGraph +} + +// NewDirectedWeightedPseudograph creates a new directed weighted pseudograph. +func NewDirectedWeightedPseudograph() *DirectedWeightedPseudograph { + return NewDirectedWeightedPseudographWithFeatures(0) +} + +// NewDirectedWeightedPseudographWithFeatures creates a new directed weighted +// pseudograph with the given graph features. +func NewDirectedWeightedPseudographWithFeatures(features GraphFeature) *DirectedWeightedPseudograph { + return &DirectedWeightedPseudograph{newDirectedGraph(features&DirectedGraphSupportedFeatures, true, true)} +} diff --git a/graph/directed_test.go b/graph/directed_test.go new file mode 100644 index 0000000..a3b34a5 --- /dev/null +++ b/graph/directed_test.go @@ -0,0 +1,204 @@ +package graph + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + SimpleDirectedGraphConstructors = []func() MutableDirectedGraph{ + func() MutableDirectedGraph { return NewSimpleDirectedGraph() }, + func() MutableDirectedGraph { return NewSimpleDirectedGraphWithFeatures(DeterministicIteration) }, + func() MutableDirectedGraph { return NewSimpleDirectedWeightedGraph() }, + func() MutableDirectedGraph { return NewSimpleDirectedWeightedGraphWithFeatures(DeterministicIteration) }, + } + + DirectedMultigraphConstructors = []func() MutableDirectedGraph{ + func() MutableDirectedGraph { return NewDirectedMultigraph() }, + func() MutableDirectedGraph { return NewDirectedMultigraphWithFeatures(DeterministicIteration) }, + func() MutableDirectedGraph { return NewDirectedWeightedMultigraph() }, + func() MutableDirectedGraph { return NewDirectedWeightedMultigraphWithFeatures(DeterministicIteration) }, + } + + DirectedPseudographConstructors = []func() MutableDirectedGraph{ + func() MutableDirectedGraph { return NewDirectedPseudograph() }, + func() MutableDirectedGraph { return NewDirectedPseudographWithFeatures(DeterministicIteration) }, + func() MutableDirectedGraph { return NewDirectedWeightedPseudograph() }, + func() MutableDirectedGraph { return NewDirectedWeightedPseudographWithFeatures(DeterministicIteration) }, + } + + DirectedConstructors = append( + append( + append([]func() MutableDirectedGraph{}, SimpleDirectedGraphConstructors...), + DirectedMultigraphConstructors...), + DirectedPseudographConstructors...) +) + +func TestDirectedMultigraphEdgeAddition(t *testing.T) { + for _, constructor := range DirectedMultigraphConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + + assert.NoError(t, g.AddEdge("a", "b", 1)) + + assert.Equal(t, uint(2), g.Vertices().Count()) + assert.True(t, g.Vertices().Contains("a")) + assert.True(t, g.Vertices().Contains("b")) + + edge, err := g.EdgeBetween("a", "b") + assert.NoError(t, err) + assert.Equal(t, 1, edge) + + edge, err = g.EdgeBetween("b", "a") + assert.Equal(t, &NotConnectedError{"b", "a"}, err) + assert.Nil(t, edge) + + assert.NoError(t, g.AddEdge("a", "b", 2)) + + edges := g.EdgesBetween("a", "b") + assert.Equal(t, uint(2), edges.Count()) + assert.True(t, edges.Contains(1)) + assert.True(t, edges.Contains(2)) + + in, err := g.InDegreeOf("a") + assert.NoError(t, err) + assert.Equal(t, uint(0), in) + + out, err := g.OutDegreeOf("a") + assert.NoError(t, err) + assert.Equal(t, uint(2), out) + } +} + +func TestDirectedConnections(t *testing.T) { + for _, constructor := range DirectedConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + g.AddVertex("c") + + assert.NoError(t, g.AddEdge("a", "b", 1)) + assert.NoError(t, g.AddEdge("b", "a", 2)) + assert.NoError(t, g.AddEdge("b", "c", 3)) + + counts := map[string]*struct{ In, Out uint }{ + "a": {In: 1, Out: 1}, + "b": {In: 1, Out: 2}, + "c": {In: 1, Out: 0}, + } + + for vertex, expected := range counts { + n, err := g.InDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.In, n, "for vertex %s", vertex) + + n, err = g.OutDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.Out, n, "for vertex %s", vertex) + } + + assert.True(t, g.RemoveEdge(1)) + + // We can't remove the same edge again, obviously. + assert.False(t, g.RemoveEdge(1)) + + counts["a"].Out-- + counts["b"].In-- + + for vertex, expected := range counts { + n, err := g.InDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.In, n, "for vertex %s", vertex) + + n, err = g.OutDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.Out, n, "for vertex %s", vertex) + } + + edge, err := g.RemoveEdgeBetween("b", "a") + assert.NoError(t, err) + assert.Equal(t, 2, edge) + + counts["a"].In-- + counts["b"].Out-- + + for vertex, expected := range counts { + n, err := g.InDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.In, n, "for vertex %s", vertex) + + n, err = g.OutDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.Out, n, "for vertex %s", vertex) + } + + assert.True(t, g.RemoveVertex("c")) + + // We can't remove the same vertex either. + assert.False(t, g.RemoveVertex("c")) + + delete(counts, "c") + counts["b"].Out-- + + for vertex, expected := range counts { + n, err := g.InDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.In, n, "for vertex %s", vertex) + + n, err = g.OutDegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected.Out, n, "for vertex %s", vertex) + } + + _, err = g.InDegreeOf("c") + assert.Equal(t, &VertexNotFoundError{Vertex: "c"}, err) + + _, err = g.OutDegreeOf("c") + assert.Equal(t, &VertexNotFoundError{Vertex: "c"}, err) + } +} + +func TestDirectedVertexNotFound(t *testing.T) { + for _, constructor := range DirectedConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + g.Connect("a", "b") + + edge, err := g.EdgeBetween("a", "c") + assert.Nil(t, edge) + assert.Equal(t, &NotConnectedError{Source: "a", Target: "c"}, err) + + edge, err = g.EdgesOf("c") + assert.Nil(t, edge) + assert.Equal(t, &VertexNotFoundError{Vertex: "c"}, err) + } +} + +func TestDirectedEdgeNotFound(t *testing.T) { + for _, constructor := range DirectedConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + g.AddVertex("c") + g.Connect("b", "c") + + edge, err := g.EdgeBetween("a", "b") + assert.Nil(t, edge) + assert.Equal(t, &NotConnectedError{Source: "a", Target: "b"}, err) + + edge, err = g.EdgeBetween("c", "b") + assert.Nil(t, edge) + assert.Equal(t, &NotConnectedError{Source: "c", Target: "b"}, err) + + v, err := g.SourceVertexOf(NewEdge()) + assert.Nil(t, v) + assert.Equal(t, ErrEdgeNotFound, err) + + v, err = g.TargetVertexOf(NewEdge()) + assert.Nil(t, v) + assert.Equal(t, ErrEdgeNotFound, err) + } +} diff --git a/graph/edge_set.go b/graph/edge_set.go new file mode 100644 index 0000000..309f5cf --- /dev/null +++ b/graph/edge_set.go @@ -0,0 +1,83 @@ +package graph + +import ( + "github.com/puppetlabs/horsehead/v2/datastructure" +) + +type unenforcedSliceEdgeSet []Edge + +func (es unenforcedSliceEdgeSet) Contains(edge Edge) bool { + for _, e := range es { + if e == edge { + return true + } + } + + return false +} + +func (es unenforcedSliceEdgeSet) Count() uint { + return uint(len(es)) +} + +func (es unenforcedSliceEdgeSet) AsSlice() []Edge { + return es +} + +func (es unenforcedSliceEdgeSet) ForEach(fn EdgeSetIterationFunc) error { + for _, edge := range es { + if err := fn(edge); err != nil { + return err + } + } + + return nil +} + +func (es *unenforcedSliceEdgeSet) Add(edge Edge) { + *es = append(*es, edge) +} + +type edgeSet struct { + storage datastructure.Set +} + +func (es *edgeSet) Contains(edge Edge) bool { + return es.storage.Contains(edge) +} + +func (es *edgeSet) Count() uint { + return uint(es.storage.Size()) +} + +func (es *edgeSet) AsSlice() []Edge { + s := make([]Edge, es.Count()) + + i := 0 + es.ForEach(func(edge Edge) error { + s[i] = edge + i++ + + return nil + }) + + return s +} + +func (es *edgeSet) ForEach(fn EdgeSetIterationFunc) error { + return es.storage.ForEachInto(fn) +} + +func (es *edgeSet) Add(edge Edge) { + es.storage.Add(edge) +} + +func (es *edgeSet) Remove(edge Edge) { + es.storage.Remove(edge) +} + +// NewMutableEdgeSet creates a new mutable edge set using the given underlying +// set to store the edges. +func NewMutableEdgeSet(storage datastructure.Set) MutableEdgeSet { + return &edgeSet{storage} +} diff --git a/graph/errors.go b/graph/errors.go new file mode 100644 index 0000000..ca60eb5 --- /dev/null +++ b/graph/errors.go @@ -0,0 +1,40 @@ +package graph + +import ( + "errors" + "fmt" +) + +// VertexNotFoundError indicates that an operation involving a given vertex +// could not be completed because that vertex does not exist in the graph. +type VertexNotFoundError struct { + Vertex Vertex +} + +func (e *VertexNotFoundError) Error() string { + return fmt.Sprintf("graph: vertex %q does not exist", e.Vertex) +} + +// NotConnectedError indicates that an operation involving two vertices could +// not be completed because no edges connect those vertices. +type NotConnectedError struct { + Source, Target Vertex +} + +func (e *NotConnectedError) Error() string { + return fmt.Sprintf("graph: not connected: %q and %q", e.Source, e.Target) +} + +var ( + // ErrEdgeAlreadyInGraph indicates that an edge could not be added to a + // graph because an edge already exists in the graph. + ErrEdgeAlreadyInGraph = errors.New("graph: edge already present") + + // ErrEdgeNotFound indicates that an operation involving an edge could not + // be completed because that edge does not exist in the graph. + ErrEdgeNotFound = errors.New("graph: edge does not exist") + + // ErrWouldCreateLoop indicates that the addition of an edge would create a + // loop in the graph, and the graph does not support loops. + ErrWouldCreateLoop = errors.New("graph: loop would be created by edge") +) diff --git a/graph/features.go b/graph/features.go new file mode 100644 index 0000000..9e5cf7f --- /dev/null +++ b/graph/features.go @@ -0,0 +1,16 @@ +package graph + +// A GraphFeature is a mathematically transparent option for a graph. Graph +// features enable desireable application-specific functionality for a given +// graph (at the possible expense of performance). +type GraphFeature uint + +const ( + // DeterministicIteration is a graph feature that causes the insertion order + // of vertices and edges for a graph to be retained. As a result, iteration + // order over the vertices and edges will be the same, and many algorithms + // will be evaluated identically for identical graph constructions. + // + // This feature is particularly useful for debugging or for unit tests. + DeterministicIteration GraphFeature = 1 << iota +) diff --git a/graph/features_test.go b/graph/features_test.go new file mode 100644 index 0000000..55bba39 --- /dev/null +++ b/graph/features_test.go @@ -0,0 +1,33 @@ +package graph + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDeterministicIterationFeature(t *testing.T) { + g := NewSimpleGraphWithFeatures(DeterministicIteration) + + g.AddVertex("A") + g.AddVertex("B") + g.AddVertex("C") + g.AddVertex("D") + g.AddVertex("E") + + g.AddEdge("A", "B", 1) + g.AddEdge("B", "C", 2) + g.AddEdge("D", "E", 3) + g.AddEdge("C", "D", 4) + g.AddEdge("A", "E", 5) + g.AddEdge("A", "D", 6) + g.AddEdge("A", "C", 7) + + for i := 0; i < 10; i++ { + assert.Equal(t, []Vertex{"A", "B", "C", "D", "E"}, g.Vertices().AsSlice()) + assert.Equal(t, []Edge{1, 2, 3, 4, 5, 6, 7}, g.Edges().AsSlice()) + + edges, err := g.EdgesOf("A") + assert.NoError(t, err) + assert.Equal(t, []Edge{1, 5, 6, 7}, edges.AsSlice()) + } +} diff --git a/graph/graph.go b/graph/graph.go new file mode 100644 index 0000000..05f979e --- /dev/null +++ b/graph/graph.go @@ -0,0 +1,334 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2003-2016, by Barak Naveh and Contributors. + +// Package gographt provides interfaces and data structures that describe +// discrete graphs. It is inspired by the excellent JGraphT library +// (http://jgrapht.org/) and makes use of some applicable parts of it. Note that +// there are licensing constraints on JGraphT that may apply to this package as +// well; you may assume some risk by using it. See +// https://github.com/jgrapht/jgrapht/wiki/Relicensing for more information. +// +// This package supports simple graphs, multigraphs, and pseudographs in both +// directed and undirected variants. Each of these variants may be weighted or +// unweighted. +// +// A unique feature of this package is support for deterministic iteration; that +// is, each graph can retain information about the order of vertices and edges +// added to it, and given an immutable copy, iterate in a consistent order (not +// necessarily that of insertion, though). Most algorithms implemented also +// support deterministic iteration. +// +// In general, public graph interfaces are immutable and the types returned by +// constructor methods are mutable. Algorithms that accept graphs will always +// use the immutable interfaces and clone the graph if needed to perform +// computations. +package graph + +// A Vertex is the type of a node used in a graph. +// +// Vertices may be any type; they are distinguished from interface{} only for +// clarity. When adding a vertex to a graph, the Go language specification +// comparison rules apply to determine whether the vertex already exists. +type Vertex interface{} + +// VertexSetIterationFunc is a callback function used by the ForEach method of a +// vertex set. +// +// This function can return datastructure.StopIteration to break from the iteration. +type VertexSetIterationFunc func(vertex Vertex) error + +// A VertexSet is a read-only collection of vertices. +type VertexSet interface { + // Contains returns true if the given vertex exists in this set, and false + // otherwise. + Contains(vertex Vertex) bool + + // Count returns the number of vertices in this set. + Count() uint + + // AsSlice returns all the vertices in this set as a slice. + AsSlice() []Vertex + + // ForEach iterates over each vertex in this set, invoking the given + // function on each iteration. If the function returns an error, iteration + // is stopped and the error is returned. + ForEach(fn VertexSetIterationFunc) error +} + +// A MutableVertexSet is an extension of VertexSet that additionally supports +// adding and removing vertices. +type MutableVertexSet interface { + VertexSet + + // Add adds the given vertex to this set. If the vertex already exists in + // this set, it is not duplicated. + Add(vertex Vertex) + + // Remove removes the given vertex from this set if it exists. + Remove(vertex Vertex) +} + +// An Edge is the type of a connection between vertices. +// +// Edges may be any type; they are distinguished from interface{} only for +// clarity. When adding an edge to a graph, the Go language specification rules +// apply to determine whether the edge already exists. +type Edge interface{} + +// EdgeSetIterationFunc is a callback function used by the ForEach method of an +// edge set. +// +// This function can return datastructure.StopIteration to break from the iteration. +type EdgeSetIterationFunc func(edge Edge) error + +// An EdgeSet is a read-only collection of edges. +type EdgeSet interface { + // Contains returns true if the given edge exists in this set, and false + // otherwise. + Contains(edge Edge) bool + + // Count returns the number of edges in this set. + Count() uint + + // AsSlice returns all the edges in this set as a slice. + AsSlice() []Edge + + // ForEach iterates over each edge in this set, invoking the given function + // on each iteration. If the function returns an error, iteration is stopped + // and the error is returned. + ForEach(fn EdgeSetIterationFunc) error +} + +// A MutableEdgeSet is an extension of EdgeSet that additionally supports adding +// and removing edges. +type MutableEdgeSet interface { + EdgeSet + + // Add adds the given edge to this set. If the edge already exists in this + // set, it is not duplicated. + Add(edge Edge) + + // Remove removes the given edge from this set if it exists. + Remove(edge Edge) +} + +// DefaultEdgeWeight is the weight of an edge connected without explicitly +// specifying a weight in a weighted graph. +const DefaultEdgeWeight = float64(1.) + +// A Graph is a structure that contains a collection of vertices, some of which +// may be related to each other by edges. +type Graph interface { + // Features returns the graph features being used for this graph. + Features() GraphFeature + + // EdgesBetween finds all edges that connect the given vertices and returns + // a read-only view of them. If the DeterministicIteration feature is used + // in this graph, the edge set will always iterate over the edges in the + // same order. + EdgesBetween(source, target Vertex) EdgeSet + + // EdgeBetween finds an arbitrary edge that connects the given vertices and + // returns it. If the DeterministicIteration feature is used in this graph, + // the returned edge will always be the same. If no edge connects the given + // vertices, an error of type NotConnectedError is returned. If prior + // knowledge ensures the vertices are connected by at least one edge, the + // error from this method can be safely ignored. + EdgeBetween(source, target Vertex) (Edge, error) + + // ContainsEdgeBetween returns true if at least one edge connects the given + // source and target vertices, and false otherwise. + ContainsEdgeBetween(source, target Vertex) bool + + // ContainsEdge returns true if the given edge exists in this graph, and + // false otherwise. + ContainsEdge(edge Edge) bool + + // ContainsVertex returns true if the given vertex exists in this graph, and + // false otherwise. + ContainsVertex(vertex Vertex) bool + + // Edges returns a read-only view of all edges in this graph. If the + // DeterministicIteration feature is used in this graph, the edge set will + // always iterate over the edges in the same order. + Edges() EdgeSet + + // EdgesOf returns a read-only view of all edges connected to the given + // vertex regardless of their direction. If the given vertex does not exist + // in this graph, an error of type VertexNotFoundError is returned. If prior + // knowledge ensures the vertex exists in the graph, the error from this + // method can be safely ignored. If the DeterministicIteration feature is + // used in this graph, the edge set will always iterate over the edges in + // the same order. + EdgesOf(vertex Vertex) (EdgeSet, error) + + // Vertices returns a read-only view of all vertices in this graph. If the + // DeterministicIteration feature is used in this graph, the vertex set will + // always iterate over the vertices in the same order. + Vertices() VertexSet + + // SourceVertexOf returns the source vertex for a given edge. If the edge + // does not exist in the graph, ErrEdgeNotFound is returned. If prior + // knowledge ensures the edge exists in the graph, the error from this + // method can be safely ignored. + SourceVertexOf(edge Edge) (Vertex, error) + + // TargetVertexOf returns the target vertex for a given edge. If the edge + // does not exist in the graph, ErrEdgeNotFound is returned. If prior + // knowledge ensures the edge exists in the graph, the error from this + // method can be safely ignored. + TargetVertexOf(edge Edge) (Vertex, error) + + // WeightOf returns the weight associated with a given edge. For unweighted + // graphs, this is always the same as DefaultEdgeWeight. If the edge does + // not exist in the graph, ErrEdgeNotFound is returned. If prior knowledge + // ensures the edge exists in the graph, the error from this method can be + // safely ignored. + WeightOf(edge Edge) (float64, error) +} + +// Mutable is a graph mixin that allows graphs to be modified. +type Mutable interface { + // Connect adds an edge between the given source and target vertices. The + // edge is created using the NewEdge function. If this graph does not + // contain either the source or target vertices, an error of type + // VertexNotFoundError is returned. If this graph does not permit multiple + // edges and the source and target vertices are already connected, + // ErrEdgeAlreadyInGraph is returned. If this graph does not permit loops + // and the source and target vertices are the same, ErrWouldCreateLoop is + // returned. + Connect(source, target Vertex) error + + // AddEdge adds the given edge between the given source and target vertices. + // If this graph already contains the given edge, ErrEdgeAlreadyInGraph is + // returned. If this graph does not contain either the source or target + // vertices, an error of type VertexNotFoundError is returned. If this graph + // does not permit multiple edges and the source and target vertices are + // already connected, ErrEdgeAlreadyInGraph is returned. If this graph does + // not permit loops and the source and target vertices are the same, + // ErrWouldCreateLoop is returned. + AddEdge(source, target Vertex, edge Edge) error + + // AddVertex adds the given vertex to the graph. If the vertex already + // exists in the graph, it is not duplicated. + AddVertex(vertex Vertex) + + // RemoveEdges removes the given edges from the graph if they exists. It + // returns true if the graph was modified, and false otherwise. + RemoveEdges(edges []Edge) bool + + // RemoveEdgesBetween removes all edges that connect the given source and + // target vertices and returns a read-only view of the removed edges. If the + // DeterministicIteration feature is used by this graph, the edge set will + // always iterate over the edges in the same order. + RemoveEdgesBetween(source, target Vertex) EdgeSet + + // RemoveEdge removes the given edge from the graph if it exists. It returns + // true if the graph was modified, and false otherwise. + RemoveEdge(edge Edge) bool + + // RemoveEdgeBetween removes an arbitrary edge connecting the given source + // and target vertices from the graph. If no edge connects the given + // vertices, an error of type NotConnectedError is returned. Otherwise, the + // removed edge is returned. If the DeterministicIteration feature is used + // by this graph, repeated calls to this function will remove edges in the + // same order. + RemoveEdgeBetween(source, target Vertex) (Edge, error) + + // RemoveVertices removes the given vertices from this graph. It returns + // true if the graph was modified, and false otherwise. + RemoveVertices(vertices []Vertex) bool + + // RemoveVertex removes the given vertex from this graph. It returns true if + // the graph was modified, and false otherwise. + RemoveVertex(vertex Vertex) bool +} + +// A DirectedGraph is a graph for which the direction of an edge's assocation to +// its vertices is important. +type DirectedGraph interface { + Graph + + // InDegreeOf returns the number of edges directed toward the given vertex. + // If the vertex does not exist in the graph, an error of type + // VertexNotFoundError is returned. If prior knowledge ensures the vertex + // exists in the graph, the error from this method can be safely ignored. + InDegreeOf(vertex Vertex) (uint, error) + + // IncomingEdgesOf returns a read-only view of the edges that are directed + // toward the given vertex. If the vertex does not exist in the graph, an + // error of type VertexNotFoundError is returned. If prior knowledge ensures + // the vertex exists in the graph, the error from this method can be safely + // ignored. + IncomingEdgesOf(vertex Vertex) (EdgeSet, error) + + // OutDegreeOf returns the number of edges directed outward from the given + // vertex. If the vertex does not exist in the graph, an error of type + // VertexNotFoundError is returned. If prior knowledge ensures the vertex + // exists in the graph, the error from this method can be safely ignored. + OutDegreeOf(vertex Vertex) (uint, error) + + // OutgoingEdgesOf returns a read-only view of the edges that are directed + // outward from the given vertex. If the vertex does not exist in the graph, + // an error of type VertexNotFoundError is returned. If prior knowledge + // ensures the vertex exists in the graph, the error from this method can be + // safely ignored. + OutgoingEdgesOf(vertex Vertex) (EdgeSet, error) +} + +// A MutableDirectedGraph is a directed graph that supports modification. +type MutableDirectedGraph interface { + DirectedGraph + Mutable +} + +// An UndirectedGraph is a graph for which the direction of an edge's assocation +// to its vertices is not important. +type UndirectedGraph interface { + Graph + + // DegreeOf returns the number of edges connected to the given vertex. If + // the vertex does not exist in the graph, an error of type + // VertexNotFoundError is returned. If prior knowledge ensures the vertex + // exists in the graph, the error from this method can be safely ignored. + DegreeOf(vertex Vertex) (uint, error) +} + +// A MutableUndirectedGraph is an undirected graph that supports modification. +type MutableUndirectedGraph interface { + UndirectedGraph + Mutable +} + +// Weighted is a graph mixin that allows edges to be assigned arbitrary +// numerical weights. These weights can be important in evaluating certain +// algorithms. +type Weighted interface { + // ConnectWithWeight associates the given source and target vertices with + // each other using the same mechanism as Connect. The given weight is used + // to label the resulting connection. + ConnectWithWeight(source, target Vertex, weight float64) error + + // AddEdgeWithWeight associates the given source and target vertices with + // each other using the given edge. The given weight is used to label the + // resulting connection. + AddEdgeWithWeight(source, target Vertex, edge Edge, weight float64) error +} + +// A MutableDirectedWeightedGraph is a directed weighted graph that supports +// modification. +type MutableDirectedWeightedGraph interface { + DirectedGraph + Weighted + Mutable +} + +// A MutableUndirectedWeightedGraph is an undirected weighted graph that +// supports modification. +type MutableUndirectedWeightedGraph interface { + UndirectedGraph + Weighted + Mutable +} diff --git a/graph/traverse/depth_first.go b/graph/traverse/depth_first.go new file mode 100644 index 0000000..11f551f --- /dev/null +++ b/graph/traverse/depth_first.go @@ -0,0 +1,101 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2003-2018, by Liviu Rau and Contributors. + +package traverse + +import ( + "reflect" + + "github.com/puppetlabs/horsehead/v2/graph" +) + +var depthFirstSentinel = struct{}{} + +type depthFirstStackElement struct { + vertex graph.Vertex + + prevStackElement *depthFirstStackElement +} + +type DepthFirstTraverser struct { + g graph.Graph + start graph.Vertex +} + +func (t *DepthFirstTraverser) forEachEdgeOf(vertex graph.Vertex, fn graph.EdgeSetIterationFunc) { + var edges graph.EdgeSet + if dg, ok := t.g.(graph.DirectedGraph); ok { + edges, _ = dg.OutgoingEdgesOf(vertex) + } else { + edges, _ = t.g.EdgesOf(vertex) + } + + edges.ForEach(fn) +} + +func (t *DepthFirstTraverser) ForEach(fn func(vertex graph.Vertex) error) error { + seen := make(map[graph.Vertex]struct{}) + + stack := &depthFirstStackElement{vertex: t.start} + for stack != nil { + var cur graph.Vertex + cur, stack = stack.vertex, stack.prevStackElement + + if _, found := seen[cur]; found { + continue + } + + seen[cur] = depthFirstSentinel + + if err := fn(cur); err != nil { + return err + } + + t.forEachEdgeOf(cur, func(edge graph.Edge) error { + next, _ := graph.OppositeVertexOf(t.g, edge, cur) + + stack = &depthFirstStackElement{ + vertex: next, + prevStackElement: stack, + } + + return nil + }) + } + + return nil +} + +func (t *DepthFirstTraverser) ForEachInto(fn interface{}) error { + fnr := reflect.ValueOf(fn) + fnt := fnr.Type() + + if fnt.NumOut() != 1 { + panic(ErrInvalidFuncSignature) + } + + return t.ForEach(func(vertex graph.Vertex) error { + p := reflect.ValueOf(vertex) + if !p.IsValid() { + p = reflect.Zero(fnt.In(0)) + } + + r := fnr.Call([]reflect.Value{p}) + + err := r[0] + if !err.IsNil() { + return err.Interface().(error) + } + + return nil + }) +} + +func NewDepthFirstTraverser(g graph.Graph, start graph.Vertex) *DepthFirstTraverser { + return &DepthFirstTraverser{ + g: g, + start: start, + } +} diff --git a/graph/traverse/errors.go b/graph/traverse/errors.go new file mode 100644 index 0000000..907dccd --- /dev/null +++ b/graph/traverse/errors.go @@ -0,0 +1,8 @@ +package traverse + +import "errors" + +var ( + ErrCyclicGraph = errors.New("traverse: graph is cyclic") + ErrInvalidFuncSignature = errors.New("traverse: invalid function signature") +) diff --git a/graph/traverse/topological_order.go b/graph/traverse/topological_order.go new file mode 100644 index 0000000..557a127 --- /dev/null +++ b/graph/traverse/topological_order.go @@ -0,0 +1,112 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2004-2017, by Marden Neubert and Contributors. + +package traverse + +import ( + "reflect" + + "github.com/puppetlabs/horsehead/v2/graph" +) + +type TopologicalOrderTraverser struct { + g graph.DirectedGraph +} + +func (t *TopologicalOrderTraverser) Vertices() ([]graph.Vertex, error) { + vertices := make([]graph.Vertex, t.g.Vertices().Count()) + + i := 0 + err := t.ForEach(func(vertex graph.Vertex) error { + vertices[i] = vertex + i++ + + return nil + }) + if err != nil { + return nil, err + } + + return vertices, nil +} + +func (t *TopologicalOrderTraverser) ForEach(fn func(vertex graph.Vertex) error) error { + if t.g.Vertices().Count() == 0 { + return nil + } + + var queue []graph.Vertex + remaining := make(map[graph.Vertex]uint) + + // Find our starting point(s). + t.g.Vertices().ForEach(func(vertex graph.Vertex) error { + if in, _ := t.g.InDegreeOf(vertex); in == 0 { + queue = append(queue, vertex) + } + + remaining[vertex], _ = t.g.InDegreeOf(vertex) + + return nil + }) + + if len(queue) == 0 { + return ErrCyclicGraph + } + + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + + if err := fn(cur); err != nil { + return err + } + + edges, _ := t.g.OutgoingEdgesOf(cur) + edges.ForEach(func(edge graph.Edge) error { + next, _ := t.g.TargetVertexOf(edge) + + if remaining[next] > 0 { + remaining[next]-- + + if remaining[next] == 0 { + queue = append(queue, next) + } + } + + return nil + }) + } + + return nil +} + +func (t *TopologicalOrderTraverser) ForEachInto(fn interface{}) error { + fnr := reflect.ValueOf(fn) + fnt := fnr.Type() + + if fnt.NumOut() != 1 { + panic(ErrInvalidFuncSignature) + } + + return t.ForEach(func(vertex graph.Vertex) error { + p := reflect.ValueOf(vertex) + if !p.IsValid() { + p = reflect.Zero(fnt.In(0)) + } + + r := fnr.Call([]reflect.Value{p}) + + err := r[0] + if !err.IsNil() { + return err.Interface().(error) + } + + return nil + }) +} + +func NewTopologicalOrderTraverser(g graph.DirectedGraph) *TopologicalOrderTraverser { + return &TopologicalOrderTraverser{g} +} diff --git a/graph/traverse/topological_order_test.go b/graph/traverse/topological_order_test.go new file mode 100644 index 0000000..3ab654b --- /dev/null +++ b/graph/traverse/topological_order_test.go @@ -0,0 +1,94 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2005-2017, by John V Sichi and Contributors. + +package traverse + +import ( + "math/rand" + "testing" + + "github.com/puppetlabs/horsehead/v2/graph" + "github.com/stretchr/testify/assert" +) + +func TestTopologicalOrderTraverser(t *testing.T) { + // This is really the test case from the JGraphT source code. + v := []string{ + "preheat oven", + "sift dry ingredients", + "stir wet ingredients", + "mix wet and dry ingredients", + "spoon onto pan", + "bake", + "cool", + "frost", + "eat", + } + + g := graph.NewSimpleDirectedGraph() + for _, i := range rand.Perm(len(v)) { + g.AddVertex(v[i]) + } + + g.Connect(v[0], v[1]) + g.Connect(v[1], v[2]) + g.Connect(v[0], v[2]) + g.Connect(v[1], v[3]) + g.Connect(v[2], v[3]) + g.Connect(v[3], v[4]) + g.Connect(v[4], v[5]) + g.Connect(v[5], v[6]) + g.Connect(v[6], v[7]) + g.Connect(v[7], v[8]) + g.Connect(v[6], v[8]) + + i := 0 + err := NewTopologicalOrderTraverser(g).ForEach(func(vertex graph.Vertex) error { + assert.Equal(t, v[i], vertex) + i++ + + return nil + }) + assert.NoError(t, err) + assert.Len(t, v, i) +} + +func TestTopologicalOrderTraverserWithDisconnectedRoots(t *testing.T) { + g := graph.NewSimpleDirectedGraphWithFeatures(graph.DeterministicIteration) + + g.AddVertex("a") + g.AddVertex("b") + g.Connect("a", "b") + + g.AddVertex(1) + g.AddVertex(2) + g.Connect(1, 2) + + vertices, err := NewTopologicalOrderTraverser(g).Vertices() + assert.NoError(t, err) + assert.Equal(t, []graph.Vertex{"a", 1, "b", 2}, vertices) +} + +func TestTopologicalOrderTraverserForEachInto(t *testing.T) { + g := graph.NewSimpleDirectedGraph() + + v := []string{"a", "b", "c"} + for _, i := range rand.Perm(len(v)) { + g.AddVertex(v[i]) + } + + g.Connect(v[0], v[1]) + g.Connect(v[1], v[2]) + + i := 0 + err := NewTopologicalOrderTraverser(g).ForEachInto(func(vertex string) error { + assert.Equal(t, v[i], vertex) + i++ + + return nil + }) + assert.NoError(t, err) + assert.Len(t, v, i) +} diff --git a/graph/undirected.go b/graph/undirected.go new file mode 100644 index 0000000..45be6e3 --- /dev/null +++ b/graph/undirected.go @@ -0,0 +1,292 @@ +// Portions of this file are derived from JGraphT, a free Java graph-theory +// library. +// +// (C) Copyright 2015-2016, by Barak Naveh and Contributors. + +package graph + +import ( + "github.com/puppetlabs/horsehead/v2/datastructure" +) + +// UndirectedGraphSupportedFeatures are the features supported by all undirected +// graphs. +const UndirectedGraphSupportedFeatures = DeterministicIteration + +type undirectedVertexSet struct { + features GraphFeature + storage datastructure.Map // map[Vertex]MutableEdgeSet +} + +func (vs *undirectedVertexSet) Contains(vertex Vertex) bool { + return vs.storage.Contains(vertex) +} + +func (vs *undirectedVertexSet) Count() uint { + return uint(vs.storage.Size()) +} + +func (vs *undirectedVertexSet) AsSlice() []Vertex { + s := make([]Vertex, 0, vs.Count()) + vs.storage.KeysInto(&s) + return s +} + +func (vs *undirectedVertexSet) ForEach(fn VertexSetIterationFunc) error { + return vs.storage.ForEachInto(func(key Vertex, value MutableEdgeSet) error { + return fn(key) + }) +} + +func (vs *undirectedVertexSet) Add(vertex Vertex) { + if vs.storage.Contains(vertex) { + return + } + + vs.storage.Put(vertex, nil) +} + +func (vs *undirectedVertexSet) Remove(vertex Vertex) { + vs.storage.Remove(vertex) +} + +func (vs *undirectedVertexSet) edgesOf(vertex Vertex) MutableEdgeSet { + if !vs.storage.Contains(vertex) { + return nil + } + + var set MutableEdgeSet + vs.storage.GetInto(vertex, &set) + + if set == nil { + if vs.features&DeterministicIteration != 0 { + set = NewMutableEdgeSet(datastructure.NewLinkedHashSet()) + } else { + set = NewMutableEdgeSet(datastructure.NewHashSet()) + } + + vs.storage.Put(vertex, set) + } + + return set +} + +type undirectedGraphOps struct { + g *baseUndirectedGraph + vertices *undirectedVertexSet +} + +func (o *undirectedGraphOps) EdgesBetween(source, target Vertex) EdgeSet { + if !o.g.ContainsVertex(source) || !o.g.ContainsVertex(target) { + return nil + } + + es := &unenforcedSliceEdgeSet{} + + o.vertices.edgesOf(source).ForEach(func(edge Edge) error { + if o.edgeHasSourceAndTarget(edge, source, target) { + es.Add(edge) + } + + return nil + }) + + return es +} + +func (o *undirectedGraphOps) EdgeBetween(source, target Vertex) (Edge, error) { + if !o.g.ContainsVertex(source) || !o.g.ContainsVertex(target) { + return nil, &NotConnectedError{Source: source, Target: target} + } + + var found Edge + err := o.vertices.edgesOf(source).ForEach(func(edge Edge) error { + if o.edgeHasSourceAndTarget(edge, source, target) { + found = edge + return datastructure.ErrStopIteration + } + + return nil + }) + + if err == datastructure.ErrStopIteration { + return found, nil + } + + return nil, &NotConnectedError{Source: source, Target: target} +} + +func (o *undirectedGraphOps) edgeHasSourceAndTarget(edge Edge, source, target Vertex) bool { + ts, _ := o.g.SourceVertexOf(edge) + tt, _ := o.g.TargetVertexOf(edge) + + return (source == ts && target == tt) || (source == tt && target == ts) +} + +func (o *undirectedGraphOps) EdgesOf(vertex Vertex) EdgeSet { + if !o.g.ContainsVertex(vertex) { + return nil + } + + return o.vertices.edgesOf(vertex) +} + +func (o *undirectedGraphOps) AddEdge(edge Edge) { + source, _ := o.g.SourceVertexOf(edge) + target, _ := o.g.TargetVertexOf(edge) + + o.vertices.edgesOf(source).Add(edge) + o.vertices.edgesOf(target).Add(edge) +} + +func (o *undirectedGraphOps) RemoveEdge(edge Edge) { + source, _ := o.g.SourceVertexOf(edge) + target, _ := o.g.TargetVertexOf(edge) + + o.vertices.edgesOf(source).Remove(edge) + o.vertices.edgesOf(target).Remove(edge) +} + +func (o *undirectedGraphOps) Vertices() MutableVertexSet { + return o.vertices +} + +func (o *undirectedGraphOps) DegreeOf(vertex Vertex) uint { + if !o.g.ContainsVertex(vertex) { + return 0 + } + + return o.vertices.edgesOf(vertex).Count() +} + +func newUndirectedGraph(features GraphFeature, allowLoops, allowMultipleEdges bool) *baseUndirectedGraph { + var vertexStorage datastructure.Map + if features&DeterministicIteration != 0 { + vertexStorage = datastructure.NewLinkedHashMap() + } else { + vertexStorage = datastructure.NewHashMap() + } + + ops := &undirectedGraphOps{ + vertices: &undirectedVertexSet{features: features, storage: vertexStorage}, + } + + g := newBaseUndirectedGraph(features, allowLoops, allowMultipleEdges, ops) + ops.g = g + + return g +} + +// +// Simple graphs +// + +// A SimpleGraph is an undirected graph that does not permit loops or multiple +// edges between vertices. +type SimpleGraph struct { + MutableUndirectedGraph +} + +// NewSimpleGraph creates a new simple graph. +func NewSimpleGraph() *SimpleGraph { + return NewSimpleGraphWithFeatures(0) +} + +// NewSimpleGraphWithFeatures creates a new simple graph with the specified +// graph features. +func NewSimpleGraphWithFeatures(features GraphFeature) *SimpleGraph { + return &SimpleGraph{newUndirectedGraph(features&UndirectedGraphSupportedFeatures, false, false)} +} + +// A SimpleWeightedGraph is a simple graph for which edges are assigned weights. +type SimpleWeightedGraph struct { + MutableUndirectedWeightedGraph +} + +// NewSimpleWeightedGraph creates a new simple weighted graph. +func NewSimpleWeightedGraph() *SimpleWeightedGraph { + return NewSimpleWeightedGraphWithFeatures(0) +} + +// NewSimpleWeightedGraphWithFeatures creates a new simple weighted graph with +// the specified graph features. +func NewSimpleWeightedGraphWithFeatures(features GraphFeature) *SimpleWeightedGraph { + return &SimpleWeightedGraph{newUndirectedGraph(features&UndirectedGraphSupportedFeatures, false, false)} +} + +// +// Multigraphs +// + +// An UndirectedMultigraph is an undirected graph that does not permit loops, +// but does permit multiple edges between any two vertices. +type UndirectedMultigraph struct { + MutableUndirectedGraph +} + +// NewUndirectedMultigraph creates a new undirected multigraph. +func NewUndirectedMultigraph() *UndirectedMultigraph { + return NewUndirectedMultigraphWithFeatures(0) +} + +// NewUndirectedMultigraphWithFeatures creates a new undirected multigraph with +// the specified graph features. +func NewUndirectedMultigraphWithFeatures(features GraphFeature) *UndirectedMultigraph { + return &UndirectedMultigraph{newUndirectedGraph(features&UndirectedGraphSupportedFeatures, false, true)} +} + +// An UndirectedWeightedMultigraph is an undirected multigraph for which edges +// are assigned weights. +type UndirectedWeightedMultigraph struct { + MutableUndirectedWeightedGraph +} + +// NewUndirectedWeightedMultigraph creates a new undirected weighted multigraph. +func NewUndirectedWeightedMultigraph() *UndirectedWeightedMultigraph { + return NewUndirectedWeightedMultigraphWithFeatures(0) +} + +// NewUndirectedWeightedMultigraphWithFeatures creates a new undirected weighted +// multigraph with the specified graph features. +func NewUndirectedWeightedMultigraphWithFeatures(features GraphFeature) *UndirectedWeightedMultigraph { + return &UndirectedWeightedMultigraph{newUndirectedGraph(features&UndirectedGraphSupportedFeatures, false, true)} +} + +// +// Pseudographs +// + +// An UndirectedPseudograph is an undirected graph that permits both loops and +// multiple edges between vertices. +type UndirectedPseudograph struct { + MutableUndirectedGraph +} + +// NewUndirectedPseudograph creates a new undirected pseudograph. +func NewUndirectedPseudograph() *UndirectedPseudograph { + return NewUndirectedPseudographWithFeatures(0) +} + +// NewUndirectedPseudographWithFeatures creates a new undirected pseudograph +// with the specified graph features. +func NewUndirectedPseudographWithFeatures(features GraphFeature) *UndirectedPseudograph { + return &UndirectedPseudograph{newUndirectedGraph(features&UndirectedGraphSupportedFeatures, true, true)} +} + +// An UndirectedWeightedPseudograph is an undirected pseudograph for which edges +// are assigned weights. +type UndirectedWeightedPseudograph struct { + MutableUndirectedWeightedGraph +} + +// NewUndirectedWeightedPseudograph creates a new undirected weighted +// pseudograph. +func NewUndirectedWeightedPseudograph() *UndirectedWeightedPseudograph { + return NewUndirectedWeightedPseudographWithFeatures(0) +} + +// NewUndirectedWeightedPseudographWithFeatures creates a new undirected +// weighted pseudograph with the specified graph features. +func NewUndirectedWeightedPseudographWithFeatures(features GraphFeature) *UndirectedWeightedPseudograph { + return &UndirectedWeightedPseudograph{newUndirectedGraph(features&UndirectedGraphSupportedFeatures, true, true)} +} diff --git a/graph/undirected_test.go b/graph/undirected_test.go new file mode 100644 index 0000000..c17cf48 --- /dev/null +++ b/graph/undirected_test.go @@ -0,0 +1,194 @@ +package graph + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +var ( + SimpleGraphConstructors = []func() MutableUndirectedGraph{ + func() MutableUndirectedGraph { return NewSimpleGraph() }, + func() MutableUndirectedGraph { return NewSimpleGraphWithFeatures(DeterministicIteration) }, + func() MutableUndirectedGraph { return NewSimpleWeightedGraph() }, + func() MutableUndirectedGraph { return NewSimpleWeightedGraphWithFeatures(DeterministicIteration) }, + } + + UndirectedMultigraphConstructors = []func() MutableUndirectedGraph{ + func() MutableUndirectedGraph { return NewUndirectedMultigraph() }, + func() MutableUndirectedGraph { return NewUndirectedMultigraphWithFeatures(DeterministicIteration) }, + func() MutableUndirectedGraph { return NewUndirectedWeightedMultigraph() }, + func() MutableUndirectedGraph { + return NewUndirectedWeightedMultigraphWithFeatures(DeterministicIteration) + }, + } + + UndirectedPseudographConstructors = []func() MutableUndirectedGraph{ + func() MutableUndirectedGraph { return NewUndirectedPseudograph() }, + func() MutableUndirectedGraph { return NewUndirectedPseudographWithFeatures(DeterministicIteration) }, + func() MutableUndirectedGraph { return NewUndirectedWeightedPseudograph() }, + func() MutableUndirectedGraph { + return NewUndirectedWeightedPseudographWithFeatures(DeterministicIteration) + }, + } + + UndirectedConstructors = append( + append( + append([]func() MutableUndirectedGraph{}, SimpleGraphConstructors...), + UndirectedMultigraphConstructors...), + UndirectedPseudographConstructors...) +) + +func TestUndirectedMultigraphEdgeAddition(t *testing.T) { + for _, constructor := range UndirectedMultigraphConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + + assert.NoError(t, g.AddEdge("a", "b", 1)) + + assert.Equal(t, uint(2), g.Vertices().Count()) + assert.True(t, g.Vertices().Contains("a")) + assert.True(t, g.Vertices().Contains("b")) + + edge, err := g.EdgeBetween("a", "b") + assert.NoError(t, err) + assert.Equal(t, 1, edge) + + assert.NoError(t, g.AddEdge("a", "b", 2)) + + edges := g.EdgesBetween("a", "b") + assert.Equal(t, uint(2), edges.Count()) + assert.True(t, edges.Contains(1)) + assert.True(t, edges.Contains(2)) + } +} + +func TestUndirectedConnections(t *testing.T) { + for _, constructor := range UndirectedConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + g.AddVertex("c") + g.AddVertex("d") + + assert.NoError(t, g.AddEdge("a", "b", 1)) + assert.NoError(t, g.AddEdge("b", "c", 2)) + assert.NoError(t, g.AddEdge("c", "d", 3)) + + counts := map[string]uint{ + "a": 1, + "b": 2, + "c": 2, + "d": 1, + } + + for vertex, expected := range counts { + n, err := g.DegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected, n, "for vertex %s", vertex) + } + + assert.True(t, g.RemoveEdge(1)) + + // We can't remove the same edge again, obviously. + assert.False(t, g.RemoveEdge(1)) + + counts["a"]-- + counts["b"]-- + + for vertex, expected := range counts { + n, err := g.DegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected, n) + } + + edge, err := g.RemoveEdgeBetween("b", "c") + assert.NoError(t, err) + assert.Equal(t, 2, edge) + + counts["b"]-- + counts["c"]-- + + for vertex, expected := range counts { + n, err := g.DegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected, n, "for vertex %s", vertex) + } + + assert.True(t, g.RemoveVertex("c")) + + // We can't remove the same vertex either. + assert.False(t, g.RemoveVertex("c")) + + delete(counts, "c") + counts["d"]-- + + for vertex, expected := range counts { + n, err := g.DegreeOf(vertex) + assert.NoError(t, err) + assert.Equal(t, expected, n, "for vertex %s", vertex) + } + + _, err = g.DegreeOf("c") + assert.Equal(t, &VertexNotFoundError{Vertex: "c"}, err) + } +} + +func TestUndirectedVertexNotFound(t *testing.T) { + for _, constructor := range UndirectedConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + g.Connect("a", "b") + + edge, err := g.EdgeBetween("a", "c") + assert.Nil(t, edge) + assert.Equal(t, &NotConnectedError{Source: "a", Target: "c"}, err) + + edge, err = g.EdgesOf("c") + assert.Nil(t, edge) + assert.Equal(t, &VertexNotFoundError{Vertex: "c"}, err) + } +} + +func TestUndirectedEdgeNotFound(t *testing.T) { + for _, constructor := range UndirectedConstructors { + g := constructor() + g.AddVertex("a") + g.AddVertex("b") + g.AddVertex("c") + g.Connect("b", "c") + + edge, err := g.EdgeBetween("a", "b") + assert.Nil(t, edge) + assert.Equal(t, &NotConnectedError{Source: "a", Target: "b"}, err) + + v, err := g.SourceVertexOf(NewEdge()) + assert.Nil(t, v) + assert.Equal(t, ErrEdgeNotFound, err) + + v, err = g.TargetVertexOf(NewEdge()) + assert.Nil(t, v) + assert.Equal(t, ErrEdgeNotFound, err) + } +} + +func ExampleSimpleGraph() { + g := NewSimpleGraph() + g.AddVertex("Charles") + g.AddVertex("Manson") + g.AddVertex("Marilyn") + g.AddVertex("Monroe") + + g.Connect("Charles", "Manson") + g.Connect("Marilyn", "Monroe") + g.Connect("Marilyn", "Manson") + + fmt.Println("Vertices:", g.Vertices().Count()) + fmt.Println("Edges:", g.Edges().Count()) + // Output: + // Vertices: 4 + // Edges: 3 +} diff --git a/graph/utils.go b/graph/utils.go new file mode 100644 index 0000000..d29f77c --- /dev/null +++ b/graph/utils.go @@ -0,0 +1,31 @@ +package graph + +import ( + "sync/atomic" +) + +var defaultEdgeIndex uint64 + +type defaultEdge struct { + index uint64 +} + +// NewEdge creates a globally unique edge that can be added to any graph. +func NewEdge() Edge { + return &defaultEdge{atomic.AddUint64(&defaultEdgeIndex, 1)} +} + +// OppositeVertexOf finds, for any graph, the vertex connected by the given edge +// that is not the given vertex. +func OppositeVertexOf(g Graph, e Edge, v Vertex) (Vertex, error) { + test, err := g.SourceVertexOf(e) + if err != nil { + return nil, err + } + + if test != v { + return test, nil + } + + return g.TargetVertexOf(e) +} diff --git a/graph/utils_test.go b/graph/utils_test.go new file mode 100644 index 0000000..765a674 --- /dev/null +++ b/graph/utils_test.go @@ -0,0 +1,26 @@ +package graph + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOppositeVertexOf(t *testing.T) { + g := NewSimpleGraph() + g.AddVertex("a") + g.AddVertex("b") + + assert.NoError(t, g.Connect("a", "b")) + edges, err := g.EdgesOf("a") + assert.NoError(t, err) + assert.Equal(t, edges.Count(), uint(1)) + + v, err := OppositeVertexOf(g, edges.AsSlice()[0], "a") + assert.NoError(t, err) + assert.Equal(t, "b", v) + + v, err = OppositeVertexOf(g, edges.AsSlice()[0], v) + assert.NoError(t, err) + assert.Equal(t, "a", v) +} diff --git a/vendor/github.com/puppetlabs/errawr-gen/LICENSE b/vendor/github.com/puppetlabs/errawr-gen/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/vendor/github.com/puppetlabs/errawr-gen/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/modules.txt b/vendor/modules.txt index 606b72c..fcdad69 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -47,7 +47,7 @@ github.com/prometheus/procfs github.com/prometheus/procfs/internal/util github.com/prometheus/procfs/nfs github.com/prometheus/procfs/xfs -# github.com/puppetlabs/errawr-gen v1.0.0 +# github.com/puppetlabs/errawr-gen v1.0.1 github.com/puppetlabs/errawr-gen/pkg/doc github.com/puppetlabs/errawr-gen/pkg/errors github.com/puppetlabs/errawr-gen/pkg/generator