Глава 6.1. Вложени цикли

В настоящата глава ще разгледаме вложените цикли и как да използваме for цикли за чертане на различни фигурки на конзолата, които се състоят от символи и знаци, разположени в редове и колони на конзолата. Ще използваме единични и вложени цикли (цикли един в друг), изчисления и проверки, за да отпечатваме на конзолата прости и не чак толкова прости фигурки по зададени размери.

Видео

Гледайте видео-урок по тази глава тук: https://www.youtube.com/watch?v=1v1yIZV7p4k&feature=youtu.be.

Пример: правоъгълник от 10 x 10 звездички

Да се начертае в конзолата правоъгълник от 10 x 10 звездички.

Вход Изход
(няма) **********
**********
**********
**********
**********
**********
**********
**********
**********
**********

Насоки и подсказки

Как работи примерът? Инициализира се цикъл с променлива i = 0, която се увеличава на всяка итерация на цикъла, докато е по-малка или равна на 10 (i <= 10). Така кодът в тялото на цикъла се изпълнява 10 пъти. В тялото на цикъла се печата на нов ред в конзолата "*".repeat(10), което създава низ от 10 звездички.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#0.

Пример: правоъгълник от N x N звездички

Да се напише програма, която въвежда цяло положително число n и печата на конзолата правоъгълник от N x N звездички.

Вход Изход Вход Изход Вход Изход
2 **
**
3 ***
***
***
4 ****
****
****
****

Насоки и подсказки

Забележка

В някои уеб браузъри еднаквите резултати в конзолата се сливат в един. Препоръчително е да използвате NodeJS за примерите. Ако все пак стигнете до този случай, можете да добавите знак за нов ред \n на края на метода за отпечатване: console.log("*".repeat(10) + "\n");

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#1.

Вложени цикли

Вложените цикли представляват конструкция, при която в тялото на един цикъл (външен) се изпълнява друг цикъл (вътрешен). За всяко завъртане на външния цикъл, вътрешният се извърта отново. Това се случва по следния начин:

  • При стартиране на изпълнение на вложени цикли първо стартира външният цикъл: извършва се инициализация на неговата управляваща променлива и след проверка за край на цикъла, се изпълнява кодът в тялото му.
  • След това се изпълнява вътрешният цикъл. Извършва се инициализация на началната стойност на управляващата му променлива, прави се проверка за край на цикъла и се изпълнява кодът в тялото му.
  • При достигане на зададената стойност за край на вътрешния цикъл, програмата се връща една стъпка нагоре и се продължава започналото изпълнение предходния (външния) цикъл. Променя се с една стъпка управляващата променлива за външния цикъл, проверява се дали условието за край е удовлетворено и започва ново изпълнение на вложения (вътрешния) цикъл.
  • Това се повтаря докато променливата на външния цикъл достигне условието за край на цикъла.

Ето и един пример, с който нагледно да илюстрираме вложените цикли. Целта е да се отпечата отново правоъгълник от n * n звездички, като за всеки ред се извърта цикъл от 1 до n, а за всяка колона се извърта вложен цикъл от 1 до n:

function drawSquare(n) {
    for (let i = 1; i <= n; i++) {
        let stars = "";

        for (let j = 1; j <= n; j++) {
            stars += "*";
        }

        console.log(stars);
    }
}

Да разгледаме примера по-горе. След инициализацията на първия (външен) цикъл, започва да се изпълнява неговото тяло, което съдържа втория (вложен) цикъл. Той сам по себе запазва низ от n на брой звездички, в променлива, и след това ги печата на един ред. След като вътрешният цикъл приключи изпълнението си при първата итерация на външния, то след това външният ще продължи. След това ще се извърши обновяване на променливата на първия цикъл и отново ще бъде изпълнен целият втори цикъл. Вътрешният цикъл ще се изпълни толкова пъти, колкото се изпълнява тялото на външния цикъл, в случая n пъти.

Пример: квадрат от звездички

Да се начертае на конзолата квадрат от N x N звездички:

Вход Изход Вход Изход Вход Изход
2 * *
* *
3 * * *
* * *
* * *
4 * * * *
* * * *
* * * *
* * * *

