Repository: dominikbraun/graph
Pull request #115 (2023-04-27 06:43:36)

Add graph v0.20 logo


Patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..3618a42
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1,1 @@
+github: [dominikbraun]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7dd2d3b..9452106 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,12 +7,15 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ## [0.20.0] - 2023-05-01
 
+**Release post: [graph Version 0.20 Is Out](https://dominikbraun.io/blog/graph-version-0.20-is-out/)**
+
 ### Added
 * Added the `Graph.AddVerticesFrom` method for adding all vertices from another graph.
 * Added the `Graph.AddEdgesFrom` method for adding all edges from another graph.
 * Added the `Graph.Edges` method for obtaining all edges as a slice.
 * Added the `Graph.UpdateEdge` method for updating the properties of an edge.
 * Added the `Store.UpdateEdge` method for updating the properties of an edge.
+* Added the `NewLike` function for creating a new graph that is "like" the given graph.
 * Added the `EdgeAttributes` functional option for setting an entire edge attributes map.
 
 ### Changed
diff --git a/README.md b/README.md
index 508abaf..f3d44c7 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# ![dominikbraun/graph](img/logo.svg)
+# <img src="img/logo.svg" width="300">
 
 A library for creating generic graph data structures and modifying, analyzing,
 and visualizing them.
diff --git a/directed.go b/directed.go
index 918c28a..eeec660 100644
--- a/directed.go
+++ b/directed.go
@@ -295,6 +295,19 @@ func (d *directed[K, T]) edgesAreEqual(a, b Edge[T]) bool {
 	return aSourceHash == bSourceHash && aTargetHash == bTargetHash
 }
 
+// copyEdge returns an argument list suitable for the Graph.AddEdge method. This
+// argument list is derived from the given edge, hence the name copyEdge.
+//
+// The last argument is a custom functional option that sets the edge properties
+// to the properties of the original edge.
 func copyEdge[K comparable](edge Edge[K]) (K, K, func(properties *EdgeProperties)) {
-	return edge.Source, edge.Target, copyEdgeProperties(edge.Properties)
+	copyProperties := func(p *EdgeProperties) {
+		for k, v := range edge.Properties.Attributes {
+			p.Attributes[k] = v
+		}
+		p.Weight = edge.Properties.Weight
+		p.Data = edge.Properties.Data
+	}
+
+	return edge.Source, edge.Target, copyProperties
 }
diff --git a/graph.go b/graph.go
index 263e788..08ca454 100644
--- a/graph.go
+++ b/graph.go
@@ -272,6 +272,34 @@ func NewWithStore[K comparable, T any](hash Hash[K, T], store Store[K, T], optio
 	return newUndirected(hash, &p, store)
 }
 
+// NewLike creates a graph that is "like" the given graph: It has the same type,
+// the same hashing function, and the same traits. The new graph is independent
+// of the original graph and uses the default in-memory storage.
+//
+//	g := graph.New(graph.IntHash, graph.Directed())
+//	h := graph.NewLike(g)
+//
+// In the example above, h is a new directed graph of integers derived from g.
+func NewLike[K comparable, T any](g Graph[K, T]) Graph[K, T] {
+	copyTraits := func(t *Traits) {
+		t.IsDirected = g.Traits().IsDirected
+		t.IsAcyclic = g.Traits().IsAcyclic
+		t.IsWeighted = g.Traits().IsWeighted
+		t.IsRooted = g.Traits().IsRooted
+		t.PreventCycles = g.Traits().PreventCycles
+	}
+
+	var hash Hash[K, T]
+
+	if g.Traits().IsDirected {
+		hash = g.(*directed[K, T]).hash
+	} else {
+		hash = g.(*undirected[K, T]).hash
+	}
+
+	return New(hash, copyTraits)
+}
+
 // StringHash is a hashing function that accepts a string and uses that exact
 // string as a hash value. Using it as Hash will yield a Graph[string, string].
 func StringHash(v string) string {
diff --git a/img/logo.svg b/img/logo.svg
index 14fc29e..f65f20e 100644
--- a/img/logo.svg
+++ b/img/logo.svg
@@ -1,55 +1,2 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   id="svg21"
-   xml:space="preserve"
-   viewBox="0 0 1225 426"
-   width="300px"
-   version="1.1"><metadata
-     id="metadata25"><rdf:RDF><cc:Work
-         rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><desc
-     id="desc2">Created with Fabric.js 2.4.6</desc><defs
-     id="defs4" /><g
-     id="g13"
-     transform="matrix(4.22,0,0,4.22,178.22,213.31)"><linearGradient
-       y2="99.999817"
-       x2="-0.00073640805"
-       y1="4.0389135e-05"
-       x1="99.99926"
-       gradientUnits="userSpaceOnUse"
-       id="SVGID_2559"><stop
-         id="stop6"
-         style="stop-color:rgb(255,0,112);stop-opacity: 1"
-         offset="0%" /><stop
-         id="stop8"
-         style="stop-color:rgb(133,0,255);stop-opacity: 1"
-         offset="100%" /></linearGradient><path
-       id="path11"
-       d="M 89.818,8.062 C 85.842,0.629 76.589,-2.17 69.156,1.813 62.605,5.326 59.672,12.923 61.779,19.769 L 35.397,33.895 c -2.8,-3.419 -7,-5.653 -11.768,-5.653 -8.435,0 -15.266,6.842 -15.266,15.269 0,8.435 6.831,15.273 15.266,15.273 3.19,0 6.158,-0.994 8.609,-2.672 l 21.719,20.653 c -3.685,6.01 -2.81,13.973 2.497,19.032 6.109,5.812 15.77,5.564 21.584,-0.545 C 83.846,89.131 83.61,79.475 77.496,73.667 72.111,68.54 63.973,68.141 58.128,72.3 L 36.477,51.705 c 1.514,-2.38 2.422,-5.168 2.422,-8.195 0,-1.469 -0.276,-2.848 -0.657,-4.188 L 64.721,25.133 c 4.556,5.393 12.392,7.06 18.856,3.603 7.429,-3.978 10.232,-13.234 6.241,-20.674 z"
-       transform="translate(-50,-50)"
-       style="opacity:1;fill:url(#SVGID_2559);fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" /></g><g
-     id="g19"
-     transform="translate(851.44,188.37)"><g
-       id="text17"
-       style="font-style:italic;font-weight:700;font-size:318px;font-family:Lato;white-space:pre;opacity:1;fill:#d600a0;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
-       aria-label="graph"><path
-         id="path27"
-         style=""
-         d="m -326.02801,129.951 q 8.904,0 16.218,-2.226 7.314,-2.226 12.561,-6.201 5.247,-3.975 8.109,-9.381 2.862,-5.406 2.862,-11.607 0,-2.225998 -0.318,-4.292998 -0.159,-2.226 -0.477,-4.134 -18.762,-3.18 -32.118,-2.703 -13.356,0.477 -21.783,3.339 -8.427,2.862 -12.402,7.631998 -3.975,4.77 -3.975,10.176 0,9.54 7.632,14.469 7.791,4.929 23.691,4.929 z m 14.151,-106.529998 q 7.155,0 12.561,-3.339 5.406,-3.339 9.063,-8.745 3.816,-5.4060005 5.724,-12.08400047 1.908,-6.83700003 2.067,-13.83299953 0,-11.925 -5.724,-18.285 -5.565,-6.36 -16.218,-6.36 -7.155,0 -12.561,3.18 -5.406,3.021 -9.063,8.268 -3.657,5.088 -5.565,11.766 -1.749,6.6779995 -1.749,13.8329995 0,12.2430005 5.565,18.9210005 5.565,6.678 15.9,6.678 z m 89.835,-80.772 -1.749,12.72 q -1.113,7.632 -7.791,10.176 l -16.695,4.293 q 0.795,3.18 1.113,6.837 0.477,3.498 0.477,7.314 0,7.3139995 -1.59,13.1969995 -1.59,5.724 -3.975,10.653 -2.226,4.9290005 -4.929,9.3810005 -2.703,4.293 -5.088,8.904 -2.226,4.452 -3.816,9.54 -1.59,4.929 -1.59,10.971 0,6.519 2.703,12.084 2.703,5.406 5.883,11.13 3.18,5.724 5.883,12.243 2.703,6.519 2.703,15.105 -0.159,12.083998 -5.565,22.895998 -5.406,10.971 -15.582,19.08 -10.017,8.268 -24.645,13.038 -14.628,4.77 -33.072,4.77 -14.946,0 -27.03,-3.18 -12.084,-3.021 -20.511,-8.586 -8.427,-5.565 -13.038,-13.197 -4.611,-7.473 -4.611,-16.536 0,-11.13 4.77,-19.397998 4.77,-8.268 12.72,-13.833 7.95,-5.724 18.285,-9.063 10.335,-3.339 21.465,-4.77 11.13,-1.431 22.26,-1.113 11.13,0.318 20.67,1.908 -1.431,-4.77 -1.431,-10.335 0,-9.222 5.088,-20.352 -6.042,4.134 -13.833,6.36 -7.632,2.226 -18.444,2.226 -9.222,0 -17.649,-3.021 -8.268,-3.021 -14.628,-9.222 -6.36,-6.201 -10.176,-15.423 -3.657,-9.381 -3.657,-21.7830005 0,-11.9249995 4.452,-23.2139995 4.452,-11.448 13.197,-20.352 8.745,-9.063 21.624,-14.469 12.879,-5.406 29.892,-5.406 9.063,0 17.172,2.067 8.268,2.067 15.105,6.36 z" /><path
-         id="path29"
-         style=""
-         d="m -166.88391,-24.119998 q 6.201,-12.084 13.197,-20.67 7.155,-8.745 14.787,-13.515 7.632,-4.929 15.741,-5.724 8.109,-0.954 16.218,2.544 l -7.155,37.524 q -19.398,-8.109 -34.026,3.975 -14.469,12.0839995 -23.214,47.541 l -8.745,72.345 h -38.478 l 19.716,-163.293 h 20.034 q 6.201,0 9.54,3.021 3.498,3.021 3.498,9.699 z" /><path
-         id="path31"
-         style=""
-         d="m -4.2318835,-37.475998 q -2.226,-0.159 -4.293,-0.159 -1.9080005,-0.159 -3.8160005,-0.159 -13.197,0 -24.486,6.042 -11.289,6.042 -19.557,16.377 -8.109,10.1759995 -12.879,23.8499995 -4.611,13.5150005 -4.611,28.6200005 0,17.172 5.406,25.122 5.406,7.95 14.787,7.95 6.519,0 12.72,-4.452 6.36,-4.611 11.925,-12.402 5.724,-7.95 10.335,-18.603 4.77,-10.653 8.268,-22.896 z m -10.8120005,103.35 q -10.812,16.695 -24.486,26.553 -13.515,9.857998 -29.574,9.857998 -9.222,0 -17.331,-3.656998 -7.95,-3.657 -13.991996,-10.812 -5.883,-7.314 -9.381,-18.126 -3.339,-10.971 -3.339,-25.599 0,-14.628 3.657,-28.461 3.657,-13.8330005 10.334996,-25.917 6.837,-12.243 16.377,-22.419 9.54,-10.176 21.306,-17.49 11.925,-7.314 25.599,-11.289 13.833,-4.134 29.0970005,-4.134 10.971,0 21.9419995,1.59 11.13,1.59 21.624,5.724 l -19.557,158.205 H -3.5958835 q -7.4730005,0 -10.1760005,-3.657 -2.544,-3.816 -2.544,-9.381 z" /><path
-         id="path33"
-         style=""
-         d="m 96.410148,61.740002 q 5.406002,5.883 12.402002,8.268 6.996,2.385 13.674,2.385 10.494,0 19.08,-6.519 8.586,-6.519 14.787,-17.013 6.201,-10.494 9.54,-23.532 3.339,-13.197 3.339,-26.3940005 0,-17.1719995 -5.724,-25.4399995 -5.565,-8.427 -15.582,-8.427 -6.519,0 -13.197,4.452 -6.678,4.452 -12.879,12.243 -6.201,7.632 -11.607,18.12599953 -5.247,10.49400047 -9.063,22.57800047 z m 8.745002,-89.04 q 11.766,-17.649 26.553,-27.984 14.946,-10.494 31.641,-10.494 10.176,0 18.444,3.816 8.268,3.657 14.151,11.13 5.883,7.473 9.063,18.921 3.18,11.448 3.18,26.8709995 0,13.833 -2.862,27.1890005 -2.862,13.356 -8.109,25.281 -5.247,11.925 -12.561,21.942 -7.155,10.017 -16.059,17.331 -8.904,7.314 -19.08,11.448 -10.017,4.133998 -20.829,4.133998 -11.13,0 -20.034,-3.974998 -8.745002,-3.975 -15.423002,-10.812 l -8.109,65.666998 h -38.16 l 26.394,-216.557998 h 19.875 q 6.201,0 9.540002,3.021 3.498,3.021 3.498,9.699 z" /><path
-         id="path35"
-         style=""
-         d="m 280.55202,-35.567998 q 11.925,-15.264 25.758,-22.737 13.992,-7.473 28.461,-7.473 10.335,0 18.444,4.134 8.268,4.134 13.515,12.084 5.247,7.791 7.314,19.398 2.226,11.448 0.318,26.2349995 l -12.402,103.8270005 h -39.114 l 12.243,-103.8270005 q 2.067,-15.8999995 -1.749,-22.8959995 -3.657,-6.996 -13.515,-6.996 -6.042,0 -12.561,3.498 -6.519,3.339 -12.561,9.699 -6.042,6.201 -11.289,15.1049995 -5.247,8.904 -8.904,19.8750005 l -10.335,85.542 h -39.273 l 28.62,-236.274002 h 39.273 z" /></g></g></svg>
\ No newline at end of file
+<?xml version="1.0" standalone="no"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 107.209 36" width="107.209" height="36"><g fill="#f90954" color="#f90954" transform="translate(0 3.9999999999999982) scale(0.2800001)"><svg width="100.0" height="100.0" x="0.0" y="0.0" viewBox="0 0 100 100"><g><path fill="currentColor" d="M89.818,8.062C85.842,0.629,76.589-2.17,69.156,1.813c-6.551,3.513-9.484,11.11-7.377,17.956L35.397,33.895   c-2.8-3.419-7-5.653-11.768-5.653c-8.435,0-15.266,6.842-15.266,15.269c0,8.435,6.831,15.273,15.266,15.273   c3.19,0,6.158-0.994,8.609-2.672l21.719,20.653c-3.685,6.01-2.81,13.973,2.497,19.032c6.109,5.812,15.77,5.564,21.584-0.545   c5.808-6.121,5.572-15.777-0.542-21.585c-5.385-5.127-13.523-5.526-19.368-1.367L36.477,51.705c1.514-2.38,2.422-5.168,2.422-8.195   c0-1.469-0.276-2.848-0.657-4.188l26.479-14.189c4.556,5.393,12.392,7.06,18.856,3.603C91.006,24.758,93.809,15.502,89.818,8.062z"></path></g></svg></g><path fill="#07216d" fill-rule="nonzero" d="M11.33 5.18L12.96 5.18L12.96 16.27Q12.96 19.49 11.39 21.04Q9.82 22.58 6.65 22.58L6.65 22.58Q4.90 22.58 3.32 22.07Q1.75 21.55 0.77 20.64L0.77 20.64L1.63 19.34Q2.54 20.16 3.85 20.62Q5.16 21.07 6.60 21.07L6.60 21.07Q9 21.07 10.13 19.96Q11.26 18.84 11.26 16.49L11.26 16.49L11.26 14.88Q10.46 16.08 9.18 16.70Q7.90 17.33 6.34 17.33L6.34 17.33Q4.56 17.33 3.11 16.55Q1.66 15.77 0.83 14.36Q0 12.96 0 11.18L0 11.18Q0 9.41 0.83 8.02Q1.66 6.62 3.10 5.86Q4.54 5.09 6.34 5.09L6.34 5.09Q7.94 5.09 9.24 5.74Q10.54 6.38 11.33 7.61L11.33 7.61L11.33 5.18ZM6.50 15.82Q7.87 15.82 8.98 15.23Q10.08 14.64 10.69 13.58Q11.30 12.53 11.30 11.18L11.30 11.18Q11.30 9.84 10.69 8.80Q10.08 7.75 8.99 7.16Q7.90 6.58 6.50 6.58L6.50 6.58Q5.14 6.58 4.04 7.15Q2.95 7.73 2.34 8.78Q1.73 9.84 1.73 11.18L1.73 11.18Q1.73 12.53 2.34 13.58Q2.95 14.64 4.04 15.23Q5.14 15.82 6.50 15.82L6.50 15.82ZM19.37 7.66Q19.97 6.38 21.16 5.74Q22.34 5.09 24.10 5.09L24.10 5.09L24.10 6.74L23.69 6.72Q21.70 6.72 20.57 7.94Q19.44 9.17 19.44 11.38L19.44 11.38L19.44 17.81L17.74 17.81L17.74 5.18L19.37 5.18L19.37 7.66ZM36.86 5.18L38.57 5.18L38.57 17.81L36.94 17.81L36.94 15.31Q36.17 16.58 34.91 17.26Q33.65 17.93 32.06 17.93L32.06 17.93Q30.29 17.93 28.85 17.11Q27.41 16.30 26.59 14.83Q25.78 13.37 25.78 11.50L25.78 11.50Q25.78 9.62 26.59 8.16Q27.41 6.70 28.85 5.89Q30.29 5.09 32.06 5.09L32.06 5.09Q33.60 5.09 34.84 5.72Q36.07 6.36 36.86 7.58L36.86 7.58L36.86 5.18ZM32.21 16.42Q33.53 16.42 34.60 15.80Q35.66 15.19 36.28 14.06Q36.89 12.94 36.89 11.50L36.89 11.50Q36.89 10.06 36.28 8.93Q35.66 7.80 34.60 7.19Q33.53 6.58 32.21 6.58L32.21 6.58Q30.86 6.58 29.80 7.19Q28.73 7.80 28.12 8.93Q27.50 10.06 27.50 11.50L27.50 11.50Q27.50 12.94 28.12 14.06Q28.73 15.19 29.80 15.80Q30.86 16.42 32.21 16.42L32.21 16.42ZM49.82 5.09Q51.60 5.09 53.04 5.89Q54.48 6.70 55.30 8.16Q56.11 9.62 56.11 11.50L56.11 11.50Q56.11 13.39 55.30 14.86Q54.48 16.32 53.05 17.12Q51.62 17.93 49.82 17.93L49.82 17.93Q48.29 17.93 47.05 17.29Q45.82 16.66 45.02 15.43L45.02 15.43L45.02 22.46L43.32 22.46L43.32 5.18L44.95 5.18L44.95 7.68Q45.72 6.43 46.98 5.76Q48.24 5.09 49.82 5.09L49.82 5.09ZM49.70 16.42Q51.02 16.42 52.10 15.80Q53.18 15.19 53.80 14.06Q54.41 12.94 54.41 11.50L54.41 11.50Q54.41 10.06 53.80 8.94Q53.18 7.82 52.10 7.20Q51.02 6.58 49.70 6.58L49.70 6.58Q48.36 6.58 47.29 7.20Q46.22 7.82 45.61 8.94Q45 10.06 45 11.50L45 11.50Q45 12.94 45.61 14.06Q46.22 15.19 47.29 15.80Q48.36 16.42 49.70 16.42L49.70 16.42ZM66.02 5.09Q68.40 5.09 69.80 6.47Q71.21 7.85 71.21 10.49L71.21 10.49L71.21 17.81L69.50 17.81L69.50 10.66Q69.50 8.69 68.52 7.66Q67.54 6.62 65.71 6.62L65.71 6.62Q63.67 6.62 62.48 7.84Q61.30 9.05 61.30 11.18L61.30 11.18L61.30 17.81L59.59 17.81L59.59 0L61.30 0L61.30 7.39Q61.99 6.29 63.22 5.69Q64.44 5.09 66.02 5.09L66.02 5.09Z" transform="translate(36, 6.708)"></path></svg>
\ No newline at end of file
diff --git a/sets.go b/sets.go
index 0c4aead..245e7fc 100644
--- a/sets.go
+++ b/sets.go
@@ -44,7 +44,7 @@ func Union[K comparable, T any](g, h Graph[K, T]) (Graph[K, T], error) {
 				}
 			}
 
-			err = union.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
+			err = union.AddEdge(copyEdge(edge))
 			if err != nil {
 				return union, fmt.Errorf("failed to add edge (%v, %v): %w", edge.Source, edge.Target, err)
 			}
@@ -122,13 +122,3 @@ func copyVertexProperties(source VertexProperties) func(*VertexProperties) {
 		p.Weight = source.Weight
 	}
 }
