about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-12-22 13:47:06 -0500
committerelioat <elioat@tilde.institute>2024-12-22 13:47:06 -0500
commit5caf1654f4bd447c47fd690eab53e39a21978c23 (patch)
treefe1def3c40eb586d34cd71288f57e5b7fb29af7a
parentcf1789eb681a648b164c69e337ac622251423e85 (diff)
downloadtour-5caf1654f4bd447c47fd690eab53e39a21978c23.tar.gz
*
-rw-r--r--js/where/where.js120
-rw-r--r--js/where/where.test.html243
-rw-r--r--js/where/where.test.js134
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