Насоки и подсказки

Задачата е аналогична на предходната. Разликата тук е, че в тази трябва да обмислим как да печатаме интервал след звездичките по такъв начин, че да няма излишни интервали в началото или края:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#2.

Пример: триъгълник от долари

Да се напише програма, която въвежда число n и печата триъгълник от долари.

Вход Изход Вход Изход Вход Изход
3 $
$ $
$ $ $
4 $
$ $
$ $ $
$ $ $ $
5 $
$ $
$ $ $
$ $ $ $
$ $ $ $ $

Насоки и подсказки

Задачата е сходна с тези за рисуване на правоъгълник и квадрат. Отново ще използваме вложени цикли, но тук има уловка. Разликата е в това, че броя на колонките, които трябва да разпечатаме, зависят от реда, на който се намираме, а не от входното число n. От примерните входни и изходни данни забелязваме, че броят на доларите зависи от това на кой ред се намираме към момента на печатането, т.е. 1 долар означава първи ред, 3 долара означават трети ред и т.н. Нека разгледаме долния пример по-подробно. Виждаме, че променливата на вложения цикъл е обвързана с променливата на външния. По този начин нашата програма печата желания триъгълник:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#3.

Пример: квадратна рамка

Да се напише програма, която въвежда цяло положително число n и чертае на конзолата квадратна рамка с размер n * n.

Вход Изход Вход Изход
3 + - +
| - |
+ - +
4 + - - +
| - - |
| - - |
+ - - +
Вход Изход Вход Изход
5 + - - - +
| - - - |
| - - - |
| - - - |
+ - - - +
6 + - - - - +
| - - - - |
| - - - - |
| - - - - |
| - - - - |
+ - - - - +

Насоки и подсказки

Можем да решим задачата по следния начин:

  • Отпечатваме горната част: първо знак +, после n-2 пъти - и накрая знак +.
  • Отпечатваме средната част: печатаме n-2 реда като първо печатаме знак |, после n-2 пъти - и накрая отново знак |. Това можем да го постигнем с вложени цикли.
  • Отпечатваме долната част: първо +, после n-2 пъти - и накрая +.

Ето и примерна имплементация на описаната идея, с вложени цикли:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#4.

Пример: ромбче от звездички

Да се напише програма, която въвежда цяло положително число n и печата ромбче от звездички с размер n.

Вход Изход Вход Изход
1 * 2  * 
* *
 * 
Вход Изход Вход Изход
3   *  
 * * 
* * *
 * * 
  *  
4    *   
  * *  
 * * * 
* * * *
 * * * 
  * *  
   *   

Насоки и подсказки

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

  • Отпечатваме n - row интервала.
  • Отпечатваме *.
  • Отпечатваме row - 1 пъти *.

Втората (долна) част ще разпечатаме по аналогичен начин, което отново оставяме на читателя да се опита да направи сам:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#5.

Пример: коледна елха

Да се напише програма, която въвежда число n (1 ≤ n ≤ 100) и печата коледна елха с височина n+1.

Вход Изход Вход Изход
1   |  
* | *
2    |   
 * | * 
** | **
Вход Изход Вход Изход
3     |    
  * | *  
 ** | ** 
*** | ***
4      |     
   * | *   
  ** | **  
 *** | *** 
**** | ****

Насоки и подсказки

От примерите виждаме, че елхата може да бъде разделена на три логически части. Първата част са звездичките и празните места преди и след тях, средната част е (интервал)|(интервал), а последната част са отново звездички, като този път празни места има само преди тях. Разпечатването може да бъде постигнато само с един цикъл и метода .repeat(n), който ще използваме един път за звездичките и един път за интервалите:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#6.

Чертане на по-сложни фигури

Да разгледаме как можем да чертаем на конзолата фигури с по-сложна логика на конструиране, за които трябва повече да помислим преди да почнем да пишем.

Пример: слънчеви очила

Да се напише програма, която въвежда цяло число n (3 ≤ n ≤ 100) и печата слънчеви очила с размер 5*n x n като в примерите:

