5. Pętelki, rangi i nie tylko, czyli piszemy wyszukiwarkę piątków trzynastego.

Znamy już kilka sposobów na wykonanie instrukcji warunkowych, ale co z pętlami, wspominanymi w pierwszym odcinku?

Akcja dla każdego elementu

Mając jakąś tablicę, np. napisaną w poprzednim odcinku tablicę nazw dni tygodnia, możemy chcieć wykonać jakąś akcję na każdym jej elemencie. Dla przykładu, chcemy wszystkie elementy wypowiedzieć dodając po każdym z nich znak kropki:

weekDays=["niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"]
weekDays.each { |wd| 
  alert "#{wd}." 
}

W nawiasach klamrowych umieszczony jest blok kodu, który ma być wykonany dla każdego (each) elementu tablicy. W naszym przypadku, ten blok składa się tylko z jednej instrukcji, ale oczywiście mogłoby ich być dużo więcej.

Skoro blok ma działać na każdym elemencie, to musi jakoś mieć do niego dostęp. Sposób ten, to zmienna zapisana między znaczkami “|”: w naszym przykładzie jest to zmienna wd (skrót od week day), ale mogłaby się nazywać dowolnie.

Każdy element, a gdzie jego indeks?

Czasem prócz samego elementu, potrzebna jest wiedza, który jest to element listy.

Skorzystamy do tego z konstrukcji each_with_index, która w bloku, prócz zmiennej z elementem, przyjmuje jeszcze drugą zawierającą indeks:

weekDays=["niedziela", "poniedziałek", "wtorek", "środa", "czwartek", "piątek", "sobota"]
weekDays.each_with_index { |wd,indx| 
  alert "#{wd} to element o indeksie #{indx}." 
}

Trzy razy przez lewe ramię…

Prócz operacji na wszystkich elementach tablicy, potrzebne są czasem pętle, które muszą być wykonane określoną ilość razy, nie operując na żadnych listach elementów.

Ruby udostępnia do tego ciekawą konstrukcję:

5.times {
  alert "test. "
}

To samo zrobimy ze znajomością numeru konkretnego przebiegu pętli.

5.times {|num|
  alert "test #{num}. "
}

Podobnie jak przy indeksach tablicy, numeracja odbywa się od wartości 0.

Zakresy czyli rangi.

Kolejnym ciekawym elementem języka Ruby są zakresy. Zapisujemy je w następujący sposób:

(1..10)

Wpisanie powyższego w konsolę nie dało zbyt interesującego rezultatu, ale łatwo może się to zmienić, gdy skorzystamy ze znanej już metody each:

(1..10).each {|i|
  alert("#{i}. ")
  }

Usłyszymy wypowiedziane kolejne liczby z podanego zakresu czyli od 1 do 10.

Konstrukcji tej można użyć zamiast znanej z innych języków pętli for, gdzie programista określa początkową i końcową wartość licznika.

Niestety, nie zadziała odwrotnie, czyli od wartości większej do mniejszej.

w dół do…

Przychodzi nam tu z pomocą metoda downto:

5.downto(1) {|i|
  alert("#{i}")
}

Jak widać, bez problemu udało się wykonać pętlę od 5 do 1, bez zera. Może więc da się i odwrotnie?

upto

Jest to odwrotność metody downto, pozwala odliczać w górę.

3.upto(7) {|i| alert("#{i}") }

Inaczej niż w poprzednich przykładach, ten ostatni zapisaliśmy w jednej linii. Jest to zapis trochę mniej czytelny, ale często spotykany w oprogramowaniu napisanym w Rubym.

Klasyczna pętla for

Jest w Rubym także i prawie klasyczna pętla for, która może operować na zakresach i tablicach:

for i in 1..5
  alert(i)
end

I podobnie dla tablic:

tab=["zerowy", "pierwszy", "drugi", "trzeci"]
for element in tab
  alert("#{element} element.")
end

Zakres z trzech kropek

Powyższa pętla for element in tablica ma jedną istotną wadę – daje dostęp do elementów tablicy, ale nie znamy wewnątrz pętli ich indeksów, a czasem może być to niezbędne.

Na pomoc przychodzą zakresy, nadarza się też okazja omówienia zakresu, w którym początek i koniec jest rozdzielony trzema, a nie dwom akropkami.

for i in (1...5)
  alert("#{a}.")
end

Jak słyszymy, taki zakres zawiera wszystkie elementy, prócz ostatniego, czyli w naszym przykładzie elementy od 1 do 4.

