diff options
author | elioat <elioat@tilde.institute> | 2024-12-22 13:47:06 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-12-22 13:47:06 -0500 |
commit | 5caf1654f4bd447c47fd690eab53e39a21978c23 (patch) | |
tree | fe1def3c40eb586d34cd71288f57e5b7fb29af7a | |
parent | cf1789eb681a648b164c69e337ac622251423e85 (diff) | |
download | tour-5caf1654f4bd447c47fd690eab53e39a21978c23.tar.gz |
*
-rw-r--r-- | js/where/where.js | 120 | ||||
-rw-r--r-- | js/where/where.test.html | 243 | ||||
-rw-r--r-- | js/where/where.test.js | 134 |
3 files changed, 497 insertions, 0 deletions
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 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Where Function Tests</title> + <meta charset="UTF-8"> + <style> + body { + font-family: monospace; + padding: 20px; + max-width: 800px; + margin: 0 auto; + line-height: 1.6; + } + .console { + background: #f5f5f5; + padding: 20px; + border-radius: 4px; + margin-top: 20px; + } + .example { + background: #e9f5ff; + padding: 15px; + border-radius: 4px; + margin: 10px 0; + } + code { + background: #f0f0f0; + padding: 2px 4px; + border-radius: 3px; + } + pre { + background: #f8f8f8; + padding: 15px; + border-radius: 4px; + overflow-x: auto; + } + </style> +</head> +<body> + <h1>Where Function Documentation & Tests</h1> + + <section> + <h2>Overview</h2> + <p> + The <code>where()</code> function provides SQL-like filtering for JavaScript arrays of objects. + It allows you to filter collections using simple or complex criteria, including nested properties. + </p> + </section> + + <section> + <h2>Basic Usage</h2> + <div class="example"> + <pre> +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 }]</pre> + </div> + </section> + + <section> + <h2>Advanced Features</h2> + + <h3>1. Nested Object Properties</h3> + <div class="example"> + <pre> +const users = [ + { + name: 'John', + preferences: { + theme: 'dark', + notifications: { email: true } + } + } +]; + +// Find users with dark theme +const darkThemeUsers = where(users, { + preferences: { theme: 'dark' } +});</pre> + </div> + + <h3>2. Dot Notation</h3> + <div class="example"> + <pre> +// Same query using dot notation +const darkThemeUsers = where(users, { + 'preferences.theme': 'dark' +});</pre> + </div> + + <h3>3. Multiple Criteria</h3> + <div class="example"> + <pre> +// Find users who are 30 AND prefer dark theme +const result = where(users, { + age: 30, + 'preferences.theme': 'dark' +});</pre> + </div> + + <h3>4. Array Matching</h3> + <div class="example"> + <pre> +const users = [ + { + name: 'John', + hobbies: ['reading', 'music'] + } +]; + +// Find users with exact hobby matches +const readers = where(users, { + hobbies: ['reading', 'music'] +});</pre> + </div> + </section> + + <section> + <h2>Key Features</h2> + <ul> + <li>Supports deep object comparison</li> + <li>Handles null/undefined values</li> + <li>Supports both nested object and dot notation</li> + <li>Exact array matching</li> + <li>Multiple criteria (AND logic)</li> + <li>Memoized for performance</li> + </ul> + </section> + + <section> + <h2>Limitations & Edge Cases</h2> + + <h3>1. Partial Array Matches</h3> + <div class="example"> + <pre> +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</pre> + </div> + + <h3>2. OR Operations</h3> + <div class="example"> + <pre> +// 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 +);</pre> + </div> + + <h3>3. Complex Comparisons</h3> + <div class="example"> + <pre> +// These operations are NOT supported: +where(users, { + age: (age) => age > 30, // Function predicates + salary: { $gt: 50000 }, // MongoDB-style operators + 'name.length': 4 // Property operations +});</pre> + </div> + + <h3>4. Regular Expressions</h3> + <div class="example"> + <pre> +// RegExp matching is NOT supported +where(users, { + email: /.*@gmail\.com$/ +}); + +// Use Array.filter() instead: +const gmailUsers = users.filter(user => + /.*@gmail\.com$/.test(user.email) +);</pre> + </div> + + <h3>5. When Not to Use</h3> + <ul> + <li><strong>Complex Filtering Logic:</strong> If you need OR conditions, ranges, or custom predicates</li> + <li><strong>Large Data Sets:</strong> When performance is critical and you need indexed operations</li> + <li><strong>Dynamic Queries:</strong> When query conditions need to be built dynamically with different operators</li> + <li><strong>Partial Matches:</strong> When you need partial array matches or string contains operations</li> + </ul> + + <h3>6. Performance Considerations</h3> + <div class="example"> + <pre> +// 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 +);</pre> + </div> + </section> + + <h2>Test Results</h2> + <div class="console" id="output"></div> + + <script> + // Capture console output + const output = document.getElementById('output'); + const originalLog = console.log; + const originalError = console.error; + + console.log = function(msg) { + output.innerHTML += msg + '<br>'; + originalLog.apply(console, arguments); + }; + + console.error = function(msg) { + output.innerHTML += `<span style="color: red">${msg}</span><br>`; + originalError.apply(console, arguments); + }; + </script> + <script src="where.js"></script> + <script src="where.test.js"></script> +</body> +</html> \ 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 |