Generator liczb losowych z podanego zakresu

W niektórych aplikacjach i przy tworzeniu testów zachodzi konieczność uzyskania losowej liczby z podanego zakresu. Wbudowana metoda random obiektu Math generuje jednak tylko liczbę z zakresu od 0 do 1 (bez jedynki). Spróbujemy więc napisać własny generator liczb losowych.

JavaScript posiada obiekt globalny Math, który ma wiele użytecznych właściwości statycznych i metod, w tym właśnie metodę random. Generuje ona losową liczbę zawartą między zerem, a jedynką (bez jedynki), lecz zwraca ją w formie liczby zmiennoprzecinkowej, np. 0.234623547.

Zdarzają się przypadki, że potrzebujemy liczby losowej w formie liczby całkowitej (integer).  Spójrzmy na poniższy przykład:

function randomNumber() {
    return Math.floor(Math.random() * 10);
}

randomNumber(); //losowa liczba od 0 do 9

for (let i = 1; i < 20; i++){
    console.log(randomNumber());
} //20 losowych liczb

Powyższa funkcja działa prawidłowo, zwracając liczbę całkowitą jednak jej wadą jest fakt, że liczby zawierają się wyłącznie w zakresie od 0 do 9. Taki zakres bywa czasami zbyt mały. Funkcję możemy jednak łatwo przerobić, aby generowała liczbę z zakresu od zera do 99:

function randomNumber() {
    return Math.floor(Math.random()*100);
}

Idąc dalej tym tropem warto byłoby ponownie przerobić funkcję tak, aby mogła zwracać liczby z zakresu od zera do dowolnie określonej liczby n:

function randomNumber(max){
   var max = parseInt(max,10);
   return Math.floor(Math.random()*max);
}

for (let i = 1; i < 50; i++){
    console.log(randomNumber(15))
}

Wywołaj powyższy kod w konsoli JavaScript i dokładnie przeanalizuj uzyskane wyniki.  Zwróć uwagę, że maksymalna wartość jaka się pojawia w wynikach do liczba 14, a oczekiwaliśmy przecież liczb od zera do 15. Nie możemy po prostu dodać jedności do zwracanego wyniku, gdyż utracilibyśmy wtedy możliwość wygenerowania jako losowej liczby wartości zero.

W niektórych przypadkach zależy nam jednak nie na zakresie od zera lecz na dowolnie wskazanym przedziale liczb. Załóżmy na przykład, że potrzebujemy generatora, który będzie zwracał liczby całkowite z zakresu od 5 do 15, łącznie z wartościami brzegowymi (czyli możliwe będzie wygenerowanie zarówno liczby 5 jak i 15). Spróbujmy napisać taką funkcję:

function randomNumber(min,max) {
    return Math.floor(Math.random()*(max-min+1)+min);
}

A teraz przetestujmy naszą funkcję w zakresie zarówno liczb dodatnich jak i ujemnych:

for (let i = 1; i < 50; i++) {
    console.log(randomNumber(5,15));
}

for (let i = 1; i < 50; i++) {
    console.log(randomNumber(-3,5));
}

Teraz teoretycznie wszystko jest w porządku, jednak tylko do momentu, gdy przypadkowo wywołamy naszą funkcję z parametrami w odwróconej kolejności:

for (let i = 1; i < 20; i++) {
    console.log(randomNumber(5,2));
}

Powyższy kod zwróci dwadzieścia liczb, wśród których będą wyłącznie cyfry 3 i 4, czyli nie uwzględniające brzegowych parametrów min i max. Jeśli zależy nam na zabezpieczeniu się przed taką sytuacją to możemy zastosować podmianę parametrów po ich weryfikacji:

function randomNumber(min,max) {
    if (min > max) {
        let v = min;
        min= max;
        max = v;
        //Można również użyć zapisu zgodnego
        // z ES6: [min,max] = [max,min];
    }
    return Math.floor(Math.random()*(max-min+1)+min);
}

W przypadku gdy pierwszy argument okaże się większy od drugiego to nastąpi ich podmiana z wykorzystaniem zmiennej tymczasowej v (są również inne sposoby na wykonanie takiej operacji, ale nie będziemy ich tutaj omawiać).

