寫在前面
每當提到正則表達式,身邊很多朋友的反應是這樣的:
“那玩意兒最好不要用,沒法維護!”,
“太複雜,不常用,學不來” ....
不可否認,正則的表達形式,讓人看起來的確有些難以理解,如果某天你接手維護一段代碼,當你滿懷好奇地打開代碼文件,發現裡面很多代碼長成這個樣子:
let funCallRegExp = /(^(\s+)?(((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\(((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?)(,((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?))*\)))(\s+)?(,(\s+)?(((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\(((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?)(,((\s+)?((?!(class|function))([a-zA-Z_][a-zA-Z0-9_]*))(\s+)?))*\)))(\s+)?)*)$/gm;
顯然,這樣的代碼是無法維護的。但是我可以從中揣測出來,它一定是做了一個比較複雜的驗證。其實對於這種使用正則做複雜校驗的情況,並不等同於洪水猛獸,也是有跡可循的,下面我就給出一種情況,並給出解決方案,希望可以拋磚引玉。
問題來了
現在有一種類似於函數調用的語法,這種調用大概是這樣的:
sum(param1, param2), avg(score)
下面列出了接近30多個case來描述對這個語法的各種限制:
describe("validate", () => { it("不同方法的調用用逗號分隔,逗號兩側可用空格分隔,也可不分隔", () => { expect(validate("sum(param1),avg(score)")).toBe(true); expect(validate("sum(param1) ,avg(score)")).toBe(true); expect(validate("sum(param1) , avg(score)")).toBe(true); expect(validate("sum(param1) avg(score)")).toBe(false); expect(validate("sum(param1)avg(score)")).toBe(false); }); it("整個語句開頭或末尾不能出現逗號或無關文字(空格除外)", () => { expect(validate(" sum(param1),avg(b)")).toBe(true); expect(validate(" sum(param1),avg(b) ")).toBe(true); expect(validate("sum(param1),avg(b) ")).toBe(true); expect(validate("a sum(test),avg(a)")).toBe(false); expect(validate(" sum(param1),avg(b)12")).toBe(false); expect(validate("sdf sum(param1),avg(b) sdf")).toBe(false); }); it("函數名&參數:字母或者下劃線打頭,可由字母數字下劃線組成", () => { expect(validate("sum(a)")).toBe(true); expect(validate("sum(a0)")).toBe(true); expect(validate("_sum(a0)")).toBe(true); expect(validate("_sum0(_a0)")).toBe(true); expect(validate("_sum0(_)")).toBe(true); expect(validate("_sum0(1)")).toBe(false); expect(validate("_sum0(1a)")).toBe(false); expect(validate("2aa(a)")).toBe(false); }); it("函數名&參數:不能包含保留字 class || function", () => { expect(validate("afunctiona(a,b)")).toBe(true); expect(validate("functiona(a,b)")).toBe(true); expect(validate("class(a), a(a,b)")).toBe(false); expect(validate("function(a,b)")).toBe(false); expect(validate("function(a,b)")).toBe(false); expect(validate("sum(a,function)")).toBe(false); expect(validate("sum(class,b)")).toBe(false); expect(validate("class1(a,b)")).toBe(true); expect(validate("function1(a,b)")).toBe(true); }); it("參數間可以存在空格", () => { expect(validate("sum(a ,b ,c)")).toBe(true); expect(validate("sum( a , b ,c)")).toBe(true); expect(validate("sum( a , b ,c) , avg(d,e , fff)")).toBe(true); }); it("函數調用可以不傳參數", () => { expect(validate("sum()")).toBe(true); expect(validate("sum( )")).toBe(true); }); });
解決方案
越是複雜的問題,越需要對這個問題進行拆解,將它轉換為一系列簡單的子問題,這些簡單的子問題,我們可以很容易的使用正則給出解決方案,然後將這些方案逐一組合起來,也就形成了最終方案。
在這裡,可以把我們需要驗證的內容拆成一下的部分:
0 . 空格 = 可以出現0個或多個空格
1 . 參數 = 由字母或下劃線開頭,字母數字或下劃線組成
2 . 無保留字參數 = 參數 && 不包含關鍵詞
3 . 可包含空格參數 = 無保留字參數 && 參數頭尾可以出現0個或多個空格
4 . 參數集 = 多個參數的組合,參數可以是 1 個或多個
5 . 參數塊 = 參數集 + 左右括號 ,可以沒有參數集,此時括號內為空,但可以出現0個或多個空格 eg . sum() or sum( )
6 . 函數名 = 無保留字參數
7 . 函數調用 = 函數名+參數塊
8 . 可包含空格的函數調用 = 函數調用 && 函數調用頭尾可以出現0個或多個空格
9 . 多函數調用 = 多個函數調用的組合,函數調用可出現1個或多個
10. 最終結果 = 多函數調用 && 頭尾不包含除空格外的其它內容
有了以上的的思路,驗證方案就已經呼之欲出了,貼出代碼:
function validate(inputStr) { // 0. 空格 = 可以出現0個或多個空格 const spaceOrEmpty = ` *`; // 匹配多個空格或空 // 1. 參數 = 由字母或下劃線開頭,字母數字或下劃線組成 const param = `[a-zA-Z_][a-zA-Z0-9_]*`; // 2. 無保留字參數 = 參數 && 不包含關鍵詞 const paramWithoutReservedWords = `(?!\\bclass\\b|\\bfunction\\b)(${param})`; // 3. 可包含空格參數 = 無保留字參數 && 參數頭尾可以出現0個或多個空格 const paramWithSpaceWithoutReservedWords = `${spaceOrEmpty}${paramWithoutReservedWords}${spaceOrEmpty}`; // 4. 參數集 = 多個參數的組合,參數可以是 1 個或多個 const params = `(${paramWithSpaceWithoutReservedWords}(,${paramWithSpaceWithoutReservedWords})*)`; // 5. 參數塊 = 參數集 + 左右括號 ,可以沒有參數集,此時括號內為空,但可以出現0個或多個空格 eg . sum() or sum( ) const paramsBlock = `\\(${params}?${spaceOrEmpty}\\)`; // 6. 函數名 = 無保留字參數 const funName = paramWithSpaceWithoutReservedWords; // 7. 函數調用 = 函數名+參數塊 const funCall = `${funName}${paramsBlock}`; // 8. 可包含空格的函數調用 = 函數調用 && 函數調用頭尾可以出現0個或多個空格 const funCallWithSpace = `${spaceOrEmpty}${funCall}${spaceOrEmpty}`; // 9. 多函數調用 = 多個函數調用的組合,函數調用可出現1個或多個 const multipleFunCall = `${funCallWithSpace}(,${funCallWithSpace})*`; // 10.最終結果 = 多函數調用 && 頭尾不包含除空格外的其它內容 const finalFunCall = `^${multipleFunCall}怎樣寫一個能夠校驗複雜規則的正則表達式?