diff --git a/README.md b/README.md index 79f4c5e..7cb9acc 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - **WHERE**: 条件过滤 - **ORDER BY**: 排序 - **LIMIT**: 限制结果数量 -- **操作符**: `=`, `!=`, `>`, `<`, `>=`, `<=`, `IN`, `CONTAINS` +- **操作符**: `=`, `!=`, `>`, `<`, `>=`, `<=`, `IN`, `CONTAINS`, `LIKE` - **逻辑**: `AND`, `OR` ## 语法示例 @@ -27,6 +27,11 @@ filter(users, "WHERE metadata.type = 'user' ORDER BY metadata.created_at DESC LI // IN 操作 filter(users, "WHERE metadata.region IN ['beijing', 'shanghai']"); + +// LIKE 操作 (支持 % 匹配任意字符,_ 匹配单个字符) +filter(products, "WHERE name LIKE '%Apple%'"); +filter(products, "WHERE name LIKE 'Apple%'"); +filter(products, "WHERE name LIKE '%Phone'"); ``` ## 使用方法 diff --git a/package.json b/package.json index 30aabce..e95cfe5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kevisual/js-filter", - "version": "0.0.1", + "version": "0.0.2", "description": "sql like filter for js arrays", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/index.ts b/src/index.ts index 6ea02e7..645b0d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ type Token = { type: 'WHERE' | 'AND' | 'OR' | 'ORDER' | 'BY' | 'ASC' | 'DESC' | 'LIMIT' - | 'EQ' | 'NEQ' | 'GT' | 'LT' | 'GTE' | 'LTE' | 'IN' | 'CONTAINS' + | 'EQ' | 'NEQ' | 'GT' | 'LT' | 'GTE' | 'LTE' | 'IN' | 'CONTAINS' | 'LIKE' | 'IDENTIFIER' | 'STRING' | 'NUMBER' | 'COMMA' | 'LBRACKET' | 'RBRACKET' | 'EOF'; value: string; @@ -63,7 +63,7 @@ class Lexer { } private isKeyword(value: string): boolean { - const keywords = ['WHERE', 'AND', 'OR', 'ORDER', 'BY', 'ASC', 'DESC', 'LIMIT', 'IN', 'CONTAINS']; + const keywords = ['WHERE', 'AND', 'OR', 'ORDER', 'BY', 'ASC', 'DESC', 'LIMIT', 'IN', 'CONTAINS', 'LIKE']; return keywords.includes(value.toUpperCase()); } @@ -138,7 +138,8 @@ class Lexer { 'DESC': 'DESC', 'LIMIT': 'LIMIT', 'IN': 'IN', - 'CONTAINS': 'CONTAINS' + 'CONTAINS': 'CONTAINS', + 'LIKE': 'LIKE' }; if (keywords[upperIdentifier]) { @@ -209,7 +210,11 @@ class Parser { let operator: string; let value: any; - if (this.currentToken.type === 'CONTAINS') { + if (this.currentToken.type === 'LIKE') { + operator = 'LIKE'; + this.eat('LIKE'); + value = this.parseValue(); + } else if (this.currentToken.type === 'CONTAINS') { operator = 'CONTAINS'; this.eat('CONTAINS'); value = this.parseValue(); @@ -284,6 +289,23 @@ class Executor { return path.split('.').reduce((acc, part) => acc?.[part], obj); } + private likeToRegex(pattern: string): RegExp { + let regex = ''; + for (let i = 0; i < pattern.length; i++) { + const char = pattern[i]; + if (char === '%') { + regex += '.*'; + } else if (char === '_') { + regex += '.'; + } else if (/[.*+?^${}()|[\]\\]/.test(char)) { + regex += '\\' + char; + } else { + regex += char; + } + } + return new RegExp(`^${regex}$`, 'i'); + } + evaluateCondition(node: ASTNode, item: any): boolean { if (node.type === 'Condition') { const fieldValue = this.getValueByPath(item, node.field!); @@ -307,6 +329,11 @@ class Executor { return Array.isArray(actualValue) && actualValue.includes(fieldValue); case 'CONTAINS': return Array.isArray(fieldValue) && fieldValue.includes(actualValue); + case 'LIKE': + if (typeof fieldValue !== 'string' || typeof actualValue !== 'string') { + return false; + } + return this.likeToRegex(actualValue).test(fieldValue); default: return false; } diff --git a/test/example.ts b/test/example.ts index 68526ee..33f7e7a 100644 --- a/test/example.ts +++ b/test/example.ts @@ -53,3 +53,23 @@ console.log('\n=== 前2个用户,按时间倒序 ==='); const limitResult = filter(users, "ORDER BY metadata.created_at DESC LIMIT 2"); console.log('Limit result length:', limitResult.length); console.log(limitResult); + +const products = [ + { name: 'Apple iPhone 15', category: 'Phone' }, + { name: 'Samsung Galaxy S24', category: 'Phone' }, + { name: 'Apple MacBook Pro', category: 'Laptop' }, + { name: 'Apple Watch', category: 'Watch' } +]; + +console.log('\n=== LIKE 测试 ==='); +console.log('\n=== 包含 Apple 的产品 (%Apple%) ==='); +console.log(filter(products, "WHERE name LIKE '%Apple%'")); + +console.log('\n=== 以 Apple 开头的产品 (Apple%) ==='); +console.log(filter(products, "WHERE name LIKE 'Apple%'")); + +console.log('\n=== 以 Phone 结尾的产品 (%Phone) ==='); +console.log(filter(products, "WHERE name LIKE '%Phone%'")); + +console.log('\n=== 第二个字符任意的产品 (A_pple%) ==='); +console.log(filter(products, "WHERE name LIKE 'A_pple%'"));