Към съдържанието

4. Arrays и списъци от записи ​

Занятие 4упражнения: 04-arrays-and-records/ в homework repositorydoc: примерният JSON документ

Днес ще се научите да:

  • създавате array, да четете и променяте елементите му по индекс и да използвате length;
  • знаете защо array-ят е обект и какви бъгове идват от това;
  • добавяте и премахвате елементи с push, pop, unshift, shift и splice; да копирате част със slice; да четете от края с at;
  • търсите с indexOf, includes и lastIndexOf и да замените верига от === с allow-list;
  • разрязвате string на array със split и да слепвате array в string с join - за версии и за стойности, пакетирани в един string ("a;b", "key=value,key=value");
  • обхождате array с for и for...of, напред и назад;
  • сумирате, броите, намирате най-големия елемент и събирате елементи в нови arrays;
  • работите с arrays от arrays (таблици);
  • намирате, броите и събирате записи в списък от обекти;
  • проверявате дали една стойност наистина е array, с Array.isArray;
  • четете кода с arrays в съществуващите правила.

Къде е мястото на тази тема в примерното правило за проверка - днешното правило проверява списък: enclosures на устройството. Числото във всеки коментар е разделът от тази лекция:

js
// 6. an allow-list instead of a chain of ===
const MODELS = ["XR-210", "XR-220"];

function rule(doc) {
    if (!doc.overview || !doc.inventory?.details) return;

    const model = doc.overview.model;
    if (!MODELS.includes(model)) return;        // 6. includes

    // 7. split: the version is the first word of the firmware level
    const level = doc.inventory.details.firmware_level ?? "";
    const version = level.split(" ")[0];        // 2, 7. split

    // 10. a list of records: check every enclosure, collect the problems
    const enclosures = doc.inventory.enclosures;
    if (!Array.isArray(enclosures)) return;     // 4. isArray
    const problems = [];                        // 1. empty array
    for (const enc of enclosures) {             // 8. for...of
        if (enc.state !== "normal") {
            problems.push(`${enc.id} is ${enc.state}`);   // 5. push
        }
        if (versionBelow(enc.firmware, "2.14")) {
            problems.push(`${enc.id} has firmware ${enc.firmware}`);
        }
    }
    if (problems.length === 0) return;          // 3. length

    const name = doc.inventory.extended_info?.json?.data?.system?.name
                 || doc.status.device_alias;
    emit(name, {
        "Model": model,
        "Version": version,
        "Problems": problems.length,
        "Details": problems.join("; ")          // 7. join
    });
}
  • versionBelow(a, b) все още е готовата helper функция от лекция 3; собствена ще напишем в занятие 5.
  • Примерният JSON документ вече има два списъка: doc.inventory.features (strings) и doc.inventory.enclosures (обекти). В раздел 11 ще изпълните това правило и ще прочетете същото правило в по-стария стил.

Истинските отчети са пълни със списъци: дискове, възли, enclosures, pools и дълги списъци от записи ключ / стойност. Правилата ги обхождат, търсят в тях и строят нови списъци за текста на alert-а.

1. Какво е array? ​

Досегашните типове - number, string, boolean, null, undefined - са примитивни (прости) типове: по една стойност. Array е съставен тип: подреден списък от стойности, събрани под едно име.

  • Стойностите в array са неговите елементи.
  • Позицията на един елемент е неговият индекс. Както в повечето езици за програмиране, броенето започва от 0.
  • Редът на елементите е важен: ["a", "b"] и ["b", "a"] са различни arrays.
  • Елементът може да бъде стойност от всеки тип - също друг array или обект.
js
// an array of strings
let colors = ["red", "orange", "yellow", "green", "blue"];

// an array with values of different types
let user = ["Ada", 34, true];

// an empty array
let arr = [];

console.log(colors);
console.log(user);
console.log(arr);
Променете кода и го изпълнете отново

Елементите се пишат в квадратни скоби [ ], разделени със запетаи. Запетая след последния елемент (trailing comma) е позволена - тя улеснява редактирането на дълги списъци с по един елемент на ред:

js
const MODELS = [
    "XR-210",
    "XR-220",
    "XR-230",       // trailing comma: fine
];
console.log(MODELS.length);     // 3

const features = doc.inventory.features;    // an array from the report
console.log(features);          // [ 'dedup', 'compression', 'encryption' ]
Променете кода и го изпълнете отново

В паметта variable-ата не пази самите елементи. Тя пази референция - стрелка - към array-я, който живее в отделна област от паметта (heap). Това е важно в раздел 4.

Анимация

Arrays в паметта: 1. Индекси

Създаване на array: [] и по-старото new Array() ​

Винаги създавайте arrays с [...]. В по-стар код ще видите и конструктора Array. Той има капан: с едно число не създава array с това число, а празен array с тази дължина:

js
const a = new Array("XR-210", "XR-220");   // same as ["XR-210", "XR-220"]
const b = new Array(3);                    // NOT [3]: 3 empty places
const c = Array.of(3);                     // [3] (ES2015)

console.log(a, a.length);
console.log(b.length);                     // 3
console.log(c, c.length);                  // [ 3 ] 1
Променете кода и го изпълнете отново

Array.from (ES2015) създава array от нещо, което прилича на списък - например string:

js
console.log(Array.from("4.2"));    // [ '4', '.', '2' ]
console.log([..."4.2"]);           // the same with spread (section 5)
Променете кода и го изпълнете отново

2. Достъп до елементите ​

Елементът се чете или променя с името на array-я, после индекса в квадратни скоби: arrayName[i]. arrayName[i] е израз - винаги дава стойност: елемента.

js
// define the array fruits
let fruits = ["apple", "banana", "strawberry"];

// read the first element
console.log("first fruit is:", fruits[0]);

// write the third element
fruits[2] = "orange";

// print the whole array
console.log("fruits =", fruits);    // [ 'apple', 'banana', 'orange' ]
Променете кода и го изпълнете отново

С отчета:

js
const features = doc.inventory.features;
console.log(features[0]);           // dedup
console.log(features[1]);           // compression

const enclosures = doc.inventory.enclosures;
console.log(enclosures[0].id);      // ENC-01 - first the index, then the key
Променете кода и го изпълнете отново

Индексът е цяло число от 0 нагоре. Работи и variable или израз:

js
const parts = ["4", "2", "1", "7"];
const i = 2;
console.log(parts[i]);          // 1
console.log(parts[i + 1]);      // 7
Променете кода и го изпълнете отново

Четене на несъществуващ индекс ​

Няма грешка. Получавате undefined:

js
let arr = [2, 4, 6];
console.log(arr[4]);      // undefined
console.log(arr[-1]);     // undefined - -1 does not count from the end here
Променете кода и го изпълнете отново

Тихо undefined

Грешният индекс не е грешка, затова бъгът се появява по-късно - например като "undefined" в текста на alert. Когато четете елемент по индекс, питайте се: може ли array-ят да е по-къс, отколкото мисля?

Запис в несъществуващ индекс ​

Елементът се записва там, а местата между тях стават празни (holes):

js
let arr = [2, 4, 6];
arr[4] = 8;
console.log(arr);           // [ 2, 4, 6, <1 empty item>, 8 ]
console.log(arr.length);    // 5
console.log(arr[3]);        // undefined
Променете кода и го изпълнете отново

(<1 empty item> е начинът, по който Node.js и тази страница показват дупка.)

Последният елемент ​

MDN: Array.prototype.at()

Индексът на последния елемент е length - 1:

js
let arr = [2, 4, 6];
console.log(arr[arr.length - 1]);    // 6
console.log(arr.at(-1));             // 6 - at() counts from the end: -1, -2 ...
console.log(arr.at(0));              // 2 - at(0) is the same as arr[0]
Променете кода и го изпълнете отново

ES2022+

