From 5caf1654f4bd447c47fd690eab53e39a21978c23 Mon Sep 17 00:00:00 2001 From: elioat Date: Sun, 22 Dec 2024 13:47:06 -0500 Subject: * --- js/where/where.js | 120 +++++++++++++++++++++++ js/where/where.test.html | 243 +++++++++++++++++++++++++++++++++++++++++++++++ js/where/where.test.js | 134 ++++++++++++++++++++++++++ 3 files changed, 497 insertions(+) create mode 100644 js/where/where.js create mode 100644 js/where/where.test.html create mode 100644 js/where/where.test.js diff --git a/js/where/where.js b/js/where/where.js new file mode 100644 index 0000000..9ff6545 --- /dev/null +++ b/js/where/where.js @@ -0,0 +1,120 @@ +/** + * A functional implementation of a query filter similar to SQL's WHERE clause + * @param {Array} collection - Array of objects to filter + * @param {Object} properties - Object containing properties to match against + * @returns {Array} - Filtered array of objects + */ +const where = (collection, properties) => { + // Handle null/undefined inputs + if (!collection || !properties) { + return []; + } + + // Cache property paths and their split versions for performance + const propertyPaths = Object.entries(properties).map(([key, value]) => ({ + path: key, + parts: key.split('.'), + value + })); + + // Optimized nested value getter with memoization + const getNestedValue = (() => { + const cache = new WeakMap(); + + return (obj, parts) => { + if (!obj) return undefined; + + let cached = cache.get(obj); + if (!cached) { + cached = new Map(); + cache.set(obj, cached); + } + + const pathKey = parts.join('.'); + if (cached.has(pathKey)) { + return cached.get(pathKey); + } + + const value = parts.reduce((current, key) => + current && current[key] !== undefined ? current[key] : undefined, + obj + ); + + cached.set(pathKey, value); + return value; + }; + })(); + + // Optimized equality comparison + const isEqual = (value1, value2) => { + // Handle null/undefined cases first for early return + if (value2 === undefined) return true; + if (value1 === value2) return true; + if (value1 === null || value2 === null) return false; + + // Handle array comparison + if (Array.isArray(value1) && Array.isArray(value2)) { + return value1.length === value2.length && + value1.every((val, idx) => isEqual(val, value2[idx])); + } + + // Handle object comparison + if (typeof value2 === 'object') { + return Object.entries(value2).every(([key, val]) => + value1 && isEqual(value1[key], val) + ); + } + + return false; + }; + + // Optimized property matcher + const matchesProperties = item => + propertyPaths.every(({ parts, value }) => + isEqual(getNestedValue(item, parts), value) + ); + + // Input validation + if (!Array.isArray(collection) || typeof properties !== 'object') { + return []; + } + + return collection.filter(matchesProperties); +}; + +// Example usage with nested structures: +/* +const data = [ + { + name: 'John', + age: 30, + preferences: { + theme: 'dark', + notifications: { email: true, sms: false } + } + }, + { + name: 'Jane', + age: 25, + preferences: { + theme: 'light', + notifications: { email: false, sms: true } + } + } +]; + +// Find users with specific preferences +const darkThemeUsers = where(data, { + preferences: { theme: 'dark' } +}); + +// Find users with specific notification settings +const emailSubscribers = where(data, { + preferences: { notifications: { email: true } } +}); +*/ + +// Export for module usage +if (typeof module !== 'undefined' && module.exports) { + module.exports = where; +} diff --git a/js/where/where.test.html b/js/where/where.test.html new file mode 100644 index 0000000..0b2fcf6 --- /dev/null +++ b/js/where/where.test.html @@ -0,0 +1,243 @@ + + + + Where Function Tests + + + + +

Where Function Documentation & Tests

+ +
+

Overview

+

+ The where() function provides SQL-like filtering for JavaScript arrays of objects. + It allows you to filter collections using simple or complex criteria, including nested properties. +

+
+ +
+

Basic Usage

+
+
+const users = [
+    { name: 'John', age: 30 },
+    { name: 'Jane', age: 25 },
+    { name: 'Bob', age: 30 }
+];
+
+// Find all users who are 30 years old
+const thirtyYearOlds = where(users, { age: 30 });
+// Result: [{ name: 'John', age: 30 }, { name: 'Bob', age: 30 }]
+
+
+ +
+

Advanced Features

+ +

1. Nested Object Properties

+
+
+const users = [
+    { 
+        name: 'John',
+        preferences: { 
+            theme: 'dark',
+            notifications: { email: true }
+        }
+    }
+];
+
+// Find users with dark theme
+const darkThemeUsers = where(users, {
+    preferences: { theme: 'dark' }
+});
+
+ +

2. Dot Notation

+
+
+// Same query using dot notation
+const darkThemeUsers = where(users, {
+    'preferences.theme': 'dark'
+});
+
+ +

3. Multiple Criteria

+
+
+// Find users who are 30 AND prefer dark theme
+const result = where(users, {
+    age: 30,
+    'preferences.theme': 'dark'
+});
+
+ +

4. Array Matching

+
+
+const users = [
+    { 
+        name: 'John',
+        hobbies: ['reading', 'music']
+    }
+];
+
+// Find users with exact hobby matches
+const readers = where(users, {
+    hobbies: ['reading', 'music']
+});
+
+
+ +
+

Key Features

+ +
+ +
+

Limitations & Edge Cases

+ +

1. Partial Array Matches

+
+
+const users = [
+    { 
+        name: 'John',
+        hobbies: ['reading', 'music', 'hiking']
+    }
+];
+
+// This will NOT match John's hobbies
+const result = where(users, {
+    hobbies: ['reading', 'music']
+}); // Returns []
+
+// Arrays must match exactly in length and content
+
+ +

2. OR Operations

+
+
+// This will NOT work for finding users aged 25 OR 30
+const result = where(users, {
+    age: [25, 30]  // This looks for an exact array match
+});
+
+// Consider using Array.filter() directly for OR operations:
+const result = users.filter(user => 
+    user.age === 25 || user.age === 30
+);
+
+ +

3. Complex Comparisons

+
+
+// These operations are NOT supported:
+where(users, {
+    age: (age) => age > 30,     // Function predicates
+    salary: { $gt: 50000 },     // MongoDB-style operators
+    'name.length': 4            // Property operations
+});
+
+ +

4. Regular Expressions

+
+
+// RegExp matching is NOT supported
+where(users, {
+    email: /.*@gmail\.com$/
+});
+
+// Use Array.filter() instead:
+const gmailUsers = users.filter(user => 
+    /.*@gmail\.com$/.test(user.email)
+);
+
+ +

5. When Not to Use

+ + +

6. Performance Considerations

+
+
+// Avoid deep nesting with large arrays
+const deeplyNested = where(largeArray, {
+    'level1.level2.level3.level4.property': value
+});
+
+// Each level of nesting increases comparison complexity
+
+// Better to use direct access if possible:
+const result = largeArray.filter(item => 
+    item.level1?.level2?.level3?.level4?.property === value
+);
+
+
+ +

Test Results

+
+ + + + + + \ No newline at end of file diff --git a/js/where/where.test.js b/js/where/where.test.js new file mode 100644 index 0000000..43638d9 --- /dev/null +++ b/js/where/where.test.js @@ -0,0 +1,134 @@ +/** + * Simple test runner for the where function + */ +const test = { + run(name, fn) { + try { + fn(); + console.log(`✅ ${name}`); + } catch (error) { + console.error(`❌ ${name}`); + console.error(error); + } + }, + + assertEquals(actual, expected, message = '') { + const actualStr = JSON.stringify(actual); + const expectedStr = JSON.stringify(expected); + if (actualStr !== expectedStr) { + throw new Error(`${message}\nExpected: ${expectedStr}\nReceived: ${actualStr}`); + } + } +}; + +// Test Data +const testData = [ + { + name: 'John', + age: 30, + hobbies: ['reading', 'music'], + address: { + city: 'New York', + country: 'USA' + }, + preferences: { + theme: 'dark', + notifications: { email: true, sms: false } + } + }, + { + name: 'Jane', + age: 25, + hobbies: ['sports'], + address: { + city: 'London', + country: 'UK' + }, + preferences: { + theme: 'light', + notifications: { email: false, sms: true } + } + }, + { + name: 'Bob', + age: 30, + hobbies: ['reading', 'music'], + address: { + city: 'New York', + country: 'USA' + }, + preferences: { + theme: 'dark', + notifications: { email: true, sms: true } + } + } +]; + +// Basic Tests +test.run('should handle empty inputs', () => { + test.assertEquals(where([], {}), [], 'Empty array and empty object'); + test.assertEquals(where(null, {}), [], 'Null collection'); + test.assertEquals(where(undefined, {}), [], 'Undefined collection'); + test.assertEquals(where(testData, null), [], 'Null properties'); +}); + +test.run('should match simple properties', () => { + const result = where(testData, { age: 30 }); + test.assertEquals(result.length, 2, 'Should find two 30-year-olds'); + test.assertEquals( + result.map(p => p.name), + ['John', 'Bob'], + 'Should find John and Bob' + ); +}); + +test.run('should match nested objects', () => { + const result = where(testData, { + address: { city: 'New York', country: 'USA' } + }); + test.assertEquals(result.length, 2, 'Should find two New Yorkers'); +}); + +test.run('should match arrays exactly', () => { + const result = where(testData, { + hobbies: ['reading', 'music'] + }); + test.assertEquals(result.length, 2, 'Should find two people with exact hobbies match'); +}); + +test.run('should match deeply nested properties', () => { + const result = where(testData, { + preferences: { notifications: { email: true } } + }); + test.assertEquals(result.length, 2, 'Should find two people with email notifications'); +}); + +test.run('should handle multiple criteria', () => { + const result = where(testData, { + age: 30, + 'preferences.theme': 'dark', + 'address.city': 'New York' + }); + test.assertEquals(result.length, 2, 'Should find people matching all criteria'); +}); + +test.run('should return empty array for non-matching criteria', () => { + const result = where(testData, { + age: 99 + }); + test.assertEquals(result, [], 'Should return empty array for non-matching criteria'); +}); + +test.run('should handle null values correctly', () => { + const dataWithNull = [...testData, { name: 'Alice', age: null }]; + const result = where(dataWithNull, { age: null }); + test.assertEquals(result.length, 1, 'Should find one person with null age'); + test.assertEquals(result[0].name, 'Alice', 'Should find Alice'); +}); + +// Run in Node.js environment +if (typeof module !== 'undefined' && module.exports) { + module.exports = { test }; +} + +process.stdout.setEncoding('utf8'); \ No newline at end of file -- cgit 1.4.1-2-gfad0