-
-func copyEdgeProperties(source EdgeProperties) func(properties *EdgeProperties) {
-	return func(p *EdgeProperties) {
-		for k, v := range source.Attributes {
-			p.Attributes[k] = v
-		}
-		p.Weight = source.Weight
-		p.Data = source.Data
-	}
-}
diff --git a/trees.go b/trees.go
index 6afe889..7201a41 100644
--- a/trees.go
+++ b/trees.go
@@ -35,10 +35,10 @@ func spanningTree[K comparable, T any](g Graph[K, T], maximum bool) (Graph[K, T]
 	edges := make([]Edge[K], 0)
 	subtrees := newUnionFind[K]()
 
-	mst := New(g.(*undirected[K, T]).hash)
+	mst := NewLike(g)
 
 	for v, adjacencies := range adjacencyMap {
-		vertex, properties, err := g.VertexWithProperties(v)
+		vertex, properties, err := g.VertexWithProperties(v) //nolint:govet
 		if err != nil {
 			return nil, fmt.Errorf("failed to get vertex %v: %w", v, err)
 		}
@@ -72,8 +72,7 @@ func spanningTree[K comparable, T any](g Graph[K, T], maximum bool) (Graph[K, T]
 		if sourceRoot != targetRoot {
 			subtrees.union(sourceRoot, targetRoot)
 
-			err := mst.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
-			if err != nil {
+			if err = mst.AddEdge(copyEdge(edge)); err != nil {
 				return nil, fmt.Errorf("failed to add edge (%v, %v): %w", edge.Source, edge.Target, err)
 			}
 		}


Test patch:
diff --git a/directed_test.go b/directed_test.go
index 82ae44a..34a9c8d 100644
--- a/directed_test.go
+++ b/directed_test.go
@@ -447,34 +447,8 @@ func TestDirected_AddEdge(t *testing.T) {
 				t.Fatalf("%s: edge with source %v and target %v not found", name, expectedEdge.Source, expectedEdge.Target)
 			}
 
-			if edge.Source != expectedEdge.Source {
-				t.Errorf("%s: edge sources don't match: expected source %v, got %v", name, expectedEdge.Source, edge.Source)
-			}
-
-			if edge.Target != expectedEdge.Target {
-				t.Errorf("%s: edge targets don't match: expected target %v, got %v", name, expectedEdge.Target, edge.Target)
-			}
-
-			if edge.Properties.Weight != expectedEdge.Properties.Weight {
-				t.Errorf("%s: edge weights don't match: expected weight %v, got %v", name, expectedEdge.Properties.Weight, edge.Properties.Weight)
-			}
-
-			if len(edge.Properties.Attributes) != len(expectedEdge.Properties.Attributes) {
-				t.Fatalf("%s: attributes length don't match: expcted %v, got %v", name, len(expectedEdge.Properties.Attributes), len(edge.Properties.Attributes))
-			}
-
-			for expectedKey, expectedValue := range expectedEdge.Properties.Attributes {
-				value, ok := edge.Properties.Attributes[expectedKey]
-				if !ok {
-					t.Errorf("%s: attribute keys don't match: expected key %v not found", name, expectedKey)
-				}
-				if value != expectedValue {
-					t.Errorf("%s: attribute values don't match: expected value %v for key %v, got %v", name, expectedValue, expectedKey, value)
-				}
-			}
-
-			if edge.Properties.Data != expectedEdge.Properties.Data {
-				t.Errorf("%s: edge data doesn't match: expected data %v, got %v", name, expectedEdge.Properties.Data, edge.Properties.Data)
+			if !edgesAreEqual(expectedEdge, edge, true) {
+				t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, edge)
 			}
 		}
 	}
@@ -617,34 +591,8 @@ func TestDirected_AddEdgesFrom(t *testing.T) {
 					t.Fatalf("failed to get edge: %v", err.Error())
 				}
 
-				if actualEdge.Source != edge.Source {
-					t.Errorf("expected edge source %v, got %v", edge.Source, actualEdge.Source)
-				}
-
-				if actualEdge.Target != edge.Target {
-					t.Errorf("expected edge target %v, got %v", edge.Source, actualEdge.Source)
-				}
-
-				if actualEdge.Properties.Weight != edge.Properties.Weight {
-					t.Errorf("expected edge weight %v, got %v", edge.Properties.Weight, actualEdge.Properties.Weight)
-				}
-
-				if len(actualEdge.Properties.Attributes) != len(edge.Properties.Attributes) {
-					t.Fatalf("expcted %v attributes, got %v", len(edge.Properties.Attributes), len(edge.Properties.Attributes))
-				}
-
-				for expectedKey, expectedValue := range edge.Properties.Attributes {
-					value, ok := actualEdge.Properties.Attributes[expectedKey]
-					if !ok {
-						t.Errorf("expected attribute %v not found", expectedKey)
-					}
-					if value != expectedValue {
-						t.Errorf("expected value %v for attribute %v, got %v", expectedValue, expectedKey, value)
-					}
-				}
-
-				if actualEdge.Properties.Data != edge.Properties.Data {
-					t.Errorf("expected data %v, got %v", edge.Properties.Data, edge.Properties.Data)
+				if !edgesAreEqual(edge, actualEdge, true) {
+					t.Errorf("expected edge %v, got %v", edge, actualEdge)
 				}
 			}
 		})