Вход Изход Вход Изход
3 ******   ******
*////*|||*////*
******   ******
4 ********    ********
*//////*||||*//////*
*//////*    *//////*
********    ********
Вход Изход
5 **********     **********
*////////*     *////////*
*////////*|||||*////////*
*////////*     *////////*
**********     **********

Насоки и подсказки

От примерите виждаме, че очилата могат да се разделят на три части – горна, средна и долна. По-долу е част от кода, с който задачата може да се реши. При рисуването на горния и долния ред трябва да се изпечатат 2 * n звездички, n интервала и 2 * n звездички:

При печатането на средната част трябва да проверим дали редът е (n - 1) / 2 - 1, тъй като от примерите е видно, че на този ред трябва да печатаме вертикални чертички вместо интервали. Проблемът с (n - 1) / 2 - 1, е че може да бъде число с десетичен остатък. Пример за n = 6: (6 - 1) / 2 - 1 => 5 / 2 - 1 => 2.5 - 1 => 1.5. Поради тази причина, трябва да приложим математически метод за премахване на десетичната част - Math.floor(...). Методът Math.floor(...) връща най-голямото цяло число, което е по-малко или равно на подаденото число:

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#7.

Пример: къщичка

Да се напише програма, която въвежда число n (2 ≤ n ≤ 100) и печата къщичка с размери n x n, точно като в примерите:

Вход Изход Вход Изход Вход Изход
2 **
||
3 -*-
***
|*|
4 -**-
****
|**|
|**|
Вход Изход Вход Изход
5 --*--
-***-
*****
|***|
|***|
8 ---**---
--****--
-******-
********
|******|
|******|
|******|
|******|

Насоки и подсказки

Разбираме от условието на задачата, че къщата е с размер n x n. Това, което виждаме от примерните вход и изход, е че:

  • Къщичката е разделена на 2 части: покрив и основа.

  • Когато n е четно число, върхът на къщичката е "тъп".
  • Когато n е нечетно число, покривът е с един ред по-голям от основата.
Покрив
  • Съставен е от звезди и тирета.
  • В най-високата си част има една или две звезди, спрямо това дали n e четно или нечетно, както и тирета.
  • В най-ниската си част има много звезди и малко или никакви долни черти.
  • С всеки един ред по-надолу, звездите се увеличават с 2, а тиретата намаляват с 2.
Основа
  • Дълга е n на брой реда.
  • Съставена е от звезди и тирета.
  • Редовете представляват 2 тирета - по едно в началото и в края на реда, както и звезди между тиретата с дължина на низа n - 2.

Подаваме n, като параметър на нашата функция:

Много е важно да проверяваме дали са валидни входните данни! В тези задачи не е проблем директно да обръщаме подаденият параметър в Number, защото изрично е казано, че ще получаваме валидни целочислени числа. Ако обаче правите по-сериозни приложения е добра практика да проверявате данните. Какво ще стане, ако вместо буквата "А", потребителя въведе число?

За да начертаем покрива, записваме колко ще е началният брой звезди в променлива stars:

  • Ако n е четно число, ще са 2 броя.
  • Ако е нечетно, ще е 1 брой.

Изчисляваме дължината на покрива. Тя е равна на половината от n. Резултата записваме в променливата roofLength:

Важно е да се отбележи че, когато n е нечетно число, дължината на покрива е по-голяма с един ред от тази на основата. В езика JavaScript, когато два целочислени типа се делят и има остатък, то резултата ще е десетично число. Пример:

    let result = 3 / 2; // резултат 1.5

Ако искаме да закръглим нагоре, трябва да използваме метода Math.ceil(…): let result = Math.ceil(3 / 2);. Резултатът от 3 / 2 е 1.5. Math.ceil(…) ще закръгли резултата от делението нагоре. В нашият случай 1.5 ще се закръгли на 2. parseInt() се използва, за да трансформираме входния параметър в тип Number.