Chcąc przespacerować się po wszystkich elementach tablicy, musimy jedynie wiedzieć, ile elementów ona zawiera. Tę informację poda nam metoda size.

tab=["zerowy", "pierwszy", "drugi", "trzeci"]
tab.size

Dostaliśmy ilość elementów, z których składa się tablica, moglibyśmy więc wykonać pętlę:

for i in 0..tab.size-1
alert("#{tab[i]} element.")
end

I dokładnie taki sam, prawidłowy efekt, uzyskamy korzystając z zakresu z trzema kropkami:

for i in 0...tab.size
alert("#{tab[i]} element.")
end

Czy wszystko jest obiektem?

Znana z poprzednich odcinków notacja kropkowa, używana do wywoływania each, upto, downto i size, nasuwa skojarzenia z odwołaniami do obiektów, które omówiliśmy poprzednio przy okazji obiektu Time.now. Istotnie w Ruby wszystko jest obiektem, nawet liczba, stąd zapis:

1.upto(10)

oznacza, że chcemy wywołać metodę upto na obiekcie 1, czyli liczbie 1 będącej obiektem.

Parametrem przesyłanym do tej metody jest w naszym przykładzie liczba 10.

Pętle nieskończone

Dotychczasowe przykłady pętli były bezpieczne, od razu było wiadomo, że wykonają się pewną ściśle określoną ilość razy.

Czas na coś bardziej intrygującego – pętle nieskończone.

while warunek
  instrukcje
end

To pętla, która będzie wykonywana podczas gdy (while) jakiś warunek jest spełniony. Pisząc taką pętlę musimy mieć pewność, że będzie ona prowadziła do zaprzestania spełnienia tego warunku, gdyż w przeciwnym razie wykonywanie programu nigdy by z niej nie wyszło.

A zatem, klasyczny przykład:

a=0
while (a<4)
  a=a+1
  alert(a)
end

Najpierw ustawiliśmy zmienną a na wartość 0, następnie deklarujemy pętlę, która ma być wykonywana podczas gdy a jest mniejsze od 4, a w bloku pętli wypowiadamy aktualną wartość zmiennej a. I co najważniejsze, również w tym bloku pętli, zwiększamy wartość a w każdym cyklu o 1, co gwarantuje, że w końcu osiągnie ona wartość, która nie będzie mniejsza od 4, co sprawi, że pętla się zakończy.

Odwrotnie, pętla until

Pętla until jest wykonywana dopuki jakiś warunek nie stanie się prawdziwy, czyli odwrotnie niż pętla while. Nasz poprzedni przykład po przepisaniu na używanie pętli until, wygląda następująco:

a=0
until (a>=4)
  a=a+1
  alert(a)
end

Następny proszę, czyli next

Instrukcja next (następny), użyta wewnątrz pętli, nakazuje przejść do wykonania następnego przebiegu danej pętli z pominięciem reszty bieżącego. W ten sposób łatwo możemy pominąć wypowiedzenie wartości a=2 w naszym przykładzie:

a=0
while (a<4)
  a=a+1
  next if a==2
  alert(a)
end

Przełamać zaklęty krąg…

Instrukcją nieco bardziej radykalną, jest break. Powoduje natychmiastowe zaprzestanie wykonywania pętli.

Niektórzy twierdzą, że jej używanie jest podobnie niesmaczne, jak posługiwanie się instrukcją return w kilku miejscach tej samej funkcji, niemniej jednak, sam język to umożliwia.

a=0
while (a<4444444)
  a=a+1
  break if a==4
  alert(a)
end

Jak słychać po uruchomieniu, pętla nie wykonała się ponad 4 miliony razy, a jedynie 3.

Nie jesteśmy przesądni, ale…

Ale na wszelki wypadek, napiszemy wyszukiwarkę piątków trzynastego.

Jak się do tego zabrać?

Z poprzedniego odcinka wiemy, jak sprawdzić dzień tygodnia dla bieżącej daty (Time.now).

Zapewne da się to zrobić również dla innych dat…

napiszemy najpierw funkcję, która dla podanego roku i miesiąca sprawdzi, czy wypada w nim piątek trzynastego.

def friday13?(year, month)
  ti = Time.local(year, month, 13)
  return ti.wday == 5
end

Nazwa funkcji ze znakiem zapytania, to używana w Ruby konwencja nazywania funkcji odpowiadających na jakieś pytanie. W naszym przypadku tym pytaniem jest, czy w podanym roku i miesiącu piątek wypada trzynastego?