@@ -723,8 +671,8 @@ func TestDirected_Edge(t *testing.T) {
 			t.Errorf("%s: target expectancy doesn't match: expected %v, got %v", name, test.args[1], edge.Target)
 		}
 
-		if !edgePropertiesAreEqual(edge.Properties, test.edge.Properties) {
-			t.Errorf("%s: edge property expectancy doesn't match: expected %v, got %v", name, test.edge.Properties, edge.Properties)
+		if !edgesAreEqual(test.edge, edge, true) {
+			t.Errorf("%s: expected edge %v, got %v", name, test.edge, edge)
 		}
 	}
 }
@@ -826,35 +774,8 @@ func TestDirected_Edges(t *testing.T) {
 					if actualEdge.Source != expectedEdge.Source || actualEdge.Target != expectedEdge.Target {
 						continue
 					}
-
-					if actualEdge.Source != expectedEdge.Source {
-						t.Errorf("expected edge source %v, got %v", expectedEdge.Source, actualEdge.Source)
-					}
-
-					if actualEdge.Target != expectedEdge.Target {
-						t.Errorf("expected edge target %v, got %v", expectedEdge.Source, actualEdge.Source)
-					}
-
-					if actualEdge.Properties.Weight != expectedEdge.Properties.Weight {
-						t.Errorf("expected edge weight %v, got %v", expectedEdge.Properties.Weight, actualEdge.Properties.Weight)
-					}
-
-					if len(actualEdge.Properties.Attributes) != len(expectedEdge.Properties.Attributes) {
-						t.Fatalf("expcted %v attributes, got %v", len(expectedEdge.Properties.Attributes), len(expectedEdge.Properties.Attributes))
-					}
-
-					for expectedKey, expectedValue := range expectedEdge.Properties.Attributes {
-						value, ok := actualEdge.Properties.Attributes[expectedKey]
-						if !ok {
-							t.Errorf("expected attribute %v not found", expectedKey)
-						}
-						if value != expectedValue {
-							t.Errorf("expected value %v for attribute %v, got %v", expectedValue, expectedKey, value)
-						}
-					}
-
-					if actualEdge.Properties.Data != expectedEdge.Properties.Data {
-						t.Errorf("expected data %v, got %v", expectedEdge.Properties.Data, expectedEdge.Properties.Data)
+					if !edgesAreEqual(expectedEdge, actualEdge, true) {
+						t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, actualEdge)
 					}
 				}
 			}
