Praca domowa 0x01. Podsumowanie

Z uwagi na przytłaczające ilości nadesłanych rozwiązań uznałem, że najlepiej będzie jak sam napiszę rozwiązanie pracy domowej i je zaprezentuję. ...

5 years ago, comments: 2, votes: 50, reward: $2.51

Z uwagi na przytłaczające ilości nadesłanych rozwiązań uznałem, że najlepiej będzie jak sam napiszę rozwiązanie pracy domowej i je zaprezentuję.

Poprzednio przenosiłem rozwiązania z Golang, jednak różnice w składni są na tyle różne, że to nie ma sensu. Publikuję zatem moje rozwiązania, ale jeśli chcesz spróbować swoich sił, zrób to śmiało i prześlij mi rozwiązania, a ja z przyjemnością je opublikuję.

1. FizzBuzz

Będziecie wypisywać coś na standardowe wyjście dla każdej liczby od 1 do 100.
Jeśli liczba jest podzielna przez 3, wypiszecie Fizz.
Jeśli liczba jest podzielna przez 5, wypiszecie Buzz.
Jeśli liczba jest podzielna przez 3 i 5 jednocześnie, wypiszecie FizzBuzz.
W każdej innej sytuacji wypiszecie wartość liczby.

Podejście pierwsze. Musimy powędrować po stu liczbach i coś z nimi zrobić.

W trakcie pisania edytor zasugerował mi skorzystanie z pętli for .. in range. Jakoś tak się przyjęło w programowaniu, że zakresy są lewostronnie domknięte i prawostronnie otwarte, co znaczy że wartość z lewej będzie w zakresie, a z prawej nie. 1..100 to zatem liczby od 1 do 99.

Zauważyłem też, że nie mogę napisać print!(i), muszę print!("{}", i). Taka konwencja.

fn fizz_buzz() {
    for i in 1..101 { // 1..100 to od 1 do 99. Prawa strona jest wyłączna
        if i % 3 == 0 {
            print!("Fizz")
        }
        if i % 5 == 0 {
            print!("Buzz")
        }

        if i % 3 != 0 && i % 5 != 0 {
            print!("{}", i) // print!(i) nie działa
        }
        print!("\n")
    }
}

Dlaczego zmienną w pętli nazwałem i? Mogłem nazwać Steven, nie ma problemu. Jakoś się tak przyjęło że w pętlach z braku laku korzysta się ze zmiennych i, j, k etc.

Podejście drugie, użyjmy pętli while:

fn fizz_buzz2() {
    let mut i = 1;
    while i <= 100 {
        if i % 3 == 0 {
            print!("Fizz") // średniki nie są zawsze potrzebne
        }
        if i % 5 == 0 {
            print!("Buzz")
        }

        if i % 3 != 0 && i % 5 != 0 {
            print!("{}", i)
        }
        print!("\n");
        i += 1; // i = i+1
    }
}

Jak wspominałem w artykule, zmienna o modyfikowalnej wartości musi mieć mut. Pętla while ma warunek wejścia/wyjścia i<= 100, a na końcu zwiększam jej wartość o jeden (zauważ, że mogę to zrobić na więcej niż jeden sposób). TIMTOWTDI - There Is More Than One Way To Do It.

Zwróciłem uwagę, że czasem muszę podawać średniki, a czasem nie. Różnie języki różnie to robią, widać że Rust wymaga średników tam, gdzie mógłby błędnie sobie rozdzielić dwie sąsiadujące komendy. Print w bloku warunku nie wymaga średnika, print na końcu już tak.

Zazwyczaj w programowaniu korzysta się z numerowania od zera. Przygotowałem zatem modyfikację powyższego kodu, gdzie tak właśnie robię:

fn fizz_buzz3() {
    let mut i = 0;
    while i < 100 {
        i = i + 1;
        if i % 3 == 0 {
            print!("Fizz")
        }
        if i % 5 == 0 {
            print!("Buzz")
        }

        if i % 3 != 0 && i % 5 != 0 {
            print!("{}", i)
        }
        print!("\n");
    }
}

i teraz ma wartość 0, while ma warunke na mniejsze, a nie mniejsze-równe, a inkrementacja (powiększenie o jeden) następuje na początku bloku pętli.

Spójrzmy jeszcze na pętlę loop:

fn fizz_buzz4() {
    let mut i = 1;
    loop {
        if i > 100 { break; }
        if i % 3 == 0 {
            print!("Fizz")
        }

//        if (i%5 == 0) ... if ( (i%5!=0) && i%3!=0 ) ==
//        if (i%5 == 0) ... else (i%3 !0)
        if i % 5 == 0 {
            print!("Buzz")
        } else if i % 3 != 0 {
            print!("{}", i)
        }
        print!("\n");
        i = i + 1;
    }
}

W pętli tej nie ma warunku końca, zamiast tego dodaję if w jej ciele i break, które przerywa wykonanie pętli i wyskakuje poza nią. break działa z każdą z pętli, to takie polecenie "Przestań robić i opuść pętlę". Mamy jeszcze jedno szczególne, continue, które przerywa dane wykonanie pętli i rozpoczyna kolejne, jeśli tylko pętla może kontyuować.

