WordCount项目相关要求
Github项目地址:https://github.com/wyf973733114/WordCount
1. 题目描述
Word Count
1. 实现一个简单而完整的软件工具(源程序特征统计程序)。
2. 进行单元测试、回归测试、效能测试,在实现上述程序的过程中使用相关的工具。
3. 进行个人软件过程(PSP)的实践,逐步记录自己在每个软件工程环节花费的时间。
2. WC 项目要求
wc.exe 是一个常见的工具,它能统计文本文件的字符数、单词数和行数。这个项目要求写一个命令行程序,模仿已有wc.exe 的功能,并加以扩充,给出某程序设计语言源文件的字符数、单词数和行数。
实现一个统计程序,它能正确统计程序文件中的字符数、单词数、行数,以及还具备其他扩展功能,并能够快速地处理多个文件。
具体功能要求:
程序处理用户需求的模式为:
wc.exe [parameter] [file_name]
基本功能列表:
wc.exe -c file.c //返回文件 file.c 的字符数 -- 已实现
wc.exe -w file.c //返回文件 file.c 的词的数目 -- 已实现
wc.exe -l file.c //返回文件 file.c 的行数 -- 已实现
扩展功能:
-s 递归处理目录下符合条件的文件。 -- 已实现
-a 返回更复杂的数据(代码行 / 空行 / 注释行)。-- 已实现
空行:本行全部是空格或格式控制字符,如果包括代码,则只有不超过一个可显示的字符,例如“{”。
代码行:本行包括多于一个字符的代码。
注释行:本行不是代码行,并且本行包括注释。一个有趣的例子是有些程序员会在单字符后面加注释:
} //注释
在这种情况下,这一行属于注释行。
[file_name]: 文件或目录名,可以处理一般通配符。
高级功能: -- 未实现
-x 参数。这个参数单独使用。如果命令行有这个参数,则程序会显示图形界面,用户可以通过界面选取单个文件,程序就会显示文件的字符数、行数等全部统计信息。
需求举例:
wc.exe -s -a *.c
返回当前目录及子目录中所有*.c 文件的代码行数、空行数、注释行数。
3. 解题思路
- 一开始想实现成终端调用的模式,所以查了一些 Swift 和 Shell 的混编资料,最后由于对 Shell 不太熟悉就换成 纯Swift 的可执行文件模式
- 考虑 -s 和 -a、-c、-l、-w的操作对象不同——前者是文件夹,后者是文件,因此将指令分成一级指令(对文件操作)和二级指令(对文件夹操作)
- 如果文件的存放路径里有wc.exe或者-s字样,不应该触发相应的方法。因此不能直接判断输入的字符串是否包含-s等关键字样
- 考虑到以后的指令内容可能进行更改或者拓展,所以实现的时候不是简单地将数据写死
4. 遇到的困难及解决方法
- 困难描述
- Swift的API经历过一次很大的革新,网上的部分资料并不能作为参考
- 做过哪些尝试
- 查找官方文档,测试,理解
- 是否解决
- 是
- 有何收获
- 所有的技术都在革新,有些方法跟不上时代更替,所以查阅资料的时候要留意它的书写日期
5.设计实现过程
- WC+String:自定义的字符串拓展方法(正则匹配、删去空格、删去特殊字符)
- WordCount:对单个文件进行读取,计算代码行数、空行数、注释行数、字符数和单词数 ,实现一级指令 —— -a、-c、-l、-w
- WordCountManager:动态创建一个或多个WorCount对象(与需要打开的文件数目对应),在此基础上实现二级指令 —— -s
- main:创建WordCountManager对象,进行二级指令操作和一级指令操作
- main函数代码:
import Foundation let manager = WordCountManager() // 二级指令操作 manager.operatingSecond() // 一级指令操作 manager.operatingFirst()
- main函数代码:
6.关键代码or设计说明
- WordCount —— 操作计算的实现、属性对外只可读:
/// 文件信息读取类 class WordCount { /// 属性对外只可读,防外部进行更改 private(set) var message: String private(set) var spaceLineCount = 0 // 空行数 private(set) var codeLineCount = 0 // 代码行数 private(set) var noteLineCount = 0 // 注释行数 private(set) var path: String = "" // 路径 /// 文件内容的行数 (只读) var lineCount: Int { get { var count = 0 message.enumerateLines { _,_ in count += 1 // 计数 } return count } } /// 文件单词数 var wordCount: Int { get { let chararacterSet = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters) // 根据 标点符号、空格、换行 进行单词计算 let components = message.components(separatedBy: chararacterSet) let words = components.filter { !$0.isEmpty } // 滤去空白的单词 return words.count } } /// 文件字符数,包括换行符和j空格符等 var charCount: Int { get { return message.count } } /// 可失败初始化器 /// - Parameter filePath: 文件路径 init?(_ filePath: String?) { var success = false (success,message) = WordCount.readFile(filePath) if success { path = filePath! }else { // 初始化失败 print(message) return nil } } /// 类方法(读取文件),返回(Bool, String)。Bool标志文件读取是否成功,成功时String中为读取的数据,失败时String中为失败的原因, /// - Parameter filePath: 文件路径 static func readFile(_ filePath: String?) -> (Bool, String) { // 文件管理者 let manager = FileManager.default guard let data = manager.contents(atPath: filePath ?? "") else { return (false,"找不到文件") // 根据传入参数找不到内容 }
guard let readString = String(data: data, encoding: String.Encoding.utf8) else { return (false,"数据解析失败") // 数据解析编码格式错误 } return (true, readString) } func dealCode() { //只执行一次 var noteFlag = false message.enumerateLines { (line, false) in self.lineType(line, noteFlag: ¬eFlag) } }
/// 传入一行字符,判断其类型(对应的属性计数将会增加) private func lineType(_ line: String, noteFlag: inout Bool) { // 正则去空格和特殊字符(预处理)、后续需求更改特殊字符的集合时可以在这里修改 let code = line.pregReplace(pattern: "[{|}| |(|)]", with: "") // 1. 先判断是不是注释 if self.isNote(code, noteFlag: ¬eFlag) { return } // 2. 再判断是不是空行 (字符串已经过预处理,只含空格和特殊字符的被解释为空行) if code.count == 0 { //代码中除空格字符外,其他字符数量大于1,为代码行 spaceLineCount += 1 return } // 3. 剩下的是代码行 codeLineCount += 1 } /// 判断该行是否是注释 /// - Parameters: /// - code: 需要判断的哪一行字符串 /// - noteFlag: 判断是否属于注释块中(/* 这中间可能会换行,然后写代码,此时将其中的代码解释为注释 */) private func isNote(_ code: String, noteFlag: inout Bool) -> Bool{ var increased = false // 标志是否是注释行 if code.contains("/*") { noteFlag = true } if noteFlag || code.hasPrefix("//") { self.noteLineCount += 1 increased = true } if code.contains("*/") { noteFlag = false } return increased } } - WordCountManager —— 指令的拓展性:
// // WordCountManager.swift // Word_Count // // Created by 峰 on 2020/3/17. // Copyright © 2020 峰. All rights reserved. // import Foundation class WordCountManager { var wordCounts: [WordCount] = [] var path: String = "" // 使用数组便于指令拓展 // 一级指令数组(对单个文件),先后次序会影响操作的优先级(左边最高) let parametersFirst = ["-a", "-c", "-w", "-l"] // 二级指令数组(对于文件夹),暂时只有一个二级指令 let parametersSecond = ["-s"] // 要进行的一级指令操作数组 var operationsFirst:[String] = [] // 要进行的二级指令操作数组 var operationsSecond:[String] = [] init() { ///输入指令进行构造 while wordCounts.count == 0 { guard let input = readLine() else { // 测试环境下会触发,真实运行环境下不会触发 debugPrint("输入不可为空") continue } // 1. 去空格形成指令 var command = input.removeAllSapce // 2. 验证指令命令 - "wc.exe" guard command.verify("wc.exe") else { print("指令错误") continue } // 2.1. 验证二级指令 for item in parametersSecond { if command.verify(item){ operationsSecond.append(item) } } // 2.2. 验证一级指令 for item in parametersFirst { if command.verify(item){ operationsFirst.append(item) } } // 2.3 没有输入指令 if operationsFirst.count + operationsSecond.count == 0 { print("没有输入的指令") continue } // 3. 存储剩下的文件路径 path = command if let _ = WordCount(path){ // 路径有效则停止构建 break } } } /// 根据二级指令数组对文件夹进行操作(这样写是为了方便拓展) func operatingSecond() { for item in operationsSecond { switch item { case "-s": //文件管理 let manager = FileManager.default // 文件名 let fileName = (path as NSString).lastPathComponent // 路径 let folder = (path as NSString).deletingLastPathComponent guard let subPaths = manager.subpaths(atPath: folder) else { print("找不到子文件或子文件夹") return } for subPath in subPaths { if (subPath as NSString).lastPathComponent == fileName, let wordCount = WordCount(folder + "/" + subPath) { wordCounts.append(wordCount) } } break default: break } } } /// 根据一级指令数组对单个文件进行操作 func operatingFirst() { // 若二级指令为空,传递文件路径进行读取、计算 if operationsSecond.count == 0, let wordCount = WordCount(path){ wordCounts.append(wordCount) } for wordCount in wordCounts { print(wordCount.path) // 如果进行了二级指令,将会对多个文件进行相同的操作 for item in operationsFirst { switch item { case "-a": // 进行代码模式处理 wordCount.dealCode() print("文件空行数为\(wordCount.spaceLineCount)") print("文件代码行数为\(wordCount.codeLineCount)") print("文件注释行数为\(wordCount.noteLineCount)") break case "-c": print("文件字符数为\(wordCount.charCount)") break case "-w": print("文件单词数为\(wordCount.wordCount)") break case "-l": print("文件行数为:\(wordCount.lineCount)") break default: print("指令未实现") break } } } } }
8.测试
- 错误处理 (1. 未输入指令wc.exe、2. 未输入具体操作、3. 输入文件不正确)
- 测试:
-
- 测试:
- 基本指令单独使用(-a、-c、-l、-w、-s)
- 测试: -a、-s
- 被测试文件的内容(0行空行,2行代码,6行注释)
- 测试结果 :
- 被测试文件的内容(0行空行,2行代码,6行注释)
- 测试:-c、-l、-w
- 被测试文件的内容(1行、3个词、9个字符(包括空格字符,如果不希望空格算字符也可在读取后用正则将空格删去))
- 测试结果:
- 被测试文件的内容(1行、3个词、9个字符(包括空格字符,如果不希望空格算字符也可在读取后用正则将空格删去))
- 测试: -a、-s
- 基本指令的组合使用(可任意组合,例如:-s -l、-s -w...)这里只演示两种
- (这里的指令组合暂时需要按指令优先级来 -s > -a > -c > -w > -l ,优先级高的要在前面)
附表:
PSP
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
Planning | 计划 | 15 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 15 | 20 |
Development | 开发 | 4*24*60(4天) | 3*24*60(3天) |
· Analysis | · 需求分析 (包括学习新技术) | 2*60 | 6*60 |
· Design Spec | · 生成设计文档 | 4*60 | 3*60 |
· Design Review | · 设计复审 (和同事审核设计文档) | 3*60 | 2*60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 5*60 | 6*60 |
· Design | · 具体设计 | 7*60 | 7*60 |
· Coding | · 具体编码 | 4*60 | 5*60 |
· Code Review | · 代码复审 | 2*60 | 3*60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 2*60 | 5*60 |
Reporting | 报告 | 3*60 | 3*60 |
· Test Report | · 测试报告 | 2*60 | 2*60 |
· Size Measurement | · 计算工作量 | 15 | 30 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 60 |
合计 | 5*24*60(5天) | 3*24*60(3天) |
学习进度条(每周追加)
第N周 | 新增代码(行) | 累计代码(行) | 本周学习耗时(小时) | 累计学习耗时(小时) | 重要成长 |
---|---|---|---|---|---|
1 | 127+43+125+4 = 296 | 296 | 8 | 8 | 进行错误处理、方便需求拓展 |
|