@@ -922,34 +843,8 @@ func TestDirected_UpdateEdge(t *testing.T) {
 				t.Fatalf("unexpected error: %v", err.Error())
 			}
 
-			if actualEdge.Source != test.updateEdge.Source {
-				t.Errorf("expected edge source %v, got %v", test.updateEdge.Source, actualEdge.Source)
-			}
-
-			if actualEdge.Target != test.updateEdge.Target {
-				t.Errorf("expected edge target %v, got %v", test.updateEdge.Source, actualEdge.Source)
-			}
-
-			if actualEdge.Properties.Weight != test.updateEdge.Properties.Weight {
-				t.Errorf("expected edge weight %v, got %v", test.updateEdge.Properties.Weight, actualEdge.Properties.Weight)
-			}
-
-			if len(actualEdge.Properties.Attributes) != len(test.updateEdge.Properties.Attributes) {
-				t.Fatalf("expcted %v attributes, got %v", len(test.updateEdge.Properties.Attributes), len(test.updateEdge.Properties.Attributes))
-			}
-
-			for expectedKey, expectedValue := range test.updateEdge.Properties.Attributes {
-				value, ok := actualEdge.Properties.Attributes[expectedKey]
-				if !ok {
-					t.Errorf("expected attribute %v not found", expectedKey)
-				}
-				if value != expectedValue {
-					t.Errorf("expected value %v for attribute %v, got %v", expectedValue, expectedKey, value)
-				}
-			}
-
-			if actualEdge.Properties.Data != test.updateEdge.Properties.Data {
-				t.Errorf("expected data %v, got %v", test.updateEdge.Properties.Data, test.updateEdge.Properties.Data)
+			if !edgesAreEqual(test.updateEdge, actualEdge, true) {
+				t.Errorf("expected edge %v, got %v", test.updateEdge, actualEdge)
 			}
 		})
 	}
