javascript

exercises

exercises.js
/**
 * 10.7 Object.fromEntries and Advanced Object Methods - Exercises
 */

/**
 * Exercise 1: Object Transform
 * Create a function that transforms object values
 */
function transformValues(obj, transformer) {
  // TODO: Apply transformer function to each value
  // Return new object with transformed values
}

// Test:
// const nums = { a: 1, b: 2, c: 3 };
// console.log(transformValues(nums, n => n * 2));  // { a: 2, b: 4, c: 6 }
// console.log(transformValues(nums, n => n.toString())); // { a: '1', b: '2', c: '3' }

/**
 * Exercise 2: Filter Object
 * Create a function that filters object properties
 */
function filterObject(obj, predicate) {
  // TODO: Keep only properties where predicate returns true
  // Predicate receives (value, key)
}

// Test:
// const scores = { math: 85, english: 60, science: 92 };
// console.log(filterObject(scores, v => v > 70));  // { math: 85, science: 92 }

/**
 * Exercise 3: Rename Keys
 * Create a function that renames object keys
 */
function renameKeys(obj, keyMap) {
  // TODO: Rename keys according to keyMap
  // keyMap = { oldKey: 'newKey' }
}

// Test:
// const user = { firstName: 'John', lastName: 'Doe' };
// const renamed = renameKeys(user, { firstName: 'first', lastName: 'last' });
// console.log(renamed);  // { first: 'John', last: 'Doe' }

/**
 * Exercise 4: Group By with Transform
 * Enhanced groupBy that can transform groups
 */
function groupByWithTransform(array, keyFn, transformFn = (x) => x) {
  // TODO: Group array by keyFn
  // Apply transformFn to each group
}

// Test:
// const products = [
//     { name: 'Apple', category: 'fruit', price: 1.5 },
//     { name: 'Banana', category: 'fruit', price: 0.75 },
//     { name: 'Carrot', category: 'vegetable', price: 0.50 }
// ];
//
// const grouped = groupByWithTransform(
//     products,
//     p => p.category,
//     group => group.reduce((sum, p) => sum + p.price, 0)
// );
// console.log(grouped);  // { fruit: 2.25, vegetable: 0.50 }

/**
 * Exercise 5: Deep Pick
 * Pick nested properties from object
 */
function deepPick(obj, paths) {
  // TODO: Pick properties at nested paths
  // paths = ['user.name', 'user.email', 'settings.theme']
}

// Test:
// const data = {
//     user: { name: 'John', email: 'john@example.com', password: 'secret' },
//     settings: { theme: 'dark', notifications: true }
// };
// console.log(deepPick(data, ['user.name', 'settings.theme']));
// // { user: { name: 'John' }, settings: { theme: 'dark' } }

/**
 * Exercise 6: Object Differ
 * Create a comprehensive object diff utility
 */
function createDiffer() {
  // TODO: Return an object with methods:
  // - diff(obj1, obj2): Return { added, removed, changed }
  // - patch(obj, diff): Apply diff to object
  // - hasChanges(diff): Check if diff has any changes
}

// Test:
// const differ = createDiffer();
// const old = { a: 1, b: 2, c: 3 };
// const new_ = { a: 1, b: 5, d: 4 };
// const d = differ.diff(old, new_);
// console.log(differ.hasChanges(d));  // true
// console.log(differ.patch(old, d));  // { a: 1, b: 5, d: 4 }

/**
 * Exercise 7: Query Builder
 * Build URL query strings with object manipulation
 */
class QueryBuilder {
  // TODO: Implement query string builder
  // constructor(baseUrl)
  // set(key, value): Set parameter
  // setAll(params): Set multiple parameters
  // remove(key): Remove parameter
  // get(key): Get parameter value
  // has(key): Check if parameter exists
  // clear(): Remove all parameters
  // toString(): Return full URL with query string
  // toObject(): Return parameters as object
  // static parse(url): Create from URL string
}

// Test:
// const qb = new QueryBuilder('https://api.example.com/search');
// qb.set('q', 'javascript').set('page', 1).set('limit', 10);
// console.log(qb.toString());
// // "https://api.example.com/search?q=javascript&page=1&limit=10"

/**
 * Exercise 8: Schema Validator
 * Validate object against schema
 */
function createValidator(schema) {
  // TODO: Return validation function
  // Schema format:
  // {
  //   name: { type: 'string', required: true },
  //   age: { type: 'number', min: 0, max: 150 },
  //   email: { type: 'string', pattern: /^.+@.+$/ }
  // }
  // Return { valid: boolean, errors: string[] }
}

// Test:
// const validate = createValidator({
//     name: { type: 'string', required: true },
//     age: { type: 'number', min: 0 }
// });
// console.log(validate({ name: 'John', age: 30 }));  // { valid: true, errors: [] }
// console.log(validate({ age: -5 }));  // { valid: false, errors: [...] }

/**
 * Exercise 9: Object Cache
 * Create a cached object store
 */
class ObjectCache {
  // TODO: Implement cache with:
  // constructor(options): { maxSize, ttl }
  // set(key, value): Store value
  // get(key): Get value (null if expired/missing)
  // has(key): Check if key exists and not expired
  // delete(key): Remove key
  // clear(): Remove all
  // entries(): Return all valid entries
  // keys(): Return all valid keys
  // values(): Return all valid values
  // cleanup(): Remove expired entries
  // stats(): Return { size, hits, misses }
}

// Test:
// const cache = new ObjectCache({ maxSize: 100, ttl: 60000 });
// cache.set('user:1', { name: 'John' });
// console.log(cache.get('user:1'));

/**
 * Exercise 10: Object Projection
 * Project and reshape objects
 */
function project(obj, projection) {
  // TODO: Reshape object according to projection
  // Projection supports:
  // - Simple mapping: { newKey: 'oldKey' }
  // - Nested paths: { name: 'user.name' }
  // - Computed: { fullName: obj => obj.first + ' ' + obj.last }
  // - Array mapping: { 'tags[]': 'categories[].name' }
}

// Test:
// const data = {
//     user: { first: 'John', last: 'Doe' },
//     metadata: { created: '2024-01-01', updated: '2024-03-15' }
// };
//
// const result = project(data, {
//     firstName: 'user.first',
//     lastName: 'user.last',
//     fullName: obj => `${obj.user.first} ${obj.user.last}`,
//     dates: { created: 'metadata.created' }
// });

/**
 * BONUS CHALLENGES
 */

/**
 * Bonus 1: Immutable Object Updates
 * Create helper for immutable nested updates
 */
function immutableUpdate(obj, path, value) {
  // TODO: Return new object with updated nested value
  // Path is dot-separated: 'user.settings.theme'
}

/**
 * Bonus 2: Object Flattening and Unflattening
 * Convert between nested and flat objects
 */
function flatten(obj, prefix = '') {
  // TODO: { a: { b: 1 } } -> { 'a.b': 1 }
}

function unflatten(obj) {
  // TODO: { 'a.b': 1 } -> { a: { b: 1 } }
}

/**
 * Bonus 3: Object Observable
 * Create observable wrapper for objects
 */
function createObservable(obj) {
  // TODO: Return proxy that emits events on changes
  // - on(event, handler): Subscribe to events
  // - off(event, handler): Unsubscribe
  // Events: 'set', 'delete', 'change'
}

// ============================================
// SOLUTION KEY (for reference)
// ============================================

/*
// Exercise 1 Solution:
function transformValues(obj, transformer) {
    return Object.fromEntries(
        Object.entries(obj).map(([key, value]) => [key, transformer(value, key)])
    );
}

// Exercise 2 Solution:
function filterObject(obj, predicate) {
    return Object.fromEntries(
        Object.entries(obj).filter(([key, value]) => predicate(value, key))
    );
}

// Exercise 3 Solution:
function renameKeys(obj, keyMap) {
    return Object.fromEntries(
        Object.entries(obj).map(([key, value]) => [keyMap[key] || key, value])
    );
}

// Exercise 4 Solution:
function groupByWithTransform(array, keyFn, transformFn = x => x) {
    const groups = array.reduce((acc, item) => {
        const key = keyFn(item);
        (acc[key] ??= []).push(item);
        return acc;
    }, {});
    
    return Object.fromEntries(
        Object.entries(groups).map(([key, group]) => [key, transformFn(group)])
    );
}

// Exercise 5 Solution:
function deepPick(obj, paths) {
    const result = {};
    
    for (const path of paths) {
        const parts = path.split('.');
        let source = obj;
        let target = result;
        
        for (let i = 0; i < parts.length; i++) {
            const part = parts[i];
            if (source === undefined) break;
            
            if (i === parts.length - 1) {
                target[part] = source[part];
            } else {
                target[part] ??= {};
                target = target[part];
                source = source[part];
            }
        }
    }
    
    return result;
}

// Exercise 6 Solution:
function createDiffer() {
    return {
        diff(obj1, obj2) {
            const keys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
            const added = {}, removed = {}, changed = {};
            
            for (const key of keys) {
                if (!(key in obj1)) added[key] = obj2[key];
                else if (!(key in obj2)) removed[key] = obj1[key];
                else if (obj1[key] !== obj2[key]) {
                    changed[key] = { from: obj1[key], to: obj2[key] };
                }
            }
            
            return { added, removed, changed };
        },
        
        patch(obj, diff) {
            const result = { ...obj };
            for (const key of Object.keys(diff.removed)) delete result[key];
            Object.assign(result, diff.added);
            for (const [key, { to }] of Object.entries(diff.changed)) {
                result[key] = to;
            }
            return result;
        },
        
        hasChanges(diff) {
            return Object.keys(diff.added).length > 0 ||
                   Object.keys(diff.removed).length > 0 ||
                   Object.keys(diff.changed).length > 0;
        }
    };
}

// Exercise 7 Solution:
class QueryBuilder {
    constructor(baseUrl) {
        const url = new URL(baseUrl);
        this.baseUrl = url.origin + url.pathname;
        this.params = Object.fromEntries(url.searchParams);
    }
    
    set(key, value) { this.params[key] = value; return this; }
    setAll(params) { Object.assign(this.params, params); return this; }
    remove(key) { delete this.params[key]; return this; }
    get(key) { return this.params[key]; }
    has(key) { return key in this.params; }
    clear() { this.params = {}; return this; }
    
    toString() {
        const qs = new URLSearchParams(this.params).toString();
        return qs ? `${this.baseUrl}?${qs}` : this.baseUrl;
    }
    
    toObject() { return { ...this.params }; }
    
    static parse(url) { return new QueryBuilder(url); }
}
*/

console.log('Complete the exercises above!');
console.log('Check the solution key at the bottom for guidance.');
Exercises - JavaScript Tutorial | DeepML