at() е ES2022 - проверете дали rule engine-ът ви го поддържа. Формата arr[arr.length - 1] работи навсякъде.

Анимация

Arrays в паметта: 1. Индекси, 2. Извън границите

3. Property-то length ​

MDN: Array: length

arrayName.length е броят на елементите. Той е динамичен: променя се, когато array-ят се промени - и записът в него променя array-я:

js
let arr = [2, 4, 6];
console.log(arr.length);     // 3

// make the array longer: the new places are empty
arr.length = 5;
console.log(arr);            // [ 2, 4, 6, <2 empty items> ]

// make it shorter: the elements at the end are deleted
arr.length = 2;
console.log(arr);            // [ 2, 4 ]

// delete all elements
arr.length = 0;
console.log(arr);            // []
Променете кода и го изпълнете отново

length винаги е с едно повече от последния индекс. Празният array има length 0.

js
const enclosures = doc.inventory.enclosures;
console.log(enclosures.length);                    // 4
console.log(enclosures[enclosures.length - 1].id); // ENC-04
Променете кода и го изпълнете отново

Чести бъгове ​

= вместо === при проверка за празнота:

js
let arr = [2, 4, 6];
if (arr.length = 0) {          // assignment, not comparison!
    console.log("Empty array!");
}
console.log(arr);              // [] - we deleted all elements by mistake
Променете кода и го изпълнете отново

arr.length = 0 е присвояване. То изтрива елементите и дава 0, което е falsy, така че if-ът дори не се изпълнява. Правилният код:

js
let arr = [2, 4, 6];
if (arr.length === 0) {
    console.log("Empty array!");
}
console.log(arr);              // [ 2, 4, 6 ]
Променете кода и го изпълнете отново

Правописна грешка в length:

js
let arr = [2, 4, 6];
if (arr.lenght) {              // lenght, not length
    console.log("Non-empty array");
} else {
    console.log("Empty array");  // this runs!
}
Променете кода и го изпълнете отново

arr.lenght не е грешка: array-ят няма property lenght, затова стойността е undefined - falsy. Правилната проверка if (arr.length) отпечатва Non-empty array. (Защо няма грешка - раздел 4 и занятие 6.) Редакторът ви помага тук: VSCode подчертава непознатите properties, когато файлът започва с // @ts-check (занятие 10).

Празен ли е? ​

ПроверкаОзначава
arr.length === 0array-ят е празен
arr.length > 0има поне един елемент
if (arr.length)същото, кратка форма (0 е falsy)
if (arr)не е проверка за празнота - [] е truthy!
js
const problems = [];
console.log(Boolean(problems));      // true  - an empty array is truthy
console.log(problems.length === 0);  // true  - the right check
Променете кода и го изпълнете отново

Анимация

Arrays в паметта: 2. Извън границите (запис в length и бъгът с =)

4. Array-ят е обект (advanced) ​

В JavaScript arrays са реализирани като обекти:

js
let arr = [2, 4, 6];
console.log(typeof arr);              // object
console.log(Array.isArray(arr));      // true  - the right check
console.log(Array.isArray("2,4,6"));  // false
console.log(Array.isArray({}));       // false
Променете кода и го изпълнете отново

Индексите са ключовете "0", "1", "2" на този обект. Затова грешен "индекс" не дава грешка - той просто става обикновено property на обекта и не се брои в length:

js
let arr = [2, 4, 6];
arr[-1] = 0;      // not a valid index - becomes a property named "-1"
arr["a"] = 10;    // not a valid index - becomes a property named "a"

console.log(arr.length);           // 3
console.log(arr["-1"], arr.a);     // 0 10
console.log(arr[1], arr["1"]);     // 4 4 - the index 1 is the key "1"
// Node.js prints arr as [ 2, 4, 6, '-1': 0, a: 10 ]
Променете кода и го изпълнете отново

Arrays идват и с вградени properties и методи (функции, закачени за array-я) за честите операции: length, push, pop, slice и много други. Не е нужно да ги учите наизуст - те са добре документирани в MDN: Array. Днес ще видите тези, които правилата използват най-често; останалите (map, filter, sort, ...) идват в занятие 7.

Array ли е? Array.isArray ​

MDN: Array.isArray()

JSON данните не винаги имат формата, която очаквате. Списъкът може да липсва, да е null или да е един обект вместо списък с един обект. typeof не може да различи array от обект - Array.isArray може:

js
console.log(typeof [], typeof {}, typeof null);   // object object object
console.log(Array.isArray(doc.inventory.enclosures));  // true
console.log(Array.isArray(doc.inventory.details));     // false - an object
console.log(Array.isArray(doc.inventory.missing));     // false - undefined
Променете кода и го изпълнете отново

Референции: две имена, един array ​

Variable-ата пази референция към array-я, а не самия array. Копирането на variable-ата копира референцията - двете имена сочат към същия array:

js
const fruits = ["apple", "banana"];
let b = fruits;           // no new array - one more arrow to the same one
b.push("kiwi");
console.log(fruits);      // [ 'apple', 'banana', 'kiwi' ] - fruits changed too!
console.log(b === fruits);            // true  - the same array
console.log([1, 2] === [1, 2]);       // false - two different arrays
Променете кода и го изпълнете отново
  • === сравнява arrays по референция: "същият array ли е?", а не "еднакви ли са елементите?".
  • const пази variable-ата (стрелката), а не array-я: fruits.push(...) и fruits[0] = ... са позволени, fruits = [...] е грешка.
js
const fruits = ["apple", "banana"];
fruits[0] = "mango";      // allowed - the array changes
console.log(fruits);
// fruits = ["plum"];     // TypeError: Assignment to constant variable.
Променете кода и го изпълнете отново

Копие е нов array със същите елементи: arr.slice() или [...arr] (раздел 5).

js
const original = doc.inventory.features;
const copy = [...original];        // a new array
copy.push("snapshots");
console.log(original.length, copy.length);   // 3 4 - the report is not changed
Променете кода и го изпълнете отново

Не променяйте отчета

В правилото doc принадлежи на engine-а. Ако една част от правилото промени списък от doc (с push, splice, sort, length = 0 ...), всеки следващ ред вижда променените данни. Работете с копие или създайте нов array.

Сравняване на два arrays

За да проверите дали два кратки arrays от strings или числа имат еднакви елементи, сравнете текста им: a.join(",") === b.join(",") (раздел 7). Проверките в домашните на този курс правят същото с JSON.stringify (занятие 9).

Анимации

Arrays в паметта: 3. Array-ят е обект, 5. Референции

5. Добавяне и премахване на елементи ​

В края и в началото: push, pop, unshift, shift ​

MDN: Array.prototype.push(), Array.prototype.pop(), Array.prototype.unshift(), Array.prototype.shift()

МетодПравиВръща
arr.push(x, ...)добавя в краяновата дължина
arr.pop()премахва последния елементпремахнатия елемент
arr.unshift(x, ...)добавя в началотоновата дължина
arr.shift()премахва първия елементпремахнатия елемент

Как да четете страница от референцията на MDN

Отворете страницата на push по-горе. Всяка страница от референцията има едни и същи части:

  • Заглавието. Array.prototype.push() е метод на всеки array: извиквате го като arr.push(...). Заглавие без prototype, като Array.isArray(), се извиква на самия Array: Array.isArray(x).
  • Syntax (синтаксис). Начините да го извикате, по един на ред. Параметър, който се появява само в някои от редовете, може да се пропусне.
  • Parameters (параметри). Какво означава всеки аргумент. "Optional" означава, че може да го пропуснете.
  • Return value (връщана стойност). Какво връща извикването. За push: новата дължина, а не array-я - колоната "Връща" в таблицата по-горе.
  • Description (описание). Подробностите. За arrays там пише дали методът променя array-я ("a mutating method") или връща нов - вижте Кои методи променят array-я? по-долу.
  • Examples (примери). Кратък код, който може да поставите в редактора и да изпълните.
  • Browser compatibility (съвместимост с браузърите). От коя версия нататък работи възможността. Таблицата има колона и за Node.js. За възможност, по-нова от ES2020, проверете дали вашият rule engine я поддържа.
