לדלג לתוכן

4.7 לולאת האירועים ותכנות פונקציונלי פתרון

פתרון - לולאת האירועים ותכנות פונקציונלי

פתרון תרגיל 1

א.

1
5
3
4
2

  • 1 ו-5 - קוד סינכרוני, רץ מיד
  • 3 - מיקרו-משימה (Promise.then)
  • 4 - מיקרו-משימה (queueMicrotask)
  • 2 - מאקרו-משימה (setTimeout)

מיקרו-משימות רצות לפי סדר ההרשמה (3 לפני 4), ושתיהן לפני המאקרו-משימה.

ב.

E
A
C
F
D
B

  • E - קוד סינכרוני
  • A - ה-async function רצה סינכרונית עד ה-await הראשון
  • C - second רצה סינכרונית עד ה-await שלה
  • F - חזרנו לקוד הסינכרוני אחרי הקריאה ל-first
  • D - המשך second אחרי ה-await (מיקרו-משימה)
  • B - המשך first אחרי ה-await (מיקרו-משימה, אבל חיכתה ש-second תסתיים)

ג.

7
2
5
3
1
6
4

  • 7 - קוד סינכרוני
  • 2 ו-5 - מיקרו-משימות (Promise.then), לפי סדר הרשמה
  • 3 - מיקרו-משימה שנוצרה מתוך ה-then של 2 (אחרי return Promise.resolve), רצה עדיין לפני מאקרו-משימות
  • 1 ו-6 - מאקרו-משימות (setTimeout), לפי סדר הרשמה
  • 4 - מאקרו-משימה שנוצרה מתוך ה-then של 3

פתרון תרגיל 2

function findPrimesAsync({ limit, chunkSize = 10000, onProgress, onComplete }) {
    const primes = [];
    let current = 2;

    function processChunk() {
        const end = Math.min(current + chunkSize, limit + 1);

        for (let i = current; i < end; i++) {
            let isPrime = true;
            for (let j = 2; j <= Math.sqrt(i); j++) {
                if (i % j === 0) {
                    isPrime = false;
                    break;
                }
            }
            if (isPrime) {
                primes.push(i);
            }
        }

        current = end;

        if (onProgress) {
            onProgress(current, limit);
        }

        if (current <= limit) {
            setTimeout(processChunk, 0);
        } else {
            if (onComplete) {
                onComplete(primes);
            }
        }
    }

    processChunk();
}

// usage
findPrimesAsync({
    limit: 5000000,
    chunkSize: 10000,
    onProgress(current, total) {
        console.log(`${Math.round(current / total * 100)}%`);
    },
    onComplete(primes) {
        console.log(`Found ${primes.length} primes`);
    }
});

פתרון תרגיל 3

// a - PURE: same input always gives same output, no side effects
function discount(price) {
    return price * 0.9;
}

// b - NOT PURE: modifies external variable `count`
// fixed:
function increment(count) {
    return count + 1;
}

// c - PURE: depends only on its input, returns a new string
function getFullName(user) {
    return `${user.firstName} ${user.lastName}`;
}

// d - NOT PURE: mutates the input array
// fixed:
function addItem(cart, item) {
    return [...cart, item];
}

// e - NOT PURE: Math.random() makes output unpredictable
// fixed (pass greeting as parameter):
function getGreeting(greeting, name) {
    return `${greeting}, ${name}!`;
}

// f - PURE: same date string always produces the same output
// (assuming the locale behavior is consistent)
function formatDate(date) {
    return new Date(date).toLocaleDateString("he-IL");
}

פתרון תרגיל 4

א. toggleTodo:

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

const todos = [
    { id: 1, text: "Learn JS", completed: false },
    { id: 2, text: "Learn React", completed: false },
    { id: 3, text: "Build project", completed: true }
];

const result = toggleTodo(todos, 2);
console.log(result[1].completed); // true
console.log(todos[1].completed);  // false (unchanged)

ב. updateNestedProperty:

function updateNestedProperty(obj, path, value) {
    if (path.length === 0) {
        return value;
    }

    const [first, ...rest] = path;

    return {
        ...obj,
        [first]: rest.length === 0
            ? value
            : updateNestedProperty(obj[first], rest, value)
    };
}

const state = {
    user: {
        name: "Alice",
        address: {
            city: "Tel Aviv",
            street: "Rothschild"
        }
    },
    settings: {
        theme: "dark"
    }
};

const newState = updateNestedProperty(state, ["user", "address", "city"], "Jerusalem");
console.log(newState.user.address.city); // "Jerusalem"
console.log(state.user.address.city);    // "Tel Aviv"
console.log(newState.settings === state.settings); // true (same reference)

ג. groupBy:

function groupBy(array, key) {
    return array.reduce((groups, item) => {
        const groupKey = item[key];
        return {
            ...groups,
            [groupKey]: [...(groups[groupKey] || []), item]
        };
    }, {});
}

const people = [
    { name: "Alice", department: "Engineering" },
    { name: "Bob", department: "Marketing" },
    { name: "Charlie", department: "Engineering" },
    { name: "Dana", department: "Marketing" },
    { name: "Eve", department: "Sales" }
];

const grouped = groupBy(people, "department");
console.log(grouped);
// {
//     Engineering: [{ name: "Alice", ... }, { name: "Charlie", ... }],
//     Marketing: [{ name: "Bob", ... }, { name: "Dana", ... }],
//     Sales: [{ name: "Eve", ... }]
// }

פתרון תרגיל 5

א. pipe:

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

ב. processUsername ו-calculatePrice:

const trim = str => str.trim();
const toLower = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, "_");
const removeInvalid = str => str.replace(/[^a-z0-9_]/g, "");

const processUsername = pipe(trim, toLower, replaceSpaces, removeInvalid);

processUsername("  Hello World 123!  "); // "hello_world_123"

const applyDiscount = price => price * 0.9;
const addVat = price => price * 1.17;
const roundToTwo = num => Math.round(num * 100) / 100;

const calculatePrice = pipe(applyDiscount, addVat, roundToTwo);

calculatePrice(100); // 105.3

ג. pipeAsync:

function pipeAsync(...fns) {
    return function (value) {
        return fns.reduce(
            (promise, fn) => promise.then(fn),
            Promise.resolve(value)
        );
    };
}

const processUser = pipeAsync(
    async (id) => {
        const response = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
        return response.json();
    },
    (user) => ({ ...user, name: user.name.toUpperCase() }),
    (user) => `User: ${user.name} (${user.email})`
);

const result = await processUser(1);
console.log(result); // "User: LEANNE GRAHAM (Sincere@april.biz)"

פתרון תרגיל 6

א. curry:

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function (...moreArgs) {
            return curried.apply(this, [...args, ...moreArgs]);
        };
    };
}

function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3));    // 6
console.log(curriedAdd(1, 2)(3));    // 6
console.log(curriedAdd(1)(2, 3));    // 6
console.log(curriedAdd(1, 2, 3));    // 6

ב. מערכת validators:

const minLength = curry((min, fieldName, value) => {
    if (value.length < min) {
        return `${fieldName} must be at least ${min} characters`;
    }
    return null;
});

const maxLength = curry((max, fieldName, value) => {
    if (value.length > max) {
        return `${fieldName} must be at most ${max} characters`;
    }
    return null;
});

const matches = curry((regex, message, fieldName, value) => {
    if (!regex.test(value)) {
        return `${fieldName}: ${message}`;
    }
    return null;
});

const required = curry((fieldName, value) => {
    if (!value || value.trim() === "") {
        return `${fieldName} is required`;
    }
    return null;
});

const validateUsername = [
    required("username"),
    minLength(3, "username"),
    maxLength(20, "username"),
    matches(/^[a-zA-Z0-9_]+$/, "only letters, numbers, and underscores", "username")
];

const validateEmail = [
    required("email"),
    matches(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "invalid email format", "email")
];

function validate(validators, value) {
    return validators
        .map(validator => validator(value))
        .filter(error => error !== null);
}

console.log(validate(validateUsername, ""));            // ["username is required"]
console.log(validate(validateUsername, "ab"));          // ["username must be at least 3 characters"]
console.log(validate(validateUsername, "hello world")); // ["username: only letters, numbers, and underscores"]
console.log(validate(validateUsername, "alice_123"));   // []
console.log(validate(validateEmail, "not-an-email"));  // ["email: invalid email format"]

פתרון תרגיל 7

function createScheduler() {
    const tasks = {
        high: [],
        normal: [],
        low: []
    };

    return {
        addTask(fn, priority = "normal") {
            if (!tasks[priority]) {
                throw new Error(`Invalid priority: ${priority}`);
            }
            tasks[priority].push(fn);
        },

        run() {
            // high priority - microtasks
            tasks.high.forEach(fn => {
                queueMicrotask(fn);
            });

            // normal priority - macrotasks
            tasks.normal.forEach(fn => {
                setTimeout(fn, 0);
            });

            // low priority - requestAnimationFrame or delayed setTimeout
            tasks.low.forEach(fn => {
                if (typeof requestAnimationFrame !== "undefined") {
                    requestAnimationFrame(fn);
                } else {
                    setTimeout(fn, 100);
                }
            });

            // clear all queues
            tasks.high = [];
            tasks.normal = [];
            tasks.low = [];
        }
    };
}

// testing
const scheduler = createScheduler();

scheduler.addTask(() => console.log("Normal 1"), "normal");
scheduler.addTask(() => console.log("High 1"), "high");
scheduler.addTask(() => console.log("Low 1"), "low");
scheduler.addTask(() => console.log("High 2"), "high");
scheduler.addTask(() => console.log("Normal 2"), "normal");

scheduler.run();

// output:
// High 1
// High 2
// Normal 1
// Normal 2
// Low 1