javascript

exercises

exercises.js
/**
 * ========================================
 * 8.1 CLASS BASICS - EXERCISES
 * ========================================
 * Difficulty: ⭐ = Easy, ⭐⭐ = Medium, ⭐⭐⭐ = Hard
 */

/**
 * EXERCISE 1: Basic Class ⭐
 *
 * Create a class `Book` with:
 * - Constructor that takes title, author, and pages
 * - A method `getSummary()` that returns "Title by Author, X pages"
 * - A method `isLong()` that returns true if pages > 300
 */
class Book {
  // Your code here
}

// const book = new Book("The Hobbit", "J.R.R. Tolkien", 310);
// console.log(book.getSummary()); // "The Hobbit by J.R.R. Tolkien, 310 pages"
// console.log(book.isLong()); // true

/**
 * EXERCISE 2: Getters and Setters ⭐
 *
 * Create a class `Rectangle` with:
 * - Constructor that takes width and height
 * - Getter `area` that returns width * height
 * - Getter `perimeter` that returns 2 * (width + height)
 * - Setter `area` that sets dimensions to a square with that area
 */
class Rectangle {
  // Your code here
}

// const rect = new Rectangle(4, 5);
// console.log(rect.area); // 20
// console.log(rect.perimeter); // 18
// rect.area = 16;
// console.log(rect.width, rect.height); // 4, 4

/**
 * EXERCISE 3: Method Chaining ⭐⭐
 *
 * Create a class `Calculator` with:
 * - Constructor that takes initial value (default 0)
 * - Methods: add(n), subtract(n), multiply(n), divide(n)
 * - All methods return `this` for chaining
 * - Method `getResult()` returns current value
 * - Method `reset()` sets value back to 0
 */
class Calculator {
  // Your code here
}

// const calc = new Calculator(10);
// console.log(calc.add(5).multiply(2).subtract(10).getResult()); // 20
// calc.reset();
// console.log(calc.getResult()); // 0

/**
 * EXERCISE 4: Validation in Constructor ⭐⭐
 *
 * Create a class `Email` with:
 * - Constructor that takes an email address
 * - Throws Error if email format is invalid
 * - Getter `address` returns the email (lowercase)
 * - Getter `domain` returns the domain part
 * - Getter `username` returns the username part
 */
class Email {
  // Your code here
}

// const email = new Email("ALICE@Example.COM");
// console.log(email.address); // "alice@example.com"
// console.log(email.domain); // "example.com"
// console.log(email.username); // "alice"
// new Email("invalid"); // Throws Error

/**
 * EXERCISE 5: Class with Generator ⭐⭐
 *
 * Create a class `Countdown` with:
 * - Constructor that takes start number
 * - Implement Symbol.iterator to count down to 0
 * - Method `reset(n)` resets to a new number
 */
class Countdown {
  // Your code here
}

// const countdown = new Countdown(5);
// console.log([...countdown]); // [5, 4, 3, 2, 1, 0]
// countdown.reset(3);
// console.log([...countdown]); // [3, 2, 1, 0]

/**
 * EXERCISE 6: Factory Methods ⭐⭐
 *
 * Create a class `Color` with:
 * - Constructor that takes r, g, b values (0-255)
 * - Static method `fromHex(hex)` creates Color from hex string
 * - Static method `fromHSL(h, s, l)` creates Color from HSL
 * - Method `toHex()` returns hex string
 * - Method `toRGB()` returns "rgb(r, g, b)"
 */
class Color {
  // Your code here
}

// const red = new Color(255, 0, 0);
// console.log(red.toHex()); // "#ff0000"
// console.log(red.toRGB()); // "rgb(255, 0, 0)"
// const blue = Color.fromHex("#0000ff");
// console.log(blue.toRGB()); // "rgb(0, 0, 255)"

/**
 * EXERCISE 7: Queue Class ⭐⭐
 *
 * Create a class `Queue` with:
 * - Methods: enqueue(item), dequeue(), peek(), isEmpty(), size()
 * - Implement Symbol.iterator
 * - Method `clear()` empties the queue
 */
class Queue {
  // Your code here
}

// const queue = new Queue();
// queue.enqueue(1).enqueue(2).enqueue(3);
// console.log(queue.peek()); // 1
// console.log(queue.dequeue()); // 1
// console.log(queue.size()); // 2
// console.log([...queue]); // [2, 3]

/**
 * EXERCISE 8: Timer Class ⭐⭐
 *
 * Create a class `Timer` with:
 * - Methods: start(), stop(), reset()
 * - Getter `elapsed` returns milliseconds elapsed
 * - Getter `running` returns boolean
 * - Method `lap()` returns current elapsed and continues
 */