След като сме изчислили дължината на покрива, завъртаме цикъл от 0 до roofLength. На всяка итерация ще:

  • Изчисляваме броя тирета, които трябва да изрисуваме. Броят ще е равен на (n - stars) / 2. Записваме го в променлива padding:

  • Отпечатваме на конзолата: "тирета" (padding / 2 на брой пъти) + "звезди" (stars пъти) + "тирета" (padding / 2 пъти):

  • Преди да свърши итерацията на цикъла добавяме 2 към stars (броя на звездите):

След като сме приключили с покрива, е време за основата. Тя е по-лесна за печатане:

  • Започваме с цикъл от 0 до n (изключено).
  • Отпечатваме на конзолата: | + * (n - 2 на брой пъти) + |.

Ако всичко сме написали както трябва, задачата ни е решена.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#8.

Пример: диамант

Да се напише програма, която въвежда цяло число n (1 ≤ n ≤ 100) и печата диамант с размер n, като в следните примери:

Вход Изход Вход Изход Вход Изход
1 *
2 ** 3 -*-
*-*
-*-
Вход Изход Вход Изход Вход Изход
4 -**-
*--*
-**-
5 --*--
-*-*-
*---*
-*-*-
--*--
6 --**--
-*--*-
*----*
-*--*-
--**--
Вход Изход Вход Изход Вход Изход
7 ---*---
--*-*--
-*---*-
*-----*
-*---*-
--*-*--
---*---
8 ---**---
--*--*--
-*----*-
*------*
-*----*-
--*--*--
---**---
9 ----*----
---*-*---
--*---*--
-*-----*-
*-------*
-*-----*-
--*---*--
---*-*---
----*----

Насоки и подсказки

Това, което знаем от условието на задачата, е че диамантът е с размер n x n.

От примерните вход и изход можем да си направим изводи, че всички редове съдържат точно по n символа и всички редове, с изключение на горните върхове, имат по 2 звезди. Можем мислено да разделим диаманта на 2 части:

  • Горна част. Тя започва от горният връх до средата.
  • Долна част. Тя започва от реда след средата до най-долния връх (включително).
Горна част
  • Ако n е нечетно, то тя започва с 1 звезда.
  • Ако n е четно, то тя започва с 2 звезди.
  • С всеки ред надолу, звездите се отдалечават една от друга.
  • Пространството между, преди и след звездите е запълнено с тирета.
Долна част
  • С всеки ред надолу, звездите се събират една с друга. Това означава, че пространството (тиретата) между тях намалява, а пространството (тиретата) отляво и отдясно се увеличава.
  • В най-долната си част е с 1 или 2 звезди, спрямо това дали n е четно или не.
Горна и долна част на диаманта
  • На всеки ред звездите са заобиколени от външни тирета, с изключение на средния ред.
  • На всеки ред има пространство между двете звезди, с изключение на първия и последния ред (понякога звездата е 1).

Подаваме стойността на n като входящ параметър (число) на функция:

Започваме да чертаем горната част на диаманта. Първото нещо, което трябва да направим, е да изчислим началната стойност на външната бройка тирета leftRight (тиретата от външната част на звездите). Тя е равна на (n - 1) / 2, закръглено надолу. За закръглянето ще използваме метода Math.floor(...), за да премахнем остатъка от деленето. Такъв може да има при входящо нечетно n:

След като сме изчислили броя на външните тирета leftRight, започваме да чертаем горната част на диаманта. Може да започнем, като завъртим цикъл от 0 до n / 2 + 1 (закръглено надолу). Като при всяка итерация на цикъла трябва да се изпълнят следните стъпки:

  • Рисуваме по конзолата левите тирета (с дължина leftRight) и веднага след тях първата звезда:

  • Ще изчислим разстоянието между двете звезди. Може да го изчислим като извадим от n дължината на външните тирета, както и числото 2 (бройката на звездите, т.е. очертанията на диаманта). Резултата от тази разлика записваме в променлива mid:

  • Ако стойността на mid е по-малка от 0, то тогава знаем, че на реда трябва да има 1 звезда. Ако е по-голяма или равна на 0, то тогава трябва да начертаем тирета с дължина mid и една звезда след тях.

  • Рисуваме на конзолата десните външни тирета с дължина leftRight:

  • В края на цикъла намаляваме leftRight с 1 (звездите се отдалечават).

