This commit is contained in:
2025-12-30 01:00:53 +08:00
parent 6ba2e23a77
commit 8a4a720f5b
4 changed files with 58 additions and 6 deletions

View File

@@ -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'");
```
## 使用方法

View File

@@ -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",

View File

@@ -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;
}

View File

@@ -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%'"));