js
let fruits = ["apple", "banana", "strawberry"];

// number of elements
console.log(fruits.length);      // 3

// add an element at the start
fruits.unshift("orange");
console.log(fruits);             // [ 'orange', 'apple', 'banana', 'strawberry' ]

// add an element at the end
fruits.push("lemon");
console.log(fruits);    // [ 'orange', 'apple', 'banana', 'strawberry', 'lemon' ]

// remove the first element
fruits.shift();
console.log(fruits);             // [ 'apple', 'banana', 'strawberry', 'lemon' ]

// remove the last element
fruits.pop();
console.log(fruits);             // [ 'apple', 'banana', 'strawberry' ]
Променете кода и го изпълнете отново

Върнатите стойности също са полезни - пример с дъгата:

js
const rainbow = ["red", "orange", "yellow", "green", "blue", "indigo", "violet"];
// in a template string, the elements are joined with ","
console.log(`rainbow: ${rainbow}`);

const lastElement = rainbow.pop();      // pop returns the removed element
console.log(`last element: ${lastElement}`);

const firstElement = rainbow.shift();   // so does shift
console.log(`first element: ${firstElement}`);

rainbow.push("black", "white");         // push accepts several elements
console.log(`after push: ${rainbow}`);

rainbow.unshift("black", "white");      // so does unshift
console.log(`after unshift: ${rainbow}`);

rainbow.reverse();                      // reverses the array ITSELF
console.log(`after reverse: ${rainbow}`);
Променете кода и го изпълнете отново

push е методът, който ще пишете най-често: той строи нов списък, по един елемент. Така правилото събира редовете на един alert:

js
const problems = [];
for (const enc of doc.inventory.enclosures) {
    if (enc.state !== "normal") {
        problems.push(enc.id + " is " + enc.state);
    }
}
console.log(problems);              // [ 'ENC-03 is degraded' ]
console.log(problems.length);       // 1
Променете кода и го изпълнете отново

Анимация

Arrays в паметта: 4. push / pop / unshift / shift

slice - копиране на част ​

MDN: Array.prototype.slice()

arr.slice(start, end) връща нов array с елементите от start до end, без да го включва. Оригиналният array не се променя.

js
const parts = ["4", "2", "1", "7"];
console.log(parts.slice(0, 2));    // [ '4', '2' ]   - indexes 0 and 1
console.log(parts.slice(1, 3));    // [ '2', '1' ]
console.log(parts.slice(2));       // [ '1', '7' ]   - without end: to the end
console.log(parts.slice(-2));      // [ '1', '7' ]   - negative: from the end
console.log(parts.slice());        // [ '4', '2', '1', '7' ] - a copy of all
console.log(parts);                // unchanged
Променете кода и го изпълнете отново

end - start е броят на елементите, които получавате. Първите три части на версия:

js
const version = doc.overview.version;          // "4.2.1.7"
const major = version.split(".").slice(0, 3);  // split: section 7
console.log(major);                            // [ '4', '2', '1' ]
Променете кода и го изпълнете отново

splice - премахване, вмъкване или замяна, на място ​

MDN: Array.prototype.splice()

arr.splice(start, deleteCount, item1, item2, ...) променя самия array: от индекс start премахва deleteCount елемента и слага новите елементи на тяхно място. Връща премахнатите елементи в нов array.

js
const models = ["XR-100", "XR-210", "XR-220", "XR-300"];

const removed = models.splice(1, 2);         // remove 2 elements from index 1
console.log(removed);                        // [ 'XR-210', 'XR-220' ]
console.log(models);                         // [ 'XR-100', 'XR-300' ]

models.splice(1, 0, "XR-250");               // remove nothing, insert at index 1
console.log(models);                         // [ 'XR-100', 'XR-250', 'XR-300' ]

models.splice(0, 1, "XR-110", "XR-120");     // replace 1 element with 2
console.log(models);    // [ 'XR-110', 'XR-120', 'XR-250', 'XR-300' ]
Променете кода и го изпълнете отново

Премахване на един елемент, когато знаете стойността му - първо намерете индекса му (раздел 6):

js
const MODELS = ["XR-210", "XR-220", "XR-230"];
const pos = MODELS.indexOf("XR-220");
if (pos !== -1) {                 // only if it is there!
    MODELS.splice(pos, 1);
}
console.log(MODELS);              // [ 'XR-210', 'XR-230' ]
Променете кода и го изпълнете отново

splice(-1, 1), когато елементът не е намерен

Без if-а indexOf дава -1, а splice(-1, 1) премахва последния елемент - отрицателното начало се брои от края.

slice копира, splice изрязва

Имената са почти еднакви, ефектът - не. Как да запомните: splice има една буква повече и прави нещо повече - променя array-я.

Анимации

Методи на arrays: 1. slice и at, 2. splice, 7. splice в loop (advanced)

Съединяване на arrays: concat и spread ​

MDN: Array.prototype.concat(), Spread syntax (...)

js
const ssd = ["ENC-01", "ENC-02"];
const hdd = ["ENC-03", "ENC-04"];

console.log(ssd.concat(hdd));          // [ 'ENC-01', 'ENC-02', 'ENC-03', 'ENC-04' ]
console.log([...ssd, ...hdd]);         // the same with spread (ES2015)
console.log([...ssd, "ENC-09"]);       // spread + one more element
console.log(ssd, hdd);                 // unchanged - both give a new array
Променете кода и го изпълнете отново

...arr (spread) "изсипва" елементите на array-я в новите [ ]. Повече за spread в занятие 6.

Кои методи променят array-я? ​

Променят array-я (mutating)Връщат нов array или стойност
push, pop, unshift, shiftslice, concat, [...a]
spliceindexOf, includes, join, at
reverse, sort (занятие 7)toReversed, toSorted, toSpliced (ES2023)
arr[i] = x, arr.length = n
js
const levels = ["4.1", "4.2", "4.3"];
const reversed = levels.toReversed();   // ES2023: a reversed COPY
console.log(reversed);                  // [ '4.3', '4.2', '4.1' ]
console.log(levels);                    // [ '4.1', '4.2', '4.3' ] - unchanged

levels.reverse();                       // reverses levels itself
console.log(levels);                    // [ '4.3', '4.2', '4.1' ]
Променете кода и го изпълнете отново

ES2023+

toReversed(), toSorted(), toSpliced() и with() са ES2023 - проверете дали rule engine-ът ви ги поддържа. arr.slice().reverse() дава обърнато копие навсякъде.

delete arr[i] в стар код ​

delete arr[i] не премахва елемента - оставя празно място, а length не се променя. Използвайте splice(i, 1):

js
const a = ["x", "y", "z"];
delete a[1];
console.log(a.length, a[1]);    // 3 undefined - a hole
const b = ["x", "y", "z"];
b.splice(1, 1);
console.log(b.length, b);       // 2 [ 'x', 'z' ]
Променете кода и го изпълнете отново

6. Търсене: indexOf, includes, lastIndexOf ​

MDN: Array.prototype.indexOf(), Array.prototype.includes(), Array.prototype.lastIndexOf()

МетодВръща
arr.indexOf(x)индексът на първото x или -1, ако няма такова
arr.lastIndexOf(x)индексът на последното x или -1
arr.includes(x)true или false (ES2016)

И трите сравняват с ===. Вторият аргумент казва откъде да започне: arr.indexOf(x, 3) търси от индекс 3.