Готови сме с горната част.

Рисуването на долната част е доста подобна на рисуването на горната част. Разликите са, че вместо да намаляваме leftRight с 1 към края на цикъла, ще увеличаваме leftRight с 1 в началото на цикъла. Също така, цикълът ще е от 0 до (n - 1) / 2.

Повторението на код се смята за лоша практика, защото кодът става доста труден за поддръжка. Нека си представим, че имаме парче код (напр. логиката за чертането на ред от диаманта) на още няколко места и решаваме да направим промяна. За целта би било необходимо да минем през всичките места и да направим промените. Нека си представим, че трябва да използвате код не 1, 2 или 3 пъти, а десетки пъти. Начин за справяне с този проблем е като се използват функции. Можете да потърсите допълнителна информация за тях в Интернет, или да прегледате глава 10. Функции.

Ако сме написали всичко коректно, задачата ни е решена.

Тестване в Judge системата

Тествайте решението си тук: https://judge.softuni.bg/Contests/Practice/Index/935#9.

Какво научихме от тази глава?

Запознахме се с конструктора с методът repeat(...) на обектите от тип String:

let foo = "*".repeat(10);

Научихме се да чертаем фигури с вложени for цикли:

for (let i = 1; i <= n; i++) {
    let stars = "";

    for (let j = 1; j <= n; j++) {
        stars += "*";
    }

    console.log(stars);
}

Упражнения: чертане на фигурки в уеб среда

Сега, след като свикнахме с вложените цикли и как да ги използваме, за да чертаем фигурки на конзолата, можем да се захванем с нещо още по-интересно: да видим как циклите могат да се използват за чертане в уеб среда. Ще направим уеб приложение, което визуализира числов рейтинг (число от 0 до 100) със звездички. Такава визуализация се среща често в сайтове за електронна търговия, ревюта на продукти, оценки на събития, рейтинг на приложения и други.

Не се притеснявайте, ако не разберете целия код, как е точно е направен и как точно работи проектът. Нормално е, сега се учим да пишем код, не сме стигнали до технологиите за уеб разработка. Ако имате трудности да си напишете проекта, следвайки описаните стъпки, гледайте видеото от началото на тази глава или питайте в СофтУни форума: https://softuni.bg/forum.

Задача: рейтинги – визуализация в уеб среда

Да се разработи JavaScript приложение за визуализация на рейтинг (число от 0 до 100). Чертаят се от 1 до 10 звездички (с половинки). Звездичките да се генерират с for цикъл.

Отваряме празна папка във файловата система с име "ratings". В нея създаваме два файла и една папка:

  • index.html
  • script.js
  • images (папка)

Сега добавяме картинките със звездичките (те са част от файловете със заданието за този проект и могат да бъдат свалени от тук). Копираме ги от Windows Explorer и ги поставяме в папката images с copy/paste.

Отваряме index.html и въвеждаме следният код:

Този код създава едно поле input-rating, в което потребителят може да въвежда число в интервала [0 … 100] и бутон [Draw], който осъществява пресмятането на звездичките с въведената стойност. Действието, което ще обработи данните, се казва drawRating. След формата се отпечатва съдържанието на <div id="ratingHolder"></div>. Кодът, който ще се съдържа в него, ще бъде динамично генериран HTML с поредица от звездички.

Добавяме функция drawRating() във файла script.js, която има следният код:

/**
 * drawRating, рисува HTML, който е нужен за визуализацията на звездичките
 * @param {Number} rating
 * @return {String} html
 */
function drawRating(rating) {
    // низ от HTML
    let html = "";

    // краен брой звезди
    let allStars = 10;

    // всички пълни звезди
    let fullStars = Math.floor(rating / allStars);

    // всички празни звезди
    let emptyStars = Math.floor((100 - rating) / allStars);

    // всички наполовина запълнени звезди
    let halfStars = allStars - fullStars - emptyStars;

    // построяване на HTML
    for (let i = 0; i < fullStars; i++) {
        html += '<img src="images/full-star.png">';
    }
    for (let i = 0; i < halfStars; i++) {
        html += '<img src="images/half-star.png">';
    }
    for (let i = 0; i < emptyStars; i++) {
        html += '<img src="images/empty-star.png">';
    }

    // връщане на готовият HTML
    return html;
}