Zrobiłem też szacher-macher z warunkami. Zauważcie że poprzednio dwa warunki sprawdzały przeciwne stany, więc jeden miał szanse powodzenia tylko w tedy, gdy drugi się nie udał. Połączyłem je zatem i wyrzuciłem zbędne sprawdzanie reszty z dzielenia przez pięć, bo już znamy jego rezultat.

Na koniec nieco inne podejście: zamiast drukować na bieżąco mogę sprawdzić warunki i utworzyć zmienną tekstową z wartością. Jeśli zmienna będzie pusta - drukuję liczbę, jeśli nie - drukuję jej wartość:

fn fizz_buzz5() {
    for i in 1..101 {
        let mut out = String::new();
        if i % 3 == 0 {
            out.push_str("Fizz")
        }
        if i % 5 == 0 {
            out.push_str("Buzz")
        }

        if out.is_empty() {
            println!("{}", i)
        } else {
            println!("{}", out)
        }
    }
}

Język Rust umożliwia nam tworzenie obiektów, tak jak to robię ze Stringiem. Potem doklejam do niego nowe kawałki, sprawdzam czy jest pusty. W przyszłości poświęcimy zmiennym tekstowym trochę wiecej czasu.

Pół choinki

Chcę, abyście wypisały pół choinki z dowolnie wybranego znaku (bez pniaka)
Choinka ma mieć trzy rzędy gałęzi, każdy niższy większy od poprzednich. Ma to wyglądać mniej więcej tak:

@
@@
@@@
@@
@@@
@@@@
@@@@@
@@@
@@@@
@@@@@
@@@@@@
@@@@@@@

Szybka analiza oczekiwanego efektu pod kątem tego, jak można podzielić całe wykonanie na pętle:

  • trzy rzędzy gałęzi
  • rzędzy mają kolejno 3, 4 i 5 wierszy
  • ilość znaków w pierwszym wierszu to kolejno 1, 2 i 3
  • każdy kolejny wiersz w rzędzie ma o jeden znak więcej od poprzedniego

Zamierzam użyć trzech zagnieżdżonych pętli:

  1. do liczenia rzędów: 0..3 (czyli 0,1,2)
  2. do liczenia wierszy w rzędzie
  3. do liczenia znaków w wierszu

Wierszy w rzędzie gałęzi jest odpowiednio 3, 4 i 5. W momencie wchodzenia do pętli mamy już liczbę identyfikującą rząd (0, 1 lub 2), więc możemy ją wykorzystać i ogólnie przyjąć, że wierszy w rzędzie jest 3 + numer rzędu, a zakres to 0..3+rzad.
Znaków w wierszu jest najmniej 1, 2 lub 3, a najwięcej 3, 5 lub 7. Pamiętajmy, że mamy w momencie wchodzenia do pętli numery rzędu oraz wiersza w nim. Dla wiersza 0 w segmencie 0 mamy mieć jeden znak, dla wiersza 1 w segmencie 1 mamy mieć ich trzy, dla wiersza 4 w segmencie 2 - siedem. Pominę tu rozwiązaywanie układu równań czy czego, ale na chłopski rozum wychodzi mi, że górne ograniczenie liczby znaków to 1 + numer rzędu + numer wiersza, a zakres to 0..1+rzad+wiersz.

fn pol_choinki1() {
    for rzad in 0..3 {
        for wiersz in 0..3 + rzad {
            for i in 0..1+rzad+wiersz {
                print!("@")
            }
            print!("\n")
        }
    }
}

Kod działa, ale podczas kompilacji dostaję uwagę, że i jest nieużywaną zmienną. Bo jest. Sugestia jest taka, aby nazwać ją _ albo czymś rozpoczętym od _, na przykład _costam. W ten sposób możemy określić wartość, która gdzie zostaje zwrócona ale nas nie interesuje. Ma to swój sens, ale też ma dwa znaczenia. _ oznacza "nie potrzebuję, posprzątaj", a _costam - "nie będę korzystał z tej wartości w moim kodzie, więc nie wypominaj mi tego, ale wartość zachowaj". W naszym przypadku nie potrzebujemy tej wartości i możemy jej się pozbyć. Drobna poprawka:

fn pol_choinki2() {
    for rzad in 0..3 {
        for wiersz in 0..3 + rzad {
            for _ in 0..1+rzad+wiersz {// nieużywana zmienna
                print!("@")
            }
            print!("\n")
        }
    }
}

Pół choinki na wyjściu błędów

Wydrukuj choinkę z zadania 2 na wyjściu błędów. Wyjaśnij czym jest wyjście błędów, standardowe wyjście, standardowe wejście.

Drukowanie na wyjściu błędów wymaga drobnej zmiany:

fn pol_choinki1() {
    for rzad in 0..3 {
        for numer_wiersza in 0..3 + rzad {
            for _ in 0..1+rzad+numer_wiersza {// nieużywana zmienna
                eprint!("@")
            }
            eprint!("\n")
        }
    }
}

