From 1ccee22de44d26c9629a3d8ac3c3ee6d1e6f62ba Mon Sep 17 00:00:00 2001 From: righ Date: Mon, 5 Aug 2024 03:25:16 +0900 Subject: [PATCH] wip update --- typescript/src/__tests__/pict.test.ts | 250 +++++++++++++++++ typescript/src/types.ts | 6 +- typescript/src/utils/pict.ts | 378 ++++++++++++++++++++++++++ 3 files changed, 632 insertions(+), 2 deletions(-) create mode 100644 typescript/src/__tests__/pict.test.ts create mode 100644 typescript/src/utils/pict.ts diff --git a/typescript/src/__tests__/pict.test.ts b/typescript/src/__tests__/pict.test.ts new file mode 100644 index 0000000..808057e --- /dev/null +++ b/typescript/src/__tests__/pict.test.ts @@ -0,0 +1,250 @@ +import { PictConstraintsLexer } from "../utils/pict"; + +describe('PictConstraintsLexer with single constraints', () => { + it('should filter correctly with LIKE and IN conditions', () => { + const lexer = new PictConstraintsLexer(` + IF [NAME] LIKE "Alic?" THEN [STATUS] IN {"Active", "Pending"} ELSE [AGE] > 20 OR [COUNTRY] = "USA"; + `, false); + const row1 = { NAME: 'Alice', STATUS: 'Active' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { NAME: 'Alice', STATUS: 'Inactive' }; + expect(lexer.filter(row2)).toBe(false); + }); + + it('should filter correctly with numeric conditions', () => { + const lexer = new PictConstraintsLexer(` + IF [PRICE] > 100 THEN [DISCOUNT] = "YES" ELSE [DISCOUNT] = "NO"; + `, false); + const row1 = { PRICE: 150, DISCOUNT: 'YES' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { PRICE: 90, DISCOUNT: 'NO' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { PRICE: 90, DISCOUNT: 'YES' }; + expect(lexer.filter(row3)).toBe(false); + }); + + it('should handle NOT conditions correctly', () => { + const lexer = new PictConstraintsLexer(` + IF NOT [PRODUCT] = "Book" THEN [AVAILABLE] = "Yes" ELSE [AVAILABLE] = "No"; + `, false); + const row1 = { PRODUCT: 'Pen', AVAILABLE: 'Yes' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { PRODUCT: 'Book', AVAILABLE: 'No' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { PRODUCT: 'Pen', AVAILABLE: 'No' }; + expect(lexer.filter(row3)).toBe(false); + }); + + it('should filter with AND conditions', () => { + const lexer = new PictConstraintsLexer(` + IF [CATEGORY] = "Electronics" AND [BRAND] = "Sony" THEN [WARRANTY] = "Included" ELSE [WARRANTY] = "Not Included"; + `, false); + const row1 = { CATEGORY: 'Electronics', BRAND: 'Sony', WARRANTY: 'Included' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { CATEGORY: 'Electronics', BRAND: 'Samsung', WARRANTY: 'Not Included' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { CATEGORY: 'Electronics', BRAND: 'Sony', WARRANTY: 'Not Included' }; + expect(lexer.filter(row3)).toBe(false); + }); + + it('should handle nested conditions with parentheses', () => { + const lexer = new PictConstraintsLexer(` + IF ([CATEGORY] = "Electronics" AND [BRAND] = "Sony") OR [BRAND] = "Apple" THEN [WARRANTY] = "Included" ELSE [WARRANTY] = "Not Included"; + `, false); + const row1 = { CATEGORY: 'Electronics', BRAND: 'Sony', WARRANTY: 'Included' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { CATEGORY: 'Electronics', BRAND: 'Apple', WARRANTY: 'Included' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { CATEGORY: 'Furniture', BRAND: 'IKEA', WARRANTY: 'Not Included' }; + expect(lexer.filter(row3)).toBe(true); + + const row4 = { CATEGORY: 'Electronics', BRAND: 'Samsung', WARRANTY: 'Included' }; + expect(lexer.filter(row4)).toBe(false); + }); + + it('should handle string equality conditions', () => { + const lexer = new PictConstraintsLexer(` + IF [NAME] = "Bob" THEN [STATUS] = "Inactive" ELSE [STATUS] = "Active"; + `, false); + const row1 = { NAME: 'Bob', STATUS: 'Inactive' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { NAME: 'Alice', STATUS: 'Active' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { NAME: 'Bob', STATUS: 'Active' }; + expect(lexer.filter(row3)).toBe(false); + }); + + it('should handle IN conditions', () => { + const lexer = new PictConstraintsLexer(` + IF [COLOR] IN {"Red", "Blue", "Green"} THEN [CATEGORY] = "Primary" ELSE [CATEGORY] = "Secondary"; + `, false); + const row1 = { COLOR: 'Red', CATEGORY: 'Primary' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { COLOR: 'Yellow', CATEGORY: 'Secondary' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { COLOR: 'Red', CATEGORY: 'Secondary' }; + expect(lexer.filter(row3)).toBe(false); + }); + + it('should handle complex conditions with nested parentheses', () => { + const lexer = new PictConstraintsLexer(` + IF ([AGE] > 20 AND ([COUNTRY] = "USA" OR [COUNTRY] = "Canada")) THEN [STATUS] = "Allowed" ELSE [STATUS] = "Denied"; + `, false); + const row1 = { AGE: 25, COUNTRY: 'USA', STATUS: 'Allowed' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { AGE: 18, COUNTRY: 'USA', STATUS: 'Denied' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { AGE: 25, COUNTRY: 'UK', STATUS: 'Denied' }; + expect(lexer.filter(row3)).toBe(true); + + const row4 = { AGE: 25, COUNTRY: 'Canada', STATUS: 'Allowed' }; + expect(lexer.filter(row4)).toBe(true); + + const row5 = { AGE: 25, COUNTRY: 'Canada', STATUS: 'Denied' }; + expect(lexer.filter(row5)).toBe(false); + }); +}); + +describe('PictConstraintsLexer with multiple constraints', () => { + it('should handle multiple constraints correctly (Test Case 1)', () => { + const lexer = new PictConstraintsLexer(` + IF [NAME] = "Alice" THEN [AGE] > 20 ELSE [AGE] < 20; + IF [COUNTRY] = "USA" THEN [STATUS] = "Active" ELSE [STATUS] = "Inactive"; + `, false); + + const row1 = { NAME: 'Alice', AGE: 25, COUNTRY: 'USA', STATUS: 'Active' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { NAME: 'Alice', AGE: 25, COUNTRY: 'Canada', STATUS: 'Inactive' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { NAME: 'Alice', AGE: 18, COUNTRY: 'USA', STATUS: 'Active' }; + expect(lexer.filter(row3)).toBe(false); + + const row4 = { NAME: 'Bob', AGE: 15, COUNTRY: 'USA', STATUS: 'Inactive' }; + expect(lexer.filter(row4)).toBe(false); + }); + + it('should handle multiple constraints correctly (Test Case 2)', () => { + const lexer = new PictConstraintsLexer(` + IF [SCORE] >= 90 THEN [GRADE] = "A" ELSE [GRADE] = "B"; + IF [MEMBER] = "YES" THEN [DISCOUNT] = "20%" ELSE [DISCOUNT] = "10%"; + `, false); + + const row1 = { SCORE: 95, GRADE: 'A', MEMBER: 'YES', DISCOUNT: '20%' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { SCORE: 85, GRADE: 'B', MEMBER: 'NO', DISCOUNT: '10%' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { SCORE: 85, GRADE: 'B', MEMBER: 'YES', DISCOUNT: '20%' }; + expect(lexer.filter(row3)).toBe(true); + + const row4 = { SCORE: 85, GRADE: 'A', MEMBER: 'YES', DISCOUNT: '10%' }; + expect(lexer.filter(row4)).toBe(false); + }); + + it('should handle multiple constraints correctly (Test Case 3)', () => { + const lexer = new PictConstraintsLexer(` + IF [TEMP] > 30 THEN [STATE] = "HOT" ELSE [STATE] = "COLD"; + IF [HUMIDITY] < 50 THEN [COMFORT] = "DRY" ELSE [COMFORT] = "HUMID"; + `, false); + + const row1 = { TEMP: 35, STATE: 'HOT', HUMIDITY: 45, COMFORT: 'DRY' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { TEMP: 25, STATE: 'COLD', HUMIDITY: 55, COMFORT: 'HUMID' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { TEMP: 25, STATE: 'HOT', HUMIDITY: 55, COMFORT: 'DRY' }; + expect(lexer.filter(row3)).toBe(false); + + const row4 = { TEMP: 35, STATE: 'HOT', HUMIDITY: 55, COMFORT: 'HUMID' }; + expect(lexer.filter(row4)).toBe(true); + }); + + it('should handle multiple constraints correctly (Test Case 4)', () => { + const lexer = new PictConstraintsLexer(` + IF [CATEGORY] = "Electronics" THEN [WARRANTY] = "Included" ELSE [WARRANTY] = "Not Included"; + IF [PRICE] > 100 THEN [DISCOUNT] = "YES" ELSE [DISCOUNT] = "NO"; + `, false); + + const row1 = { CATEGORY: 'Electronics', WARRANTY: 'Included', PRICE: 150, DISCOUNT: 'YES' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { CATEGORY: 'Furniture', WARRANTY: 'Not Included', PRICE: 90, DISCOUNT: 'NO' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { CATEGORY: 'Electronics', WARRANTY: 'Not Included', PRICE: 150, DISCOUNT: 'NO' }; + expect(lexer.filter(row3)).toBe(false); + + const row4 = { CATEGORY: 'Furniture', WARRANTY: 'Not Included', PRICE: 150, DISCOUNT: 'YES' }; + expect(lexer.filter(row4)).toBe(true); + }); + + it('should handle multiple constraints correctly (Test Case 5)', () => { + const lexer = new PictConstraintsLexer(` + IF [COLOR] = "Red" THEN [CATEGORY] = "Primary" ELSE [CATEGORY] = "Secondary"; + IF [QUANTITY] < 10 THEN [STOCK] = "Low" ELSE [STOCK] = "High"; + `, false); + + const row1 = { COLOR: 'Red', CATEGORY: 'Primary', QUANTITY: 5, STOCK: 'Low' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { COLOR: 'Blue', CATEGORY: 'Secondary', QUANTITY: 20, STOCK: 'High' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { COLOR: 'Red', CATEGORY: 'Secondary', QUANTITY: 5, STOCK: 'High' }; + expect(lexer.filter(row3)).toBe(false); + + const row4 = { COLOR: 'Red', CATEGORY: 'Primary', QUANTITY: 20, STOCK: 'High' }; + expect(lexer.filter(row4)).toBe(true); + }); + + it('should handle multiple constraints correctly (Test Case 6)', () => { + const lexer = new PictConstraintsLexer(` + IF [SIZE] = "Large" THEN [AVAILABILITY] = "In Stock" ELSE [AVAILABILITY] = "Out of Stock"; + IF ([DISCOUNT] = "YES" AND [MEMBER] = "YES") THEN [PRICE] < 100 ELSE [PRICE] >= 100; + `, false); + + const row1 = { SIZE: 'Large', AVAILABILITY: 'In Stock', DISCOUNT: 'YES', MEMBER: 'YES', PRICE: 90 }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { SIZE: 'Medium', AVAILABILITY: 'Out of Stock', DISCOUNT: 'NO', MEMBER: 'NO', PRICE: 120 }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { SIZE: 'Large', AVAILABILITY: 'In Stock', DISCOUNT: 'YES', MEMBER: 'NO', PRICE: 110 }; + expect(lexer.filter(row3)).toBe(true); + }); + + it('should handle multiple constraints correctly (Test Case 7)', () => { + const lexer = new PictConstraintsLexer(` + IF [SEASON] = "Winter" THEN [CLOTHING] = "Coat" ELSE [CLOTHING] = "Shirt"; + IF ([TEMP] < 0 AND [WEATHER] = "Snowy") THEN [ACTIVITY] = "Skiing" ELSE [ACTIVITY] = "Running"; + `, false); + + const row1 = { SEASON: 'Winter', CLOTHING: 'Coat', TEMP: -5, WEATHER: 'Snowy', ACTIVITY: 'Skiing' }; + expect(lexer.filter(row1)).toBe(true); + + const row2 = { SEASON: 'Summer', CLOTHING: 'Shirt', TEMP: 25, WEATHER: 'Sunny', ACTIVITY: 'Running' }; + expect(lexer.filter(row2)).toBe(true); + + const row3 = { SEASON: 'Winter', CLOTHING: 'Coat', TEMP: 5, WEATHER: 'Sunny', ACTIVITY: 'Running' }; + expect(lexer.filter(row3)).toBe(true); + }); +}); diff --git a/typescript/src/types.ts b/typescript/src/types.ts index c55d536..340da9a 100644 --- a/typescript/src/types.ts +++ b/typescript/src/types.ts @@ -12,10 +12,12 @@ export type MappingTypes = { indices: IndicesType; }; -export type FilterType = (row: { +export type FilterRowType = { [key: string]: any; [index: number]: any; -}) => boolean; +} + +export type FilterType = (row: FilterRowType) => boolean; export type PairType = number[]; diff --git a/typescript/src/utils/pict.ts b/typescript/src/utils/pict.ts new file mode 100644 index 0000000..c9a38f3 --- /dev/null +++ b/typescript/src/utils/pict.ts @@ -0,0 +1,378 @@ +import { FilterType, FilterRowType } from "../types"; + +type Token = { + type: string; + value: string; +} + +type CacheType = { + [key: string]: any; +}; + +export class PictConstraintsLexer { + private tokens: Token[] = []; + private cache: CacheType = {}; + public filters: (FilterType | null)[] = []; + public errors: string[][] = []; + + constructor(private input: string, private debug=false) { + this.input = input; + this.debug = debug; + this.tokenize(); + this.analyze(); + } + private tokenize(): Token[] { + const constraints = this.input; + const tokens: Token[] = []; + let buffer = ''; + let insideQuotes = false; + let insideBraces = false; + + const addToken = (type: string, value: string) => { + tokens.push({ type, value }); + }; + + for (let i = 0; i < constraints.length; i++) { + const char = constraints[i]; + + if (char === '"') { + insideQuotes = !insideQuotes; + buffer += char; + if (!insideQuotes) { + addToken('STRING', buffer); + buffer = ''; + } + } else if (insideQuotes) { + buffer += char; + } else if (char === '[') { + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + buffer = ''; + } + buffer += char; + } else if (char === ']') { + buffer += char; + tokens.push(classifyToken(buffer)); + buffer = ''; + } else if (char === '{') { + insideBraces = true; + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + buffer = ''; + } + addToken('LBRACE', char); + } else if (char === '}') { + insideBraces = false; + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + buffer = ''; + } + addToken('RBRACE', char); + } else if (char === ',' && insideBraces) { + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + buffer = ''; + } + addToken('COMMA', char); + } else if ('[]=<>!();:'.includes(char) && !insideBraces) { + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + buffer = ''; + } + if (char === '<' || char === '>' || char === '!' || char === '=') { + const nextChar = constraints[i + 1]; + if (nextChar === '=') { + tokens.push(classifyToken(char + '=')); + i++; + } else if (char === '<' && nextChar === '>') { + tokens.push(classifyToken('<>')); + i++; + } else { + tokens.push(classifyToken(char)); + } + } else { + tokens.push(classifyToken(char)); + } + } else if (isWhiteSpace(char) && !insideBraces) { + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + buffer = ''; + } + let whitespaceBuffer = char; + while (i + 1 < constraints.length && isWhiteSpace(constraints[i + 1])) { + whitespaceBuffer += constraints[++i]; + } + addToken('WHITESPACE', whitespaceBuffer); + } else if (isWhiteSpace(char) && insideBraces) { + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + buffer = ''; + } + addToken('WHITESPACE', char); + } else { + buffer += char; + } + } + + if (buffer.length > 0) { + tokens.push(classifyToken(buffer)); + } + this.tokens = tokens; + return tokens; + } + + private analyze() { + let tokenIndex = 0; + let setIndex = 0; + let regexIndex = 0; + let errorMessages: string[] = []; + const errors: string[][] = []; + const tokens = this.tokens; + const filters: (FilterType | null)[] = []; + + const nextToken = () => { + while (tokenIndex < tokens.length && tokens[tokenIndex].type === 'WHITESPACE') { + tokenIndex++; + } + return tokenIndex < tokens.length ? tokens[tokenIndex++] : null; + } + + const parseExpression: () => string = () => { + let expr = parseTerm(); + let token = nextToken(); + while (token && token.type === 'OPERATOR' && token.value === 'OR') { + const right = parseTerm(); + expr = `(${expr} || ${right})`; + token = nextToken(); + } + tokenIndex--; // Go back one token + return expr; + } + + const parseTerm: () => string = () => { + let term = parseFactor(); + let token = nextToken(); + while (token && token.type === 'OPERATOR' && token.value === 'AND') { + const right = parseFactor(); + term = `(${term} && ${right})`; + token = nextToken(); + } + tokenIndex--; // Go back one token + return term; + } + + const parseFactor: () => string = () => { + let token = nextToken(); + if (token && token.type === 'OPERATOR' && token.value === 'NOT') { + const factor = parseFactor(); + return `!(${factor})`; + } else if (token && token.type === 'LPAREN') { + const expr = parseExpression(); + token = nextToken(); + if (!token || token.type !== 'RPAREN') { + errorMessages.push('Expected closing parenthesis'); + return 'false'; + } + return `(${expr})`; + } else { + tokenIndex--; // Go back one token + return parseCondition(); + } + } + + const parseCondition: () => string = () => { + const left = parseOperand(); + const comparerToken = nextToken(); + if (!comparerToken || comparerToken.type !== 'COMPARER') { + errorMessages.push('Expected comparer'); + return 'false'; + } + const comparer = comparerToken.value; + if (comparer === 'IN') { + const right = parseSet(); + return `${right}.has(${left})`; + } + const right = parseOperand(); + switch (comparer) { + case '=': + return `${left} === ${right}`; + case '<>': + return `${left} !== ${right}`; + case '>': + return `${left} > ${right}`; + case '<': + return `${left} < ${right}`; + case '>=': + return `${left} >= ${right}`; + case '<=': + return `${left} <= ${right}`; + case 'LIKE': + const regexPattern = right.slice(1, -1).replace(/\*/g, '.*').replace(/\?/g, '.'); // remove quotes and replace wildcards + const regexKey = `re_${regexIndex++}`; + if (!this.cache[regexKey]) { + this.cache[regexKey] = new RegExp('^' + regexPattern + '$'); + } + return `this.cache['${regexKey}'].test(${left})`; + default: + errorMessages.push(`Unknown comparer: ${comparer}`); + return 'false'; + } + } + + const parseSet: () => string = () => { + const elements: string[] = []; + let token = nextToken(); + if (token && token.type === 'LBRACE') { + token = nextToken(); + while (token && token.type !== 'RBRACE') { + if (token.type === 'STRING') { + elements.push(token.value.slice(1, -1)); // remove quotes + } else if (token.type !== 'COMMA' && token.type !== 'WHITESPACE') { + errorMessages.push(`Unexpected token: ${token.value}`); + } + token = nextToken(); + } + } else { + errorMessages.push(`Expected '{' but found ${token ? token.value : 'null'}`); + } + const setKey = `set_${setIndex++}`; + if (!this.cache[setKey]) { + this.cache[setKey] = new Set(elements); + } + return `this.cache['${setKey}']`; + } + + const parseOperand: () => string = () => { + const token = nextToken(); + if (token == null) { + errorMessages.push('Unexpected end of input'); + return 'false'; + } + if (token.type === 'REF') { + const key = token.value.slice(1, -1); // remove [ and ] + return `row["${key}"]`; + } else if (token.type === 'STRING') { + const value = token.value; // keep quotes for string literals + return `${value}`; + } else if (token.type === 'NUMBER') { + return token.value; + } else if (token.type === 'BOOLEAN') { + return token.value === 'TRUE' ? 'true' : 'false'; + } else if (token.type === 'NULL') { + return 'null'; + } else { + errorMessages.push(`Unexpected token: ${token.value}`); + return 'false'; + } + } + + while (tokenIndex < tokens.length) { + const token = nextToken(); + if (token == null) { + break; + } + if (token.type === 'IF') { + const condition = parseExpression(); + const thenToken = nextToken(); + if (!thenToken || thenToken.type !== 'THEN') { + errorMessages.push('Expected THEN'); + break; + } + const action = parseExpression(); + let elseAction = 'false'; + const elseToken = nextToken(); + if (elseToken && elseToken.type === 'ELSE') { + elseAction = parseExpression(); + } else { + tokenIndex--; // Go back one token if ELSE is not found + } + + const filterCode = `return (${condition} ? (${action}) : (${elseAction}));`; + try { + if (this.debug) { + console.log(`code[${filters.length}]:`, filterCode); + } + const f = this.makeClosure(filterCode); + filters.push(f as FilterType); + } catch (e) { + filters.push(null); + // @ts-ignore + errorMessages.push(e.message); + } + errors.push(errorMessages); + errorMessages = []; + } else if (token.type === 'SEMICOLON') { + // do nothing + } else { + errorMessages.push(`Unexpected token: ${token.value}`); + errors.push(errorMessages); + break; + } + } + this.filters = filters; + this.errors = errors; + } + + private makeClosure (code: string) { + return new Function('row', code).bind(this) as FilterType; + } + + filter(row: FilterRowType, ...additionalFilters: FilterType[]): boolean { + for (const f of this.filters) { + if (f == null) { + continue; + } + if (!f(row)) { + return false; + } + } + for (const f of additionalFilters) { + if (!f(row)) { + return false; + } + } + return true; + } +} + +function classifyToken(token: string): Token { + if (token.startsWith('[') && token.endsWith(']')) { + return { type: 'REF', value: token }; + } + if (token.startsWith('"') && token.endsWith('"')) { + return { type: 'STRING', value: token }; + } + if (!isNaN(parseFloat(token))) { + return { type: 'NUMBER', value: token }; + } + if (['TRUE', 'FALSE'].includes(token.toUpperCase())) { + return { type: 'BOOLEAN', value: token.toUpperCase() }; + } + if (token.toUpperCase() === 'NULL') { + return { type: 'NULL', value: token.toUpperCase() }; + } + if (['IF', 'ELSE', 'THEN'].includes(token.toUpperCase())) { + return { type: token.toUpperCase(), value: token.toUpperCase() }; + } + if (['=', '<>', '>', '<', '>=', '<=', 'IN', 'LIKE'].includes(token.toUpperCase())) { + return { type: 'COMPARER', value: token.toUpperCase() }; + } + if (['AND', 'OR', 'NOT'].includes(token.toUpperCase())) { + return { type: 'OPERATOR', value: token.toUpperCase() }; + } else { + switch (token) { + case '(': return { type: 'LPAREN', value: token }; + case ')': return { type: 'RPAREN', value: token }; + case '{': return { type: 'LBRACE', value: token }; + case '}': return { type: 'RBRACE', value: token }; + case ',': return { type: 'COMMA', value: token }; + case ':': return { type: 'COLON', value: token }; + case ';': return { type: 'SEMICOLON', value: token }; + default: throw new Error(`Unknown token: ${token}`); + } + } +} + +const isWhiteSpace = (char: string) => { + return char === ' ' || char === '\n' || char === '\t'; +};