diff --git a/attr.go b/attr.go index b31bdc3..2eed381 100644 --- a/attr.go +++ b/attr.go @@ -34,7 +34,10 @@ func (a Attrs) String() string { } elems := []string{} for key, val := range a { - elems = append(elems, fmt.Sprintf("%v:%v", key, val)) + valStr := fmt.Sprintf("%v", val) + if valStr != "" { + elems = append(elems, fmt.Sprintf("%s:%s", key, valStr)) + } } sort.Strings(elems) return fmt.Sprintf("[%s]", strings.Join(elems, ",")) diff --git a/edge.go b/edge.go index c00581b..2cf9bc3 100644 --- a/edge.go +++ b/edge.go @@ -2,12 +2,18 @@ package graphman import ( "fmt" + "sort" "strings" ) +// +// Edge +// + type Edge struct { - src *Vertex - dst *Vertex + src *Vertex + dst *Vertex + deleted bool // temporary variable used by gc() Attrs } @@ -56,8 +62,22 @@ func (e *Edge) OtherVertex(id string) *Vertex { return nil } +// +// Edges +// + type Edges []*Edge +func (e Edges) filtered() Edges { + filtered := Edges{} + for _, edge := range e { + if !edge.deleted { + filtered = append(filtered, edge) + } + } + return filtered +} + func (e Edges) String() string { items := []string{} for _, edge := range e { @@ -65,3 +85,52 @@ func (e Edges) String() string { } return fmt.Sprintf("{%s}", strings.Join(items, ",")) } + +func (e Edges) Equals(other Edges) bool { + tmp := map[*Edge]int{} + for _, edge := range e { + tmp[edge]++ + } + for _, edge := range other { + tmp[edge]-- + } + for _, v := range tmp { + if v != 0 { + return false + } + } + return true +} + +// AllCombinations returns the different combinations of Edges in a group of Edges +// +// Adapted from https://github.com/mxschmitt/golang-combinations +func (e Edges) AllCombinations() EdgesCombinations { + combinations := EdgesCombinations{} + length := uint(len(e)) + + for combinationBits := 1; combinationBits < (1 << length); combinationBits++ { + var combination Edges + for object := uint(0); object < length; object++ { + if (combinationBits>>object)&1 == 1 { + combination = append(combination, e[object]) + } + } + combinations = append(combinations, combination) + } + + return combinations +} + +// +// EdgesCombinations +// + +type EdgesCombinations []Edges + +func (ec EdgesCombinations) LongestToShortest() EdgesCombinations { + sort.Slice(ec, func(i, j int) bool { + return len(ec[i]) > len(ec[j]) + }) + return ec +} diff --git a/examples/pertify/la-methode-pert-p101.yml b/examples/pertify/la-methode-pert-p101.yml index a1501cd..c033e57 100644 --- a/examples/pertify/la-methode-pert-p101.yml +++ b/examples/pertify/la-methode-pert-p101.yml @@ -3,59 +3,79 @@ actions: - id: "a" estimate: [7] + title: "a" - id: "b" + title: "b" estimate: [8] - id: "c" + title: "c" estimate: [9] - id: "d" + title: "d" estimate: [10] depends_on: ["a", "e"] - id: "e" + title: "e" estimate: [2] depends_on: ["b", "f"] - id: "f" + title: "f" estimate: [6] depends_on: ["c"] - id: "g" + title: "g" estimate: [14] depends_on: ["b", "f"] - id: "h" + title: "h" estimate: [7] depends_on: ["b", "f"] - id: "i" + title: "i" estimate: [6] depends_on: ["b", "f"] - id: "j" + title: "j" estimate: [5] depends_on: ["c"] - id: "k" + title: "k" estimate: [2] depends_on: ["c"] - id: "l" + title: "l" estimate: [2] depends_on: ["d", "g"] - id: "m" + title: "m" estimate: [18] depends_on: ["l", "h"] - id: "n" + title: "n" estimate: [9] depends_on: ["i", "j"] - id: "o" + title: "o" estimate: [16] depends_on: ["k"] - id: "p" + title: "p" estimate: [21] depends_on: ["k"] - id: "q" + title: "q" estimate: [9] depends_on: ["m", "r"] - id: "r" + title: "r" estimate: [12] depends_on: ["n", "o"] - id: "s" + title: "s" estimate: [14] depends_on: ["p"] - id: "t" + title: "t" estimate: [6] depends_on: ["s"] diff --git a/examples/pertify/pert-and-cpm-p35.yml b/examples/pertify/pert-and-cpm-p35.yml new file mode 100644 index 0000000..849918e --- /dev/null +++ b/examples/pertify/pert-and-cpm-p35.yml @@ -0,0 +1,47 @@ +actions: + - id: A + title: A + - id: B + title: B + - id: C + title: C + - id: D + title: D + depends_on: ["3"] + - id: E + title: E + depends_on: ["4"] + - id: F + title: F + depends_on: ["5"] + - id: G + title: G + depends_on: ["6"] + - id: H + title: H + - id: I + title: I + depends_on: ["7"] + +states: + - id: "1" + title: "1" + depends_on: ["A"] + - id: "2" + title: "2" + depends_on: ["B"] + - id: "3" + title: "3" + depends_on: ["C", "1"] + - id: "4" + title: "4" + depends_on: ["1", "2"] + - id: "5" + title: "5" + depends_on: ["1", "2"] + - id: "6" + title: "6" + depends_on: ["1", "2"] + - id: "7" + title: "7" + depends_on: ["2", "H"] diff --git a/graph.go b/graph.go index 972473a..da7f77c 100644 --- a/graph.go +++ b/graph.go @@ -40,7 +40,7 @@ func (g Graph) SinkVertices() Vertices { sinks = append(sinks, vertex) } } - return sinks + return sinks.filtered() } func (g Graph) ConnectedSubgraphs() Graphs { @@ -81,7 +81,7 @@ func (g Graph) ConnectedSubgraphFromVertex(start *Vertex) *Graph { func (g Graph) SourceVertices() Vertices { sources := Vertices{} for _, vertex := range g.vertices { - if vertex.IsSource() { + if !vertex.deleted && vertex.IsSource() { sources = append(sources, vertex) } } @@ -186,7 +186,32 @@ func (g Graph) Edges() Edges { func (g Graph) Vertices() Vertices { sort.Sort(g.vertices) - return g.vertices + return g.vertices.filtered() +} + +func (g *Graph) gc() uint { + removed := uint(0) + verticesToDelete := Vertices{} + for _, vertex := range g.vertices { + if vertex.deleted { + verticesToDelete = append(verticesToDelete, vertex) + } + } + edgesToDelete := Edges{} + for _, edge := range g.edges { + if edge.deleted { + edgesToDelete = append(edgesToDelete, edge) + } + } + for _, edge := range edgesToDelete { + removed++ + g.RemoveEdge(edge.src.id, edge.dst.id) + } + for _, vertex := range verticesToDelete { + removed++ + g.RemoveVertex(vertex.id) + } + return removed } func (g *Graph) AddVertex(id string, attrs ...Attrs) *Vertex { @@ -211,6 +236,7 @@ func (g *Graph) AddVertex(id string, attrs ...Attrs) *Vertex { func (g *Graph) RemoveVertex(id string) bool { for k, v := range g.vertices { if v.id == id { + v.deleted = true g.vertices = append(g.vertices[:k], g.vertices[k+1:]...) return true } @@ -219,8 +245,9 @@ func (g *Graph) RemoveVertex(id string) bool { } func (g *Graph) RemoveEdge(src, dst string) bool { - for k, v := range g.edges { - if v.src.id == src && v.dst.id == dst { + for k, e := range g.edges { + if e.src.id == src && e.dst.id == dst { + e.deleted = true g.edges = append(g.edges[:k], g.edges[k+1:]...) return true } diff --git a/pert_config.go b/pert_config.go index 6d531c0..511e87c 100644 --- a/pert_config.go +++ b/pert_config.go @@ -3,6 +3,7 @@ package graphman import ( "fmt" "log" + "strings" ) type PertAction struct { @@ -45,7 +46,7 @@ func FromPertConfig(config PertConfig) *Graph { graph.AddVertex(state.ID, attrs) for _, dependency := range state.DependsOn { graph.AddEdge( - pertPostID(dependency), + graph.pertGetDependencyEnd(dependency), state.ID, Attrs{}.SetPertZeroTimeActivity(), ) @@ -80,9 +81,10 @@ func FromPertConfig(config PertConfig) *Graph { ) case 1: // only one dependency dependency := action.DependsOn[0] + graph.AddEdge( - pertPostID(dependency), - pertPreID(action.ID), + graph.pertGetDependencyEnd(dependency), + pertPostID(action.ID), attrs, ) default: @@ -93,7 +95,7 @@ func FromPertConfig(config PertConfig) *Graph { ) for _, dependency := range action.DependsOn { graph.AddEdge( - pertPostID(dependency), + graph.pertGetDependencyEnd(dependency), pertPreID(action.ID), Attrs{}.SetPertZeroTimeActivity(), ) @@ -124,37 +126,124 @@ func FromPertConfig(config PertConfig) *Graph { } for _, vertex := range graph.Vertices() { - if vertex.Attrs.GetTitle() == "" { + if !pertIsUntitledState(vertex) && vertex.Attrs.GetTitle() == "" { vertex.Attrs.SetTitle(vertex.id) } } if !config.Opts.NoSimplify { // simplify the graph - verticesToDelete := map[string]bool{} // creating a map so we can iterate over vertices while deleting some entries - for _, vertex := range graph.Vertices() { - if pertIsUntitledState(vertex) || true { - if vertex.OutDegree() == 1 { // remove dummy states with only one dummy successor - successor := vertex.SuccessorEdges()[0] - if pertIsZeroTimeActivity(successor) { - for _, predecessor := range vertex.PredecessorEdges() { - predecessor.dst = successor.dst - } - graph.RemoveEdge(vertex.id, successor.dst.id) - verticesToDelete[vertex.id] = true - } - } + for { + pertRemoveDummySteps(graph) + pertMergeDummyActionGroups(graph) + if removed := graph.gc(); removed == 0 { + break } } - for id := range verticesToDelete { - graph.RemoveVertex(id) - } } return graph } +func pertRemoveDummySteps(graph *Graph) { + // remove dummy states with only one dummy successor + for _, vertex := range graph.Vertices() { + if vertex.deleted || !pertIsUntitledState(vertex) || vertex.OutDegree() != 1 { + continue + } + successor := vertex.SuccessorEdges()[0] + if pertIsZeroTimeActivity(successor) { + for _, predecessor := range vertex.PredecessorEdges() { + predecessor.dst = successor.dst + } + graph.RemoveEdge(vertex.id, successor.dst.id) + vertex.deleted = true + } + } +} + +func pertMergeDummyActionGroups(graph *Graph) { + // merge dummy action groups + for _, vertex := range graph.Vertices() { + if vertex.deleted || vertex.OutDegree() < 2 { + continue + } + for _, combination := range vertex.SuccessorEdges().AllCombinations().LongestToShortest() { + if len(combination) < 2 { + continue + } + onlyActiveDummies := true + for _, edge := range combination { + if edge.deleted || edge.src.deleted || edge.dst.deleted || !pertIsZeroTimeActivity(edge) { + onlyActiveDummies = false + break + } + } + if !onlyActiveDummies { + continue + } + predecessors := combination[0].dst.PredecessorVertices() + same := true + for i := 1; i < len(combination); i++ { + if !predecessors.Equals(combination[i].dst.PredecessorVertices()) { + same = false + break + } + } + if !same { + continue + } + successors := Vertices{} + for _, edge := range combination { + successors = append(successors, edge.dst) + } + predecessors = predecessors.Unique() + successors = successors.Unique() + + ids := []string{} + titles := []string{} + for _, successor := range successors { + ids = append(ids, successor.id) + if title := successor.Attrs.GetTitle(); title != "" { + titles = append(titles, title) + } + } + metaID := strings.Join(ids, ",") + attrs := Attrs{} + if len(titles) > 0 { + attrs.SetTitle(strings.Join(titles, " + ")) + } else { + attrs.SetPertUntitledState() + } + metaVertex := graph.AddVertex(metaID, attrs) + for _, predecessor := range predecessors { + depID := graph.pertGetDependencyEnd(predecessor.id) + graph.AddEdge(depID, metaID, Attrs{}.SetPertZeroTimeActivity()) + } + for _, successor := range successors { + for _, successorSuccessor := range successor.successors { + successorSuccessor.src = metaVertex + } + for _, predecessor := range predecessors { + graph.RemoveEdge(predecessor.id, successor.id) + } + successor.deleted = true + } + break + } + } +} + func pertPreID(id string) string { return fmt.Sprintf("pre_%s", id) } func pertPostID(id string) string { return fmt.Sprintf("post_%s", id) } +func (g *Graph) pertGetDependencyEnd(dependency string) string { + // if dependency is a vertex, the end is the vertex itself + if vertex := g.GetVertex(dependency); vertex != nil { + return vertex.id + } + // else, we need to take the post_{edge} + return pertPostID(dependency) +} + func pertIsZeroTimeActivity(edge *Edge) bool { pert := edge.GetPert() return pert != nil && pert.IsZeroTimeActivity diff --git a/pert_config_test.go b/pert_config_test.go index 7003b75..c62e048 100644 --- a/pert_config_test.go +++ b/pert_config_test.go @@ -55,5 +55,5 @@ actions: graph := FromPertConfig(config) fmt.Println(graph) // Output: - // {(Start,pre_5)[[pert:To=4,Tm=6,Tp=10,Te=6.33,σe=1,Ve=1,title:Prepare foundation]],(Start,pre_5)[[pert:To=2,Tm=4,Tp=7,Te=4.17,σe=0.83,Ve=0.69,title:Make & position door frames]],(Start,pre_9)[[pert:To=7,Tm=9,Tp=12,Te=9.17,σe=0.83,Ve=0.69,title:Lay drains & floor base]],(post_5,Finish)[[pert:To=2,Tm=4,Tp=5,Te=3.83,σe=0.5,Ve=0.25,title:Install service & settings]],(pre_5,post_5)[[pert:To=7,Tm=10,Tp=15,Te=10.33,σe=1.33,Ve=1.78,title:Erect walls]],(pre_6,pre_9)[[pert:To=1,Tm=2,Tp=4,Te=2.17,σe=0.5,Ve=0.25,title:Plaster ceilings]],(post_7,pre_6)[[pert:]],(post_5,Finish)[[pert:To=4,Tm=6,Tp=8,Te=6,σe=0.67,Ve=0.44,title:Erect roof]],(post_7,Finish)[[pert:To=7,Tm=9,Tp=11,Te=9,σe=0.67,Ve=0.44,title:Install door & windows]],(pre_9,pre_10)[[pert:To=1,Tm=2,Tp=3,Te=2,σe=0.33,Ve=0.11,title:Fit gutters & pipes]],(pre_10,Finish)[[pert:To=1,Tm=2,Tp=3,Te=2,σe=0.33,Ve=0.11,title:Paint outside]]} + // {(Start,pre_5)[[pert:To=4,Tm=6,Tp=10,Te=6.33,σe=1,Ve=1,title:Prepare foundation]],(Start,pre_5)[[pert:To=2,Tm=4,Tp=7,Te=4.17,σe=0.83,Ve=0.69,title:Make & position door frames]],(Start,pre_9)[[pert:To=7,Tm=9,Tp=12,Te=9.17,σe=0.83,Ve=0.69,title:Lay drains & floor base]],(post_5,pre_6)[[pert:To=2,Tm=4,Tp=5,Te=3.83,σe=0.5,Ve=0.25,title:Install service & settings]],(pre_5,post_5)[[pert:To=7,Tm=10,Tp=15,Te=10.33,σe=1.33,Ve=1.78,title:Erect walls]],(pre_6,pre_9)[[pert:To=1,Tm=2,Tp=4,Te=2.17,σe=0.5,Ve=0.25,title:Plaster ceilings]],(post_7,pre_6)[[]],(post_5,post_7)[[pert:To=4,Tm=6,Tp=8,Te=6,σe=0.67,Ve=0.44,title:Erect roof]],(post_7,pre_10)[[pert:To=7,Tm=9,Tp=11,Te=9,σe=0.67,Ve=0.44,title:Install door & windows]],(pre_9,pre_10)[[pert:To=1,Tm=2,Tp=3,Te=2,σe=0.33,Ve=0.11,title:Fit gutters & pipes]],(pre_10,Finish)[[pert:To=1,Tm=2,Tp=3,Te=2,σe=0.33,Ve=0.11,title:Paint outside]],Finish} } diff --git a/vertex.go b/vertex.go index 8f1a6ee..52d3342 100644 --- a/vertex.go +++ b/vertex.go @@ -20,6 +20,7 @@ type Vertex struct { prev *Edge visited bool } + deleted bool // temporary variable used before a gc() } func newVertex(id string, attrs ...Attrs) *Vertex { @@ -39,16 +40,19 @@ func newVertex(id string, attrs ...Attrs) *Vertex { func (v Vertex) IsSource() bool { return v.InDegree() == 0 } func (v Vertex) IsSink() bool { return v.OutDegree() == 0 } -func (v Vertex) InDegree() int { return len(v.predecessors) } -func (v Vertex) OutDegree() int { return len(v.successors) } +func (v Vertex) InDegree() int { return len(v.PredecessorEdges()) } +func (v Vertex) OutDegree() int { return len(v.SuccessorEdges()) } func (v Vertex) Degree() int { return v.InDegree() + v.OutDegree() } -func (v Vertex) PredecessorEdges() Edges { return v.predecessors } -func (v Vertex) SuccessorEdges() Edges { return v.successors } +func (v Vertex) PredecessorEdges() Edges { return v.predecessors.filtered() } +func (v Vertex) SuccessorEdges() Edges { return v.successors.filtered() } +func (v Vertex) IsIsolated() bool { return v.Degree() == 0 } func (v Vertex) PredecessorVertices() Vertices { vertices := Vertices{} for _, edge := range v.predecessors { - vertices = append(vertices, edge.src) + if !edge.deleted && !edge.src.deleted { + vertices = append(vertices, edge.src) + } } return vertices.Unique() } @@ -56,26 +60,28 @@ func (v Vertex) PredecessorVertices() Vertices { func (v Vertex) SuccessorVertices() Vertices { vertices := Vertices{} for _, edge := range v.successors { - vertices = append(vertices, edge.dst) + if !edge.deleted && !edge.dst.deleted { + vertices = append(vertices, edge.dst) + } } return vertices.Unique() } -func (v Vertex) IsIsolated() bool { - return len(v.predecessors) == 0 && len(v.successors) == 0 -} - func (v Vertex) Edges() Edges { - return append(v.predecessors, v.successors...) + return append(v.predecessors, v.successors...).filtered() } func (v Vertex) Neighbors() Vertices { neighbors := Vertices{} for _, edge := range v.predecessors { - neighbors = append(neighbors, edge.src) + if !edge.deleted && !edge.src.deleted { + neighbors = append(neighbors, edge.src) + } } for _, edge := range v.successors { - neighbors = append(neighbors, edge.dst) + if !edge.deleted && !edge.dst.deleted { + neighbors = append(neighbors, edge.dst) + } } return neighbors.Unique() } @@ -98,6 +104,9 @@ func (v *Vertex) WalkSuccessorVertices(fn VerticesWalkFunc) error { } func (v *Vertex) walkSuccessorVerticesRec(fn VerticesWalkFunc, previous *Vertex, depth int, visited map[string]bool) error { + if v.deleted { + return nil + } if visited[v.id] { return nil } @@ -105,7 +114,7 @@ func (v *Vertex) walkSuccessorVerticesRec(fn VerticesWalkFunc, previous *Vertex, if err := fn(v, previous, depth); err != nil { return err } - for _, successor := range v.successors { + for _, successor := range v.SuccessorEdges() { if err := successor.dst.walkSuccessorVerticesRec(fn, v, depth+1, visited); err != nil { return err } @@ -119,6 +128,9 @@ func (v *Vertex) WalkPredecessorVertices(fn VerticesWalkFunc) error { } func (v *Vertex) walkPredecessorVerticesRec(fn VerticesWalkFunc, previous *Vertex, depth int, visited map[string]bool) error { + if v.deleted { + return nil + } if visited[v.id] { return nil } @@ -126,7 +138,7 @@ func (v *Vertex) walkPredecessorVerticesRec(fn VerticesWalkFunc, previous *Verte if err := fn(v, previous, depth); err != nil { return err } - for _, predecessor := range v.predecessors { + for _, predecessor := range v.PredecessorEdges() { if err := predecessor.dst.walkPredecessorVerticesRec(fn, v, depth+1, visited); err != nil { return err } @@ -140,6 +152,9 @@ func (v *Vertex) WalkAdjacentVertices(fn VerticesWalkFunc) error { } func (v *Vertex) walkAdjacentVerticesRec(fn VerticesWalkFunc, previous *Vertex, depth int, visited map[string]bool) error { + if v.deleted { + return nil + } if visited[v.id] { return nil } @@ -147,12 +162,12 @@ func (v *Vertex) walkAdjacentVerticesRec(fn VerticesWalkFunc, previous *Vertex, if err := fn(v, previous, depth); err != nil { return err } - for _, successor := range v.successors { + for _, successor := range v.SuccessorEdges() { if err := successor.dst.walkAdjacentVerticesRec(fn, v, depth+1, visited); err != nil { return err } } - for _, predecessor := range v.predecessors { + for _, predecessor := range v.PredecessorEdges() { if err := predecessor.src.walkAdjacentVerticesRec(fn, v, depth+1, visited); err != nil { return err } @@ -178,9 +193,19 @@ func (v Vertices) Len() int { return len(v) } func (v Vertices) Swap(i, j int) { v[i], v[j] = v[j], v[i] } func (v Vertices) Less(i, j int) bool { return v[i].id < v[j].id } +func (v Vertices) filtered() Vertices { + filtered := Vertices{} + for _, vertex := range v { + if !vertex.deleted { + filtered = append(filtered, vertex) + } + } + return filtered +} + func (v Vertices) Unique() Vertices { m := map[string]*Vertex{} - for _, v := range v { + for _, v := range v.filtered() { m[v.id] = v } uniques := Vertices{} @@ -190,3 +215,19 @@ func (v Vertices) Unique() Vertices { sort.Sort(uniques) return uniques } + +func (v Vertices) Equals(other Vertices) bool { + tmp := map[*Vertex]int{} + for _, vertex := range v.filtered() { + tmp[vertex]++ + } + for _, vertex := range other.filtered() { + tmp[vertex]-- + } + for _, v := range tmp { + if v != 0 { + return false + } + } + return true +} diff --git a/viz/graphviz.go b/viz/graphviz.go index cd330f2..33c09ad 100644 --- a/viz/graphviz.go +++ b/viz/graphviz.go @@ -105,8 +105,10 @@ func attrsGeneric(a graphman.Attrs, attrs map[string]string, opts *Opts) { case "rankdir", "shape", "style": attrs[k] = v.(string) default: - line := fmt.Sprintf("\n%s: %v", k, v) - attrs[string(graphviz.Comment)] += line + if vStr := fmt.Sprintf("%v", v); vStr != "" { + line := fmt.Sprintf("\n%s: %s", k, vStr) + attrs[string(graphviz.Comment)] += line + } } } }