class Timer {
  // Your code here
}

// const timer = new Timer();
// timer.start();
// // ... some time passes ...
// console.log(timer.elapsed); // Time in ms
// console.log(timer.running); // true
// timer.stop();
// console.log(timer.running); // false

/**
 * EXERCISE 9: TodoList Class ⭐⭐⭐
 *
 * Create a class `TodoList` with:
 * - Method `add(text, priority = "normal")` - returns todo id
 * - Method `complete(id)` - marks todo as complete
 * - Method `remove(id)` - removes todo
 * - Getter `pending` - returns array of incomplete todos
 * - Getter `completed` - returns array of completed todos
 * - Getter `all` - returns all todos
 * - Implement Symbol.iterator (iterates pending todos)
 */
class TodoList {
  // Your code here
}

// const todos = new TodoList();
// const id1 = todos.add("Learn JavaScript", "high");
// const id2 = todos.add("Build project");
// todos.complete(id1);
// console.log(todos.pending.length); // 1
// console.log(todos.completed.length); // 1

/**
 * EXERCISE 10: EventEmitter Class ⭐⭐⭐
 *
 * Create a class `EventEmitter` with:
 * - Method `on(event, callback)` - register listener
 * - Method `off(event, callback)` - remove listener
 * - Method `once(event, callback)` - one-time listener
 * - Method `emit(event, ...args)` - trigger event
 * - Method `listenerCount(event)` - return count
 */
class EventEmitter {
  // Your code here
}

// const emitter = new EventEmitter();
// const handler = (data) => console.log("Received:", data);
// emitter.on("message", handler);
// emitter.emit("message", "Hello"); // "Received: Hello"
// emitter.off("message", handler);
// emitter.emit("message", "World"); // (nothing)

/**
 * EXERCISE 11: Validator Class ⭐⭐⭐
 *
 * Create a class `Validator` with:
 * - Method `addRule(name, validatorFn)` - adds validation rule
 * - Method `validate(value, rules)` - validates against rules
 * - Returns { valid: boolean, errors: string[] }
 * - Built-in rules: required, minLength(n), maxLength(n), email, number
 */
class Validator {
  // Your code here
}

// const validator = new Validator();
// validator.addRule("uppercase", (val) =>
//     val === val.toUpperCase() ? null : "Must be uppercase"
// );
// const result = validator.validate("hello", ["required", "uppercase"]);
// console.log(result); // { valid: false, errors: ["Must be uppercase"] }

/**
 * EXERCISE 12: Cache Class ⭐⭐⭐
 *
 * Create a class `Cache` with:
 * - Constructor takes max size (default 100)
 * - Method `set(key, value, ttl)` - ttl in ms (optional)
 * - Method `get(key)` - returns value or undefined if expired
 * - Method `has(key)` - returns boolean
 * - Method `delete(key)` - removes entry
 * - Method `clear()` - clears all
 * - Getter `size` - current number of entries
 * - Automatically evicts oldest when max size reached
 */
class Cache {
  // Your code here
}

// const cache = new Cache(3);
// cache.set("a", 1);
// cache.set("b", 2);
// cache.set("c", 3);
// cache.set("d", 4); // Evicts "a"
// console.log(cache.get("a")); // undefined
// console.log(cache.get("b")); // 2

/**
 * EXERCISE 13: State Machine Class ⭐⭐⭐
 *
 * Create a class `StateMachine` with:
 * - Constructor takes initial state and transitions config
 * - Getter `state` returns current state
 * - Method `can(event)` - returns if transition is valid
 * - Method `transition(event)` - performs transition
 * - Method `onEnter(state, callback)` - callback when entering state
 * - Method `onLeave(state, callback)` - callback when leaving state
 */
class StateMachine {
  // Your code here
}

// const light = new StateMachine("red", {
//     red: { timer: "green" },
//     green: { timer: "yellow" },
//     yellow: { timer: "red" }
// });
// light.onEnter("green", () => console.log("Go!"));
// console.log(light.state); // "red"
// light.transition("timer");
// console.log(light.state); // "green", logs "Go!"

/**
 * EXERCISE 14: Observable Class ⭐⭐⭐
 *
 * Create a class `Observable` with:
 * - Constructor takes initial value
 * - Getter/setter `value`
 * - Method `subscribe(callback)` - returns unsubscribe function
 * - Subscribers are called with (newValue, oldValue) when value changes
 * - Method `map(fn)` - returns new Observable that transforms value
 */
class Observable {
  // Your code here
}

// const num = new Observable(5);
// const unsubscribe = num.subscribe((newVal, oldVal) => {
//     console.log(`Changed from ${oldVal} to ${newVal}`);
// });
// num.value = 10; // "Changed from 5 to 10"
// unsubscribe();
// num.value = 15; // (nothing)

/**
 * EXERCISE 15: Database Model Class ⭐⭐⭐
 *
 * Create a class `Model` with:
 * - Constructor takes data object
 * - Static `fields` property defines field config
 * - Getters/setters for each field
 * - Method `validate()` returns { valid, errors }
 * - Method `toJSON()` returns plain object
 * - Static `create(data)` factory method
 * - Static `find(predicate)` searches created instances
 */
class Model {
  // Your code here
}

// class User extends Model {
//     static fields = {
//         name: { required: true, minLength: 2 },
//         email: { required: true, type: "email" },
//         age: { type: "number", min: 0 }
//     };
// }
// const user = User.create({ name: "Alice", email: "alice@test.com", age: 25 });
// console.log(user.validate()); // { valid: true, errors: {} }

// ============================================
// SOLUTIONS (Hidden below - try first!)
// ============================================

/*
// SOLUTION 1:
class Book {
    constructor(title, author, pages) {
        this.title = title;
        this.author = author;
        this.pages = pages;
    }
    
    getSummary() {
        return `${this.title} by ${this.author}, ${this.pages} pages`;
    }
    
    isLong() {
        return this.pages > 300;
    }
}

// SOLUTION 2:
class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }
    
    get area() {
        return this.width * this.height;
    }
    
    set area(value) {
        const side = Math.sqrt(value);
        this.width = side;
        this.height = side;
    }
    
    get perimeter() {
        return 2 * (this.width + this.height);
    }
}

// SOLUTION 3:
class Calculator {
    constructor(initial = 0) {
        this.value = initial;
    }
    
    add(n) {
        this.value += n;
        return this;
    }
    
    subtract(n) {
        this.value -= n;
        return this;
    }
    
    multiply(n) {
        this.value *= n;
        return this;
    }
    
    divide(n) {
        if (n === 0) throw new Error("Division by zero");
        this.value /= n;
        return this;
    }
    
    getResult() {
        return this.value;
    }
    
    reset() {
        this.value = 0;
        return this;
    }
}

// SOLUTION 4:
class Email {
    constructor(address) {
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(address)) {
            throw new Error("Invalid email format");
        }
        this._address = address.toLowerCase();
    }
    
    get address() {
        return this._address;
    }
    
    get domain() {
        return this._address.split("@")[1];
    }
    
    get username() {
        return this._address.split("@")[0];
    }
}

// SOLUTION 5:
class Countdown {
    constructor(start) {
        this.start = start;
    }
    
    *[Symbol.iterator]() {
        for (let i = this.start; i >= 0; i--) {
            yield i;
        }
    }
    
    reset(n) {
        this.start = n;
    }
}

// SOLUTION 6:
class Color {
    constructor(r, g, b) {
        this.r = Math.max(0, Math.min(255, r));
        this.g = Math.max(0, Math.min(255, g));
        this.b = Math.max(0, Math.min(255, b));
    }
    
    static fromHex(hex) {
        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        if (!result) throw new Error("Invalid hex color");
        return new Color(
            parseInt(result[1], 16),
            parseInt(result[2], 16),
            parseInt(result[3], 16)
        );
    }
    
    static fromHSL(h, s, l) {
        s /= 100;
        l /= 100;
        const c = (1 - Math.abs(2 * l - 1)) * s;
        const x = c * (1 - Math.abs((h / 60) % 2 - 1));
        const m = l - c / 2;
        let r, g, b;
        
        if (h < 60) { r = c; g = x; b = 0; }
        else if (h < 120) { r = x; g = c; b = 0; }
        else if (h < 180) { r = 0; g = c; b = x; }
        else if (h < 240) { r = 0; g = x; b = c; }
        else if (h < 300) { r = x; g = 0; b = c; }
        else { r = c; g = 0; b = x; }
        
        return new Color(
            Math.round((r + m) * 255),
            Math.round((g + m) * 255),
            Math.round((b + m) * 255)
        );
    }
    
    toHex() {
        const toHex = n => n.toString(16).padStart(2, "0");
        return `#${toHex(this.r)}${toHex(this.g)}${toHex(this.b)}`;
    }
    
    toRGB() {
        return `rgb(${this.r}, ${this.g}, ${this.b})`;
    }
}

// SOLUTION 7:
class Queue {
    constructor() {
        this.items = [];
    }
    
    enqueue(item) {
        this.items.push(item);
        return this;
    }
    
    dequeue() {
        return this.items.shift();
    }
    
    peek() {
        return this.items[0];
    }
    
    isEmpty() {
        return this.items.length === 0;
    }
    
    size() {
        return this.items.length;
    }
    
    clear() {
        this.items = [];
        return this;
    }
    
    *[Symbol.iterator]() {
        yield* this.items;
    }
}

// SOLUTION 8:
class Timer {
    constructor() {
        this._startTime = null;
        this._elapsed = 0;
        this._running = false;
    }
    
    start() {
        if (!this._running) {
            this._startTime = Date.now();
            this._running = true;
        }
        return this;
    }
    
    stop() {
        if (this._running) {
            this._elapsed += Date.now() - this._startTime;
            this._running = false;
        }
        return this;
    }
    
    reset() {
        this._elapsed = 0;
        this._startTime = this._running ? Date.now() : null;
        return this;
    }
    
    lap() {
        return this.elapsed;
    }
    
    get elapsed() {
        if (this._running) {
            return this._elapsed + (Date.now() - this._startTime);
        }
        return this._elapsed;
    }
    
    get running() {
        return this._running;
    }
}

// SOLUTION 9:
class TodoList {
    constructor() {
        this._todos = [];
        this._nextId = 1;
    }
    
    add(text, priority = "normal") {
        const id = this._nextId++;
        this._todos.push({
            id,
            text,
            priority,
            completed: false,
            createdAt: new Date()
        });
        return id;
    }
    
    complete(id) {
        const todo = this._todos.find(t => t.id === id);
        if (todo) {
            todo.completed = true;
            todo.completedAt = new Date();
        }
        return todo;
    }
    
    remove(id) {
        const index = this._todos.findIndex(t => t.id === id);
        if (index > -1) {
            return this._todos.splice(index, 1)[0];
        }
        return null;
    }
    
    get pending() {
        return this._todos.filter(t => !t.completed);
    }
    
    get completed() {
        return this._todos.filter(t => t.completed);
    }
    
    get all() {
        return [...this._todos];
    }
    
    *[Symbol.iterator]() {
        yield* this.pending;
    }
}

// SOLUTION 10:
class EventEmitter {
    constructor() {
        this._events = new Map();
    }
    
    on(event, callback) {
        if (!this._events.has(event)) {
            this._events.set(event, []);
        }
        this._events.get(event).push(callback);
        return this;
    }
    
    off(event, callback) {
        if (this._events.has(event)) {
            const callbacks = this._events.get(event);
            const index = callbacks.indexOf(callback);
            if (index > -1) callbacks.splice(index, 1);
        }
        return this;
    }
    
    once(event, callback) {
        const wrapper = (...args) => {
            callback(...args);
            this.off(event, wrapper);
        };
        return this.on(event, wrapper);
    }
    
    emit(event, ...args) {
        if (this._events.has(event)) {
            for (const callback of this._events.get(event)) {
                callback(...args);
            }
        }
        return this;
    }
    
    listenerCount(event) {
        return this._events.has(event) ? this._events.get(event).length : 0;
    }
}

// SOLUTION 11:
class Validator {
    constructor() {
        this._rules = new Map();
        
        // Built-in rules
        this.addRule("required", val => 
            !val || val.length === 0 ? "This field is required" : null
        );
        this.addRule("email", val =>
            val && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val) ? "Invalid email" : null
        );
        this.addRule("number", val =>
            val && isNaN(Number(val)) ? "Must be a number" : null
        );
    }
    
    addRule(name, validatorFn) {
        this._rules.set(name, validatorFn);
        return this;
    }
    
    validate(value, rules) {
        const errors = [];
        
        for (const rule of rules) {
            const validator = this._rules.get(rule);
            if (validator) {
                const error = validator(value);
                if (error) errors.push(error);
            }
        }
        
        return { valid: errors.length === 0, errors };
    }
}

// SOLUTION 12:
class Cache {
    constructor(maxSize = 100) {
        this._maxSize = maxSize;
        this._cache = new Map();
    }
    
    set(key, value, ttl = null) {
        if (this._cache.size >= this._maxSize) {
            const oldestKey = this._cache.keys().next().value;
            this._cache.delete(oldestKey);
        }
        
        this._cache.set(key, {
            value,
            expires: ttl ? Date.now() + ttl : null
        });
        return this;
    }
    
    get(key) {
        const entry = this._cache.get(key);
        if (!entry) return undefined;
        
        if (entry.expires && Date.now() > entry.expires) {
            this._cache.delete(key);
            return undefined;
        }
        
        return entry.value;
    }
    
    has(key) {
        return this.get(key) !== undefined;
    }
    
    delete(key) {
        return this._cache.delete(key);
    }
    
    clear() {
        this._cache.clear();
        return this;
    }
    
    get size() {
        return this._cache.size;
    }
}

// SOLUTION 13:
class StateMachine {
    constructor(initialState, transitions) {
        this._state = initialState;
        this._transitions = transitions;
        this._enterCallbacks = {};
        this._leaveCallbacks = {};
    }
    
    get state() {
        return this._state;
    }
    
    can(event) {
        return !!(this._transitions[this._state] && 
                  this._transitions[this._state][event]);
    }
    
    transition(event) {
        if (!this.can(event)) {
            throw new Error(`Invalid transition: ${event} from ${this._state}`);
        }
        
        const newState = this._transitions[this._state][event];
        
        // Leave callbacks
        if (this._leaveCallbacks[this._state]) {
            for (const cb of this._leaveCallbacks[this._state]) {
                cb();
            }
        }
        
        this._state = newState;
        
        // Enter callbacks
        if (this._enterCallbacks[this._state]) {
            for (const cb of this._enterCallbacks[this._state]) {
                cb();
            }
        }
        
        return this;
    }
    
    onEnter(state, callback) {
        if (!this._enterCallbacks[state]) {
            this._enterCallbacks[state] = [];
        }
        this._enterCallbacks[state].push(callback);
        return this;
    }
    
    onLeave(state, callback) {
        if (!this._leaveCallbacks[state]) {
            this._leaveCallbacks[state] = [];
        }
        this._leaveCallbacks[state].push(callback);
        return this;
    }
}

// SOLUTION 14:
class Observable {
    constructor(initialValue) {
        this._value = initialValue;
        this._subscribers = [];
    }
    
    get value() {
        return this._value;
    }
    
    set value(newValue) {
        const oldValue = this._value;
        if (newValue !== oldValue) {
            this._value = newValue;
            for (const subscriber of this._subscribers) {
                subscriber(newValue, oldValue);
            }
        }
    }
    
    subscribe(callback) {
        this._subscribers.push(callback);
        return () => {
            const index = this._subscribers.indexOf(callback);
            if (index > -1) {
                this._subscribers.splice(index, 1);
            }
        };
    }
    
    map(fn) {
        const mapped = new Observable(fn(this._value));
        this.subscribe((newVal) => {
            mapped.value = fn(newVal);
        });
        return mapped;
    }
}

// SOLUTION 15:
class Model {
    static instances = [];
    static fields = {};
    
    constructor(data = {}) {
        this._data = {};
        
        for (const [field, config] of Object.entries(this.constructor.fields)) {
            Object.defineProperty(this, field, {
                get: () => this._data[field],
                set: (value) => { this._data[field] = value; },
                enumerable: true
            });
            
            if (data[field] !== undefined) {
                this._data[field] = data[field];
            }
        }
        
        this.constructor.instances.push(this);
    }
    
    validate() {
        const errors = {};
        let valid = true;
        
        for (const [field, config] of Object.entries(this.constructor.fields)) {
            const value = this._data[field];
            const fieldErrors = [];
            
            if (config.required && !value) {
                fieldErrors.push("Required");
            }
            
            if (config.minLength && value && value.length < config.minLength) {
                fieldErrors.push(`Min length: ${config.minLength}`);
            }
            
            if (config.type === "email" && value && 
                !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
                fieldErrors.push("Invalid email");
            }
            
            if (config.type === "number" && value !== undefined && isNaN(value)) {
                fieldErrors.push("Must be a number");
            }
            
            if (config.min !== undefined && value < config.min) {
                fieldErrors.push(`Min value: ${config.min}`);
            }
            
            if (fieldErrors.length > 0) {
                errors[field] = fieldErrors;
                valid = false;
            }
        }
        
        return { valid, errors };
    }
    
    toJSON() {
        return { ...this._data };
    }
    
    static create(data) {
        return new this(data);
    }
    
    static find(predicate) {
        return this.instances.filter(predicate);
    }
}
*/

console.log('========================================');
console.log('Class Basics Exercises Loaded!');
console.log('Uncomment the test code to check your solutions.');
console.log('========================================');
Exercises - JavaScript Tutorial | DeepML