本文首發於我的個人博客QIMING.INFO,轉載請帶上鍊接及署名。
本文是ThoughtWorks校園招聘的一道作業題,要求做一個小型文本預處理器,題目看似簡單,實際做起來還是挺有挑戰性的。現在早已經過了Thoughtworks的作業提交時間,所以將此文放出來,如Thoughtworks認爲侵權了,可聯繫本人,本人將刪除文章。
1 問題說明
本次作業的題目是一個小型文本預處理器,輸入文本內容及指定寬度以後,對文本進行預處理以便後續進行固定寬度的排版。其中定義瞭如下的概念:
- 空白字符(white space):指空格 ’ '。
- 文本字符(character):指大寫或者小寫的英文字母。
- 節(segment):指一串(大於或者等於一個)連續的空白字符或者文本字符。
當給定寬度後,大於這個寬度的字符會被折行。而折行不會顯示任何連字符(例如 “-”),也無需對 空白字符 進行額外處理。作業要求編寫一個函數,該函數的輸入爲兩個參數:
- 需要處理的文本
- 排版寬度。
該函數的返回值爲預處理後的文本。預處理後的文本爲每一節,及其所在的行號。中間以分號隔開。若一個節跨越了多行,則行號用逗號隔開,並從小到大進行排列。例如,假設輸入爲:
The main theme of education in engineering school is learning to teach yourself,排版寬度指定爲 30,
則輸出爲:
The(1); (1);main(1); (1);theme(1); (1);of(1); (1);education(1); (1);in(1);(2);engineering(2); (2);school(2); (2);is(2); (2);learning(2,3); (3);to(3);(3);teach(3); (3);yourself(3);
2 構建代碼
2.1 代碼思想
要給每一節都標上其所在的行數,本人的思想是:
- 首先將所給文本分節;
- 然後需知道每一節的第一個和最後一個字符所在原文本中的位置;
- 根據排版寬度求出每一節第一個字符和最後一個字符在排版後的文本中的所在行;
- 根據每一節的首尾字符所在行得到此節所在的所有行;
- 將每個節的內容及其所在的所有行依次按要求輸出。
2.2 將文本分節
2.2.1 計算節數
首先先得到一共有多少個節,在這裏使用正則表達式將文本以任意個連續空格分節:
String[] strArr = text.split("\\s+");
可得到有多少個字母字符節,再通過判斷原始文本首尾是否爲空白字符得到節數segmentNum。
2.2.2 獲取每一節的數據並存放
每一節的數據包括它的內容和尾字符在原始文本中的位置。在這裏我定義了一個String[]類型的segmentArray用來存放每一個節的內容;定義了一個int[]類型的segmentLastCharNo用來記錄每一節的尾字符在原始文本中的位置;定義segmentNo用來指示當前爲第幾個節。
遍歷原始文本,若當前字符與下一個字符相比由 空白字符 變爲 字母字符 或由 字母字符 變爲 空白字符 時,認爲當前字符爲一個節的尾字符,下一個字符則爲下一個節的開始,代碼如下:
for (int i = 0; i < text.length() - 1; i++) {
char c1 = text.charAt(i);
char c2 = text.charAt(i + 1);
if ((c1 == ' ' && c2 != ' ') || (c1 != ' ' && c2 == ' ')) {
segmentArray[segmentNo] += c1;
segmentLastCharNo[segmentNo] = i;
segmentNo++;
} else {
segmentArray[segmentNo] += c1;
}
if (i == text.length() - 2) {
segmentArray[segmentNo] += c2;
segmentLastCharNo[segmentNo] = i + 1;
}
}
2.3 求每一節的首尾字符在原文本的位置
當知道每一節的內容及尾字符所在位置後,由內容長度可得該節首字符所在位置(用segmentFirstCharNo表示):
int segmentFirstCharNo = segmentLastCharNo[i] - segmentArray[i].length() + 1;
2.4 求每一節的首尾字符在排版後的文本中的行號
用原文本中字符的位置除以排版寬度並向下取整即爲此字符在排版後文本中所在的行號。用此方法便可得到每一節的首尾字符的行號。
2.5 求每一節的行號
因爲有的節可能會不跨行或跨多行,即節所在的行號可能爲一到多個,因此爲每一個節定義一個存儲行號的int型數組segmentlines,從首字符所在行遍歷至尾字符所在行,將行號填入segmentlines中,即可得到此節所有的行號。
2.6 輸出結果
按上文所說,現在已經得到了存放所有節的數組segmentArray及每個節的所有行號,那麼遍歷segmentArray的元素並將其所在行號拼接成一個字符串,最後輸出此字符串即可。
3 運行代碼
上文實現了TextProcessor類,爲調用此類,另編寫文件StartTextProcessor.java文件,在此文件中通過main函數調用TextProcessor類。
將本項目文件導入編譯器Eclipse,java版本爲1.8,右鍵src文件夾下的StartTextProcessor.java文件,選擇Run As –> java application
,即可運行此代碼。然後在控制檯輸入所要排版的文本與排版寬度,即可得到結果。
4 正確性驗證
爲了驗證本次所編寫代碼的正確性,本人使用了單元測試這一方法。本次單元測試使用編譯器Eclipse提供的插件EclEmma進行。EclEmma是一個免費的用來測試Java代碼覆蓋率的Eclipse插件,可以用EclEmma直接在Eclipse工作區中測試Java程序,分析代碼覆蓋率,並且在Java編輯器中高亮顯示源文件的代碼覆蓋情況。
4.1 安裝插件
在Eclipse中點擊菜單help –> Eclipse MarketPlace
,在對話框中搜索EclEmma
後點擊Install
,按照提示下一步即可完成安裝。安裝完成後,出現圖標如下圖所示即爲插件安裝成功。
4.2 編寫測試用例
在編寫測試用例時,首先考慮異常輸入,其次是正常輸入能否返回正確的結果。爲此,編寫測試用例目錄如下:
- 當文本中出現 空白字符 或 文本字符 以外的字符時應輸出相應錯誤信息;
- 當輸入寬度小於10時或大於80時應輸出相應錯誤信息;
- 當文本中有連續空格時仍能得到正確結果;
- 當文本以空白字符開頭或結尾時仍能得到正確結果;
- 當文本中某一節可跨多行時仍能得到正確結果,即羅列出此節所在的所有行號;
- 當文本爲純文本字符或純空白字符時仍能得到正確結果;
4.3 測試結果
根據上文測試用例目錄編寫了14條測試用例,寫在TextProcessorTest.java中,通過 EclEmma 運行後顯示14條測試用例全部通過且代碼覆蓋率100%。
5 總結
首先開心的是順利完成此次作業,並且通過了所有的測試用例。在完成此作業中又複習了Java的基礎知識,以及單元測試的基礎操作。
其次是對完成本次作業的過程的一些思考:在軟件開發中,分析問題、拆分問題,無論所做的是大項目還是小項目,都對理清思路很有幫助。