• בלוג
  • עמוד 103
  • איך לסנן מערך עם גודל תוצאה מקסימלי ב JavaScript

איך לסנן מערך עם גודל תוצאה מקסימלי ב JavaScript

05/02/2022

הפונקציה filter של JavaScript יודעת לסנן מערך לפי תנאי מסוים. אבל מה קורה אם אתם רוצים להגביל את מספר התוצאות ש filter תחזיר? הנה דוגמה קטנה והצעה לפיתרון-

1. הקוד של popular-english-wods

בקוד של popular-english-words, בה השתמשתי לפני כמה ימים כדי לכתוב חיקוי לוורדל, מצאתי את הפונקציה הבאה:

getMostPopularFilter(count, test) {
    var result = []
    list.every(word => {
        if (test(word)) {
            result.push(word)
        }
        if (result.length == count) return false
        return true
    })
    return result
}

מטרת הפונקציה לקחת מערך של מילים מהמשתנה list, לסנן ממנו רק מילים שמתאימות לתנאי מסוים שמתקבל בפרמטר test ולהחזיר לא יותר מ count מילים שמתאימות לתנאי.

הפונקציה עובדת אבל קריאה שלה משאירה טעם רע בפה. במבט ראשון זה מרגיש כאילו אפשר היה לסגור את זה בשורה אחת עם filter. אבל במבט מעמיק יותר אנחנו מבינים ש Array.prototype.filter לא באמת היה עוזר לנו כאן, בגלל מגבלת האורך. אם הייתי מנסה לשכתב את זה להשתמש בפילטר הקוד היה נראה כך:

return list.filter(test).slice(0, count);

הרבה יותר יפה מהגירסה המקורית, אבל גם הרבה פחות יעיל. בגלל שהחיתוך נעשה אחרי הסינון, אז ברשימת מילים גדולה הקוד יעשה הרבה עבודה מיותרת.

2. אם רק היו לנו Generators ...

ג'נרטור הוא דרך לייצג סידרה של ערכים בלי לחשב אותם. תחשבו על דף הוראות של איקאה במקום ארון מורכב. אם יום אחד נרצה להרכיב את הארון נלך לפי ההוראות ב Generator, אבל הרבה יותר נוח לשים בקופסה את החלקים ודף ההוראות מאשר ארון מורכב מלא.

ב JavaScript אני יכול לכתוב Generator אם אוסיף כוכבית לפני שם הפונקציה. פונקציה רגילה מחזירה ערך, ו Generator מחזיר משהו שיודע לחשב ערכים. בתוך פונקציית ה Generator אני משתמש במילה yield כדי להחזיר את הערך הבא, ובשום שלב לא צריך לחשב מראש את כל הערכים של הסידרה.

בדוגמה של פילטר אני יכול לכתוב את ה Generator הבא:

function *ifilter(arr, p) {
  for (let x of arr) {
    if (p(x)) {
      yield x;
    }
  }
}

הלולאה for ... of יודעת לרוץ גם על Generator וגם על מערכים רגילים. הפקודה yield אומרת שאנחנו יכולים לבקש "ערך אחד" מתוך ה Generator, ואז הפונקציה מחזירה את הערך וגם זוכרת את המקום שבו הפסיקה לרוץ. כשנבקש את הערך הבא הלולאה תמשיך מאותה נקודה אחרי ה yield האחרון שרץ.

אם אנסה להפעיל קוד כזה:

const res = ifilter([10, 20, 30], x => x > 10);
console.log(res);

התוצאה לא תהיה המערך של כל האיברים שגדולים מ 10, אלא:

Object [Generator] {}

בשביל להפוך את ה Generator למערך אני משתמש ב Array.prototype.from כלומר:

const res = ifilter([10, 20, 30], x => x > 10);
console.log(Array.from(res));

ורק בהפעלה של Array.from בעצם מתבצע הסינון.

3. הרכבת Generators

מה שיפה ב Generators זה שאם יש לי יותר מאחד, אני יכול לחבר אותם ביחד ולהפוך את התוצאה למערך רק בסוף. אז נכתוב גם את islice:

function *islice(arr, start, count) {
  for (let i=0; i < start; i++) {
    arr.next();
  }
  for (let i=0; i < count; i++) {
    const { value, done } = arr.next();
    if (done) { return; }
    yield value;
  }
}

ופה כבר יש טריק חדש - הפונקציה next של Generator "מזיזה" את הגנרטור איבר אחד קדימה בלי להחזיר אותו. היא מחזירה אוביקט שמכיל את המפתח value עם הערך שהתקבל, והמפתח done שמקבל true אם אין יותר ערכים בגנרטור.

בשביל לשלב את שניהם ביחד אני יכול לכתוב:

return Array.from(
    islice(
        ifilter(list, test),
        0,
        count));

וזה כבר יותר דומה לגירסת השורה אחת שרציתי להגיע אליה בהתחלה, אבל עם הבונוס של הביצועים. הגנרטור islice יעצור אחרי count איברים ולא ימשוך יותר איברים מהגנרטור ifilter, ולכן ifilter יחשב רק את האיברים שהוא באמת צריך.