Pozostaje jeszcze jeden problem. Gdy wywołamy naszą funkcję z parametrami (5.3, 2.9) to będzie ona generować liczby z zakresu od 3 do 6. O ile wartość minimalna 3 może być tutaj spodziewana (matematyczne zaokrąglenie 2.9 do 3) o tyle pytanie, czy dopuszczamy zmianę również wartości maksymalnej (w tym wypadku do 6).

Ponad to co się stanie gdy do funkcji przekażemy wartości inne niż liczby całkowite?

randomNumber('xyx',5); //NaN
randomNumber(null,2); //generuje liczby od 0 do 2

W pierwszym wywołaniu otrzymaliśmy wartość NaN, co oznacza, że nie udało się wykonać operacji matematycznych wewnątrz funkcji randomNumber. To zachowanie prawdopodobnie jest przewidywalne, ale co się stało w drugiej sytuacji?

Otóż tuta funkcja działa prawidłowo i zwraca liczby z zakresu od zera do dwóch. Skąd się wzięła wartość minimalna równa zero? Nastąpiła tu niejawna konwersja typu null do wartości zero-jedynkowej (odzwierciedlającej konwersję na true/false). Null konwertuje się w JavaScript do false, zatem w formie numerycznej otrzymaliśmy wartość 0 i została ona uznana za naszą wartość minimalną.

Aby uniknąć takich problemów warto zastosować dodatkowe zabezpieczenia. Niestety w JavaScript nie możemy narzucić stosowania argumentów wyłącznie określonego typu, co wynika m.in. z faktu, że jest to język o słabej kontroli typów. Musimy więc dokonać samodzielnej kontroli:

function randomNumber(min,max) {
    try {
        if (typeof min !== 'number' || typeof max !== 'number') {
            throw Error('Min i max nie są liczbami całkowitymi!');
        }
        if (min > max) {
            let v = min;
            min= parseInt(max,10);
            max = parseInt(v,10);
        }
        return Math.floor(Math.random()*(max-min+1)+min);
    } catch (err) {
        return err.message;
    }
}

randomNumber(null,2); //'Min i max nie są...'
randomNumber('x', 5); //'Min i max nie są...'
randomNumber(5.3,2.9); //Liczby od 2 do 5

Teraz wszystkie testy dają pozytywne wyniki zgodne z naszymi oczekiwaniami.  Zastosowaliśmy również metodę parseInt na przekazanych parametrach aby świadomie operować wyłącznie na części całkowitej liczby, co pozwoli uniknąć nieporozumień związanych np. z wywołaniem randomNumber(5.3,2.9) – teraz funkcja operuje na wartościach od 2 do 5. Jeśli dopuszczamy zaokrąglanie liczb to zastosujmy metodę Math.round w miejsce parseInt.

Czy jednak funkcja jest całkowicie zabezpieczona przed złymi wartościami parametrów wejściowych? A co jeśli podana zostanie liczba większa niż maksymalna dopuszczalna liczba całkowita w JavaScript, czyli (2^53-1)? Funkcja w takim wypadku nie zwróci błędu, ale nie będzie też generowała liczb z pożądanego zakresu. Można by więc wprowadzić dodatkowy warunek, sprawdzający czy wartość min i max zawierają się w zakresie od Number.MIN_SAFE_INTEGER do Number.MAX_SAFE_INTEGER, lecz to już pozostawiam czytelnikowi.

Zaczęliśmy od prostej funkcji (praktycznie jednolinijkowej), a zakończyliśmy na funkcji zawierającej dwie instrukcje warunkowe i blok try…catch. Nie napiszę jednak, abyś zawsze stosował funkcję, do której doszliśmy pod koniec artykułu. Chciałem tylko na stosunkowo prostym przykładzie pokazać jak ważne są dokładne testy i przewidywanie ewentualnych błędów przy podawaniu parametrów funkcji. To którą z omawianych w artykule funkcji warto zastosować zależy od tego, jaką masz pewność co do poprawności danych wejściowych i jakich oczekujesz rezultatów.

Wiele poradników szumnie mówi o testach jednostkowych i specjalnych narzędzi dedykowanych właśnie do tego celu. Owszem, jeśli będziesz pisał dużo kodu to takie narzędzia są bardzo przydatne, ale najprostsze testy można wykonać również w konsoli przy użyciu pętli z wywołaniem console.log(…).

Pamiętaj, że testy są ważne w przypadku każdego kodu – nawet teoretycznie tak trywialnego jak wygenerowanie liczby z podanego zakresu…

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *