OO_pre_9

OO_pre_9

Charles Lv7

OO_pre部分精品贴

Part 1

自动化测试

省流不看版

基于递归下降和形式化文法的随机数据生成 + 基于shell脚本的简易评测机。

一、基于递归下降和形式化文法的随机数据生成器

结合题目的形式化描述,我们可以容易得到以下文法:

符号[…]表示方括号内包含的为可选项

符号{…}表示花括号内包含的为可重复 0 次或多次的项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
消息 Message -> PersonMessage | GroupMessage
个人消息 PersonMessage -> Date '-' UserName '@' UserName ' ' ':' '"' MessageContent '"' ';'
日期 Date -> Year'/'Month'/'Day
年 Year -> 1-9999
月 Mon -> 0-99
日 Day -> 0-99
用户名 UserName -> {大小写英文字母 | 数字}
消息内容 MessageCount -> { 大小写英文字母 | 数字 | 空格 | '?' | '!' | ',' | '.' }
群聊消息 GroupMessage -> Date '-' UserName ':' '"' MessageContent ['@' UserName ' '] '"' ';'
指令 order -> qsend | qrecv | qdate
查询发送者 qsend -> 'qsend ' ['-v'] [param] '"' SenderName '"' ['-c' '"' cOrderContent '"']
查询接收者 qrecv -> 'qrecv ' ['-v'] [param] '"' RecvName '"' ['-c' '"' cOrderContent '"']
发送者名 SenderName -> {大小写英文字母 | 数字}
接收者名 RecvName -> {大小写英文字母 | 数字}
c参数后字符串 cOrderContent -> { 大小写英文字母 | 数字 | 空格 | '?' | '!' | ',' | '.'|'@' }
参数 param -> '-ssq' | '-ssr' | '-pre' | '-pos'
查询日期 qdate -> 'qdate ' Date

基于此文法就可以利用递归下降法生成符合题目要求的随机数据,需要注意的特殊条件有以下几个:

  • GroupMessage@ userName 只出现一次
  • '-v'param的出现顺序可以调换以模拟非法指令

具体以生成 PersonMessage的过程为例:

1 在Message函数中以 50%的概率返回 personMessage

1
2
3
4
5
6
7
8
public static String Message() {
int rate = random.nextInt(2);
if (rate == 1) { //控制Message种类
return personMessage();
} else {
return groupMessage();
}
}

2 在personMessage函数中以文法格式返回正确的 personMessage,其中 randomInt函数生成一个[10,50)的正整数, Date函数返回正确的日期, UserName函数会返回程序最开始生成的 用户名池 中的一个用户名(这样做可以增加用户名重复的概率,提高测试强度),MessageContent函数下文讲解。

1
2
3
4
5
6
7
8
9
public static String personMessage() {
//同一个Message中userName可能相同也可能不同
int messageLength = randomInt(10, 50);
return Date() + '-' + UserName() + '@' + UserName() + ' '
+ ':' + '"' + (messageLength, false) + '"' + ';';
}
public static int randomInt(int min, int max) {
return random.nextInt(max) % (max - min + 1) + min;
}

3 MessageContent函数返回消息内容,length参数决定生成的消息长度,第二个参数可以决定消息内容中是否会有概率出现一次 @ userName

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static String MessageContent(int length, Boolean type) {
StringBuilder message = new StringBuilder();
String userName = "";
String special = " !?.,"; //特殊字符:空格、!、?、.、,
for(int i = 0; i < length; i++) {
int chatType = random.nextInt(4);
switch (chatType){
case 0:
message.append(random.nextInt(10));//数字
break;
case 1:
message.append((char) (random.nextInt(26) + 97));//小写字母
break;
case 2:
message.append((char) (random.nextInt(26) + 65));//大写字母
break;
case 3:
int rate = random.nextInt(5);
message.append(special.charAt(rate));
break;
}
}
if (type) {
userName = '@' + UserName() + ' ';
int indexOfUserName = random.nextInt(length); //随机插入messageContent
String output = "";
output += message.substring(0, indexOfUserName) + userName + message.substring(indexOfUserName);
return output;
} else {
return message.toString();
}
}

至此我们就成功生成了一条 PersonMessage,其他文法成分同理

二、导出 jar

为了方便后续使用脚本进行自动评测,我们需要和小伙伴各自导出自己程序的 jar包(相当于C语言编译出的可执行文件 .exe),方法非常简单,我只做简单概述,遇到困难可以私信助教

1 文件->项目结构

图片描述

2 工件->添加

图片描述

3 jar->具有依赖项的模块

图片描述

4 点击索引文件,选择程序入口文件(主类),我是 MainClass

图片描述

5 一路确定+应用,最后在导航栏找到构建,选择构建工件

图片描述

6 选择你的jar构建即可在 src同级的 out文件夹下的 artifacts文件夹下找到jar

图片描述

三、基于 shell脚本的自动评测机

这部分利用上面生成好的数据和导出的jar实现一个自动评测机,需要一定的 shell编程基础,但其实不是特别难。

用法:

  1. 组织文件结构如下:

图片描述

其中 test文件夹下存放测试数据,如testcase1.txt,而cjj.jar/ccy.jar是对拍程序的jar包,auto.sh就是执行脚本。

  1. 需要选一个基准对拍程序,比如我这里选择了 cjj.jar,那么脚本就会出现这样的语句
1
java -jar cjj.jar < input.txt > cjj.txt

之后我们在 names变量中添加其他人的jar包名即可

脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/bin/bash

names=("ccy") #names变量

for dir in $(ls ./) #results of the command
do
if [ -d $dir ]; then
for testcase in $(ls ./$dir) #results of the command
do
cp $dir/$testcase input.txt #cp from dir/testcase to input.txt
echo "$dir/$testcase" "begin"
echo "======= " cjj " ======"
java -jar cjj.jar < input.txt > cjj.txt
echo
for name in ${names[*]} #其他人的
do
echo "======= " $name " ======"
java -jar ${name}.jar < input.txt > ${name}.txt
echo
diff ${name}.txt cjj.txt > difference.txt #-y side-by-side -W在使用-y参数的时候指定栏宽
if [ $? -ne 0 ] ; then # $?返回上一条指令的结果
echo ${name} "wrong at" "$dir/$testcase"
exit 1
fi
done
echo "===================="
echo "$dir/$testcase" "end"
echo
echo
done
fi
done

Part 2

第六次作业指南

本次作业总体新增三大需求,下面对这三大需求分别分析并实现

需求1:处理指令异常问题

qsend/qrecv

这两条指令的异常情况相同,故可将二者归为一类,有如下两种可能:

  1. 不出现-v, 但出现-ssq/-ssr/-pre/-pos
  2. 出现-v且出现-ssq/-ssr/-pre/-pos,但是后者在前者之前

该判断较为简单,只需通过正则表达式分别对-v以及-ssq/-ssr/-pre/-pos进行匹配并获得二者出现的index(如果有的话)并进行比较

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static boolean isRightCmdUser(String content) {
// 该方法,如果是正确的指令,则返回true;如果是错误的指令,则输出题目要求信息后返回false
int indexFour;
int indexV;
Pattern pattern1 = Pattern.compile("-ssq|-ssr|-pre|-pos"); // 是否出现了四个指令
Pattern pattern2 = Pattern.compile("-v"); //是否出现-v
Matcher matcher = pattern1.matcher(content); // 先用pattern1对-ssq|-ssr|-pre|-pos进行匹配
if (matcher.find()) { // 匹配到-ssq|-ssr|-pre|-pos 再进行如下讨论,否则直接返回true
indexFour = matcher.start();
matcher = pattern2.matcher(content); // 匹配到-ssq|-ssr|-pre|-pos后再用pattern2匹配-v
if (matcher.find()) { //匹配到-v则比较二者的index大小是否满足要求
indexV = matcher.start();
if (indexFour < indexV) { //-v和-ssq|-ssr|-pre|-pos都存在但是-v在后面
System.out.println("Command Error!: Not Vague Query! \"" + content + "\"");
return false;
}
} else { //-v不存在但是-ssq存在
System.out.println("Command Error!: Not Vague Query! \"" + content + "\"");
return false;
}
}
return true; // 到此步,没返回false就返回true
}

qdate

该指令出现异常的可能情况比较繁杂,分类讨论的思路较多,但是每一个if-else都保证遵循严谨的逻辑关系,则该判断并没有太大的难度。下面只给出一些值得注意的点

1
2
3
4
1. day 与 month 的输入值可能是0,注意在`if(day <= 31/30/...)`的同时不要忘记条件`day > 0 `   
2. 什么是闰年: 满足如下条件之一,即为闰年(int year = Integer.parseInt(yearString);)
1. year % 4 == 0 && year % 100 != 0
2. year % 400 == 0

需求2:判断某个消息是否符合指令要求

qsend/qrecv

为了判断某则消息是否符合指令要求,我们首先需要知道同消息sender/receiver name的匹配原则(严格匹配,前缀匹配等等),在此我们可以定义此需求下的第一个方法。

Step1 : 判断匹配模式的Method —— judgeMatchType(String)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static int judgeMatchType(String instruction) {
//该方法传入一个参数instruction,表示指令内容
//其功能为返回指令的匹配类型(每个类型给予一个编号)
// 0 : 严格匹配 1 : 子串匹配 2 : 子序列匹配 3 : 前缀匹配 4 : 后缀匹配
int findModel;
Pattern pattern = Pattern.compile("-v");
Matcher matcher = pattern.matcher(temp);
if (matcher.find()) { // if (匹配到-v)
findModel = 1; // 先默认为子串匹配
pattern = Pattern.compile("-ssq|-ssr|-pre|-pos"); //正则,四者中任意一个均可被成功匹配
matcher = pattern.matcher(temp);
if (matcher.find()) { // if (匹配到-ssq|-ssr|-pre|-pos)
// 此处只列举需要修改findModel的情况(除-ssr外),若未找到,则无需对findModel修改,采用默认值1即可
switch (matcher.group()) { //根据具体的-ssq|-ssr|-pre|-pos对findModel进行修改
case "-ssq":
findModel = 2;
break;
case "-pre":
findModel = 3;
break;
case "-pos":
findModel = 4;
break;
default:
}
}
} else { // 未匹配到-ssq|-ssr|-pre|-pos,则为严格匹配
findModel = 0;
}
}

在判断过匹配模式后,我们需要对具体的匹配的模式进行实现。

Step2 : 判断字符串small是否与big匹配

是否匹配要针对不同的匹配模式来考虑。考虑到不同模式之间的重合性(small可能既是big的子串,又是big的前缀),我们可以定义多个方法来分别对每个模式进行判断(而不是只定义一个方法)来使得思路更加清晰。

findModel == 0 (严格匹配)

该匹配模式看small和big是否相同,直接使用small.equal(big)来判断即可,可以不定义方法

findModel == 1(子串匹配)

1
2
3
4
5
6
7
public static boolean isSonSerial(String big, String small) {
//small是big的子串则返回true,否则返回false
Pattern pattern = Pattern.compile(small.replace("?","[?]"));
//关于为什么要使用方法replace在末尾的"附加说明"中有解释
Matcher matcher = pattern.matcher(big);
return matcher.find(); // 通过正则,能在big中找到small即满足子串匹配
}

findModel == 2(子序列匹配)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static boolean isSonLine(String big, String small) {  
//small是big的子序列则返回true,否则返回false
char[] charBig = big.toCharArray();
char[] charSmall = small.toCharArray();
// 将两个String型变量均转换成字符数组类型,以便对进行逐位的操作
int pointSmall = 0; // 该int变量为一个指向“指针”
for (char c : charBig) {
if (c == charSmall[pointSmall]) {
pointSmall++;
if (pointSmall == small.length()) {
//pointSmall能加到与small的length相同的大小,则说明small中从前至后每位均依次在big的不同index处出现,说明small是big的子序列,并直接返回
return true;
}
}
}
return false; // 程序能执行到这一步,说明未能return true,则直接return false
}

findModel == 3(前缀匹配)

1
2
3
4
5
6
7
public static boolean isPre(String big, String small) {
//small是big的前缀则返回true,否则返回false
Pattern pattern = Pattern.compile(small.replace("?","[?]"));
Matcher matcher = pattern.matcher(big);
return matcher.find() && matcher.start() == 0; //此时的match为满足条件的第一个匹配,match.start()即为第一个成功匹配的起始index
// “small 是 big 的前缀” 等价于 “big中small子串第一次出现的位置的起始index == 0”
}

findModel == 4(后缀匹配)

1
2
3
4
5
6
7
8
9
10
11
12
13
public static boolean isPos(String big, String small) {
//small是big的后缀则返回true,否则返回false
Pattern pattern = Pattern.compile(small.replace("?","[?]"));
Matcher matcher = pattern.matcher(big);
// "small 是 big 的后缀" 等价于 "在big中,存在一个small子串,使得该子串的末index + 1 == big.length"
while (matcher.find()) {
if (matcher.end() == big.length()) {
//注意!!! matcher.end()方法返回匹配结果的末尾index + 1,故if条件的右边直接为length
return true;
}
}
return false;
}

qdate

对于qdate指令,在qdate指令合法的前提下本次作业的判断方法同上次作业判断,便不再赘述

需求3:实现关键字和谐功能

关键字和谐的功能在最后一步:输出正确答案时进行。这是三条指令所共有的需求,因此我们可以将其封装为一个方法供三条指令调用

1
2
3
4
5
6
7
public static void printAnswer(String instruction, String messageContent){
//该方法传入两个参数,instruction为指令内容,messageContent为消息内容
/*该方法实现功能:判断instruction是否要求屏蔽处理(指令是否含-c)——
如果不要求,则将messageContent原文输出
如果要求, 则将messageContent中的相关内容屏蔽后输出
*/
}

下面介绍该方法的具体实现,主要分为如下两个step

Step1 : 判断是否需要屏蔽(并获得屏蔽语句)

(通过String的一些方法完全可以实现相关功能,此处介绍正则版本)

1
2
3
4
5
6
7
8
9
Pattern pattern = Pattern.compile("-c +\"([@a-zA-Z0-9 ?,.!]+)\""); 
// 注意:此处是需要加上@的,在具体的消息中可能存在@他人的情况,@符号也要考虑被屏蔽
// -c后的空格后的+是为了防 止-c与后续字符串之间有多个空格出现的情况,如果不存在这种情况则可去掉+
Matcher matcher = pattern.matcher(messageContent);
if(matcher.find()) { // 找到-c命令,此时捕获组matcher.group(1)即为需要屏蔽掉的关键字符串内容
// 相关变量存入matcher.group(1) ...
} else { // 未找到-c,将消息内容原封不动输出
// ...
}

Step2 : 实现关键字屏蔽功能

(除下述方法外,群中大佬也提到了通过StringBuffer类来实现此功能等,此处只介绍一种思路) 直接给出如下代码,通过注释来展现思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
char[] tempContent = messageContent.toCharArray();
//String类的方法toCharArray()将其调用者转换为char[]类型,以便对每个index上的字符进行修改
/*和谐仅仅针对于一条messageContent中双引号出现的部分,也就是语句中的内容,且双引号
在一条指令中出现且只出现一对,故可做如下处理 */
int sentenceBegin = content.indexOf("\""); //返回字符串中第一次出现双引号的位置
Pattern pattern = Pattern.compile(sonString.replace("?","[?]"));
//sonString为需要屏蔽掉(替换成同长度的*)的字符串
Matcher matcher = pattern.matcher(messageContent);
while (matcher.find()) {
// 在需要进行屏蔽处理的语句中遍历每个子串出现的位置
// 如果扫到,就将字符数组对应位置置为*
if (matcher.start() >= sentenceBegin + 1) {//sentenceBegin+1表示引号内容的开始处
// 注意!!只有双引号内部的字符串需要被和谐,故需要该if判断来保证这一点
for (int i = matcher.start(); i <= matcher.end() - 1; ++i) {
tempContent[i] = '*';
//将需被和谐串的位置全部替换为*,最后直接将替换后的串输出即可
}
}
}
System.out.println(tempContent);

附加说明:正则表达式转义问题

关于异常PatternSyntaxException

报此异常大概是因为正则表达式的书写有了问题。
在本题中有可能是因为在使用Pattern.compile(string) (string是一个String量)时,string内部有 问号 ,而问号在正则表达式中是有特殊含义的(类似于关键字),所以我们需要对string中的问号进行转义,具体方法为: 将问号转换为[问号]即可,为此我们可以使用String类的方法replace(String string1,String string2)(该方法将调用者的string1子串全部替换为string2字符串)来将string中所有的问号替换成[问号](在上文代码中已经体现),具体例子如下:

1
2
Pattern pattern = Pattern.compile(string.replace("?","[?]"));
// 后续操作...
  • Title: OO_pre_9
  • Author: Charles
  • Created at : 2023-01-31 10:32:59
  • Updated at : 2023-06-17 14:13:59
  • Link: https://charles2530.github.io/2023/01/31/oo-pre-9/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments