使用Go语言生成Excel任务表依赖图(Markdown文件mermaid图)
一、前言
在游戏中,任务是非常常见的玩法,可能会有主线任务,支线任务以及其它一些类型的任务,各任务可能还会有前置任务,即需要完成某个任务之后,才能做当前任务。在游戏开发中,配置表可以使用Excel来编辑,如果是任务表,可能会是如下配置方式: TaskIDTaskTitlePreTask10任务10020任务20011任务111021任务2120
当任务比较多的时候,它们的依赖关系将变得不直观,很容易出错,出错也不容易发现。
有没比较直观的方式进行查看,排错呢?笔者想到了目前非常流程的Markdown文件,它可以简单地通过文本的方式输入然后输出强大的各种图表。这里就可以使用mermaid图来直观展现。
关于mermaid图可以去官网 https://mermaid.js.org/intro/查看用例。
下图为生成后的效果图:
注意:mermaid图在渲染时,如果不设置subgraph则可能会出现乱序问题,即不是按代码中出现的顺序渲染。
二、实现
为了方便Go读取Excel,需要使用相关的Excel库,笔者使用 excelize库。
根据前面的效果图,可以知道,这其实就是一个深度优先的树,实现方式有两种,一种是使用递归的方式来实现,这种方式实现起来简单,但是如果层次很深,那可能会出现栈溢出;另一种方式就是使用栈的方式来实现,将每一层节点先压栈,然后从栈顶取出一个节点然后再将其所有子节点入栈,再从栈顶取出一个节点处理,依此类推,直到栈中所有节点处理完毕。
下面列出使用递归方式实现的版本:
1/*
2MIT License
3
4# Copyright (c) 2023 WittonBell
5
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24package main
25
26import (
27 "flag"
28 "fmt"
29 "os"
30 "path/filepath"
31 "strings"
32
33 "github.com/xuri/excelize/v2"
34)
35
36var taskIDField string
37var taskTitleField string
38var preTaskField string
39var noCaseSensitive bool // 是否不区分大小写
40var fieldNameRow uint // 字段名所在行号
41var dataStartRow uint // 数据开始行号
42
43type node struct {
44 taskID string
45 taskTitle string
46}
47
48type multiMap map[string][]*node
49
50func (slf multiMap) Add(key string, nd *node) {
51 if len(slf) == 0 {
52 slf[key] = []*node{nd}
53 } else {
54 slf[key] = append(slf[key], nd)
55 }
56}
57
58func (slf multiMap) Get(key string) []*node {
59 if slf == nil {
60 return nil
61 }
62 return slf[key]
63}
64
65func (slf multiMap) Del(key string) {
66 delete(slf, key)
67}
68
69func searchKeyCol(rows *excelize.Rows) (TaskIDCol, PreTaskIDCol, TitleCol int) {
70 row, err := rows.Columns()
71 if err != nil {
72 fmt.Println(err.Error())
73 }
74
75 for i, col := range row {
76 name := col
77 if noCaseSensitive {
78 name = strings.ToLower(col)
79 }
80 if name == preTaskField {
81 PreTaskIDCol = i + 1
82 } else if name == taskIDField {
83 TaskIDCol = i + 1
84 } else if name == taskTitleField {
85 TitleCol = i + 1
86 }
87 }
88 return
89}
90
91func readExcel(filePath string) multiMap {
92 fd, err := excelize.OpenFile(filePath)
93 if err != nil {
94 fmt.Printf("读取文件`%s`失败", filePath)
95 return nil
96 }
97 defer func() {
98 fd.Close()
99 }()
100 TaskIDCol, PreTaskIDCol, TitleCol := -1, -1, -1
101 sheetName := fd.GetSheetName(0)
102 rows, err := fd.Rows(sheetName)
103 if err != nil {
104 return nil
105 }
106 defer func() {
107 rows.Close()
108 }()
109
110 m := multiMap{}
111 for i := 1; rows.Next(); i++ {
112 if i == int(fieldNameRow) {
113 TaskIDCol, PreTaskIDCol, TitleCol = searchKeyCol(rows)
114 isOk := true
115 if TaskIDCol < 0 {
116 isOk = false
117 fmt.Printf("要求字段名:%s\n", taskIDField)
118 }
119 if PreTaskIDCol < 0 {
120 isOk = false
121 fmt.Printf("要求字段名:%s\n", preTaskField)
122 }
123 if TitleCol < 0 {
124 isOk = false
125 fmt.Printf("要求字段名:%s\n", taskTitleField)
126 }
127 if !isOk {
128 return nil
129 }
130 }
131 if i < int(dataStartRow) {
132 continue
133 }
134 TaskIDCell, err := excelize.CoordinatesToCellName(TaskIDCol, i)
135 if err != nil {
136 continue
137 }
138 PreTaskIDCell, err := excelize.CoordinatesToCellName(PreTaskIDCol, i)
139 if err != nil {
140 continue
141 }
142
143 TitleColCell, err := excelize.CoordinatesToCellName(TitleCol, i)
144 if err != nil {
145 continue
146 }
147
148 TaskID, err := fd.GetCellValue(sheetName, TaskIDCell)
149 if err != nil || TaskID == "" {
150 continue
151 }
152
153 Title, err := fd.GetCellValue(sheetName, TitleColCell)
154 if err != nil || Title == "" {
155 continue
156 }
157
158 PreTaskID, err := fd.GetCellValue(sheetName, PreTaskIDCell)
159 if err != nil {
160 continue
161 }
162
163 if PreTaskID == "" {
164 PreTaskID = "0"
165 }
166
167 m.Add(PreTaskID, &node{taskID: TaskID, taskTitle: Title})
168 }
169
170 return m
171}
172
173func usage() {
174 w := flag.CommandLine.Output()
175 fmt.Fprintf(w, "%s 应用程序是将Excel任务表中的关系转换成Markdown的mermaid图,方便使用Markdown工具直观地查看任务依赖。", filepath.Base(os.Args[0]))
176 fmt.Fprintln(w)
177 fmt.Fprintf(w, "命令格式:%s -hr [字段所在行号] -dr [数据起始行号] [-nc] -id [任务ID字段名] -t [任务标题字段名] -pid [前置任务ID字段名] -o <输出文件> <Excel文件路径>", filepath.Base(os.Args[0]))
178 fmt.Fprintln(w)
179 flag.CommandLine.PrintDefaults()
180 fmt.Fprintln(w, " -h")
181 fmt.Fprintln(w, " \t显示此帮助")
182}
183
184func main() {
185 var outputFile string
186
187 flag.CommandLine.Usage = usage
188 flag.BoolVar(&noCaseSensitive, "nc", false, "字段名不区分大小写")
189 flag.UintVar(&fieldNameRow, "hr", 1, "字段所在行号")
190 flag.UintVar(&dataStartRow, "dr", 2, "数据起始行号")
191 flag.StringVar(&taskIDField, "id", "ID", "-id [任务ID字段名]")
192 flag.StringVar(&taskTitleField, "t", "Title", "-t [任务标题字段名]")
193 flag.StringVar(&preTaskField, "pid", "PreTask", "-pid [前置任务ID字段名]")
194 flag.StringVar(&outputFile, "o", "任务图.md", "-o <输出文件>")
195
196 flag.Parse()
197 if flag.NArg() < 1 {
198 usage()
199 return
200 }
201 if noCaseSensitive {
202 taskIDField = strings.ToLower(taskIDField)
203 taskTitleField = strings.ToLower(taskTitleField)
204 preTaskField = strings.ToLower(preTaskField)
205 }
206 mapTask := readExcel(flag.Arg(0))
207 buildGraph(mapTask, outputFile)
208}
209
210func buildGraph(mapTask multiMap, outputFile string) {
211 graph := "```mermaid\ngraph TB\n"
212 graph += "subgraph \n"
213 root := mapTask.Get("0")
214 for _, v := range root {
215 graph += visit(rootNodeName, v, mapTask)
216 }
217 graph += "end\n"
218 graph += "```"
219
220 os.WriteFile(outputFile, []byte(graph), os.ModePerm)
221
222 fmt.Println("完成")
223}
224
225func visit(parent string, nd *node, mapTask multiMap) string {
226 slice := mapTask.Get(nd.taskID)
227 graph := fmt.Sprintf("%s --> %s:%s\n", parent, nd.taskID, nd.taskTitle)
228 if parent == rootNodeName {
229 graph += "subgraph \n"
230 }
231 for _, x := range slice {
232 graph += visit(fmt.Sprintf("%s:%s", nd.taskID, nd.taskTitle), x, mapTask)
233 }
234 mapTask.Del(nd.taskID)
235 if parent == rootNodeName {
236 graph += "end\n"
237 }
238 return graph
239}
使用栈实现的版本笔者放在 excelTask2md了。
- 原文作者:Witton
- 原文链接:https://wittonbell.github.io/posts/2023/2023-08-04-使用Go语言生成Excel任务表依赖图Markdown文件mermaid图/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议. 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。