You can transform your graph by means of
filter
, filterNot
foldLeft
, foldLeftOuter
map
methods andflatMap
methods.The mapping functionality is provided by methods with numerous signatures to meet all possible needs. The below examples use directed and undirected graphs but, notably, hypergraph transformation is supported by methods with dedicated signatures.
The examples below make use of
import scala.concurrent.duration._ import scalax.collection.OuterImplicits._ import scalax.collection.edges._ import scalax.collection.generic.AnyEdge import scalax.collection.immutable.Graph // or mutable
val g = Graph[Int, AnyEdge](2 ~> 3, 3 ~ 1, 5) g filter (nodeP = _ >= 2) // Graph(NodeSet(2, 3, 5), EdgeSet(2 ~> 3)) g filter (edgeP = _.isDirected) // Graph(NodeSet(1, 2, 3, 5), EdgeSet(2 ~> 3)) g filter (nodeP = _ >= 2, edgeP = _.isUndirected) // Graph(NodeSet(2, 3, 5))
val g = Graph(1 ~> 2, 1 ~> 3, 4) g.foldLeft(Graph.empty[Int, DiEdge[Int]])( opNode = { case (cum, g.InnerNode(n, _)) if n.isIsolated => cum + n case (cum, _) => cum }, opEdge = { case (cum, g.InnerEdge(e, _)) if cum.size % 2 == 0 => cum + e case (cum, _) => cum } ) // Graph(NodeSet(1, 2, 4), EdgeSet(1 ~> 2)) or // Graph(NodeSet(1, 3, 4), EdgeSet(1 ~> 3))
To recall, edges of a generic graph may be of any type. Thus mapping a generic graph is similar to mapping any collection in the Scala standard library. The only difference is that you are free to pass a mapping function for nodes and edges in separate.
Graph(1 ~ 2).map( fNode = _.outer + 1, fEdge = (n1: Int, n2: Int) => n1 ~> n2 ) // Graph(NodeSet(2, 3), EdgeSet(2 ~> 3))
As to
Here is an example for how you would transform directed or undirected edges to two directed edges each while incrementing node values:
Graph(1 ~ 2).flatMap( fNode = _.outer + 1 :: Nil, fEdge = (n1s: Seq[Int], n2s: Seq[Int]) => (n1s, n2s) match { case (Seq(n1, _*), Seq(n2, _*)) => List(n1 ~> n2, n2 ~> n1) } // Graph(NodeSet(2, 3), EdgeSet(2 ~> 3, 3 ~> 2))
Mapping typed graphs is more complex because you cannot transform the node of a typed edge to an arbitrary type.
For instance, having edges like Friends(one: Person, another: Person)
,
you cannot replace Persons with Strings
unless you also transform the edge to a type that allows for ends of type String.
For this reason we distinguish between two kind of mapping:
mapBound
, andmap
. Using the example of a net of flights represented by g
,
assume that airport hamburg
gets closed unexpectedly.
Then, you could replace all flights from/to hamburg
by flights from/to frankfurt
by
g.mapBound((airport: g.NodeT) => if (airport == hamburg) frankfurt else airport)
You could transform the above net of flights to a Graph[String, DiEdge[String]]
by
g.map( fNode = (airport: g.NodeT) => airport.outer.toString, fEdge = (from: String, to: String) => DiEdge(from, to) )
Note that you need to mix in PartialEdgeMapper
into your typed edge class and implement
def map[N]: PartialFunction[(N, N), MyEdge]
to define how to deal with node mapping with respect to your ADT of edges.
If this partial function is not defined for some N
, the library will fall back to a built-in edge type.
In the flight example,
any mapped Airport
yields a new Flight
with labels retained by calling copy
.
Actually, retaining labels is not really meaningful since duration
, departure
etc.
should reflect the new Airports
.
As an extension of the above mapBound
example, suppose that you
not only need to replace hamburg
by frankfurt
but also restrict flights to those having a duration of less than 1 hour:
def ham(flight: Flight): Boolean = flight.departure == hamburg || flight.destination == hamburg def shortHam(flight: Flight): Boolean = ham(flight) && flight.duration < 1.hour g.flatMapBound( fNode = (airport: g.NodeT) => (if (airport == hamburg) frankfurt else airport.outer) :: Nil, fEdge = (edge: g.EdgeT, newDepartures: Seq[Airport], newDestinations: Seq[Airport]) => { val outer = edge.outer if (shortHam(outer)) outer.copy(departure = newDepartures.head, destination = newDestinations.head) :: Nil else if (ham(outer)) Nil else outer :: Nil } )
To demonstrate an escaping flat map, you could also transform the above net of Flight
s
to a Graph[String, DiEdge[String]]
while maintaining the functionality of the previous flatMapBound
in one step:
g.flatMap( fNode = (airport: g.NodeT) => { def existing = airport.outer.toString if (airport == hamburg) existing :: frankfurt.toString :: Nil else existing :: Nil }, fEdge = (edge: g.EdgeT, from: Seq[String], to: Seq[String]) => { val outer = edge.outer if (shortHam(outer)) { def replaced(a: Airport) = (if (a == hamburg) frankfurt else a).toString DiEdge(replaced(outer.departure), replaced(outer.destination)) :: Nil } else if (ham(outer)) Nil else DiEdge(from.head, to.head) :: Nil } )