javascript

exercises

exercises.js
/**
 * =====================================================
 * 5.9 PURE FUNCTIONS & SIDE EFFECTS - EXERCISES
 * =====================================================
 * Practice writing predictable, testable functions
 */

/**
 * Exercise 1: Identify Pure vs Impure
 *
 * Mark each function as Pure or Impure and explain why.
 */
console.log('Exercise 1: Identify Pure vs Impure');

// Function A
function add(a, b) {
  return a + b;
}
// TODO: Pure or Impure? Why?

// Function B
let total = 0;
function addToTotal(n) {
  total += n;
  return total;
}
// TODO: Pure or Impure? Why?

// Function C
function getRandomNumber(max) {
  return Math.floor(Math.random() * max);
}
// TODO: Pure or Impure? Why?

// Function D
function formatDate(date) {
  return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`;
}
// TODO: Pure or Impure? Why?

/**
 * Exercise 2: Convert to Pure - Array Operations
 *
 * Convert these impure functions to pure functions.
 */
console.log('\nExercise 2: Convert to Pure - Arrays');

// ❌ Impure - mutates array
function addItemImpure(arr, item) {
  arr.push(item);
  return arr;
}

// TODO: Write pure version
function addItem(arr, item) {
  // Your code here
}

// ❌ Impure - mutates array
function removeLastImpure(arr) {
  arr.pop();
  return arr;
}

// TODO: Write pure version
function removeLast(arr) {
  // Your code here
}

// Test:
// const original = [1, 2, 3];
// console.log(addItem(original, 4));  // [1, 2, 3, 4]
// console.log(original);              // [1, 2, 3] (unchanged)

/**
 * Exercise 3: Convert to Pure - Object Operations
 *
 * Convert these impure functions to pure functions.
 */
console.log('\nExercise 3: Convert to Pure - Objects');

// ❌ Impure - mutates object
function updateNameImpure(person, newName) {
  person.name = newName;
  return person;
}

// TODO: Write pure version
function updateName(person, newName) {
  // Your code here
}

// ❌ Impure - mutates nested object
function updateCityImpure(person, newCity) {
  person.address.city = newCity;
  return person;
}

// TODO: Write pure version
function updateCity(person, newCity) {
  // Your code here
}

// Test:
// const person = { name: "Alice", address: { city: "NYC" } };
// console.log(updateName(person, "Bob"));   // { name: "Bob", ... }
// console.log(person.name);                 // "Alice" (unchanged)

/**
 * Exercise 4: Pure Math Functions
 *
 * Write pure functions for these mathematical operations.
 */
console.log('\nExercise 4: Pure Math Functions');

// TODO: Calculate factorial
function factorial(n) {
  // Your code here
}

// TODO: Calculate average
function average(numbers) {
  // Your code here
}

// TODO: Clamp value between min and max
function clamp(value, min, max) {
  // Your code here
}

// TODO: Round to N decimal places
function roundTo(value, decimals) {
  // Your code here
}

// Test:
// console.log(factorial(5));      // 120
// console.log(average([1,2,3,4,5])); // 3
// console.log(clamp(15, 0, 10)); // 10
// console.log(roundTo(3.14159, 2)); // 3.14

/**
 * Exercise 5: Pure String Functions
 *
 * Write pure functions for string operations.
 */
console.log('\nExercise 5: Pure String Functions');

// TODO: Create URL slug from text
function slugify(text) {
  // "Hello World!" → "hello-world"
}

// TODO: Truncate with ellipsis
function truncate(str, maxLength) {
  // "Hello World", 8 → "Hello..."
}

// TODO: Capitalize first letter of each word
function titleCase(str) {
  // "hello world" → "Hello World"
}

// Test:
// console.log(slugify("Hello World!"));  // "hello-world"
// console.log(truncate("Hello World", 8)); // "Hello..."
// console.log(titleCase("hello world")); // "Hello World"

/**
 * Exercise 6: Todo List Pure Functions
 *
 * Implement pure functions for a todo list.
 */
console.log('\nExercise 6: Todo List');

// TODO: Add a new todo
function addTodo(todos, text) {
  // Return new array with added todo
  // { id: Date.now(), text, completed: false }
}

// TODO: Toggle a todo's completed status
function toggleTodo(todos, id) {
  // Return new array with toggled todo
}

// TODO: Remove a todo by id
function removeTodo(todos, id) {
  // Return new array without the todo
}

// TODO: Get only completed todos
function getCompleted(todos) {
  // Return new array of completed todos
}

// Test:
// let todos = [];
// todos = addTodo(todos, "Learn JS");
// todos = addTodo(todos, "Build app");
// console.log(todos);

/**
 * Exercise 7: Shopping Cart Pure Functions
 *
 * Implement pure functions for a shopping cart.
 */
console.log('\nExercise 7: Shopping Cart');

// TODO: Add item to cart (or increase quantity if exists)
function addToCart(cart, item) {
  // Your code here
}

// TODO: Remove item from cart
function removeFromCart(cart, itemId) {
  // Your code here
}

// TODO: Update item quantity
function updateQuantity(cart, itemId, quantity) {
  // Your code here
}

// TODO: Calculate cart total
function getTotal(cart) {
  // Your code here
}

// Test:
// let cart = [];
// cart = addToCart(cart, { id: 1, name: "Widget", price: 10 });
// cart = addToCart(cart, { id: 2, name: "Gadget", price: 25 });
// console.log(getTotal(cart));  // 35

/**
 * Exercise 8: Data Transformation Pipeline
 *
 * Create pure functions that can be chained together.
 */
console.log('\nExercise 8: Data Pipeline');

const users = [
  { id: 1, name: 'Alice', age: 25, active: true },
  { id: 2, name: 'Bob', age: 30, active: false },
  { id: 3, name: 'Charlie', age: 35, active: true },
];

// TODO: Filter active users
function filterActive(users) {
  // Your code here
}

// TODO: Sort by age (ascending)
function sortByAge(users) {
  // Your code here
}

// TODO: Extract names only
function getNames(users) {
  // Your code here
}

// TODO: Use all three in a pipeline
// const result = getNames(sortByAge(filterActive(users)));
// console.log(result);  // ["Alice", "Charlie"]

/**
 * Exercise 9: Memoization
 *
 * Implement memoization for pure functions.
 */
console.log('\nExercise 9: Memoization');

function memoize(fn) {
  // TODO: Implement memoization
  // Cache results based on arguments
}

// Test:
// let callCount = 0;
// const expensiveFn = memoize((n) => {
//     callCount++;
//     return n * n;
// });
// console.log(expensiveFn(5)); // 25
// console.log(expensiveFn(5)); // 25 (cached)
// console.log(callCount);      // 1

/**
 * Exercise 10: Dependency Injection
 *
 * Refactor to make the function pure using dependency injection.
 */
console.log('\nExercise 10: Dependency Injection');

// ❌ Impure - uses Date directly
function getAgeImpure(birthYear) {
  return new Date().getFullYear() - birthYear;
}

// TODO: Make it pure by injecting current year
function getAge(birthYear, currentYear) {
  // Your code here
}

// ❌ Impure - uses random
function shuffleImpure(arr) {
  return [...arr].sort(() => Math.random() - 0.5);
}

// TODO: Make it pure by injecting random function
function shuffle(arr, randomFn) {
  // Your code here
}

// Test:
// console.log(getAge(1990, 2024));  // 34
// console.log(shuffle([1,2,3], () => 0.5));  // Predictable

// =====================================================
// ADVANCED EXERCISES
// =====================================================

/**
 * Bonus 1: Deep Update
 *
 * Pure function to update nested property by path.
 */
console.log('\nBonus 1: Deep Update');

function updateIn(obj, path, value) {
  // TODO: Implement
  // updateIn({ a: { b: 1 } }, ['a', 'b'], 2) → { a: { b: 2 } }
}

// Test:
// const state = { user: { profile: { name: "Alice" } } };
// const newState = updateIn(state, ['user', 'profile', 'name'], 'Bob');
// console.log(newState.user.profile.name);  // "Bob"
// console.log(state.user.profile.name);     // "Alice"

/**
 * Bonus 2: Compose Function
 *
 * Create a compose function for pure function composition.
 */
console.log('\nBonus 2: Compose');

function compose(...fns) {
  // TODO: Implement right-to-left composition
}

function pipe(...fns) {
  // TODO: Implement left-to-right composition
}

// Test:
// const addOne = x => x + 1;
// const double = x => x * 2;
// const stringify = x => `Result: ${x}`;
//
// const process = pipe(addOne, double, stringify);
// console.log(process(5));  // "Result: 12"

/**
 * Bonus 3: Immutable State Manager
 *
 * Create a simple state manager using only pure functions.
 */
console.log('\nBonus 3: State Manager');

function createStore(initialState, reducer) {
  // TODO: Implement
  // return { getState, dispatch }
}

// Test:
// const reducer = (state, action) => {
//     switch (action.type) {
//         case 'INCREMENT': return { ...state, count: state.count + 1 };
//         case 'DECREMENT': return { ...state, count: state.count - 1 };
//         default: return state;
//     }
// };
//
// const store = createStore({ count: 0 }, reducer);
// store.dispatch({ type: 'INCREMENT' });
// console.log(store.getState());  // { count: 1 }

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

/*
// Exercise 1 Solutions:
// A: Pure - same input always gives same output, no side effects
// B: Impure - modifies external variable 'total'
// C: Impure - non-deterministic (random)
// D: Pure - if passed the same Date object, returns same string

// Exercise 2 Solution:
function addItem(arr, item) {
    return [...arr, item];
}

function removeLast(arr) {
    return arr.slice(0, -1);
}

// Exercise 3 Solution:
function updateName(person, newName) {
    return { ...person, name: newName };
}

function updateCity(person, newCity) {
    return {
        ...person,
        address: { ...person.address, city: newCity }
    };
}

// Exercise 4 Solution:
function factorial(n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

function average(numbers) {
    if (numbers.length === 0) return 0;
    return numbers.reduce((a, b) => a + b, 0) / numbers.length;
}

function clamp(value, min, max) {
    return Math.min(Math.max(value, min), max);
}

function roundTo(value, decimals) {
    const factor = Math.pow(10, decimals);
    return Math.round(value * factor) / factor;
}

// Exercise 5 Solution:
function slugify(text) {
    return text
        .toLowerCase()
        .trim()
        .replace(/[^\w\s-]/g, '')
        .replace(/[\s_-]+/g, '-');
}

function truncate(str, maxLength) {
    if (str.length <= maxLength) return str;
    return str.slice(0, maxLength - 3) + '...';
}

function titleCase(str) {
    return str.split(' ')
        .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
        .join(' ');
}

// Exercise 6 Solution:
function addTodo(todos, text) {
    return [...todos, { id: Date.now(), text, completed: false }];
}

function toggleTodo(todos, id) {
    return todos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
    );
}

function removeTodo(todos, id) {
    return todos.filter(todo => todo.id !== id);
}

function getCompleted(todos) {
    return todos.filter(todo => todo.completed);
}

// Exercise 7 Solution:
function addToCart(cart, item) {
    const existing = cart.find(i => i.id === item.id);
    if (existing) {
        return cart.map(i =>
            i.id === item.id ? { ...i, quantity: (i.quantity || 1) + 1 } : i
        );
    }
    return [...cart, { ...item, quantity: 1 }];
}

function removeFromCart(cart, itemId) {
    return cart.filter(item => item.id !== itemId);
}

function updateQuantity(cart, itemId, quantity) {
    if (quantity <= 0) return removeFromCart(cart, itemId);
    return cart.map(item =>
        item.id === itemId ? { ...item, quantity } : item
    );
}

function getTotal(cart) {
    return cart.reduce((sum, item) => sum + item.price * (item.quantity || 1), 0);
}

// Exercise 8 Solution:
function filterActive(users) {
    return users.filter(u => u.active);
}

function sortByAge(users) {
    return [...users].sort((a, b) => a.age - b.age);
}

function getNames(users) {
    return users.map(u => u.name);
}

// Exercise 9 Solution:
function memoize(fn) {
    const cache = new Map();
    return function(...args) {
        const key = JSON.stringify(args);
        if (!cache.has(key)) {
            cache.set(key, fn(...args));
        }
        return cache.get(key);
    };
}

// Exercise 10 Solution:
function getAge(birthYear, currentYear) {
    return currentYear - birthYear;
}

function shuffle(arr, randomFn) {
    return [...arr].sort(() => randomFn() - 0.5);
}

// Bonus 1 Solution:
function updateIn(obj, path, value) {
    const [first, ...rest] = path;
    if (rest.length === 0) {
        return { ...obj, [first]: value };
    }
    return {
        ...obj,
        [first]: updateIn(obj[first] || {}, rest, value)
    };
}

// Bonus 2 Solution:
function compose(...fns) {
    return function(value) {
        return fns.reduceRight((acc, fn) => fn(acc), value);
    };
}

function pipe(...fns) {
    return function(value) {
        return fns.reduce((acc, fn) => fn(acc), value);
    };
}

// Bonus 3 Solution:
function createStore(initialState, reducer) {
    let state = initialState;
    
    return {
        getState() {
            return state;
        },
        dispatch(action) {
            state = reducer(state, action);
            return state;
        }
    };
}
*/
Exercises - JavaScript Tutorial | DeepML