@@ -1493,24 +1388,42 @@ func vertexPropertiesAreEqual(a, b VertexProperties) bool {
 	return true
 }
 
-func edgePropertiesAreEqual(a, b EdgeProperties) bool {
-	if a.Weight != b.Weight {
+func edgesAreEqual[K comparable](a, b Edge[K], directed bool) bool {
+	directedOk := a.Source == b.Source &&
+		a.Target == b.Target
+
+	undirectedOk := directedOk ||
+		a.Source == b.Target &&
+			a.Target == b.Source
+
+	if directed && !directedOk {
 		return false
 	}
 
-	if a.Data != b.Data {
+	if !directed && !undirectedOk {
 		return false
 	}
 
-	// A length check is required because in the iteration below, a.Attributes
-	// could be empty and thus circumvent the comparison.
-	if len(a.Attributes) != len(b.Attributes) {
+	if a.Properties.Weight != b.Properties.Weight {
 		return false
 	}
 
-	for key, aValue := range a.Attributes {
-		bValue, ok := b.Attributes[key]
-		if !ok || aValue != bValue {
+	if len(a.Properties.Attributes) != len(b.Properties.Attributes) {
+		return false
+	}
+
+	for aKey, aValue := range a.Properties.Attributes {
+		bValue, ok := b.Properties.Attributes[aKey]
+		if !ok {
+			return false
+		}
+		if bValue != aValue {
+			return false
+		}
+	}
+
+	for bKey := range b.Properties.Attributes {
+		if _, ok := a.Properties.Attributes[bKey]; !ok {
 			return false
 		}
 	}
diff --git a/graph_test.go b/graph_test.go
index 0067902..0de9141 100644
--- a/graph_test.go
+++ b/graph_test.go
@@ -33,6 +33,66 @@ func TestNew(t *testing.T) {
 	}
 }
 
+func TestNewLike(t *testing.T) {
+	tests := map[string]struct {
+		g        Graph[int, int]
+		vertices []int
+	}{
+		"new directed graph of integers": {
+			g:        New(IntHash, Directed()),
+			vertices: []int{1, 2, 3},
+		},
+		"new undirected weighted graph of integers": {
+			g:        New(IntHash, Weighted()),
+			vertices: []int{1, 2, 3},
+		},
+	}
+
+	for name, test := range tests {
+		t.Run(name, func(t *testing.T) {
+			for _, vertex := range test.vertices {
+				_ = test.g.AddVertex(vertex)
+			}
+
+			h := NewLike(test.g)
+
+			if len(test.vertices) > 0 {
+				if _, err := h.Vertex(test.vertices[0]); err == nil {
+					t.Errorf("expected vertex %v not to exist in h", test.vertices[0])
+				}
+			}
+
+			if test.g.Traits().IsDirected {
+				actual, ok := h.(*directed[int, int])
+				if !ok {
+					t.Fatalf("type assertion to *directed failed")
+				}
+
+				expected := test.g.(*directed[int, int])
+
+				if actual.hash(42) != expected.hash(42) {
+					t.Errorf("expected hash %v, got %v", expected.hash, actual.hash)
+				}
+			} else {
+				actual, ok := h.(*undirected[int, int])
+				if !ok {
+					t.Fatalf("type assertion to *directed failed")
+				}
+
+				expected := test.g.(*undirected[int, int])
+
+				if actual.hash(42) != expected.hash(42) {
+					t.Errorf("expected hash %v, got %v", expected.hash, actual.hash)
+				}
+			}
+
+			if !traitsAreEqual(h.Traits(), test.g.Traits()) {
+				t.Errorf("expected traits %+v, got %+v", test.g.Traits(), h.Traits())
+			}
+		})
+	}
+}
+
 func TestStringHash(t *testing.T) {
 	tests := map[string]struct {
 		value        string
diff --git a/sets_test.go b/sets_test.go
index ea1faa9..d46a2dc 100644
--- a/sets_test.go
+++ b/sets_test.go
@@ -134,7 +134,7 @@ func TestDirectedUnion(t *testing.T) {
 		}
 
 		for _, edge := range test.gEdges {
-			_ = g.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
+			_ = g.AddEdge(copyEdge(edge))
 		}
 
 		h := New(IntHash, Directed())
@@ -144,7 +144,7 @@ func TestDirectedUnion(t *testing.T) {
 		}
 
 		for _, edge := range test.hEdges {
-			_ = h.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
+			_ = h.AddEdge(copyEdge(edge))
 		}
 
 		union, err := Union(g, h)
diff --git a/trees_test.go b/trees_test.go
index b983d03..18fd310 100644
--- a/trees_test.go
+++ b/trees_test.go
@@ -94,7 +94,7 @@ func TestUndirectedMinimumSpanningTree(t *testing.T) {
 			}
 
 			for _, edge := range test.edges {
-				_ = g.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
+				_ = g.AddEdge(copyEdge(edge))
 			}
 
 			mst, _ := MinimumSpanningTree(g)
@@ -199,7 +199,7 @@ func TestUndirectedMaximumSpanningTree(t *testing.T) {
 			}
 
 			for _, edge := range test.edges {
-				_ = g.AddEdge(edge.Source, edge.Target, copyEdgeProperties(edge.Properties))
+				_ = g.AddEdge(copyEdge(edge))
 			}
 
 			mst, _ := MaximumSpanningTree(g)
diff --git a/undirected_test.go b/undirected_test.go
index adc0955..c304eb3 100644
--- a/undirected_test.go
+++ b/undirected_test.go
@@ -392,34 +392,8 @@ func TestUndirected_AddEdge(t *testing.T) {
 				t.Fatalf("%s: edge with source %v and target %v not found", name, expectedEdge.Source, expectedEdge.Target)
 			}
 
-			if edge.Source != expectedEdge.Source {
-				t.Errorf("%s: edge sources don't match: expected source %v, got %v", name, expectedEdge.Source, edge.Source)
-			}
-
-			if edge.Target != expectedEdge.Target {
-				t.Errorf("%s: edge targets don't match: expected target %v, got %v", name, expectedEdge.Target, edge.Target)
-			}
-
-			if edge.Properties.Weight != expectedEdge.Properties.Weight {
-				t.Errorf("%s: edge weights don't match: expected weight %v, got %v", name, expectedEdge.Properties.Weight, edge.Properties.Weight)
-			}
-
-			if len(edge.Properties.Attributes) != len(expectedEdge.Properties.Attributes) {
-				t.Fatalf("%s: attributes length don't match: expcted %v, got %v", name, len(expectedEdge.Properties.Attributes), len(edge.Properties.Attributes))
-			}
-
-			for expectedKey, expectedValue := range expectedEdge.Properties.Attributes {
-				value, ok := edge.Properties.Attributes[expectedKey]
-				if !ok {
-					t.Errorf("%s: attribute keys don't match: expected key %v not found", name, expectedKey)
-				}
-				if value != expectedValue {
-					t.Errorf("%s: attribute values don't match: expected value %v for key %v, got %v", name, expectedValue, expectedKey, value)
-				}
-			}
-
-			if edge.Properties.Data != expectedEdge.Properties.Data {
-				t.Errorf("%s: edge data doesn't match: expected data %v, got %v", name, expectedEdge.Properties.Data, edge.Properties.Data)
+			if !edgesAreEqual(expectedEdge, edge, false) {
+				t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, edge)
 			}
 		}
 	}
@@ -562,34 +536,8 @@ func TestUndirected_AddEdgesFrom(t *testing.T) {
 					t.Fatalf("failed to get edge (%v, %v): %v", edge.Source, edge.Target, err.Error())
 				}
 
-				if actualEdge.Source != edge.Source {
-					t.Errorf("expected edge source %v, got %v", edge.Source, actualEdge.Source)
-				}
-
-				if actualEdge.Target != edge.Target {
-					t.Errorf("expected edge target %v, got %v", edge.Source, actualEdge.Source)
-				}
-
-				if actualEdge.Properties.Weight != edge.Properties.Weight {
-					t.Errorf("expected edge weight %v, got %v", edge.Properties.Weight, actualEdge.Properties.Weight)
-				}
-
-				if len(actualEdge.Properties.Attributes) != len(edge.Properties.Attributes) {
-					t.Fatalf("expcted %v attributes, got %v", len(edge.Properties.Attributes), len(edge.Properties.Attributes))
-				}
-
-				for expectedKey, expectedValue := range edge.Properties.Attributes {
-					value, ok := actualEdge.Properties.Attributes[expectedKey]
-					if !ok {
-						t.Errorf("expected attribute %v not found", expectedKey)
-					}
-					if value != expectedValue {
-						t.Errorf("expected value %v for attribute %v, got %v", expectedValue, expectedKey, value)
-					}
-				}
-
-				if edge.Properties.Data != edge.Properties.Data {
-					t.Errorf("expected data %v, got %v", edge.Properties.Data, edge.Properties.Data)
+				if !edgesAreEqual(edge, actualEdge, false) {
+					t.Errorf("%s: expected edge %v, got %v", name, edge, actualEdge)
 				}
 			}
 		})
@@ -717,8 +665,8 @@ func TestUndirected_Edge(t *testing.T) {
 			t.Errorf("%s: target expectancy doesn't match: expected %v, got %v", name, test.args[1], edge.Target)
 		}
 
-		if !edgePropertiesAreEqual(edge.Properties, test.edge.Properties) {
-			t.Errorf("%s: edge property expectancy doesn't match: expected %v, got %v", name, test.edge.Properties, edge.Properties)
+		if !edgesAreEqual(test.edge, edge, false) {
+			t.Errorf("%s: expected edge %v, got %v", name, test.edge, edge)
 		}
 	}
 }
@@ -820,35 +768,8 @@ func TestUndirected_Edges(t *testing.T) {
 					if actualEdge.Source != expectedEdge.Source || actualEdge.Target != expectedEdge.Target {
 						continue
 					}
-
-					if actualEdge.Source != expectedEdge.Source {
-						t.Errorf("expected edge source %v, got %v", expectedEdge.Source, actualEdge.Source)
-					}
-
-					if actualEdge.Target != expectedEdge.Target {
-						t.Errorf("expected edge target %v, got %v", expectedEdge.Source, actualEdge.Source)
-					}
-
-					if actualEdge.Properties.Weight != expectedEdge.Properties.Weight {
-						t.Errorf("expected edge weight %v, got %v", expectedEdge.Properties.Weight, actualEdge.Properties.Weight)
-					}
-
-					if len(actualEdge.Properties.Attributes) != len(expectedEdge.Properties.Attributes) {
-						t.Fatalf("expcted %v attributes, got %v", len(expectedEdge.Properties.Attributes), len(expectedEdge.Properties.Attributes))
-					}
-
-					for expectedKey, expectedValue := range expectedEdge.Properties.Attributes {
-						value, ok := actualEdge.Properties.Attributes[expectedKey]
-						if !ok {
-							t.Errorf("expected attribute %v not found", expectedKey)
-						}
-						if value != expectedValue {
-							t.Errorf("expected value %v for attribute %v, got %v", expectedValue, expectedKey, value)
-						}
-					}
-
-					if actualEdge.Properties.Data != expectedEdge.Properties.Data {
-						t.Errorf("expected data %v, got %v", expectedEdge.Properties.Data, expectedEdge.Properties.Data)
+					if !edgesAreEqual(expectedEdge, actualEdge, false) {
+						t.Errorf("%s: expected edge %v, got %v", name, expectedEdge, actualEdge)
 					}
 				}
 			}
@@ -916,34 +837,8 @@ func TestUndirected_UpdateEdge(t *testing.T) {
 				t.Fatalf("unexpected error: %v", err.Error())
 			}
 
-			if actualEdge.Source != test.updateEdge.Source {
-				t.Errorf("expected edge source %v, got %v", test.updateEdge.Source, actualEdge.Source)
-			}
-
-			if actualEdge.Target != test.updateEdge.Target {
-				t.Errorf("expected edge target %v, got %v", test.updateEdge.Source, actualEdge.Source)
-			}
-
-			if actualEdge.Properties.Weight != test.updateEdge.Properties.Weight {
-				t.Errorf("expected edge weight %v, got %v", test.updateEdge.Properties.Weight, actualEdge.Properties.Weight)
-			}
-
-			if len(actualEdge.Properties.Attributes) != len(test.updateEdge.Properties.Attributes) {
-				t.Fatalf("expcted %v attributes, got %v", len(test.updateEdge.Properties.Attributes), len(test.updateEdge.Properties.Attributes))
-			}
-
-			for expectedKey, expectedValue := range test.updateEdge.Properties.Attributes {
-				value, ok := actualEdge.Properties.Attributes[expectedKey]
-				if !ok {
-					t.Errorf("expected attribute %v not found", expectedKey)
-				}
-				if value != expectedValue {
-					t.Errorf("expected value %v for attribute %v, got %v", expectedValue, expectedKey, value)
-				}
-			}
-
-			if actualEdge.Properties.Data != test.updateEdge.Properties.Data {
-				t.Errorf("expected data %v, got %v", test.updateEdge.Properties.Data, test.updateEdge.Properties.Data)
+			if !edgesAreEqual(test.updateEdge, actualEdge, false) {
+				t.Errorf("expected edge %v, got %v", test.updateEdge, actualEdge)
 			}
 		})
 	}