【补充】第一次个人项目出现的bug

新程序包下载(密码:4kp6)

>>>>>直接上代码,问题出在随机分数的生成上,确实出现了一些非常鱼唇的错误,不过已经提交了就没办法了,在这里发出来仅供参考吧:

修改前:

 1 public static Fraction nextFrac(int width, Random seed)  2  {  3             Random rd = seed;  4             int down1 = (int)(rd.NextDouble() * (width - 1) + 1);//随机生成低于width的分母  5             int quo1 = (int)(rd.NextDouble() * (width - 2) + 1);//随机生成低于width-1的带分数整数部分  6             long up1 = (long)(rd.NextDouble() * (width - 1) + 1);//随机生成低于width的分子 8             up1 += quo1*down1;  9             return new Fraction(up1, down1); 10         }

修改后:

1 public static Fraction nextFrac(int width, Random seed) 2  { 3             Random rd = seed; 4             int down1 = (int)(rd.NextDouble() * (width - 1) + 1);//随机生成低于width的分母 5             long up1 = (long)(rd.NextDouble() * (width *down1-1) + 1);//随机生成低于分母*width的分子7             return new Fraction(up1, down1); 8         }

对于随机数元素的生成范围,第一次要求作业中有如下规定:

使用 -r 参数控制题目中数值(自然数、真分数和真分数分母)的范围,例如

 

Myapp.exe -r 10

 

将生成10以内(不包括10)的四则运算题目

对于我之前的处理方法,即 分数 = 整数[1,width-1)+ 分子[1,width)/分母[1,width)

其值域实际上为[1,2*width-2),而且导致生成的分数至少大于1,实际上少了很多种情况...............................................

特别的,当width取2时,由于width-2=0,随机数失去意义,这样取值范围就固定变成了[1,2],这就与老师在课程要求上的说明冲突了;

同理可得:

1 int down1 = (int)(rd.NextDouble() * (width - 1) + 1); 2 long up1 = (long)(rd.NextDouble() * (width *width-1) + 1);

这样的处理方式也是存在问题的,值域实际上变成了[1,width^2)

值得说明的一点是,仍然保留了random*(width-1)+1这一设定,以规避分母或分子为0的情况,主要是为了减少无意义操作数(0)的出现频率和规避一些不合法情况

 

程序设计过程中出现的其他Bug汇总:

 

1.  当范围较大,读取表达式计算结果时,容易出现分子超int范围的情况,笔者在之前的博客中已经提到过,为了支持足够的范围,需要采用Long整型存储分子分母,因为计算过程中可能出现超int范围的分子分母结果,因此相应的,读取答案的时候,我们也要采用Int64.Parse来录入结果,并用相应的Long型变量存储,而非常用的Int32.Parse(感谢乾麻提醒)

 

2.  求最大公约数,也即约分的方法一定要考虑传入参数含0的情况,以及,需要考虑,对于公约数为0的情况,约分时应当怎样处理,是否需要报异常等

 

3.  对于计算模块,自栈顶向下逐步运算时(从中缀表达式的角度来看就是从右往左算),一定要考虑前一个操作符是否为“-”号,对于没有添加括号的情况,盲目按序运算会导致错误 如 (3-2-1),当计算完一个括号内的值后,需要取前一个运算符判断是否为×或÷,因为在这两个符号后面跟括号的情况下,其运算会被延后,一旦其后所跟的括号内式子得出结果,该运算应当被首先考虑。

需要注意的是,如果不对每个生成的子表达式加上括号,其实际运算优先顺序可能与表达式建立过程不符。 如e=>e1÷e2=>e3÷e4÷e2=>3÷2÷1 (减法同)

非常建议生成一些不加括号,运算符也多于3的中缀表达式,对于检查计算模块的正确性很有帮助

例如:6×5+8÷(3-2) ; 

213'3/5 - 65×(3+5-(2)-4)

为了批量检查计算模块的正确性,可以使用excel,这里分享一下具体的用法:

S1:

我们需要对程序做一点点扩展,使其通过命令行的控制,能够输出同时被excel和计算模块解析的式子

也即:将生成带分数全部转为假分数形式,并括起来(避免除法过程的二义性)

笔者做法如下:

 

 1 class user {
 2     ...
 3         public static bool mixed = false;
 4     ...
 5 class program {
 6     ...
 7         if (args[i] == "-m")
 8                 {
 9                     user.mixed = true;//命令行参数控制是否以假分数形式输出
10                 }
11     ...
12         ExerciseWriter.Write((j + 1).ToString() + ". \0\t" +         expStr.getExpStr() + "\r\n");
13          AnswerWriter.Write((j + 1).ToString() + ". \0\t"  + answer1 + "\r\n");//输出制表符,这样直接粘到Excel里会变成两列,表达式和序号就直接分离了
14                     
15 class Fraction { public String express()
16         ...
17             long quo = up / down;
18             long res = up % down;
19             String s = "";
20             if (user.mixed) return (s = "(" + up + "/" + down + ")");
21             else ...//正常输出 
22         ...        

 

需要注意的是,因为有制表符的存在,所以读取式子用replace去除空白符应当用“\\s+”匹配而不是空格“ ”

S2

将式子粘到excel里,会发现自动分成两列了(比如A、B两列吧),然后我们用excel自带的查找替换工具,将B列表达式出现的×÷替换为*/;

在C列输入 =”=“&B1,并拖动格式手柄应用到其余行(如果数字太大,就拖滚动条到表格底部,用shift选中底部到头部的所有单元格,按ctrl+d即可)

再按ctrl+c复制,粘贴选择左上角粘贴选项里的 ”粘贴数值——值“,使用查找替换工具,将”=“替换为”=“,所有表达式的值就悉数计算出来了;

S3

 

采用同样的办法把答案复制到D、E列,并用同样的办法求其值(答案实际上多为分数,可以看成一个简单表达式),用S2的办法求其值;

再在F列中计算C列,E列两列值之差并用sum求和,如sum为0,则基本可以确保程序的计算模块已经完全没有问题了

 

4.  在用Dictionary<String Answer ,List<String> usedCase>这一数据结构时发现索键求值的过程存在很多数据错误,经过仔细的检查,发现问题在于将List对象Add到Dictionary里的时候,直接传引用作为值放进去了,因为笔者的程序设计的时候,只有一个usedCase随表达式生成函数传入,clear,add,传出,那么无疑所有键的键值都是这一个List,它被修改的话相应的Dictionary里的所有键键值就会被修改,所以实际上作为value传入的时候是需要通过拷贝构造的,至于中间出现的数据混乱情况,可能是在修改这一模块时只修改了一部分所致吧。

 

5.  计算表达式时,为了避免栈空取值的情况,需要时刻关注栈的容量,但有一个简洁的办法可以改善性能,那就是预置栈底元素(比如#之类的作为标记),这样只需访问栈中的一个值而不是全部

 

6.  对于负分数的表示形式,应规范成仅分子为负或仅整数部分为负(读入78/-4,转为-20'1/2或-39/2),假分数转换成带分数的时候,因为分子由取余操作产生,其值可能为负,这种情况下,需要手动让分子加上分母的值,然后整数部分-1来简约形式,避免-5’-39/78这种情况的产生,恰恰因为我们的项目要求里对减法做了约定来规避负数的产生,这一点才不容易被发觉,试着生成一些负分数的String表示形式,或者让分子,分母,整数部分取0,看看会有怎样的结果

 

7.  养成好习惯,随手关门,随手close

Ps:如果输入文件路径无效试着转绝对地址吧,方法为System.IO.Path.GetFullPath(YourAddress),另外输出文件时需统一编码(再次感谢乾麻提醒),如:

StreamWriter GradeWrite = new StreamWriter(Grade, System.Text.Encoding.Unicode);

 

8.   通过Opnum和OpLim控制表达式的推导生成过程,显然,当Opnum = 0的时候应规避 e=>n的文法规则,当Opnum = OpLim时应规避二元式的生成,并在当前表达式已经被括号括起来的情况下,规避e=>(e)的文法规则来避免括号冗余,以避免增加计算模块负担

 

9.  一个比较常见的情况是生成表达式的时候,明明用了random却出现了大规模的重复情况,这是因为random生成随机数是基于当前系统时间的,由此生成一个伪随机序列,如果程序执行的效率高,同时又有多个random对象被新建用于生成随机数的话就会出现重复,对于这样的情况,最好的办法是全程只用一个random对象,让其作为随机种子传入即可

 

10.  暂时只注意到这么多,有新的会补充

【补充】第一次个人项目出现的bug_第1张图片

 

你可能感兴趣的:(【补充】第一次个人项目出现的bug)