20行代码实现一个简单的Regex Engine

446 阅读2分钟

项目地址


npm install or yarn //安装依赖

npm run test // 运行测试文件

我们将会实现一个简单正则引擎,它的规则如下

语法 含义 example 匹配
a 匹配文本字面量 "a" "a"
* 匹配0或多个前字符 "a*" "","a","aa"
? 匹配0或1个前字符 "a?" "","a"
. 匹配任意字符 "." "a","b"
^ 起始匹配符 "^a" "a","aa","ab"
$ 结尾匹配符 "a$" "aaa","bba"

我们用Typescript编写Code

先考虑单个字符匹配

单个字符,情况简单的多。它有以下五种情况:

/**
 * 
 * @param pattern 匹配的字符
 * @param char    需要被匹配的字符
 * 
 * case 1 : matchOneChar('','a')->true
 * case 2 : matchOneChar('a','')->false
 * case 3 : matchOneChar('.','b')->true
 * case 4 : matchOneChar('a','b')->false
 * case 5 : matchOneChar('a','a')->true  
 * 
 * 单个字符匹配有以上情况
 */
const matchOneChar =(pattern:string,char:string):boolean=>{
    if(!pattern) return true;   //case 1
    if(!char) return false; //case 2
    if(pattern === ".") return true; //case 3
    return pattern === char;  // case 4,5
}

配上测试

import {search,matchOneChar} from './regex';

describe("匹配单个字符",()=>{
    it("匹配字符为空",()=>{
        expect(matchOneChar('','a')).toBe(true)
    });
    it("匹配内容为空",()=>{
        expect(matchOneChar('a','')).toBe(false);
    })
    it("特殊字符 . 匹配",()=>{
        expect(matchOneChar('.','a')).toBe(true);
        expect(matchOneChar('.','b')).toBe(true);
    })
    it("匹配字符是否相同",()=>{
        expect(matchOneChar('a','a')).toBe(true);
        expect(matchOneChar('a','b')).toBe(false);
    })
})

多个字符的匹配

先考虑头部对齐匹配

仅考虑文本匹配

/**
 * 
 * @param pattern 匹配字符
 * @param text    匹配文本
 */
const match =(pattern:string,text:string):boolean=>{
    if (pattern === "") return true;
    if (!text) return false;
    return matchOneChar(pattern[0],text[0]) &&match(pattern.slice(1),text.slice(1));
}

添加 “$”匹配符

/**
 * 
 * @param pattern 匹配字符
 * @param text    匹配文本
 */
const match =(pattern:string,text:string):boolean=>{
    if (pattern === "") return true;
    if (pattern === "$"&& text==="") return true;
    if (!text) return false;
    return matchOneChar(pattern[0],text[0]) &&match(pattern.slice(1),text.slice(1));
}

添加 “?”匹配符

const match =(pattern:string,text:string):boolean=>{
    if (pattern === "") return true;
    if (pattern === "$"&& text==="") return true;
    if (!text) return false;
    //  添加 “?”匹配
    if (pattern[1] === '?'){
        return matchOneChar(pattern[0],text[0])&&match(pattern.slice(2),text.slice(1))  || match(pattern.slice(2),text);
    }
    return matchOneChar(pattern[0],text[0]) &&match(pattern.slice(1),text.slice(1));
}

添加 “*”匹配符

const match =(pattern:string,text:string):boolean=>{
    if (pattern === "") return true;
    if (pattern === "$"&& text==="") return true;
    if (!text) return false;
    if (pattern[1] === '?'){
        return matchOneChar(pattern[0],text[0])&&match(pattern.slice(2),text.slice(1))  || match(pattern.slice(2),text);
    }
    if (pattern[1] === '*'){
        return matchOneChar(pattern[0],text[0])&&match(pattern,text.slice(1))  || match(pattern.slice(2),text);
    }

    return matchOneChar(pattern[0],text[0]) &&match(pattern.slice(1),text.slice(1));
}

头部不对齐情况

可以通过转换匹配符或是匹配文本转换成头部对齐情况,有两种处理方案:

  • 匹配符前面加上“.*”
const search =(pattern:string,text:string):boolean=>{
    if (pattern[0] === '^') {
        return match(pattern.slice(1), text);
      } else {
        return match('.*' + pattern, text);
      }
}
  • 拆分匹配文本,只要又一个匹配上就OK
const search =(pattern:string,text:string):boolean=>{
    if (pattern[0] === '^') {
        return match(pattern.slice(1), text);
      } else {
        return text.split('').some((_, index) => {
            return match(pattern, text.slice(index));
          });
      }
}

总结:

思考过程由顶层开始

头部不对齐 -> 头部对齐 -> 单个字符匹配 将大的问题一步一步根据条件拆分成小问题,如此往复。 整个实现代码在20行左右。