js
const states = ["normal", "normal", "degraded", "normal"];
console.log(states.indexOf("normal"));       // 0
console.log(states.lastIndexOf("normal"));   // 3
console.log(states.indexOf("degraded"));     // 2
console.log(states.indexOf("failed"));       // -1 - not found
console.log(states.includes("failed"));      // false
console.log(states.indexOf("normal", 1));    // 1 - search from index 1
Променете кода и го изпълнете отново

Allow-lists ​

В лекция 3 правилото проверяваше модела с &&: if (model !== "XR-210" && model !== "XR-220") return;. С пет модела този ред става труден за четене. Allow-list пази моделите в array:

js
const MODELS = ["XR-210", "XR-220", "XR-230"];
const model = doc.overview.model;

if (!MODELS.includes(model)) {
    console.log("not our model - stop");
} else {
    console.log(model + " is in the list");
}
Променете кода и го изпълнете отново

Добавянето на модел вече е промяна в данните, а не в логиката. Списък от модели, които трябва да се пропуснат, е deny-list - същият код без !.

Същото работи за всяко фиксирано множество от стойности:

js
const BAD_STATES = ["degraded", "failed", "offline"];
const NO_VALUE = [undefined, null, "", "N/A"];

console.log(BAD_STATES.includes(doc.inventory.enclosures[2].state));  // true
console.log(NO_VALUE.includes(doc.inventory.details.bandwidth));  // true - ""
Променете кода и го изпълнете отново

Стар стил: indexOf(...) !== -1 ​

includes се появи в ES2016. По-старите правила пишат същото с indexOf:

По-стари правилаМодерно
MODELS.indexOf(model) !== -1MODELS.includes(model)
MODELS.indexOf(model) === -1!MODELS.includes(model)
MODELS.indexOf(model) >= 0MODELS.includes(model)
MODELS.indexOf(model) > 0бъг: пропуска индекс 0
["XR-210"].includes(m) !== true!["XR-210"].includes(m)
js
const MODELS = ["XR-210", "XR-220"];
console.log(MODELS.indexOf("XR-210") > 0);    // false - but it IS in the list!
console.log(MODELS.includes("XR-210"));       // true
Променете кода и го изпълнете отново

indexOf и includes се различават само за NaN: [NaN].indexOf(NaN) е -1, [NaN].includes(NaN) е true.

Търсенето е точно - първо нормализирайте ​

js
const MODELS = ["XR-210", "XR-220"];
const fromReport = " xr-220";

console.log(MODELS.includes(fromReport));                      // false
console.log(MODELS.includes(fromReport.trim().toUpperCase())); // true
Променете кода и го изпълнете отново

trim() премахва интервалите в двата края, toUpperCase() прави главни букви (повече в занятие 8).

indexOf върху обекти

indexOf и includes сравняват с ===, затова при обекти търсят същия обект, а не "обект със същите ключове": doc.inventory.enclosures.includes({ id: "ENC-01" }) е false. За да намерите запис по един от ключовете му, използвайте loop (раздел 10) - или find (занятие 7).

И strings имат indexOf и includes

"4.2.1.7-B00".includes("-"), "XR-220".indexOf("-") (дава 2), "4.2.1.7".at(-1) и "4.2.1.7".slice(0, 3) работят върху strings по същия начин. Повече в занятие 8.

Анимации

Методи на arrays: 3. indexOf и includes; Алгоритми с arrays: 6. Търсене с break (advanced) - какво прави indexOf отвътре

7. Strings и arrays: split и join ​

split - от string към array ​

MDN: String.prototype.split()

str.split(separator) разрязва string-а на всеки разделител и връща array от парчетата. Разделителите отпадат. Парчетата винаги са strings.

js
console.log("4.2.1.7".split("."));          // [ '4', '2', '1', '7' ]
console.log("CON-01;CON-02".split(";"));    // [ 'CON-01', 'CON-02' ]
console.log("XR-220".split("-"));           // [ 'XR', '220' ]
console.log("4.2.1.7".split("-"));          // [ '4.2.1.7' ] - no "-": one piece
console.log("abc".split(""));               // [ 'a', 'b', 'c' ] - characters
Променете кода и го изпълнете отново

Три употреби, които ще видите във всяко правило:

js
// 1. the first word: the version before " (build ...)"
const level = doc.inventory.details.firmware_level;
console.log(level.split(" ")[0]);               // 4.2.1.7

// 2. the last part of an id: split, then take the last element
const alias = "lab-dev-01";
console.log(alias.split("-").pop());            // 01
console.log(alias.split("-").at(-1));           // 01 (ES2022)

// 3. a list packed into one string
const consoles = doc.inventory.details.consoles.split(";");
console.log(consoles, consoles.length);         // [ 'CON-01', 'CON-02' ] 2
Променете кода и го изпълнете отново

Версии: от strings към числа ​

split дава strings, а strings се сравняват символ по символ - "10" < "9" е true (лекция 3). Преобразувайте всяка част с Number, преди да сравнявате:

js
const parts = "4.10.0.3".split(".");
console.log(parts);                   // [ '4', '10', '0', '3' ]
console.log(parts[1] < "9");          // true  - wrong, string comparison

const numbers = [];
for (const part of parts) {           // for...of: section 8
    numbers.push(Number(part));
}
console.log(numbers);                 // [ 4, 10, 0, 3 ]
console.log(numbers[1] < 9);          // false - right
Променете кода и го изпълнете отново

Това е първата стъпка на versionBelow - цялата функция ще напишете в занятие 5. (В занятие 7 loop-ът се свива до parts.map(Number).)

Пакетирани стойности: "key=value,key=value" ​

Отчетите често пакетират няколко стойности в един string. Две разделяния ги разпакетират: първо по разделителя на списъка, после по разделителя ключ / стойност:

js
const settings = doc.inventory.details.settings;   // "SA=2,SG=1,SN=2"
const pairs = settings.split(",");
console.log(pairs);                     // [ 'SA=2', 'SG=1', 'SN=2' ]

let sg = "";
for (const pair of pairs) {
    const kv = pair.split("=");         // [ 'SG', '1' ] - kv[0] key, kv[1] value
    if (kv[0] === "SG") {
        sg = kv[1];
        break;
    }
}
console.log(sg);                        // 1 - a string!
Променете кода и го изпълнете отново

Стойност, която съдържа разделителя

"note=a=b".split("=") дава [ 'note', 'a', 'b' ] - стойността също се разрязва. Разделете само на първото = с indexOf и slice на string-а:

js
const pair = "note=a=b";
const pos = pair.indexOf("=");
console.log(pair.slice(0, pos));     // note
console.log(pair.slice(pos + 1));    // a=b
Променете кода и го изпълнете отново

(split има втори аргумент, ограничение: "note=a=b".split("=", 2) дава [ 'note', 'a' ] - останалото отпада, така че тук не помага.)

Капаните на split ​

js
console.log("".split(";"));             // [ '' ] - ONE empty string, not []
console.log("".split(";").length);      // 1 - "one console"?!
console.log("CON-01;".split(";"));      // [ 'CON-01', '' ] - empty piece
console.log("CON-01; CON-02".split(";"));   // [ 'CON-01', ' CON-02' ] - a space
Променете кода и го изпълнете отново

Проверете string-а преди да го разрежете и почистете парчетата:

js
const raw = "CON-01; CON-02;";
const list = [];
if (raw !== "") {
    for (const piece of raw.split(";")) {
        const name = piece.trim();      // remove the spaces
        if (name !== "") {              // skip empty pieces
            list.push(name);
        }
    }
}
console.log(list);                      // [ 'CON-01', 'CON-02' ]
Променете кода и го изпълнете отново

join - от array към string ​

MDN: Array.prototype.join()

arr.join(separator) е обратното на split: слепва елементите в един string, с разделителя между тях. Без аргумент разделителят е ",".

