班级 | 班级社区 |
作业要求 | 软件工程实践第一次作业-CSDN社区 |
作业目标 | 完成一个具有可视化界面的科学计算器 |
参考文献 | Fyne |
目录
作业要求
项目源码地址
作业目标
0. 界面及功能展示
1. PSP表格
2. 解题思路描述
3. 核心代码
4. 设计与实现过程
5. 程序性能改进
6. 单元测试展示
7. 异常处理
8. 心路历程和收获
环境:go 1.20.7 | windows | mingw64
编程工具: VS Code
要求链接软件工程实践第一次作业-CSDN社区要求链接
代码地址
完成一个具有可视化界面的科学计算器
软工计算器演示
PSP | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 30 | 30 |
• Estimate | • 估计这个任务需要多少时间 | 20 | 15 |
Development | 开发 | 300 | 340 |
• Analysis | • 需求分析 (包括学习新技术) | 60 | 90 |
• Design Spec | • 生成设计文档 | 20 | 20 |
• Design Review | • 设计复审 | 30 | 30 |
• Coding Standard | • 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
• Design | • 具体设计 | 30 | 50 |
• Coding | • 具体编码 | 240 | 300 |
• Code Review | • 代码复审 | 60 | 30 |
• Test | • 测试(自我测试,修改代码,提交修改) | 100 | 100 |
Reporting | 报告 | 120 | 100 |
• Test Repor | • 测试报告 | 20 | 20 |
• Size Measurement | • 计算工作量 | 10 | 10 |
• Postmortem & Process Improvement Plan | • 事后总结, 并提出过程改进计划 | 90 | 70 |
合计 | 900 | 915 |
1. GUI问题
什么是GUI,如何使用GUI ,通过调用特定的GUI库实现的接口,可以实现GUI的窗口创建,窗口布局,按钮布局,设置函数响应等功能
2. 计算表达式
用户输入表达式字符串,程序需要识别该字符串,包括识别数字,运算符等,比较难的是括号匹配问题,这边选择通过堆栈实现括号的匹配问题,但是在三角函数方面这个并不适用,这里选择使用的是递归实现
func equals() func() {
return func() {
// 切割换行
lines := strings.Split(entry.Text, "\n")
fmt.Println("lines:", len(lines))
// 空表达式不变
if len(lines) == 0 || (lines[0] == "" && len(lines) == 1) {
entry.Text = ""
entry.Refresh()
log.Println("empty expression")
return
}
line := lines[len(lines)-1]
// 错误切除
if len(lines) >= 3 || strings.Contains(entry.Text, "error") {
entry.Text = line
if strings.Contains(lines[0], "error") {
entry.SetText("")
log.Println("continue error calculate")
return
}
entry.Refresh()
}
// 溢出切除
if strings.Contains(line, "Inf") {
entry.SetText("error:inf calculate\n")
entry.Refresh()
log.Println("error:inf calculate")
return
}
// 三角函数处理
for {
if strings.Contains(line, "sin") || strings.Contains(line, "cos") || strings.Contains(line, "tan") {
res, err := Hsin(line)
if err != nil {
entry.SetText("error:sin or cos calculate expression\n")
entry.Refresh()
log.Println(err)
return
}
log.Println("--------------------")
_, _, trim_exp, err := TrimTec(line)
if err != nil {
entry.SetText("error:sin or cos trim expression\n")
entry.Refresh()
log.Println(err)
return
}
log.Println("--------------------")
line = strings.Replace(line, trim_exp, fmt.Sprint(float32(res)), 1)
log.Println("line 三角函数切割 :", line)
} else {
break
}
}
line = strings.Trim(line, "+/x")
// 次方替换
if strings.Contains(line, "^") {
line = strings.ReplaceAll(line, "^", "**")
}
// 到此阶段只剩下基本的表达式,高级的运算符已经处理成数字和基本运算符
// 表达式求值
expr, err := govaluate.NewEvaluableExpression(line)
if err != nil || expr == nil {
if strings.Contains(err.Error(), "transition") {
entry.Text = fmt.Sprint("transition error")
}
entry.Text = fmt.Sprint("error:wrong expression\n")
entry.Refresh()
return
}
result, err := expr.Evaluate(nil)
if err != nil || result == nil {
if strings.Contains(err.Error(), "transition") {
entry.Text = fmt.Sprint("transition error")
}
entry.Text = fmt.Sprint("error:unespected error please clear\n")
}
entry.Text += "=\n"
entry.Text += fmt.Sprint(result)
entry.Refresh()
}
}
递归求三角函数
// 求解表达式的第一个三角函数值
func Hsin(exp string) (float64, error) {
restr := ""
preindex := 0
lastindex := 0
pkcnt := 0
lkcnt := 0
begin := false
sin := false
tan := false
rjudge := false
if strings.Contains(exp, "sin") || strings.Contains(exp, "cos") ||
strings.Contains(exp, "tan") {
for index, v := range exp {
ch := string(v)
if begin && ch == "(" {
pkcnt++
} else if begin && ch == ")" {
lkcnt++
if lkcnt == pkcnt { //括号匹配
lastindex = index
restr = exp[preindex+3 : lastindex]
break
}
}
if ch == "r" {
rjudge = true
}
if (ch == "i" || ch == "o" || ch == "a") && !begin {
preindex = index
log.Println("--------------")
begin = true
}
if ch == "s" && !begin {
sin = true
} else if ch == "t" && !begin {
tan = true
}
}
if pkcnt != lkcnt {
log.Println("error expresiion")
return 0, errors.New("error expresiion")
}
exp = restr
} else {
expr, err := govaluate.NewEvaluableExpression(exp)
if err != nil || expr == nil {
log.Println("expression:", exp)
log.Println("error expresiion1")
return 0, err
}
result, err := expr.Evaluate(nil)
if err != nil {
log.Println("error expresiion2")
return 0, err
}
log.Println("output:", result.(float64))
return result.(float64), nil
}
log.Println("exp:", exp)
result, err := Hsin(exp)
if err != nil {
return 0, err
}
if sin {
log.Println("sin------:", result)
if rjudge {
if !judgeValue(result) {
return 0, errors.New("error:value error")
}
return math.Asin(result) * 180 / math.Pi, nil
}
return math.Sin(result * math.Pi / 180), nil
} else if tan {
log.Println("tan------:", result)
if rjudge {
return math.Atan(result) * 180 / math.Pi, nil
}
return math.Tan(result * math.Pi / 180), nil
}
if rjudge {
if !judgeValue(result) {
return 0, errors.New("error:value error")
}
return math.Acos(result) * 180 / math.Pi, nil
}
log.Println("cos------:", result)
return math.Cos(result * math.Pi / 180), nil
}
// 获得第一个cos或者sin表达式
func TrimTec(exp string) (preindex, lastindex int, res string, err error) {
begin := false
pkcnt := 0
lkcnt := 0
rjudge := false
for index, v := range exp {
if string(v) == "r" && !begin {
rjudge = true
}
if (string(v) == "s" || string(v) == "c" || string(v) == "t") && !begin {
preindex = index
begin = true
}
if begin {
if string(v) == "(" {
pkcnt++
} else if string(v) == ")" {
lkcnt++
if lkcnt == pkcnt {
lastindex = index
log.Println("preindex:", preindex)
log.Println("lastindex:", lastindex)
if rjudge {
res = exp[preindex-1 : lastindex+1]
return
}
res = exp[preindex : lastindex+1]
return
}
}
}
}
err = errors.New("error:expression error")
return
}
导入所需的库:
fyne:GUI库,用于创建GUI窗口,编排GUI布局, 自定义图形化界面,内嵌处理函数
math:提供数学计算方法 和 数学常量, 如: Π
创建计算器窗口:
创建一个fyne窗口,设置logo,设置显示位置,窗口大小。
创建显示输入和输出的文本框:
创建一个Entry(文本框)部件的对象,用于显示输入输出。
可以使用entry.SetText("内容")来设置文本框中的内容,或者直接对entry.Text进行操作每次操作完后需要entry.Refresh刷新,用户才能实时的在文本框中获取最新的内容
设置文本对齐方式、边框大小等属性。
响应函数:
input :input函数用于输入用户在按钮输入的内容如数字,运算符, 支持过滤连续输入运算符
equls:equals函数用于处理表达式并返回结果,使用堆栈,递归以及调用库的方式实现表达式的计算,这个方法中不仅计算的细节很多,处理错误的细节也要注意
计算错误时,返回error:错误内容,用户只需要再次输入新的内容则会清空文本框并开启新的输入,或者直接点击清空按钮 c
sign:sign函数用于将传入的数进行正负切换
back:back函数用于 删除表达式最后一个字符
其他内置函数:
Hsin 获得第一个cos或者sin表达式TrimTec 获得第一个cos或者sin表达式
judgeValue 判断反三角函数输入值是否在定义域内
文本框和按钮布局顺序:
包括科学函数按钮、操作符按钮、数字按钮、等号等。
使用循环创建按钮,并为每个按钮绑定相应的事件处理函数。
最后启动窗口即可
优化方式:使用递归识别并计算表达式中的三角函数值,降低时间复杂度,代码可见3. 核心代码中的Hsin函数
覆盖率测试结果(这边仅测试主要功能函数,另外的百分之二十九左右为程序的启动函数,例如GUI布局设置,窗口创建之类的代码这边不包含进去,覆盖率实现基本全覆盖)
覆盖率测试代码
func TestCover(t *testing.T) {
a := app.New()
_ = a.NewWindow("Calculator")
entry = widget.NewEntry()
entry.MultiLine = true
entry.Resize(fyne.NewSize(150, 150))
funcs := equals()
// 表达式测试 各种运算符检测 结果检测
entry.Text = "1+2+3+2^3+rsin(sin(30))+rtan(tan(30))+rcos(cos(60))"
t.Log(entry.Text)
funcs()
entry.Text = "error:xxx"
funcs()
entry.Text = "Inf"
funcs()
entry.Text = "rsin(10)"
funcs()
entry.Text = "rcos(10)"
funcs()
// 重复运算符过滤
ifunc := input("+")
ifunc()
ifunc()
// 正负反转
entry.Text = "3"
sfunc := sign()
sfunc()
entry.Text = "3.3"
sfunc()
// 退格测试
entry.Text = "8^9999"
bfunc := back()
bfunc()
funcs()
entry.Text = "\n"
bfunc()
entry.Text = ""
bfunc()
t.Log(entry.Text)
}
覆盖率优化经验:增加测试用例,尽可能地将代码的错误都跑过一遍,主要还是看编程人对自身代码的熟悉程度,需要了解每个异常发生原因以及每个分支的进入条件,编写相应的测试用例
单元测试代码
func TestCalculateSC(t *testing.T) {
// 1.判断表达式是否有三角函数
// 2.有则放入Hsin求得第一个函数的值
// 3.然后将值替换原本的三角函数
// 4.回到1
// 5.没有三角函数了在将整个表达式求解
exp := "1+sin(sin(10+110))+rsin(0.5)"
for {
if strings.Contains(exp, "sin") || strings.Contains(exp, "cos") {
res, err := Hsin(exp)
if err != nil {
t.Log(err)
t.Fail()
}
_, _, trim_exp, err := TrimTec(exp)
if err != nil {
t.Log(err)
t.Fail()
}
exp = strings.Replace(exp, trim_exp, fmt.Sprint(float32(res)), 1)
t.Log("change", exp)
} else {
break
}
}
}
func TestEvaluate(t *testing.T) {
expr, err := govaluate.NewEvaluableExpression("1+3%4-1/2+2^3")
if err != nil || expr == nil {
fmt.Println("err:", err)
t.Log("error")
t.Fail()
}
result, err := expr.Evaluate(nil)
t.Log("res:", result)
}
单元测试结果
随着程序的开发,有些细节的错误没有处理会导致程序直接崩溃,闪退
如: 表达式输入错误,返回的值无法表示时返回Inf,输入的值不在数学函数的定义域之内,用户和程序显示内容不同步等错误
大部分的错误只需要处理好临界条件即可,在golang 中函数返回错误,然后在函数返回后判断返回的错误是否为空即可进入分支处理,同时有些库函数并不返回错误,因此还是需要好好处理临界的条件判断,如输入值是否在定义域内,我们需要在计算前判断一下输入值的大小,如果不满足则直接返回错误不进行后续的操作
再比如:返回的值无法表示的时候返回INF ,程序在判断用户输入时,需要判断表达式内是否含有INF字符串,存在则返回表达式错误
心路历程:先想到的是GUI编程,之前只开发过Web,并没有windows GUI相关经验,然后去百度了解,搜索相关的资料,GUI的用法之类的,然后想到的表达式的处理,这是一个相对比较复杂的过程,我通过自己实现内部函数递归算法和调用库函数的方式实现了表达式的计算,这里面有很多细节需要考虑,不过我认为在这门课中本次任务的目标还是对GUI的使用,毕竟换个环境或者题目这些细节可能又需要重新去考虑
用户界面使用优化:在一开始实现的过程中,总是会遇到感觉很别扭的地方,比如闪退,返回错误显示在文本框还要再次点击清空才能再次输入表达式,我通过自身体验去优化这些地方,使得用户不会出现闪退,以及能够在发生错误后只需点击任一按钮即可清空并开始新的表达式输入
学习收获:收获了关于GUI库:fyne的使用、字符串处理,计算表达式处理,单元测试,和GUI编程经验
总结:了解了GUI编程的相关知识,锻炼了GUI编程能力,