Горният код взима въведеното число rating, прави малко пресмятания и изчислява броя пълни звездички, броя половинки звездички и броя празни звездички, след което генерира HTML код, който нарежда няколко картинки със звездички една след друга, за да сглоби от тях картинката с рейтинга. Подготвеният HTML код се връща като резултат от функцията и е готов за по-нататъшно използване. Към момента резултатът от тази функция не може да се използва, защото няма как да го свържем с бутона. Въвеждаме функция, която се казва drawHandler() и съдържа следният код:

/**
 * drawHandler, функция която се изпълнява, когато потребителя клика върху
 * бутона "Draw".
 * @return {Void}
 */
function drawHandler() {
    // намиране на инпут елемента, който държи числото на рейтинга
    // и вземане на неговата стойност
    let ratingInput = document.getElementById("input-rating");

    // по подразбиране всички стойности от форми идват като "string"
    // за това се налага да ги обърнем в число чрез "parseInt()"
    let rating = parseInt(ratingInput.value);

    // намиране на елемента, който държи звездичките
    let ratingHolder = document.getElementById("ratingHolder");

    // генериране на HTML на база въведеният от потребителя рейтинг
    let html = drawRating(rating);

    // рисуване на страницата
    ratingHolder.innerHTML = html;
}

Фунцкията drawHandler() прави няколко неща:

  • Намира HTML елемента, който държи рейтинга (input-rating) и взема неговата стойност.
  • Обръща стойността от низ към число.
  • Намира HTML елемента, който ще държи звездичките (ratingHolder).
  • Генерира HTML-а на звездичките, чрез drawRating(...) функцията.
  • Поставя ново генерираният HTML чрез innerHTML метод в елемента ratingHolder.

Имаме нужда от още една функция, която да обедини горните две и да ги свърже с HTML елементите. Тази функция се казва appInit(), и както името подсказва, нейната роля е да стартира нашето приложение. Въвеждаме следният код във функцията appInit():

/**
 * appInit, отговаря за първоначалното изпълнение на нашата програма
 * @return {Void}
 */
function appInit() {
    // намиране на бутон елемента в HTML
    let button = document.getElementById("input-draw");

    // Закачане към събитието "click" за изпълнение на рисуването
    button.addEventListener("click", drawHandler);

    // първоначално изрисуване на рейтинга
    drawHandler();
}

След като имаме всички функции въведени е време да стартираме нашето приложение. Обърнете внимание, че script.js е включен в края на нашият файл, точно преди затварящият </body> таг на страницата. Това е добра практика и осигуря по-бързо зареждане на DOM дървото. Това също ни позволява да изпълняваме JavaScript код, който използва HTML елементи. При тези условия можем да бъдем сигурни, че всички те са вече заредени в паметта на браузъра.

Въпреки това, вместо да извикаме директно appInit() на края на файла, ще добавим още една добра практика:

/**
 * Стартиране на приложението асинхронно, чрез "event listener".
 * Слушане за "DOMContentLoaded".
 */
document.addEventListener("DOMContentLoaded", appInit);

Събитието DOMContentLoaded осигурява, че браузърът е приключил всички действия по създаването на DOM дървото. Закачайки се на него с addEventListener(...) осигурява правилното изпълнение на нашата JavaScript програма.

Когато браузърът е готов ще изпълни нашата стартираща функция appInit(). Резултатът от функцията е:

  • Закачане на функцията drawHandler() към събитието click върху нашият Draw бутон.
  • Първоначално извикване на drawHandler(), за да попълним звездичките на база на текущият HTML.

Ако имате проблеми с примерния проект по-горе, гледайте видеото в началото на тази глава. Там приложението е направено на живо стъпка по стъпка с много обяснения. Или питайте във форума на СофтУни: https://softuni.bg/forum.

results matching ""

    No results matching ""