js
const parts = ["4", "2", "1", "7"];
console.log(parts.join("."));       // 4.2.1.7
console.log(parts.join(""));        // 4217
console.log(parts.join());          // 4,2,1,7
console.log([].join(", "));         // "" - an empty string
console.log(String(parts));         // 4,2,1,7 - the same as join()
Променете кода и го изпълнете отново

join строи текстовете на alerts. Събирайте частите с push и ги слепете веднъж накрая:

js
const findingParts = [];
const actionParts = [];

findingParts.push("Firmware 4.1.9.3 is below the minimum 4.2.0.0.");
actionParts.push("Upgrade as soon as possible.");
findingParts.push("The authentication service has no password.");
actionParts.push("Set a password for the service account.");

const details = {
    "Description": findingParts.join(" "),
    "Action": actionParts.join(" ")
};
console.log(details.Description);
console.log(details.Action);
Променете кода и го изпълнете отново

Така избягвате класическия loop text = text + ", " + item, който оставя излишно ", " в началото или в края.

Анимации

Методи на arrays: 4. split и join, 5. Пакетирани стойности

8. Обхождане на array ​

Често трябва да направим едно и също с всеки елемент. Класическият начин е for loop по индексите, от 0 до length - 1:

js
let fruits = ["apple", "banana", "strawberry"];

for (let i = 0; i < fruits.length; i++) {
    console.log(`Element ${i}: ${fruits[i]}`);
}
// Element 0: apple
// Element 1: banana
// Element 2: strawberry
Променете кода и го изпълнете отново

Пишете i < arr.length, а не i <= arr.length: последният индекс е length - 1, а arr[length] е undefined.

Анимация

Алгоритми с arrays: 1. Обхождане с for

В обратен ред ​

Започнете от последния индекс, продължете докато i >= 0, стъпка i--:

js
let fruits = ["apple", "banana", "strawberry"];

for (let i = fruits.length - 1; i >= 0; i--) {
    console.log(`Element ${i}: ${fruits[i]}`);
}
// Element 2: strawberry
// Element 1: banana
// Element 0: apple
Променете кода и го изпълнете отново

Най-новият запис в списък с история често е последният - обхождайте назад и спрете с break при първото съвпадение.

Анимация

Алгоритми с arrays: 2. Обратен ред

for...of - елементите, без индекса ​

MDN: for...of

for...of (ES2015) дава елементите един по един. Използвайте го, когато не ви трябва индексът - това е в повечето случаи:

js
for (const feature of doc.inventory.features) {
    console.log(feature);
}

// with the index too: entries() gives [index, element] pairs
for (const [i, feature] of doc.inventory.features.entries()) {
    console.log(i, feature);
}
Променете кода и го изпълнете отново

const тук е наред: всяко завъртане получава нова variable feature. ([i, feature] е destructuring - повече в занятие 6.)

ИзползвайтеКога
for (const x of arr)трябват ви само елементите - по подразбиране
for (let i = 0; i < arr.length; i++)трябва ви индексът, стъпка, различна от 1, или два arrays с един и същ индекс
for (let i = arr.length - 1; i >= 0; i--)назад или когато премахвате елементи по време на обхождането

for...in не е за arrays

for (const i in arr) обхожда ключовете: те са strings ("0", "1", ...), и идват и допълнителните properties (arr.a от раздел 4). По-старите правила го използват върху arrays - работи за обикновени arrays, но i + 1 дава "01", а не 1! Използвайте for...in за обекти (занятие 6), а for...of или for - за arrays.

js
const arr = ["a", "b"];
for (const i in arr) {
    console.log(i, typeof i, i + 1);    // 0 string 01, then 1 string 11
}
Променете кода и го изпълнете отново

arr.forEach(function (x) { ... }) - още един loop, който ще видите в правилата - приема функция, затова идва в занятия 5 и 7.

Пример: всеки елемент, квадратът му, сумата ​

Пример с бъг. Изпълнете го - кой ред е грешен?

js
const numbers = [1, 2, 3, 4, 5];

console.log("Every element of the array:");
for (let i = 0; i < numbers.length; i++) {
    console.log("\t" + numbers[i]);
}

console.log("Every element, to the power of 2:");
for (let i = 0; i < numbers.length; i++) {
    console.log("\t" + numbers[i] ** 2);
}

console.log("The sum of the elements:");
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
    sum += i;
}
console.log("\t" + sum);       // 10?! 1 + 2 + 3 + 4 + 5 is 15
Променете кода и го изпълнете отново
Отговор

sum += i добавя индекса, а не елемента: 0 + 1 + 2 + 3 + 4 = 10. Трябва да е sum += numbers[i]. С for...of този бъг не може да се случи: for (const n of numbers) { sum += n; }.

Акумулаторът: сума и брой ​

Една variable събира резултата, докато loop-ът върви. Дайте ѝ правилната начална стойност преди loop-а: 0 за сума или брой, "" за текст, [] за списък.

js
// the sum of all numbers from 1 to 10
let sum = 0;
for (let x = 1; x <= 10; x++) {
    sum += x;
}
console.log("sum = " + sum);     // 55

// the same idea on the report: count the enclosures in a state
let normal = 0;
for (const enc of doc.inventory.enclosures) {
    if (enc.state === "normal") {
        normal++;
    }
}
const total = doc.inventory.enclosures.length;
console.log(`${normal} of ${total} enclosures are normal`);
Променете кода и го изпълнете отново

Проверете се: отпечатайте сумата на всички положителни четни числа в [-2, 3, 7, 8, -1, 4, 2]. (Очаквано: 14.)

Отговор
js
const numbers = [-2, 3, 7, 8, -1, 4, 2];
let sum = 0;
for (const n of numbers) {
    if (n > 0 && n % 2 === 0) {
        sum += n;
    }
}
console.log(sum);    // 8 + 4 + 2 = 14

Същото за диапазон от числа, без array: сумата от 1 до 100 и нечетните числа под 100, които се делят на 11:

js
let total = 0;
for (let i = 1; i <= 100; i++) {
    total += i;
}
console.log("sum:", total);       // 5050

const odd11 = [];
for (let i = 1; i < 100; i++) {
    if (i % 2 !== 0 && i % 11 === 0) {
        odd11.push(i);
    }
}
console.log(odd11);               // [ 11, 33, 55, 77, 99 ]
Променете кода и го изпълнете отново

Анимация

Алгоритми с arrays: 3. Сума на елементите

Намиране на елементи по условие ​

Четните числа в array:

js
const numbers = [2, 3, 5, 8, 5];
for (const n of numbers) {
    if (n % 2 === 0) {
        console.log(n);       // 2, 8
    }
}
Променете кода и го изпълнете отново

Отрицателните нечетни числа:

js
const numArray = [-1, -2, -3, -4, -5, 0, 1, 2, 3, 4, 5];
console.log("The negative odd array elements are:");
for (const n of numArray) {
    if (n < 0 && n % 2 !== 0) {
        console.log(n);       // -1, -3, -5
    }
}
Променете кода и го изпълнете отново

Две задачи в един loop: нечетните числа и нечетните положителни числа:

js
const a = [0, 2, 3, 41, 5, 60, 7, 80];
for (const n of a) {
    if (n % 2 !== 0) console.log(n);             // 3, 41, 5, 7
}

const b = [0, 2, -3, 41, -5, -60, 7, 11, -80];
for (const n of b) {
    if (n > 0 && n % 2 !== 0) console.log(n);    // 41, 7, 11
}
Променете кода и го изпълнете отново

Събиране в нови arrays ​

Вместо да отпечатвате, съберете съвпадащите елементи в нов array с push. Задачата separateArrayElementsInNewArrays: положителните четни числа отиват в един array, отрицателните нечетни - в друг (нулата не е нито положителна, нито отрицателна):