Dotychczas mówiliśmy o funkcjach z jednym parametrem. Nasza funkcja przyjmuje dwa parametry, rok i miesiąc, ale mogłoby być ich więcej, zależnie od potrzeb.

ti = Time.local(year, month, 13)

Tworzymy zmienną ti, do której funkcją Time.local przypiszemy obiekt daty i czasu ustawiony na zadany w parametrze funkcji rok i miesiąc, oraz 13 dzień tego miesiąca. Następna linia może się wydawać dziwna:

  return ti.wday == 5

Instrukcję return już znamy, zwraca ona wartość funkcji. Podwójny znak równości też nie jest nam obcy, używaliśmy go do porównań w instrukcjach warunkowych.

Ale co te dwie konstrukcje robią obok siebie?

Zmienne logiczne

Dotychczas poznaliśmy zmienne liczbowe, tekstowe (łańcuchowe), oraz tablicowe. Nie omówiliśmy jednak typu zmiennych najbardziej podstawowego, czyli zmiennych logicznych.

Zmienne logiczne mogą przyjmować dokładnie dwie wartości: prawda (true) lub fałsz (false).

Warunek dla instrukcji warunkowych, również może być albo prawdziwy, albo fałszywy, spełnia więc on definicję zmiennej logicznej.

Co więcej, to co podajemy jako warunek dla instrukcji warunkowych, jest przetwarzane na postać zmiennej logicznej.

Wpiszmy w konsoli:

1==4

Otrzymaliśmy odpowiedź:

: #=> false

Czyli zmienną logiczną fałsz, bo istotnie, 1 nie jest równe 4.

Na pytanie:

4==4

maszyna odpowiada:

: #=> true

Idąc tym tropem, można sobie zadać pytanie, czy prawidłowa będzie konstrukcja:

if true
alert("prawda")
end

I okazuje się, że tak, stworzyliśmy instrukcję warunkową, która zawsze będzie prawdziwa.

Gdybyśmy analogicznie napisali pętlę while true, byłaby to pętla nieskończona, z której wyjście mogłoby się odbyć tylko przy pomocy break.

Warunki, albo zmienne logiczne, można łączyć spójnikami logicznymi (i – and, lub – or). Można też tworzyć ich zaprzeczenia przy pomocy słówka “not”. Te operatory mają swoje jednoznakowe odpowiedniki: or – “|”, and – “&”, not – “!”.

Można to szybko sprawdzić w konsoli:

not true
: #=> false

not false
: #=> true

true or false
: #=> true

false and true
: #=> false

Wracając do poszukiwania piątków trzynastego, wyrażenie znajdujące się po słowie return w naszej funkcji, będzie miało wartość true, albo false, zależnie od tego czy w danym przypadku ti.wday jest równe 5 (piątek).

Wyszukiwarka piątków trzynastego

Chcąc ustalić w jakich miesiącach dla danego roku wypada piątek trzynastego, sprawdzimy wszystkie dwanaście miesięcy badanego roku naszą funkcją i wypiszemy tylko te, dla których zwróci ona wartość true (prawda).

def friday13?(year, month)
  ti = Time.local(year, month, 13)
  return ti.wday == 5
end

year = 2021
for month in 1..12
  if friday13?(year, month)
    alert("month=#{month}")
  end
end

Otrzymujemy odpowiedź “month=8”, co oznacza, że piątek trzynastego wypadał w roku 2021 w sierpniu.

A jak było w roku 2020? Zmieniając jedną linię i uruchamiając nasz program, dowiadujemy się, że piątki trzynastego wypadały w marcu i listopadzie roku 2020.

Ćwiczenie na zakończenie

W jaki sposób szybko sprawdzić, kiedy wypadają piątki trzynastego we wszystkich latach od 2001 do 2030?

W którym roku jest ich najwięcej?

