javascript

exercises

exercises.js
/**
 * =====================================================
 * 3.10 OPTIONAL CHAINING OPERATOR - EXERCISES
 * =====================================================
 * Practice safe property access with ?.
 */

/**
 * Exercise 1: Basic Property Access
 *
 * Use optional chaining to safely access the user's city.
 * Return "Unknown" if any part of the chain is missing.
 */
function getUserCity(user) {
  // TODO: Return user.address.city safely, or "Unknown" if missing
  // Hint: Use ?. and ??
}

// Test cases:
console.log('Exercise 1:');
console.log(getUserCity({ name: 'Alice', address: { city: 'NYC' } })); // "NYC"
console.log(getUserCity({ name: 'Bob', address: {} })); // "Unknown"
console.log(getUserCity({ name: 'Charlie' })); // "Unknown"
console.log(getUserCity(null)); // "Unknown"

/**
 * Exercise 2: Bracket Notation Access
 *
 * Access a property dynamically using bracket notation with optional chaining.
 * Return undefined if the path doesn't exist.
 */
function getPropertyByName(obj, propName) {
  // TODO: Safely access obj[propName] and return its value
  // Then access the 'data' property of that value if it exists
}

// Test cases:
console.log('\nExercise 2:');
const testObj = {
  users: { data: [1, 2, 3] },
  posts: { data: ['a', 'b'] },
};
console.log(getPropertyByName(testObj, 'users')); // { data: [1, 2, 3] }
console.log(getPropertyByName(testObj, 'comments')); // undefined
console.log(getPropertyByName(null, 'users')); // undefined

/**
 * Exercise 3: Safe Method Calls
 *
 * Safely call the format method on the formatter object.
 * If the method doesn't exist, return the original value.
 */
function formatValue(formatter, value) {
  // TODO: Call formatter.format(value) if it exists
  // Otherwise return the original value
}

// Test cases:
console.log('\nExercise 3:');
const upperFormatter = { format: (s) => s.toUpperCase() };
console.log(formatValue(upperFormatter, 'hello')); // "HELLO"
console.log(formatValue({}, 'hello')); // "hello"
console.log(formatValue(null, 'hello')); // "hello"

/**
 * Exercise 4: Deep Object Access
 *
 * Extract specific data from a deeply nested API response.
 * Use optional chaining throughout.
 */
function extractApiData(response) {
  // TODO: Return an object with:
  // - userId: from response.data.user.id (default: null)
  // - userName: from response.data.user.profile.name (default: "Anonymous")
  // - email: from response.data.user.contact.email (default: "No email")
  // - firstRole: from response.data.user.roles[0] (default: "guest")
}

// Test cases:
console.log('\nExercise 4:');
const fullResponse = {
  data: {
    user: {
      id: 123,
      profile: { name: 'John Doe' },
      contact: { email: 'john@example.com' },
      roles: ['admin', 'editor'],
    },
  },
};
console.log(extractApiData(fullResponse));
// { userId: 123, userName: "John Doe", email: "john@example.com", firstRole: "admin" }

const partialResponse = {
  data: {
    user: {
      id: 456,
    },
  },
};
console.log(extractApiData(partialResponse));
// { userId: 456, userName: "Anonymous", email: "No email", firstRole: "guest" }

console.log(extractApiData(null));
// { userId: null, userName: "Anonymous", email: "No email", firstRole: "guest" }

/**
 * Exercise 5: Array Element Access
 *
 * Safely get items from potentially missing arrays.
 */
function getFirstAndLast(data) {
  // TODO: Return an object with:
  // - first: first element of data.items array (or null)
  // - last: last element of data.items array (or null)
  // Hint: For last element, you'll need to calculate the index
}

// Test cases:
console.log('\nExercise 5:');
console.log(getFirstAndLast({ items: ['a', 'b', 'c'] })); // { first: "a", last: "c" }
console.log(getFirstAndLast({ items: ['only'] })); // { first: "only", last: "only" }
console.log(getFirstAndLast({ items: [] })); // { first: null, last: null }
console.log(getFirstAndLast({})); // { first: null, last: null }
console.log(getFirstAndLast(null)); // { first: null, last: null }

/**
 * Exercise 6: Callback Execution
 *
 * Create a function that safely executes optional callbacks.
 */
function processWithCallbacks(data, options) {
  // TODO:
  // 1. Call options.onStart() if it exists
  // 2. Process data by converting to uppercase (if string)
  // 3. Call options.onProcess(result) if it exists
  // 4. Call options.onComplete(result) if it exists
  // 5. Return the processed result
}

// Test cases:
console.log('\nExercise 6:');
const result1 = processWithCallbacks('hello', {
  onStart: () => console.log('Starting...'),
  onComplete: (r) => console.log('Done:', r),
});
console.log('Result:', result1); // Starting... Done: HELLO Result: HELLO

const result2 = processWithCallbacks('world', null);
console.log('Result:', result2); // Result: WORLD (no callbacks called)

/**
 * Exercise 7: Configuration Merger
 *
 * Merge user config with defaults, using optional chaining
 * to safely access nested properties.
 */
function mergeConfig(userConfig) {
  const defaults = {
    theme: 'light',
    fontSize: 14,
    sidebar: { visible: true, width: 250 },
    notifications: { email: true, push: false },
  };

  // TODO: Return a merged config object
  // User config values should override defaults when present
  // Use optional chaining to safely access userConfig properties
}

// Test cases:
console.log('\nExercise 7:');
console.log(mergeConfig({ theme: 'dark' }));
// { theme: "dark", fontSize: 14, sidebar: { visible: true, width: 250 }, ... }

console.log(mergeConfig({ sidebar: { width: 300 } }));
// { theme: "light", fontSize: 14, sidebar: { visible: true, width: 300 }, ... }

console.log(mergeConfig(null));
// defaults object

console.log(mergeConfig(undefined));
// defaults object

/**
 * Exercise 8: Safe JSON Path Access
 *
 * Create a function that accesses nested properties using a path string.
 * Example: "user.profile.name" should access obj.user.profile.name
 */
function getByPath(obj, path) {
  // TODO: Split path by '.' and access each property safely
  // Return undefined if any part of the path is missing
  // Hint: Use reduce with optional chaining
}

// Test cases:
console.log('\nExercise 8:');
const testData = {
  user: {
    profile: {
      name: 'Alice',
      settings: {
        theme: 'dark',
      },
    },
  },
};

console.log(getByPath(testData, 'user.profile.name')); // "Alice"
console.log(getByPath(testData, 'user.profile.settings.theme')); // "dark"
console.log(getByPath(testData, 'user.address.city')); // undefined
console.log(getByPath(null, 'user.name')); // undefined

/**
 * Exercise 9: Event Handler System
 *
 * Create a mini event system with safe handler invocation.
 */
function createEventSystem() {
  const handlers = {};

  return {
    // TODO: Implement on(event, handler) - registers a handler
    on(event, handler) {},

    // TODO: Implement off(event) - removes handler (safe if doesn't exist)
    off(event) {},

    // TODO: Implement emit(event, data) - calls handler safely
    emit(event, data) {},

    // TODO: Implement emitAll(data) - calls all handlers safely
    emitAll(data) {},
  };
}

// Test cases:
console.log('\nExercise 9:');
const events = createEventSystem();
events.on('click', (data) => console.log('Clicked:', data));
events.emit('click', { x: 100, y: 200 }); // Clicked: { x: 100, y: 200 }
events.emit('hover', { x: 50 }); // Nothing (no handler)
events.off('click');
events.emit('click', { x: 0 }); // Nothing (handler removed)

/**
 * Exercise 10: Safe Array Operations
 *
 * Perform safe operations on arrays that might not exist.
 */
function safeArrayOps(data) {
  // TODO: Return an object with:
  // - length: length of data.items array (or 0)
  // - sum: sum of data.items if they're numbers (or 0)
  // - first: first item (or null)
  // - mapped: items mapped to uppercase if strings (or empty array)
  // Use optional chaining for all access
}

// Test cases:
console.log('\nExercise 10:');
console.log(safeArrayOps({ items: [1, 2, 3, 4, 5] }));
// { length: 5, sum: 15, first: 1, mapped: [1, 2, 3, 4, 5] }

console.log(safeArrayOps({ items: ['a', 'b', 'c'] }));
// { length: 3, sum: 0, first: "a", mapped: ["A", "B", "C"] }

console.log(safeArrayOps({ items: [] }));
// { length: 0, sum: 0, first: null, mapped: [] }

console.log(safeArrayOps(null));
// { length: 0, sum: 0, first: null, mapped: [] }

// =====================================================
// BONUS CHALLENGES
// =====================================================

/**
 * Bonus 1: Optional Chaining vs Logical AND
 *
 * Demonstrate the difference between ?. and && with falsy values.
 */
function compareApproaches(obj) {
  // TODO: Return an object showing the difference between
  // && approach and ?. approach for accessing obj.value.toString()
  // when obj.value is 0
}

console.log('\nBonus 1:');
console.log(compareApproaches({ value: 0 }));
// { andResult: 0, optionalResult: "0" }

/**
 * Bonus 2: Chain Builder
 *
 * Create a utility that allows building safe property access chains.
 */
function createChain(obj) {
  // TODO: Return an object that allows method chaining for safe access
  // Example: createChain(obj).get('a').get('b').value()
  // Should return obj?.a?.b or undefined
}

console.log('\nBonus 2:');
const chainData = { a: { b: { c: 42 } } };
// console.log(createChain(chainData).get('a').get('b').get('c').value());  // 42
// console.log(createChain(chainData).get('x').get('y').value());  // undefined

// =====================================================
// SOLUTIONS (Uncomment to check your answers)
// =====================================================

/*
// Exercise 1 Solution:
function getUserCity(user) {
    return user?.address?.city ?? "Unknown";
}

// Exercise 2 Solution:
function getPropertyByName(obj, propName) {
    return obj?.[propName];
}

// Exercise 3 Solution:
function formatValue(formatter, value) {
    return formatter?.format?.(value) ?? value;
}

// Exercise 4 Solution:
function extractApiData(response) {
    return {
        userId: response?.data?.user?.id ?? null,
        userName: response?.data?.user?.profile?.name ?? "Anonymous",
        email: response?.data?.user?.contact?.email ?? "No email",
        firstRole: response?.data?.user?.roles?.[0] ?? "guest"
    };
}

// Exercise 5 Solution:
function getFirstAndLast(data) {
    const items = data?.items;
    const length = items?.length ?? 0;
    return {
        first: items?.[0] ?? null,
        last: length > 0 ? items[length - 1] : null
    };
}

// Exercise 6 Solution:
function processWithCallbacks(data, options) {
    options?.onStart?.();
    const result = typeof data === 'string' ? data.toUpperCase() : data;
    options?.onProcess?.(result);
    options?.onComplete?.(result);
    return result;
}

// Exercise 7 Solution:
function mergeConfig(userConfig) {
    const defaults = {
        theme: "light",
        fontSize: 14,
        sidebar: { visible: true, width: 250 },
        notifications: { email: true, push: false }
    };
    
    return {
        theme: userConfig?.theme ?? defaults.theme,
        fontSize: userConfig?.fontSize ?? defaults.fontSize,
        sidebar: {
            visible: userConfig?.sidebar?.visible ?? defaults.sidebar.visible,
            width: userConfig?.sidebar?.width ?? defaults.sidebar.width
        },
        notifications: {
            email: userConfig?.notifications?.email ?? defaults.notifications.email,
            push: userConfig?.notifications?.push ?? defaults.notifications.push
        }
    };
}

// Exercise 8 Solution:
function getByPath(obj, path) {
    return path.split('.').reduce((acc, key) => acc?.[key], obj);
}

// Exercise 9 Solution:
function createEventSystem() {
    const handlers = {};
    
    return {
        on(event, handler) {
            handlers[event] = handler;
        },
        off(event) {
            delete handlers[event];
        },
        emit(event, data) {
            handlers[event]?.(data);
        },
        emitAll(data) {
            Object.values(handlers).forEach(handler => handler?.(data));
        }
    };
}

// Exercise 10 Solution:
function safeArrayOps(data) {
    const items = data?.items ?? [];
    return {
        length: items?.length ?? 0,
        sum: items?.reduce?.((a, b) => typeof b === 'number' ? a + b : a, 0) ?? 0,
        first: items?.[0] ?? null,
        mapped: items?.map?.(item => typeof item === 'string' ? item.toUpperCase() : item) ?? []
    };
}

// Bonus 1 Solution:
function compareApproaches(obj) {
    return {
        andResult: obj && obj.value && obj.value.toString(),
        optionalResult: obj?.value?.toString?.()
    };
}

// Bonus 2 Solution:
function createChain(obj) {
    let current = obj;
    return {
        get(prop) {
            current = current?.[prop];
            return this;
        },
        value() {
            return current;
        }
    };
}
*/
Exercises - JavaScript Tutorial | DeepML