js
const numbersArr = [1, 6, -3, 5, 2, -8, 0, 4, 3];
const evenNums = [];
const oddNums = [];

for (const n of numbersArr) {
    if (n > 0 && n % 2 === 0) {
        evenNums.push(n);
    } else if (n < 0 && n % 2 !== 0) {
        oddNums.push(n);
    }
}
console.log(`evenNums: ${evenNums}`);    // 6,2,4
console.log(`oddNums: ${oddNums}`);      // -3
Променете кода и го изпълнете отново

С отчета - id-тата на enclosures, които не са на firmware "2.14":

js
const outdated = [];
for (const enc of doc.inventory.enclosures) {
    if (enc.firmware !== "2.14") {
        outdated.push(enc.id);
    }
}
console.log(outdated);             // [ 'ENC-02', 'ENC-04' ]
console.log(outdated.join(", "));  // ENC-02, ENC-04
Променете кода и го изпълнете отново

Анимация

Алгоритми с arrays: 5. Филтриране в нови arrays

Най-големият (и най-малкият) елемент ​

Вземете първия елемент за най-голям засега, после го сравнете с всеки следващ:

js
const temps = [31, 34, 37, 29, 38, 30];
let max = temps[0];
let maxIndex = 0;
for (let i = 1; i < temps.length; i++) {
    if (temps[i] > max) {
        max = temps[i];
        maxIndex = i;
    }
}
console.log(`max = ${max}, index = ${maxIndex}`);    // max = 38, index = 4
Променете кода и го изпълнете отново

Не започвайте от 0

let max = 0; изглежда безобидно, но само с отрицателни числа резултатът е 0 - стойност, която я няма в array-я. Започнете от arr[0]. И първо проверете, че array-ят не е празен: [][0] е undefined.

Math.max(...temps) дава същия максимум на един ред (spread подава елементите като отделни аргументи; Math в занятие 8) - но не и индекса.

Анимация

Алгоритми с arrays: 4. Най-голям елемент

Търсене с break ​

Когато ви трябва един елемент, спрете при първото съвпадение:

js
const ids = ["ENC-01", "ENC-02", "ENC-03", "ENC-04"];
let index = -1;                  // -1 = "not found"
for (let i = 0; i < ids.length; i++) {
    if (ids[i] === "ENC-03") {
        index = i;
        break;                   // do not look further
    }
}
console.log(index);              // 2
Променете кода и го изпълнете отново

За обикновена стойност това е просто ids.indexOf("ENC-03"). Loop пишете, когато условието е нещо повече от === - например "първият enclosure, който не е normal" (раздел 10).

Въртене в кръг: % ​

В уеб страница един бутон може да сменя цвета на една кутия при всяко щракване, като обикаля списък с цветове. Без браузъра идеята е индексът с % length:

js
const colors = ["blue", "purple", "red"];
let i = 0;
for (let click = 1; click <= 7; click++) {
    console.log(`click ${click}: ${colors[i]}`);
    i = (i + 1) % colors.length;      // 0, 1, 2, 0, 1, 2, ...
}
Променете кода и го изпълнете отново

Така се разпределят проверки върху списък от конзоли, една след друга (round robin).

9. Arrays от arrays (двумерни) ​

В JavaScript няма истински двумерни (или многомерни) arrays. Но елементът може да е от всеки тип - също array - затова таблицата е array от arrays: всеки елемент е ред, а всеки ред е array.

js
const matrix = [
    ["a", "b", "c"],
    ["d", "e", "f"],
    ["g", "h", "i"],
];
console.log("matrix:", matrix);
Променете кода и го изпълнете отново

Достъп: първо редът, после колоната ​

js
const matrix = [
    ["a", "b", "c"],
    ["d", "e", "f"],
    ["g", "h", "i"],
];
console.log("row 0, col 2:", matrix[0][2]);   // c
console.log("row 1, col 1:", matrix[1][1]);   // e
console.log("row 2, col 0:", matrix[2][0]);   // g
console.log(matrix.length, matrix[0].length); // 3 3 - rows, columns
Променете кода и го изпълнете отново

matrix[1][1] се чете на две стъпки: matrix[1] дава реда ["d", "e", "f"], а [1] от този ред дава "e".

Анимации

Arrays в паметта: 6. Двумерен array (advanced), 7. Общ ред (advanced) - защо [row, row] е капан

Обхождане по редове и колони ​

Два вложени loops: външният по редовете (i), вътрешният по колоните на текущия ред (j):

js
const matrix = [
    ["a", "b", "c"],
    ["d", "e", "f"],
    ["g", "h", "i"],
];
const rows = matrix.length;
const cols = matrix[0].length;

for (let i = 0; i < rows; i++) {
    console.log("row:", i);
    for (let j = 0; j < cols; j++) {
        console.log("  " + matrix[i][j]);
    }
}
Променете кода и го изпълнете отново

С for...of същото е по-кратко - и работи, когато редовете са с различна дължина:

js
const matrix = [["a", "b", "c"], ["d", "e"], ["f"]];
for (const row of matrix) {
    console.log(row.join(" | "));
}
Променете кода и го изпълнете отново

Сумата на главния диагонал (m[0][0] + m[1][1] + m[2][2]) иска само един loop:

js
const m = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];
let sum = 0;
for (let i = 0; i < m.length; i++) {
    sum += m[i][i];
}
console.log(`sum = ${sum}`);    // 1 + 5 + 9 = 15
Променете кода и го изпълнете отново

Анимация

Алгоритми с arrays: 7. Двумерен array (advanced)

Проверете се: fruitsData пази имената в ред 0, а цените в ред 1. Отпечатайте apple = 1.2, orange = 2, cherry = 3.5 с loop.

js
const fruitsData = [
    ["apple", "orange", "cherry"],
    [1.2, 2.0, 3.5],
];

// the long way:
// console.log(fruitsData[0][0] + " = " + fruitsData[1][0]);
// console.log(fruitsData[0][1] + " = " + fruitsData[1][1]);
// console.log(fruitsData[0][2] + " = " + fruitsData[1][2]);

// your loop here:
Променете кода и го изпълнете отново
Отговор

Името и цената на един плод са в една и съща колона j, затова обхождайте колоните:

js
for (let j = 0; j < fruitsData[0].length; j++) {
    console.log(fruitsData[0][j] + " = " + fruitsData[1][j]);
}

2.0 се отпечатва като 2: за JavaScript 2.0 и 2 са едно и също число.

Таблица в отчета ​

Отчетите понякога изпращат таблица като редове от стойности, а имената на колоните - в отделен списък, като първия ред на CSV файл например:

js
const header = ["id", "temperature"];
const rows = [
    ["ENC-01", "31"],
    ["ENC-02", "34"],
    ["ENC-03", "38"],
];
const col = header.indexOf("temperature");   // find the column by its name
for (const row of rows) {
    if (Number(row[col]) > 35) {
        console.log(row[0] + " is too hot: " + row[col] + " C");
    }
}
Променете кода и го изпълнете отново

header.indexOf(...) вместо фиксирано row[1]: кодът продължава да работи, ако експортът добави колона отпред.

10. Списъци от записи ​

Записът е група от стойности за едно нещо - един enclosure, един диск, една настройка. В JSON записът е обект { key: value, ... } (обектите подробно: занятие 6). Списъкът от записи е array от такива обекти, всички с едни и същи ключове:

js
const enclosures = doc.inventory.enclosures;
console.log(enclosures.length);        // 4
console.log(enclosures[0]);   // the whole first record: an object
console.log(enclosures[2].state);      // degraded - index first, then the key

for (const enc of enclosures) {
    console.log(enc.id, enc.type, enc.firmware, enc.state);
}
Променете кода и го изпълнете отново

Записи като arrays или като обекти? ​

По-старият код често пази записа като array:

js
// [gender, age, first name, last name]
const students = [
    ["male", 24, "Ivan", "Ivanov"],
    ["female", 22, "Maria", "Petrova"],
    ["male", 18, "Georgi", "Georgiev"],
    ["male", 21, "Nikolai", "Nikolov"],
];

// the youngest student: start with the first one, compare with the rest
let youngest = students[0];
for (const student of students) {
    if (student[1] < youngest[1]) {
        youngest = student;
    }
}
console.log(`The youngest: ${youngest[2]} ${youngest[3]}, ${youngest[1]} years old.`);
Променете кода и го изпълнете отново

Работи, но какво е student[1]? Трябва да помните, че индекс 1 е възрастта. Обектът го казва: student.age. Затова JSON отчетите използват обекти за записите.

Проверете се: намерете най-възрастния студент. (Очаквано: Ivan Ivanov, 24.)

Отговор

Същият loop с > вместо <: if (student[1] > oldest[1]) { oldest = student; }, като започнете с let oldest = students[0];. Честа грешка е да се започне с minAge = students[0][1] и да се остави youngestStudent недефинирана - ако първият студент е най-младият, програмата гърми. Започването с целия първи запис избягва това.

Задача в същата форма - владетели и годините на управлението им. Имената са с разбъркани малки и главни букви, затова една малка helper функция ги оправя:

js
const rulers = [
    ["asPARuH", 681, 700],
    ["tERveL", 700, 718],
    ["SeVaR", 738, 753],
    ["telERIG", 766, 777],
    ["kaRDAm", 777, 803],
    ["krum", 803, 814],
    ["OmurTAg", 814, 831],
];

// ready-made: first letter upper case, the rest lower case (session 8)
function capitalize(str) {
    return str[0].toUpperCase() + str.slice(1).toLowerCase();
}

// the rulers who ruled more than 15 years
for (const ruler of rulers) {
    const years = ruler[2] - ruler[1];
    if (years > 15) {
        console.log(`${capitalize(ruler[0])} ruled ${years} years.`);
    }
}
Променете кода и го изпълнете отново

Намиране, броене, събиране ​

Три въпроса, които задавате на един списък от записи отново и отново:

js
const enclosures = doc.inventory.enclosures;

// FIND: the first record that matches - stop with break
let firstBad;                                  // undefined = not found
for (const enc of enclosures) {
    if (enc.state !== "normal") {
        firstBad = enc;
        break;
    }
}
console.log(firstBad ? firstBad.id : "all normal");    // ENC-03

// COUNT: how many records match
let hddCount = 0;
for (const enc of enclosures) {
    if (enc.type.startsWith("HDD")) {
        hddCount++;
    }
}
console.log(hddCount);                         // 2

// COLLECT: a value from every matching record
const oldFirmware = [];
for (const enc of enclosures) {
    if (enc.firmware !== "2.14") {
        oldFirmware.push(`${enc.id} (${enc.firmware})`);
    }
}
console.log(oldFirmware.join(", "));           // ENC-02 (2.10), ENC-04 (2.10)
Променете кода и го изпълнете отново

startsWith проверява началото на string - занятие 8. В занятие 7 тези три loops стават find, filter(...).length и filter + map.

Анимация

Методи на arrays: 6. Списък от записи

Дълъг списък от записи ключ / стойност ​

Някои експорти изобщо не изпращат вложен обект. Всяка стойност е малък запис, а списъкът има хиляди такива:

js
const records = [
    { group: "Global Resources", item: "SYS", code: "BU", label: "Release", value: "10.12.39.0" },
    { group: "Global Resources", item: "SYS", code: "FN", label: "Family", value: "AR9" },
    { group: "Enclosures", item: "ENC-01", code: "FW", label: "Firmware", value: "2.14" },
    { group: "Enclosures", item: "ENC-01", code: "ST", label: "State", value: "normal" },
    { group: "Enclosures", item: "ENC-02", code: "FW", label: "Firmware", value: "2.10" },
    { group: "Enclosures", item: "ENC-02", code: "ST", label: "State", value: "degraded" },
    { group: "Partitions", item: "P1", code: "SN", label: "Settings", value: "SA=2,SG=2,SN=2" },
];

// one value: the record with item "SYS" and code "BU"
let release;
for (const r of records) {
    if (r.item === "SYS" && r.code === "BU") {
        release = r.value;
        break;
    }
}
console.log(release);                   // 10.12.39.0

// all values of one kind: the firmware of every enclosure
const firmware = [];
for (const r of records) {
    if (r.group === "Enclosures" && r.code === "FW") {
        firmware.push(r.item + "=" + r.value);
    }
}
console.log(firmware);                  // [ 'ENC-01=2.14', 'ENC-02=2.10' ]
Променете кода и го изпълнете отново

Папката с домашните има цял експорт в тази форма: Homework/data/report-records.json (79 записа). Задачите за упражнение го използват.

Хиляди записи: не търсете в loop

За да намерите една стойност, loop обхожда списъка - веднъж е наред. Но "за всеки enclosure намери състоянието му", написано като loop в loop, обхожда целия списък отново за всеки enclosure: с 9500 записа и 100 enclosures това са почти милион сравнения. Обходете списъка веднъж и съберете каквото ви трябва. В занятие 7 ще построим индекс (обект или Map с ключ item) веднъж и след това всяко търсене е мигновено.

Един запис или списък? ​

Едни и същи данни могат да дойдат като списък в един отчет и като единичен обект в друг (например когато има само един enclosure). Първо ги нормализирайте до array, тогава останалата част от кода има само една форма:

js
function asList(value) {
    if (Array.isArray(value)) return value;           // already a list
    if (value === undefined || value === null) return [];  // nothing
    return [value];                                   // one value -> a list of one
}

console.log(asList(doc.inventory.enclosures).length);       // 4
console.log(asList({ id: "ENC-09", state: "normal" }));     // a list of one
console.log(asList(undefined));                             // []
Променете кода и го изпълнете отново

(function с return е тема на занятие 5 - четете го като "върни тази стойност".)

11. Четене на съществуващи правила ​

Първо изпълнете правилото от началото на лекцията. emit тук е фалшив: само отпечатва alert-а.

js
function emit(id, details) {          // fake: prints instead of creating an alert
    console.log("ALERT for " + id + ": " + JSON.stringify(details));
}

// ready-made helper - we write our own in session 5
function versionBelow(a, b) {
    const pa = String(a).split(".").map(Number);
    const pb = String(b).split(".").map(Number);
    for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
        const x = pa[i] || 0;
        const y = pb[i] || 0;
        if (x !== y) return x < y;
    }
    return false;
}

const MODELS = ["XR-210", "XR-220"];

function rule(doc) {
    if (!doc.overview || !doc.inventory?.details) return;

    const model = doc.overview.model;
    if (!MODELS.includes(model)) return;

    const level = doc.inventory.details.firmware_level ?? "";
    const version = level.split(" ")[0];

    const enclosures = doc.inventory.enclosures;
    if (!Array.isArray(enclosures)) return;
    const problems = [];
    for (const enc of enclosures) {
        if (enc.state !== "normal") {
            problems.push(`${enc.id} is ${enc.state}`);
        }
        if (versionBelow(enc.firmware, "2.14")) {
            problems.push(`${enc.id} has firmware ${enc.firmware}`);
        }
    }
    if (problems.length === 0) return;

    const name = doc.inventory.extended_info?.json?.data?.system?.name
                 || doc.status.device_alias;
    emit(name, {
        "Model": model,
        "Version": version,
        "Problems": problems.length,
        "Details": problems.join("; ")
    });
}

console.log("example JSON document:");
rule(doc);