Do print! dopisałem e jak error. Jakoś tak się przyjęło, że uruchamiany program ma ze sobą połączone trzy strumienie danych: dwa wyjściowe i jeden wejściowy. Nazywa się je standardowym wejściem, standardowym wyjściem i wyjściem błędów (z angielskie dokładnie byłby to standardowy błąd). Rozdzielenie strumienia wyjścia na dwa umożliwia stworzenie takie programu, aby ignorować w nim wypisane na wyjście rezultaty chyba że są błędami. W aplikacjach graficznych tak bardzo się na to nie patrzy jak w przypadku konsolowych, gdzie wyjście z jednego programu może trafić na wejście drugiego, aby szybko przetwarzać strumienie danych. Gdybym na przykład miał program, który zapisuje mi w logu informacje w stylu:

20190429 12:00:43 Wynik operacji: sukces
20190429 12:00:44 Wynik operacji: sukces
20190429 12:00:45 Wynik operacji: sukces
20190429 12:00:45 Wynik operacji: blad1
20190429 12:00:45 Wynik operacji: blad2
20190429 12:00:45 Wynik operacji: sukces
20190429 12:01:43 Wynik operacji: sukces
20190429 12:02:43 Wynik operacji: sukces

I powiedzmy że takich informacji jest pełno, setki albo tysiące, albo setki tysięcy wpisów, gdybym zauważył, że błąd2 występuje częściej niż zwykle i obawiam się, że mamy jakieś przejściowe problemy, mógłbym zrobić tak:

grep blad2 log | cut -c 1-14 | uniq -c

aby dowiedzieć się, ile błędów tego typu miałem na minutę. grep filtruje zawartość pliku log i wypisuje na standardowe wyjście tylko te wiersze, które zawierają blad2, cut przyjmuje to na standardowym wejściua a na wyjściu zwraca znaki 1-14 z każdego wiersza, czyli datę, godziny i minuty, a uniq -c łączy zduplikowane linie z wejścia i dodaje do nich liczbę wystąpień. Rezultat byłby mniej więcej taki:

...
234: 20190429 12:00
23: 20190429 12:02
...

Wygodnie, nie? Może odstraszać, ale zdarzało mi się pracować z milionami wpisów z danymi i taka operacja ratuje tyłek. Jeśli jeszcze chciałbym to zapisać do pliku, wyglądałoby to tak:

grep blad2 log | cut -c 1-14 | uniq -c > plik.txt

> to znak mówiący "zapisz dane ze standardowego wyjścia do pliku". Jeśli program popełni jakiś błąd, strumień błędów nie został przekierowany do pliku, więc wynik wyświetli się pod poleceniem. Więcej teraz nie podaję, bo i tak nie będę teraz Ciebie wprowadzał w tajniki pracy w trybie tekstowym.

Cała choinka

Chodzi o to, abyście wypisały pełną choinkę, nie tylko pół.

Ma to wyglądać tak:

      @@
     @@@@
    @@@@@@
     @@@@
    @@@@@@
   @@@@@@@@
  @@@@@@@@@@
    @@@@@@
   @@@@@@@@
  @@@@@@@@@@
 @@@@@@@@@@@@
@@@@@@@@@@@@@@

Małpek ma być w każdej linijce dwa razy więcej niż w połowie choinki, ale to jeszcze za mało, potrzebujemy jeszcze znaków, aby wykonać symetryczne wcięcia. Nie w każdym przypadku to wyjdzie, ale przy programowaniu zazwyczaj korzysta się z czcionek o stałej szerokości znaków, więc spacja zajmuje tyle co @. Jak duże muszą być zatem wcięcia?

Ostatnia linia ma 14 małpek i zero wcięć. Pierwsza zaś ma małpki dwie. W połówce choinki jest odpowiednio 7 i 1. Suma znaków małp i spacji w połowie choinki musi być w każdej linijce taka sama i wynieść 7.

Budując na bazie poprzednich rozwiązań otrzymałem taki rezultat:

fn cala_choinka() {
    for segment in 0..3 {
        for rozmiar_polowy_wiersza in 1 + segment..4 + 2 * segment {
            for _ in 0..7-rozmiar_polowy_wiersza {
                print!(" ")
            }
            for _ in 0..2*rozmiar_polowy_wiersza {
                print!("@")
            }
            print!("\n")
        }
    }
}

I to by było na tyle.

Jak widać, zadania nie są jakieś bardzo trudne. Tym raze skupialiśmy się na kilku elementach struktury programu, szczególnie były to pętle i warunki. W językach programowania czasem nazywa się je także poleceniami warunkowego skoku, z perspektywy komputera są to bowiem grupy poleceń gdzie na podstawie wyrażenia decyduje się o skoczeniu do konkretnej linii z poleceniem. Czasem skacze się naprzód, czasem wstecz. Czase pomija się kilka poleceń, czasem się je powtarza. Czasem wyskakuje się z grupy poleceń tak, aby już nie wracać do niej.

W następnej lekcji zapoznamy się nieco z pojęciem danych. Do zobaczenia.