From d52e15a609e43e54bafaeaaf3889dc868e40f2d3 Mon Sep 17 00:00:00 2001 From: Ochi Daiki Date: Sun, 3 Nov 2024 23:48:54 +0900 Subject: [PATCH] add Hour and CriticalPath (#25) --- README.md | 178 ++++++++++++++++++++++++++++++++++++----------- dag.svg | 60 +--------------- dag_critical.svg | 1 + mermaid.go | 3 + node.go | 16 +++-- node_short.go | 5 ++ plantuml.go | 111 +++++++++++++++++++++++++++-- 7 files changed, 266 insertions(+), 108 deletions(-) create mode 100644 dag_critical.svg diff --git a/README.md b/README.md index 7ccedf9..8a9938a 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,12 @@ func main() { developFeature1.Note("xxが担当") reviewDevelopFeature1 := g.Task("レビュー対応") - developFeature2 := g.Task("feature2開発").Note("yyが担当") + developFeature2 := g.Task("feature2開発") + developFeature2.Note("yyが担当") reviewDevelopFeature2 := g.Task("レビュー対応") - prepareInfra := g.Task("インフラ準備").Note("zzが担当") + prepareInfra := g.Task("インフラ準備") + prepareInfra.Note("zzが担当") test := g.Task("結合テスト") release := g.Task("リリース") @@ -53,7 +55,7 @@ func main() { reviewDesign.Con(prepareInfra).Con(test) test.Con(release).Con(finish) - g.Done(design, reviewDesign, developFeature2, finish) + g.Done(design, reviewDesign, developFeature1, reviewDevelopFeature1, developFeature2) uml, err := dag.UML() if err != nil { @@ -69,14 +71,14 @@ func main() { rectangle "ゴール(目的)" as 1 usecase "設計" as 2 #DarkGray usecase "レビュー対応" as 3 #DarkGray -usecase "feature1開発" as 4 +usecase "feature1開発" as 4 #DarkGray note left xxが担当 end note -usecase "レビュー対応" as 5 +usecase "レビュー対応" as 5 #DarkGray usecase "結合テスト" as 9 usecase "リリース" as 10 -usecase "finish" as 11 #DarkGray +usecase "finish" as 11 usecase "feature2開発" as 6 #DarkGray note left yyが担当 @@ -107,8 +109,104 @@ end note ![image](dag.svg) +### Critical path + +1. `go run main.go > dag.pu` + +```go +package main + +import ( + "fmt" + "os" + + g "github.com/ddddddO/gdag" +) + +func main() { + var dag *g.Node = g.DAG("ゴール(目的)") + + var design *g.Node = g.Task("設計").Hour(10) + reviewDesign := g.Task("レビュー対応").Hour(2) + + developFeature1 := g.Task("feature1開発").Hour(20) + developFeature1.Note("xxが担当") + reviewDevelopFeature1 := g.Task("レビュー対応").Hour(1.5) + + developFeature2 := g.Task("feature2開発").Hour(15) + developFeature2.Note("yyが担当") + reviewDevelopFeature2 := g.Task("レビュー対応").Hour(1.5) + + prepareInfra := g.Task("インフラ準備").Hour(15) + prepareInfra.Note("zzが担当") + + test := g.Task("結合テスト").Hour(20) + release := g.Task("リリース").Hour(2) + finish := g.Task("finish") + + dag.Con(design).Con(reviewDesign).Con(developFeature1).Con(reviewDevelopFeature1).Con(test) + reviewDesign.Con(developFeature2).Con(reviewDevelopFeature2).Con(test) + reviewDesign.Con(prepareInfra).Con(test) + test.Con(release).Con(finish) + + g.Done(design, reviewDesign, developFeature1, reviewDevelopFeature1, developFeature2) + + // If you do not want to represent critical path, use `dag.UMLNoCritical()`. + uml, err := dag.UML() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + fmt.Println(uml) +} +``` + +``` +@startuml +rectangle "ゴール(目的)" as 1 +usecase "設計 (10.0h)" as 2 #DarkGray-Yellow +usecase "レビュー対応 (2.0h)" as 3 #DarkGray-Yellow +usecase "feature1開発 (20.0h)" as 4 #DarkGray-Yellow +note left +xxが担当 +end note +usecase "レビュー対応 (1.5h)" as 5 #DarkGray-Yellow +usecase "結合テスト (20.0h)" as 9 #Yellow +usecase "リリース (2.0h)" as 10 #Yellow +usecase "finish" as 11 #Yellow +usecase "feature2開発 (15.0h)" as 6 #DarkGray +note left +yyが担当 +end note +usecase "レビュー対応 (1.5h)" as 7 +usecase "インフラ準備 (15.0h)" as 8 +note left +zzが担当 +end note + +1 --> 2 +2 --> 3 +3 --> 4 +4 --> 5 +5 --> 9 +9 --> 10 +10 --> 11 +3 --> 6 +6 --> 7 +7 --> 9 +3 --> 8 +8 --> 9 + +@enduml +``` + +2. dag.pu to png or svg +![image](dag_critical.svg) + ## Mermaid +※ Mermaid method does not support critical paths. + 1. `go run main.go` ```go @@ -124,22 +222,22 @@ import ( func main() { var dag *g.Node = g.DAG("ゴール(目的)") - var design *g.Node = g.Task("設計") - reviewDesign := g.Task("レビュー対応") + var design *g.Node = g.Task("設計").Hour(10) + reviewDesign := g.Task("レビュー対応").Hour(2) - developFeature1 := g.Task("feature1開発") - developFeature1.Note("noop") - reviewDevelopFeature1 := g.Task("レビュー対応") + developFeature1 := g.Task("feature1開発").Hour(20) + developFeature1.Note("xxが担当") + reviewDevelopFeature1 := g.Task("レビュー対応").Hour(1.5) - developFeature2 := g.Task("feature2開発") - developFeature2.Note("noop") - reviewDevelopFeature2 := g.Task("レビュー対応") + developFeature2 := g.Task("feature2開発").Hour(15) + developFeature2.Note("yyが担当") + reviewDevelopFeature2 := g.Task("レビュー対応").Hour(1.5) - prepareInfra := g.Task("インフラ準備") - prepareInfra.Note("noop") + prepareInfra := g.Task("インフラ準備").Hour(15) + prepareInfra.Note("zzが担当") - test := g.Task("結合テスト") - release := g.Task("リリース") + test := g.Task("結合テスト").Hour(20) + release := g.Task("リリース").Hour(2) finish := g.Task("finish") dag.Con(design).Con(reviewDesign).Con(developFeature1).Con(reviewDevelopFeature1).Con(test) @@ -147,7 +245,7 @@ func main() { reviewDesign.Con(prepareInfra).Con(test) test.Con(release).Con(finish) - g.Done(design, reviewDesign, developFeature2, finish) + g.Done(design, reviewDesign, developFeature1, reviewDevelopFeature1, developFeature2) mermaid, err := dag.Mermaid() if err != nil { @@ -156,23 +254,22 @@ func main() { } fmt.Println(mermaid) } - ``` ``` graph TD classDef doneColor fill:#868787 1("ゴール(目的)") -2(["設計"]):::doneColor -3(["レビュー対応"]):::doneColor -4(["feature1開発"]) -5(["レビュー対応"]) -9(["結合テスト"]) -10(["リリース"]) -11(["finish"]):::doneColor -6(["feature2開発"]):::doneColor -7(["レビュー対応"]) -8(["インフラ準備"]) +2(["設計 (10.0h)"]):::doneColor +3(["レビュー対応 (2.0h)"]):::doneColor +4(["feature1開発 (20.0h)"]):::doneColor +5(["レビュー対応 (1.5h)"]):::doneColor +9(["結合テスト (20.0h)"]) +10(["リリース (2.0h)"]) +11(["finish"]) +6(["feature2開発 (15.0h)"]):::doneColor +7(["レビュー対応 (1.5h)"]) +8(["インフラ準備 (15.0h)"]) 1 --> 2 2 --> 3 @@ -194,16 +291,16 @@ classDef doneColor fill:#868787 graph TD classDef doneColor fill:#868787 1("ゴール(目的)") -2(["設計"]):::doneColor -3(["レビュー対応"]):::doneColor -4(["feature1開発"]) -5(["レビュー対応"]) -9(["結合テスト"]) -10(["リリース"]) -11(["finish"]):::doneColor -6(["feature2開発"]):::doneColor -7(["レビュー対応"]) -8(["インフラ準備"]) +2(["設計 (10.0h)"]):::doneColor +3(["レビュー対応 (2.0h)"]):::doneColor +4(["feature1開発 (20.0h)"]):::doneColor +5(["レビュー対応 (1.5h)"]):::doneColor +9(["結合テスト (20.0h)"]) +10(["リリース (2.0h)"]) +11(["finish"]) +6(["feature2開発 (15.0h)"]):::doneColor +7(["レビュー対応 (1.5h)"]) +8(["インフラ準備 (15.0h)"]) 1 --> 2 2 --> 3 @@ -219,7 +316,6 @@ classDef doneColor fill:#868787 8 --> 9 ``` - ## CheckList 1. `go run main.go` diff --git a/dag.svg b/dag.svg index 5de6151..498c2ca 100644 --- a/dag.svg +++ b/dag.svg @@ -1,59 +1 @@ -ゴール(目的)設計レビュー対応feature1開発xxが担当レビュー対応結合テストリリースfinishfeature2開発yyが担当レビュー対応インフラ準備zzが担当 \ No newline at end of file +ゴール(目的)設計レビュー対応feature1開発xxが担当レビュー対応結合テストリリースfinishfeature2開発yyが担当レビュー対応インフラ準備zzが担当 \ No newline at end of file diff --git a/dag_critical.svg b/dag_critical.svg new file mode 100644 index 0000000..12c0d40 --- /dev/null +++ b/dag_critical.svg @@ -0,0 +1 @@ +ゴール(目的)設計 (10.0h)レビュー対応 (2.0h)feature1開発 (20.0h)xxが担当レビュー対応 (1.5h)結合テスト (20.0h)リリース (2.0h)finishfeature2開発 (15.0h)yyが担当レビュー対応 (1.5h)インフラ準備 (15.0h)zzが担当 \ No newline at end of file diff --git a/mermaid.go b/mermaid.go index ea1013e..4704151 100644 --- a/mermaid.go +++ b/mermaid.go @@ -57,6 +57,9 @@ func (*mermaidGenerator) renderComponent(node *Node) string { ret += s case usecase: s := fmt.Sprintf("%d([\"%s\"])", node.index, node.text) + if node.hour > 0 { + s = fmt.Sprintf("%d([\"%s (%.1fh)\"])", node.index, node.text, node.hour) + } if len(node.colorMermaid) != 0 { s += fmt.Sprintf(":::%s", node.colorMermaid) } diff --git a/node.go b/node.go index 0432698..33e0724 100644 --- a/node.go +++ b/node.go @@ -1,15 +1,18 @@ package gdag type Node struct { - nodeType nodeType - index int // mermaidの識別子としても利用する - text string - note string + nodeType nodeType + index int // mermaidの識別子としても利用する + text string + note string + hour float64 // 見積時間 + color string // done: #DarkGray colorMermaid string // done: doneColor // parent *Node // TODO: 現状、中間ノードのためにおいてる downstream []*Node + startPoint bool // レンダリング処理の最初の node ということ } type nodeType string @@ -85,6 +88,11 @@ func (current *Node) Note(note string) *Node { return current } +func (current *Node) Hour(hour float64) *Node { + current.hour = hour + return current +} + func (current *Node) isDone() bool { return current.color == colorDone } diff --git a/node_short.go b/node_short.go index fba9686..3621830 100644 --- a/node_short.go +++ b/node_short.go @@ -19,3 +19,8 @@ func (upstream *Node) C(current *Node) *Node { func (current *Node) N(note string) *Node { return current.Note(note) } + +// H is short name of Hour func. +func (current *Node) H(hour float64) *Node { + return current.Hour(hour) +} diff --git a/plantuml.go b/plantuml.go index b164ff8..f551f72 100644 --- a/plantuml.go +++ b/plantuml.go @@ -6,22 +6,104 @@ import ( // UML outputs dag PlantUML format. func (start *Node) UML() (string, error) { - ug := newUMLGenerator() + start.startPoint = true + cc := newCriticalPathCalculator() + ug := newUMLGenerator(cc.getCriticalPaths(start)) ret := "@startuml" + "\n" ret += ug.generateComponents(start) + "\n" ret += ug.generateRelations(start) + "\n" ret += "@enduml" + + start.startPoint = false + return ret, nil +} + +// UMLNoCritical outputs dag PlantUML format that does not represent critical path. +func (start *Node) UMLNoCritical() (string, error) { + start.startPoint = true + ug := newUMLGenerator(nil) + + ret := "@startuml" + "\n" + ret += ug.generateComponents(start) + "\n" + ret += ug.generateRelations(start) + "\n" + ret += "@enduml" + + start.startPoint = false return ret, nil } +type criticalPath struct { + path map[int]struct{} // key は Node の index + sumHour float64 +} + +func (cp *criticalPath) contains(n *Node) bool { + _, ok := cp.path[n.index] + return ok +} + +type criticalPathCalculator struct { + allPaths [][]*Node // start からすべてのパス +} + +func newCriticalPathCalculator() *criticalPathCalculator { + return &criticalPathCalculator{} +} + +func (cc *criticalPathCalculator) getCriticalPaths(start *Node) []*criticalPath { + cc.walk(start, []*Node{}) + + criticalPaths := []*criticalPath{} + for _, path := range cc.allPaths { + critical := &criticalPath{path: map[int]struct{}{}} + for _, n := range path { + critical.path[n.index] = struct{}{} + critical.sumHour += n.hour + } + + if critical.sumHour == 0 { + continue + } + if len(criticalPaths) == 0 { + criticalPaths = append(criticalPaths, critical) + continue + } + if critical.sumHour == criticalPaths[0].sumHour { + criticalPaths = append(criticalPaths, critical) + continue + } + if critical.sumHour > criticalPaths[0].sumHour { + criticalPaths = []*criticalPath{critical} + continue + } + } + + return criticalPaths +} + +func (cc *criticalPathCalculator) walk(current *Node, path []*Node) { + path = append(path, current) + + if len(current.downstream) == 0 { + cc.allPaths = append(cc.allPaths, path) + return + } + + for _, n := range current.downstream { + cc.walk(n, path) + } +} + type umlGenerator struct { + criticalPaths []*criticalPath uniqueComponents map[int]struct{} uniqueRelations map[string]struct{} } -func newUMLGenerator() *umlGenerator { +func newUMLGenerator(criticalPaths []*criticalPath) *umlGenerator { return ¨Generator{ + criticalPaths: criticalPaths, uniqueComponents: map[int]struct{}{}, uniqueRelations: map[string]struct{}{}, } @@ -37,21 +119,33 @@ func (ug *umlGenerator) generateComponent(node *Node) string { } ug.uniqueComponents[node.index] = struct{}{} - ret := (*umlGenerator)(nil).renderComponent(node) + ret := ug.renderComponent(node) for _, d := range node.downstream { ret += ug.generateComponent(d) } return ret } -func (*umlGenerator) renderComponent(node *Node) string { +func (ug *umlGenerator) renderComponent(node *Node) string { ret := "" switch node.nodeType { case rectangle, usecase: s := fmt.Sprintf("%s \"%s\" as %d", node.nodeType, node.text, node.index) + if node.hour > 0 { + s = fmt.Sprintf("%s \"%s (%.1fh)\" as %d", node.nodeType, node.text, node.hour, node.index) + } + if len(node.color) != 0 { s += fmt.Sprintf(" %s", node.color) + if ug.isCritical(node) && !node.isDAG() { + s += fmt.Sprintf("-%s", "Yellow") + } + } else { + if ug.isCritical(node) && !node.isDAG() { + s += fmt.Sprintf(" %s", "#Yellow") + } } + s += "\n" ret += s } @@ -61,6 +155,15 @@ func (*umlGenerator) renderComponent(node *Node) string { return ret } +func (ug *umlGenerator) isCritical(current *Node) bool { + for _, cp := range ug.criticalPaths { + if cp.contains(current) { + return true + } + } + return false +} + func (ug *umlGenerator) generateRelations(start *Node) string { return ug.generateRelation(start, "") }