console.log("all enclosures fine:");
rule({
    overview: { model: "XR-210" },
    inventory: {
        details: { firmware_level: "4.2.1.7 (build 412.17)" },
        enclosures: [{ id: "ENC-01", firmware: "2.14", state: "normal" }]
    },
    status: { device_alias: "LAB-DEV-02" }
});
Променете кода и го изпълнете отново

Сменете състоянието на ENC-01 във втория документ на "failed" и изпълнете отново. После сменете enclosures на един обект вместо списък (махнете [ ]). Какво става и защо?

Ето същото правило в по-стария стил. Прочетете го спокойно и решете какво да модернизирате:

js
function emit(id, details) {
    console.log("ALERT for " + id + ": " + JSON.stringify(details));
}

function versionBelow(a, b) {
    var pa = String(a).split(".");
    var pb = String(b).split(".");
    for (var i = 0; i < Math.max(pa.length, pb.length); i++) {
        var x = parseInt(pa[i], 10) || 0;
        var y = parseInt(pb[i], 10) || 0;
        if (x < y) return true;
        if (x > y) return false;
    }
    return false;
}

var MODELS = new Array("XR-210", "XR-220");

function rule(doc) {  // the engine calls rule(doc) - keep this line as it is
    if (!doc.overview || !doc.inventory || !doc.inventory.details) return;

    var model = doc.overview.model;
    if (MODELS.indexOf(model) == -1) return;

    var level = String(doc.inventory.details.firmware_level || "");
    var version = level.split(" ")[0];

    var enclosures = doc.inventory.enclosures;
    if (!enclosures || !enclosures.length) return;
    var problems = new Array();
    for (var i = 0; i < enclosures.length; i++) {
        var enc = enclosures[i];
        if (enc.state != "normal") {
            problems.push(enc.id + " is " + enc.state);
        }
        if (versionBelow(enc.firmware, "2.14") == true) {
            problems.push(enc.id + " has firmware " + enc.firmware);
        }
    }
    if (problems.length == 0) return;

    var info = doc.inventory.extended_info;
    var name = info && info.json && info.json.data && info.json.data.system
               && info.json.data.system.name || doc.status.device_alias;

    var details = new Object();
    details["Model"] = model;
    details["Version"] = version;
    details["Problems"] = problems.length;
    details["Details"] = problems.join("; ");
    emit(name, details);
}

console.log("example JSON document:");
rule(doc);

console.log("enclosures sent as a string:");
rule({
    overview: { model: "XR-210" },
    inventory: { details: { firmware_level: "4.2.1.7" }, enclosures: "ENC-01" },
    status: { device_alias: "LAB-DEV-03" }
});
Променете кода и го изпълнете отново

Проверете се:

  1. Избройте разликите между двете версии. Кои променят само стила и кои биха могли да променят резултата?
  2. new Array("XR-210", "XR-220") създава същия списък като ["XR-210", "XR-220"]. Какво би създало new Array(2)? Защо [...] е по-сигурно?
  3. Изпълнете старото правило: вторият документ изпраща enclosures като string. Какво прави старото правило с него и какво би направило модерното?
  4. if (!enclosures || !enclosures.length) return; и if (!Array.isArray(enclosures)) return; - еднакво ли се държат за []? За undefined? За един обект { id: "ENC-01", ... }?
  5. Един отчет има firmware_level: "4.2.1.7" - без частта (build ...). Каква е version в двете правила?
Отговор
  1. Само стил: var -> const / let; new Array(...) -> [...]; indexOf(model) == -1 -> !includes(model) (същото за strings); == / != -> === / !==; for с индекс -> for...of; == true -> нищо; + -> template strings; веригата с && -> ?.; new Object() + details["..."] -> обектен литерал. Биха могли да променят резултата: guard-ът за enclosures (въпроси 3 и 4) и String(... || "") срещу ?? "".
  2. Празен array с дължина 2 (две празни места), а не [2]. [...] винаги означава "тези елементи".
  3. String-ът има length (6), затова старият guard го пропуска и loop-ът обхожда символите: enc е "E", "N", ... enc.state и enc.firmware са undefined, така че всеки символ дава два "проблема" (versionBelow(undefined, "2.14") също е true: parseInt дава NaN, а || 0 го прави 0). Дванадесет проблема като "undefined is undefined" - безсмислен alert. Модерното правило спира: string-ът не е array.
  4. []: старото правило спира на guard-а (![].length е true); модерното минава нула пъти през loop-а и спира на problems.length === 0 - същият резултат. undefined: и двете спират. Един обект: обектът няма length, затова старото правило спира; Array.isArray е false, затова и модерното спира - и двете пропускат enclosure-а. Поправка: const enclosures = asList(doc.inventory.enclosures); (раздел 10).
  5. "4.2.1.7" и в двете: когато няма интервал, split(" ") дава array с целия string, а [0] е този string.

12. Упражнения ​

Във вашето копие на Homework repository първо вземете новите задачи:

bash
git pull --no-rebase --no-edit upstream main
node 04-arrays-and-records/practice/task1-predict.js

Всеки файл обяснява задачата в началото. Пишете кода си между редовете your code; проверките в края отпечатват OK или FAIL.

ЗадачаФайлКакво правите
1task1-predict.jsПредсказвате 20 резултата от операции с arrays, после се проверявате
2task2-model-list.jsAllow-list от модели: проверка, добавяне без повторения, премахване
3task3-split-values.jssplit и join: частите на версия като числа, последната част на id, пакетирани списъци и strings key=value
4task4-records.jsСписък от 79 записа ключ / стойност: намиране на стойност, броене, събиране
5task5-classic-loops.jsСума, максимум и филтриране върху обикновени arrays и върху таблица

Обобщение ​

ТемаЗапомнете
ArrayПодреден списък; индекс от 0; [...] за създаване; елементи от всеки тип
Достъпarr[i]; липсващ индекс дава undefined, без грешка; последният: arr[arr.length - 1] или arr.at(-1)
lengthБрой елементи, с едно повече от последния индекс; записът в него променя array-я; [] е truthy - проверявайте length === 0
Обектtypeof е "object"; използвайте Array.isArray; === сравнява референции; const не замразява array-я
Добавяне / премахванеpush / pop в края, unshift / shift в началото, splice навсякъде - те променят array-я
Копиеslice(start, end) (end не се включва), [...arr], concat - нови arrays
ТърсенеindexOf (индекс или -1), includes (true / false), lastIndexOf; точно === - първо нормализирайте
Stringssplit(sep) -> array от strings; join(sep) -> string; "".split(",") е [""]
Loopsfor...of за елементите, for с i за индексите и назад; не for...in
МоделиАкумулатор (сума, брой), максимум от arr[0], събиране с push, търсене с break
Таблициmatrix[row][col]; вложени loops
ЗаписиArray от обекти; намиране, броене, събиране с едно обхождане; без търсене в loop
Стар стилnew Array(...), indexOf(x) !== -1, for (var i ...), !arr.length - четете го, пишете модерната форма

Домашно ​

  1. 04-arrays-and-records/homework.js (в homework repository): имената в пакетиран string с firmware нива, редове за alert от три списъка, празните pools и една пакетирана настройка в експорта със записи, и още две задачи с loops: четни / нечетни числа и бъг за поправяне в loop-а на една игра.
  2. Задачите за упражнение, които не сте довършили в час - препоръчително, защото следващите задачи и курсовият проект ги използват отново. Всяка проверка трябва да отпечата OK.
  3. По желание: вземете едно от вашите правила и намерете всеки array в него. Откъде идва всеки (от отчета, от split, от самото правило)? Има ли indexOf(...) !== -1 или for...in върху array, които бихте модернизирали? Променя ли правилото array от doc?

Още за четене ​

Следващото занятие: функции. Helper функциите, които използвахте готови - versionBelow, asList, capitalize - ще напишете сами и ще се научите да четете по-стария код function (x) { ... } в съществуващите правила.

Създаден за