17 komentarzy

  1. Więc co? Zrobić warunek główny w pętli, potem jakiś warunek który np jak się coś wydarzy, typu pętla ma mi np sprawdzać, czy wczytalem ileś tam … Chociaż nie, to for, ale mniejsza… Np. dla sprawdzania sobie koordynatów np Pętla jest dopóki x y z nie jest xx yy i zz. Ale w pewnym przypadku mogę chcieć przerwać pętle w którymś konkretnym miejscu, typu npc stwierdzi, że a w sumie jednak za daleko itd. Czyli warunek i continue?

  2. Ok, I understand. I don’t know how this app-samples repo should be organized and maintained… And what about pull requests to apps existing, and discussed on this blog… Should not they be in sync with this material?

  3. The friday13th is not part of Elten, so I don’t know where your pull requests are?
    Apart of it, we’re not developing this software, it is only for educational purposes, we rather don’t plan to improve this demo programs if it doesn’t contain any errors. But thanks again for your interest in this blog and Elten development.

  4. Ja bym to ujął tak:
    Nie tworzyłbym breaków zbędnych tam, gdzie ładniejszym rozwiązaniem jest pętla while, ale też bym ich zbytnio nie demonizował.

  5. A jednak czytelność kodu na breakach traci, że pętla się nie opiera na ściśle określonym warunku zakończenia, tylko na jakichś dziwnych manipulacjach w jej wnętrzu, które niespodzianie mogą ją zakończyć.

  6. W Rubym praktycznie nie ma to znaczenia, bo i tak język jest interpretowany, a więc niezależnie od tego, jak to rzeczywiście pod maską działa, i tak interpretacja więcej dorzuca, niż drobne zmiany. Na takie szczegóły warto zwracać uwagę, ale na przykład programując sterowniki.

  7. Prawda jest taka ze w dzisiejszych czasach to sa mikrooptymalizacje. Nasze procesory sa tak mocne ze ni ma znaczenia czy uzyje break raz czy 5 razy w petli. Programisci dzisiejszych czasow robia duzo wieksze bledy

  8. Jest to złe w kontekście niskopoziomowej optymalizacji kodu dla planowania jego wykonywania przez procesor.
    W kontekście czytelności takiej pętli, też może być problematyczne.
    Ćwiczeniowiec, który nas uczył podstaw programowania w C, mocno zwracał na tę sprawę uwagę i obcinał punkty.
    A w życiu, wiadomo, zdania są podzielone.
    https://softwareengineering.stackexchange.com/questions/58237/are-break-and-continue-bad-programming-practices

  9. Swoją drogą, czemu niby użycie instrukcji "break" miałoby być jakieś złe? Ni wyobrażam sobie inaczej programować interfejsu czy logiki dajmy na to gry FPS, gdzie wszystko musi odświerzać sięN razy na sekundę. Z tych pętli trzeba jakośwychodzić.

  10. Yup, this is because comments use Markdown, somehow. It shall have been removd by now AFAIK but its still here.
    Good tutorials though.

  11. Congratulations, Bomberman, well done piece of code and works nice.
    Regarding your questions:
    1. Time local works on unix timestamps, so count of seconds from 1 jan 1970.
    32-bit signed number has maximum limit of about 2**31.
    Dividing it by 3600 (seconds in hour) then by 24 (hours in day), then by 365 (days in year) we get 68.
    1970+68 is 2038.
    Unfortunately, RGSS used by Elten has no support for Ruby library allowing nearly unlimited date and time operations.
    2. Conversion of hashes to headings in comments is the mechanism of Elten itself I suppose.

  12. all ruby code comments in the comments has turned to headings. but i want to comment the line of code, but the # sign is disappeared, and the field has turned into a heading level 1.

  13. #code start
    def friday13ret(year)
    rval=[]
    for month in 1..12
    ti = Time.local(year, month, 13)
    rval.push(month) if ti.wday == 5
    #alert(year.to_s+"d"+month.to_s) #debug if ti.wday==5
    end
    return rval
    end
    compairval=[0,2000]
    w=""
    #this is a little array to show when the friday 13th was the most times
    for year in 2001..2035
    #why the time local works till year 2038? i wanted to check till year 2050!
    arr=friday13ret(year)
    w+="in "+year.to_s+", friday the thirteenth "
    if year<Time.now.year
    w+="was in "
    elsif year>Time.now.year
    w+="will be in "
    else
    w+="is in "
    end
    for i in arr
    months=["nothing","january","february","march","april","may","june","july","august","september","october","november","december"]
    #the first or zero element is "nothing" because the indexing of month in Time object is from 1 to 12, not from 0 to 11
    w+=months[i].to_s+", " if i!=nil
    end
    w+="\r\n"
    if arr.size>compairval[0]
    compairval=[arr.size,year]
    end

    end
    w+="and the hellest year is "+compairval[1].to_s+", it has "+compairval[0].to_s+" friday13s."
    input_text("the statistic of friday13",EditBox::Flags::MultiLine|EditBox::Flags::ReadOnly,w)
    #code end

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *