# Core User Guide: Transform Your Graph

You can transform your graph by means of

1. `filter`, `filterNot`
2. `foldLeft`, `foldLeftOuter`
3. `map` methods and
4. `flatMap` 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

1. Increment node values by 1.
2. Transform any edge to a directed edge.

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:

1. mapping within your edge ADT, called `mapBound`, and
2. mapping that is free to escape your edge ADT, called `map`.

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))
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