16 липня 2023 р.

Числа

У сучасному JavaScript існує два типи чисел:

  1. Звичайні числа в JavaScript, що зберігаються у 64-бітному форматі IEEE-754, також відомі як “подвійні точні числа з плаваючою комою”. Це числа, які ми використовуємо більшість часу, і про них ми поговоримо в цьому розділі.

  2. Числа BigInt, для відображення цілих чисел довільної довжини. Іноді вони потрібні, оскільки звичайне число не може безпечно перевищувати (253-1) або бути менше ніж -(253-1), як ми згадували раніше в розділі Типи даних. Оскільки числа BigInt використовуються в декількох спеціальних областях, їм присвячено окремий розділ BigInt.

То ж тут ми поговоримо про звичайні числа. Поглибимо наші знання про них.

Більше способів написання числа

Уявіть, нам потрібно написати 1 мільярд. Прямий спосіб це:

let billion = 1000000000;

Також можна використовувати знак підкреслення (нижню риску) _ в якості розділювача:

let billion = 1_000_000_000;

В цьому випадку знак підкреслення _ відіграє роль “синтаксичного цукру”, він робить число більш читабельним. Рушій JavaScript просто ігнорує _ між цифрами, тому для нього це мільярд, як і в прикладі вище.

В реальному житті ми намагаємося уникати написання довгих рядків з нулями. Ми надто ліниві для цього. Зазвичай ми напишемо щось на кшталт "1 млрд" для мільярда або "7.3 млрд" для 7 мільярдів 300 мільйонів. Те саме стосується більшості великих чисел.

У JavaScript можна скоротити число, додавши букву "е" та кількість нулів після неї:

let billion = 1e9;  // 1 мільярд, буквально: 1 та 9 нулів

alert( 7.3e9 );  // 7.3 мільярдів (таке ж саме що 7300000000 чи 7_300_000_000)

Іншими словами, "e" помножує число на 1 із заданим числом нулів.

1e3 === 1 * 1000; // e3 означає *1000
1.23e6 === 1.23 * 1000000; // e6 означає *1000000

Тепер напишемо щось дуже маленьке. Наприклад, 1 мікросекунда (одна мільйонна частина секунди):

let mсs = 0.000001;

Як і раніше, нам допоможе використання "e". Якщо ми хочемо уникнути явного запису нулів, ми можемо написати:

let mcs = 1e-6; // шість нулів зліва від 1

Якщо порахувати нулі в 0.000001, їх буде 6. Так що, цілком очікувано, що це 1e-6.

Іншими словами, від’ємне число після "е" означає ділення на 1 з заданою кількістю нулів:

// -3 ділиться на 1 з 3 нулями
1e-3 === 1 / 1000; // 0.001

// -6 ділиться на 1 з 6 нулями
1.23e-6 === 1.23 / 1000000; // 0.00000123

// an example with a bigger number
1234e-2 === 1234 / 100; // 12.34, decimal point moves 2 times

Двійкові, вісімкові та шістнадцяткові числа

Шістнадцяткові числа широко використовуються в JavaScript для представлення кольорів, кодування символів та багатьох інших речей. Тому, цілком очікувано, що існує коротший спосіб їх написання: 0x, а потім саме число.

Наприклад:

alert( 0xff ); // 255
alert( 0xFF ); // 255 (те саме, регістр не має значення)

Двійкові та вісімкові системи числення рідко використовуються, але також підтримуються за допомогою префіксів “0b” і “0o”:

let a = 0b11111111; // двійкова форма 255
let b = 0o377; // вісімкова форма 255

alert( a == b ); // true, те саме число 255 з обох сторін

Є лише 3 системи числення з такою підтримкою. Для інших систем числення ми повинні використовувати функцію parseInt (яку ми побачимо далі в цьому розділі).

toString(base)

Метод num.toString(base) повертає рядкове представлення num в системі числення із заданим base.

Наприклад:

let num = 255;

alert( num.toString(16) );  // ff
alert( num.toString(2) );   // 11111111

base може бути від 2 до 36. За замовчуванням це 10.

Загальні випадки використання для цього є:

  • base=16 використовується для шістнадцяткових кольорів, кодування символів тощо, цифри можуть бути 0..9 або A..F.

  • base=2 в основному для налагодження бітових операцій, цифри можуть бути 0 або 1.

  • base=36 є максимальним, цифри можуть бути 0..9 або A..Z. Весь латинський алфавіт використовується для позначення числа. Комічно, але користь від системи для найбільших чисел полягає у перетворенні довгого числового ідентифікатора у щось коротше, наприклад, для генерації короткого URL. Для цього достатньо представити його в системі числення з базою 36:

    alert( 123456..toString(36) ); // 2n9c
Дві крапки для виклику методу

Зверніть увагу, що дві крапки в 123456..toString(36) – це не помилка. Якщо ми хочемо викликати метод безпосередньо на число, наприклад toString у наведеному вище прикладі, тоді нам потрібно поставити дві крапки .. після нього.

Якби ми помістили одну крапку: 123456.toString(36), тоді виникла б помилка, оскільки синтаксис JavaScript передбачає десяткову частину після першої точки. І якщо ми розмістимо ще одну крапку, то JavaScript розпізнає, що десяткова частина порожня, і далі йде метод.

Також можна написати (123456).toString(36).

Округлення

Однією з найбільш використовуваних операцій при роботі з числами є округлення.

Існує кілька вбудованих функцій для округлення:

Math.floor
Округляє вниз: 3.1 стає 3, та -1.1 стає -2.
Math.ceil
Округляє вверх: 3.1 стає 4, та -1.1 стає -1.
Math.round
Округляє до найближчого цілого числа: 3.1 стає 3, 3.6 стає 4, 3.5 теж округлить до 4.
Math.trunc (не підтримується в Internet Explorer)
Видаляє все після десяткової крапки без округлення: 3.1 стає 3, -1.1 стає -1.

Ось таблиця для узагальнення відмінностей між ними:

Math.floor Math.ceil Math.round Math.trunc
3.1 3 4 3 3
3.6 3 4 4 3
-1.1 -2 -1 -1 -1
-1.6 -2 -1 -2 -1

Ці функції охоплюють усі можливі способи поводження з десятковою частиною числа. Але що робити, якщо ми хотіли б округлити число до n-ної цифри після десяткової крапки?

Наприклад, ми маємо 1.2345 і хочете округлити його до двох цифр, щоб отримати 1.23.

Є два способи зробити це:

  1. Помножити та розділити.

    Наприклад, щоб округлити число до другої цифри після десяткової крапки, ми можемо помножити число на 100, викликати функцію округлення і потім поділити його назад.

    let num = 1.23456;
    
    alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
  2. Метод toFixed(n) округляє число до n цифр після точки та повертає рядкове представлення результату.

    let num = 12.34;
    alert( num.toFixed(1) ); // "12.3"

    Це округляє вгору або вниз до найближчого значення, подібно до Math.round:

    let num = 12.36;
    alert( num.toFixed(1) ); // "12.4"

    Зверніть увагу, що результат toFixed – це рядок. Якщо десяткова частина коротша, ніж потрібно, нулі додаються в кінці:

    let num = 12.34;
    alert( num.toFixed(5) ); // "12.34000", додано нулі, щоб зробити рівно 5 цифр

    Ми можемо перетворити його на число, використовуючи унарний плюс +num.toFixed(5) або Number().

Неточні розрахунки

Із середини, число представлено у 64-бітному форматі IEEE-754, тому для його зберігання треба саме 64 біти: 52 з них використовуються для зберігання цифр, 11 – відповідають за позицію десяткової крапки (для цілих чисел вони дорівнюють нулю), а 1 біт – для знака.

Якщо число занадто велике, та переповнює 64-біти, воно буде перетворене на спеціальне числове значення Infinity(Нескінченність):

alert( 1e500 ); // Infinity

Що може бути трохи менш очевидним, але трапляється досить часто, це втрата точності.

Розглянемо цей (хибний!) тест:

alert( 0.1 + 0.2 == 0.3 ); // false

Все вірно, якщо ми перевіримо, чи сума 0.1 та 0.2 дорівнює 0.3, отримаємо false.

Дивно! Що це тоді, якщо не 0.3?

alert( 0.1 + 0.2 ); // 0.30000000000000004

Оце так! Уявіть, що ви робите вебсайт для електронних покупок, і відвідувач кладе в кошик товари $0.10 та $0.20. Загальна сума замовлення складе $0.30000000000000004. Це може здивувати будь-кого.

Але чому так відбувається?

Число зберігається в пам’яті у його двійковій формі, послідовність бітів – одиниць і нулів. Але дроби на кшталт 0.1, 0.2, які виглядають просто в десятковій системі числення, насправді є нескінченними дробами у своїй двійковій формі.

Іншими словами, що таке 0.1? Це одиниця розділена на десять 1/10 – одна десята. У десятковій системі такі числа досить легко представити, але якщо порівняти його з однією третиною: 1/3, то ми стикаємось з нескінченним дробом 0.33333(3).

Отже, поділ на 10 гарантовано працює в десятковій системі, але поділ на 3 – ні. З цієї ж причини в системі двійкових чисел поділ на 2 гарантовано працює, але 1/10 стає нескінченним двійковим дробом.

Просто немає можливості зберігати рівно 0.1 або рівно 0.2 за допомогою двійкової системи, так само як немає можливості зберігати одну третю, як десятковий дріб.

Числовий формат IEEE-754 вирішує це шляхом округлення до найближчого можливого числа. Ці правила округлення зазвичай не дозволяють нам побачити “крихітні втрати точності”, але вони існують.

Ми можемо побачити це на прикладі:

alert( 0.1.toFixed(20) ); // 0.10000000000000000555

І коли ми підсумовуємо два числа, їх “втрати на точність” складаються.

Ось чому 0.1 + 0.2 не є 0.3.

Не тільки JavaScript

Ця ж проблема існує у багатьох інших мовах програмування.

PHP, Java, C, Perl, Ruby дають абсолютно однаковий результат, оскільки використовують один цифровий формат.

Чи можемо ми вирішити проблему? Звичайно, найнадійніший метод – округлення результату за допомогою методу toFixed(n):

let sum = 0.1 + 0.2;
alert( sum.toFixed(2) ); // "0.30"

Зауважте, що toFixed завжди повертає рядок, щоб число гарантовано мало дві цифри після десяткової крапки. Це насправді зручно, якщо у нас є електронні покупки та нам потрібно показати $0.30. В інших випадках ми можемо використовувати одинарний плюс, щоб для приведення його до числа:

let sum = 0.1 + 0.2;
alert( +sum.toFixed(2) ); // 0.3

Ми також можемо тимчасово помножити числа на 100 (або більше число), щоб перетворити їх на цілі числа, виконати математичні операції і поділити назад. Якщо ми робимо розрахунки з цілими числами, помилка дещо зменшується, але ми все одно отримуємо її при діленні:

alert( (0.1 * 10 + 0.2 * 10) / 10 ); // 0.3
alert( (0.28 * 100 + 0.14 * 100) / 100); // 0.4200000000000001

Отже, підхід множення/ділення зменшує помилку, але не видаляє її повністю.

Іноді можна спробувати уникнути проблем з дробами. Якщо ми маємо справу з магазином, то можемо зберігати ціни в центах замість доларів. Але що робити, якщо ми застосуємо знижку в розмірі 30%? На практиці повністю уникнути дробів вдається досить рідко. Просто округліть їх, щоб вирізати “хвости”, коли це потрібно.

Цікавий факт

Спробуйте запустити:

// Привіт! Я число, що збільшується само по собі
alert( 9999999999999999 ); // покаже 10000000000000000

Це ще один приклад тієї ж проблеми: втрата точності. Для числа існує 64 біти, 52 з них можна використовувати для зберігання цифр, але цього недостатньо. Так зникають найменш значущі цифри.

JavaScript не викликає помилку в таких випадках. Він робить все можливе, щоб число відповідало бажаному формату, та на жаль, цей формат недостатньо великий.

Два нулі

Ще одним кумедним наслідком внутрішньої реалізації чисел є наявність двох нулів: 0 і -0.

Це тому, що знак представлений одним бітом, тому його можна встановити або не встановити для будь-якого числа, включаючи нуль.

У більшості випадків відмінність непомітна, оскільки оператори підходять до них як до однакових.

Перевірки: isFinite та isNaN

Пам’ятаєте ці два особливі числові значення?

  • Infinity (та -Infinity) – це особливе числове значення, яке більше (менше) ніж усе.
  • NaN представляє помилку.

Вони належать до типу number, але не є “нормальними” числами, тому для їх перевірки існують спеціальні функції:

  • isNaN(value) перетворює свій аргумент у число, а потім перевіряє його на належність до NaN:

    alert( isNaN(NaN) ); // true
    alert( isNaN("str") ); // true

    Але чи потрібна нам ця функція? Чи не можемо ми просто використати порівняння === NaN? Вибачте, але відповідь – ні. Значення NaN унікальне тим, що воно нічому не дорівнює, включаючи себе:

    alert( NaN === NaN ); // false
  • isFinite(value) перетворює свій аргумент в число і повертає true, якщо це звичайне число, та false, якщо NaN/Infinity/-Infinity:

    alert( isFinite("15") ); // true
    alert( isFinite("str") ); // false, тому що це спеціальне значення: NaN
    alert( isFinite(Infinity) ); // false, тому що це спеціальне значення: Infinity

Іноді isFinite використовується для перевірки того, чи є значення рядка звичайним числом:

let num = +prompt("Enter a number", '');

// буде істинним, якщо ви не введете Infinity, -Infinity чи не число
alert( isFinite(num) );

Зауважте, що порожній рядок, або рядок з пробілів трактується як 0 у всіх числових функціях, включаючи isFinite.

Number.isNaN і Number.isFinite

Методи Number.isNaN і Number.isFinite є більш “суворими” версіями функцій isNaN і isFinite. Вони не перетворюють свій аргумент автоматично на число, а перевіряють, чи належить він до типу number.

  • Number.isNaN(value) повертає true, якщо аргумент належить до типу number і має значення NaN. У будь-якому іншому випадку він повертає false.

    alert( Number.isNaN(NaN) ); // true
    alert( Number.isNaN("str" / 2) ); // true
    
    // Зверніть увагу на різницю:
    alert( Number.isNaN("str") ); // false, тому що "str" це рядок, а не число
    alert( isNaN("str") ); // true, оскільки isNaN перетворює рядок "str" ​​на число та отримує NaN як результат цього перетворення
  • Number.isFinite(value) повертає true, якщо аргумент належить до типу number і не є NaN/Infinity/-Infinity. У будь-якому іншому випадку він повертає false.

    alert( Number.isFinite(123) ); // true
    alert( Number.isFinite(Infinity) ); // false
    alert( Number.isFinite(2 / 0) ); // false
    
    // Зверніть увагу на різницю:
    alert( Number.isFinite("123") ); // false, тому що "123" це рядок, а не число
    alert( isFinite("123") ); // true, оскільки isFinite перетворює рядок "123" на число 123

У певному сенсі Number.isNaN і Number.isFinite простіші та зрозуміліші, ніж функції isNaN і isFinite. Однак на практиці переважно використовуються isNaN і isFinite, оскільки вони коротші для написання.

Порівняння з Object.is

Існує спеціальний вбудований метод Object.is, який порівнює значення як ===, але є більш надійним для двох виключень:

  1. Працює з NaN: Object.is(NaN, NaN) === true, і це добре.
  2. Значення 0 і-0 різні: Object.is(0, -0) === false, технічно це правда, оскільки внутрішньо число має біт знаків, який може бути різним, навіть якщо всі інші біти – нулі.

У всіх інших випадках Object.is(a, b) те саме, що a === b.

Ми згадуємо тут Object.is, оскільки він часто використовується в специфікації JavaScript. Коли для внутрішнього алгоритму потрібно порівняти два значення, щоб вони були абсолютно однаковими, він використовує Object.is (ще його називають SameValue).

parseInt та parseFloat

Числове перетворення за допомогою плюса + або Number() є суворим, тож якщо значення не є гарантованим числом, то станеться помилка:

alert( +"100px" ); // NaN

Винятком є пробіли на початку або в кінці рядка, оскільки вони ігноруються.

Але в реальному житті ми часто маємо значення в конкретних одиницях, наприклад, "100px" або "12pt" в CSS. Також у багатьох країнах символ валюти йде після значення, тому у нас є "19€" і ми хочемо отримати число з цього.

Ось для чого призначені parseInt та parseFloat.

Вони “читають” число з рядка, до поки можуть, у разі помилки зчитане число повертається. Функція parseInt повертає ціле число, тоді як parseFloat повертає число з плаваючою крапкою:

alert( parseInt('100px') ); // 100
alert( parseFloat('12.5em') ); // 12.5

alert( parseInt('12.3') ); // 12, тільки частина цілого числа
alert( parseFloat('12.3.4') ); // 12.3, друга крапка зупиняє зчитування

Бувають ситуації, в яких parseInt/parseFloat повернуть NaN, коли не вдалось прочитати жодної цифри:

alert( parseInt('a123') ); // NaN, перший символ зупиняє процес
Другий аргумент parseInt(str, radix)

Функція parseInt() має необов’язковий другий аргумент. Він вказує основу системи числення, тому parseInt також може проаналізувати рядки шістнадцяткових, двійкових та інших чисел:

alert( parseInt('0xff', 16) ); // 255
alert( parseInt('ff', 16) ); // 255, без 0x також працює

alert( parseInt('2n9c', 36) ); // 123456

Інші математичні функції

JavaScript має вбудований Math об’єкт, який містить невелику бібліотеку математичних функцій та констант.

Декілька прикладів:

Math.random()

Повертає випадкове число від 0 до 1 (не включаючи 1).

alert( Math.random() ); // 0.1234567894322
alert( Math.random() ); // 0.5435252343232
alert( Math.random() ); // ... (будь-яке випадкове число)
Math.max(a, b, c...) / Math.min(a, b, c...)

Повертає найбільше/найменше число з довільної кількості аргументів.

alert( Math.max(3, 5, -10, 0, 1) ); // 5
alert( Math.min(1, 2) ); // 1
Math.pow(n, power)

Повертає n, зведене у ступінь power.

alert( Math.pow(2, 10) ); // 2 у ступені 10 = 1024

Об’єкт Math включає ще багато функцій і констант, в тому числі тригонометрію. Детальніше про об’єкт Math можна почитати в документації.

Підсумки

Щоб записати числа з багатьма нулями:

  • Додайте "e" з числом нулів до числа. Як і: 123e6 те саме, що 123 з 6 нулями 123000000.
  • Від’ємне число після "е" призводить до ділення числа на 1 із заданими нулями. Наприклад 123e-6 означає 0.000123 (123 мільйони).

Для різних систем числення:

  • Можна записувати числа безпосередньо в шістнадцятковій (0x), вісімковій (0o) та двійковій (0b) системах.
  • parseInt(str, base) розбирає рядок str на ціле число чисельної системи із заданим base, 2 ≤ base ≤ 36.
  • num.toString(base) перетворює число в рядок в системі числення за допомогою заданої base.

Для регулярних тестів чисел:

  • isNaN(value) перетворює свій аргумент на число, а потім перевіряє його на NaN
  • Number.isNaN(value) перевіряє, чи належить його аргумент до типу number, і якщо так, перевіряє його на NaN
  • isFinite(value) перетворює свій аргумент на число, а потім перевіряє, чи не є NaN/Infinity/-Infinity
  • Number.isFinite(value) перевіряє, чи належить його аргумент до типу number, і якщо так, перевіряє, чи не є NaN/Infinity/-Infinity

Для перетворення значень на зразок 12pt та 100px у число:

  • Використовуйте parseInt/parseFloat для “не суворого” перетворення, яке зчитує число з рядка, а потім повертає значення, яке вдалося прочитати перед помилкою.

Для дробів:

  • Округлюйте за допомогою Math.floor, Math.ceil, Math.trunc, Math.round або num.toFixed(precision).
  • Пам’ятайте, що при роботі з дробами втрачається точність.

Більше математичних функцій:

  • Дивіться об’єкт Math, коли вони вам потрібні. Бібліотека дуже мала, але охоплює основні потреби.

Завдання

важливість: 5

Напишіть скрипт, який просить відвідувача ввести два числа, і в результаті показує їх суму.

Запустити демонстрацію

P.S. Тут є цікавий момент з типами

let a = +prompt("Перше число?", "");
let b = +prompt("Друге число?", "");

alert( a + b );

Зверніть увагу на одиничний плюс + перед prompt. Це відразу перетворює значення в число.

В іншому випадку, a іb будуть рядками, і в результаті вони об’єднаються (конкатинуються), тобто: "1" + "2" = "12".

важливість: 4

Згідно з документацією Math.round і toFixed, округлюють до найближчого числа: 0..4 ведуть вниз, а5..9 ведуть вгору.

Наприклад:

alert( 1.35.toFixed(1) ); // 1.4

У подібному прикладі нижче, чому 6.35 округляється до 6.3, а не 6.4?

alert( 6.35.toFixed(1) ); // 6.3

Як правильно округлити 6.35?

Десятковий дріб 6.35 являє собою нескінченний двійковий код. Як завжди в таких випадках, він зберігається з втратою точності.

Подивимось:

alert( 6.35.toFixed(20) ); // 6.34999999999999964473

Втрата точності може спричинити як збільшення, так і зменшення числа. У цьому конкретному випадку число стає трохи меншим, тому воно округляється вниз.

А що для 1.35?

alert( 1.35.toFixed(20) ); // 1.35000000000000008882

Тут втрата точності зробила число трохи більшим, тому воно округляється вверх.

Як ми можемо виправити проблему з числом 6.35, якщо хочемо, щоб воно було правильно округлене?

Ми повинні наблизити його до цілого числа до округлення:

alert( (6.35 * 10).toFixed(20) ); // 63.50000000000000000000

Зауважте, що 63.5 взагалі не має втрат на точність. Це тому, що десяткова частина 0.5 насправді є 1/2. Дроби, розділені на 2, точно представлені у двійковій системі, і ми можемо її округлити:

alert( Math.round(6.35 * 10) / 10 ); // 6.35 -> 63.5 -> 64(округлене) -> 6.4
важливість: 5

Створіть функцію readNumber, яка запросить число, поки відвідувач не введе дійсне числове значення.

Отримане значення потрібно повернути як число.

Відвідувач також може зупинити процес, ввівши порожній рядок або натиснувши “CANCEL”. У цьому випадку функція повинна повернути null.

Запустити демонстрацію

Відкрити пісочницю з тестами.

function readNumber() {
  let num;

  do {
    num = prompt("Введіть число", 0);
  } while ( !isFinite(num) );

  if (num === null || num === '') return null;

  return +num;
}

alert(`Read: ${readNumber()}`);

Рішення є дещо складнішим, аніж здається, тому що нам потрібно обробляти null та порожні рядки.

Таким чином, ми фактично приймаємо вхід, поки він не стане “звичайним числом”. Обидва null (скасування), та порожній рядок відповідають цій умові, оскільки в числовій формі вони є 0.

Після того, як ми зупинилися, нам потрібно обробити null і порожній рядок спеціально (повернути null), тому що їх перетворення у число поверне 0.

Відкрити рішення із тестами в пісочниці.

важливість: 4

Цей цикл безкінечний. Він ніколи не закінчується. Чому?

let i = 0;
while (i != 10) {
  i += 0.2;
}

Це тому, що i ніколи не буде дорівнювати 10.

Запустіть код, щоб побачити реальні значення i:

let i = 0;
while (i < 11) {
  i += 0.2;
  if (i > 9.8 && i < 10.2) alert( i );
}

Жодне значення не 10.

Такі речі трапляються через втрати точності при додаванні дробів на зразок 0.2.

Висновок: уникайте порівняннь при роботі з десятковими дробами.

важливість: 2

Вбудована функція Math.random() створює випадкове значення від 0 до1 (не враховуючи 1).

Напишіть функцію random(min, max) для створення випадкового числа з плаваючою крапкою від min до max (не враховуючи max).

Приклади його роботи:

alert( random(1, 5) ); // 1.2345623452
alert( random(1, 5) ); // 3.7894332423
alert( random(1, 5) ); // 4.3435234525

Нам потрібно перевести всі значення з інтервалу 0…1 у значення від min доmax.

Це можна зробити у два етапи:

  1. Якщо помножити випадкове число з 0…1 на max-min, то інтервал можливих значень збільшується від 0..1 до 0..max-min.
  2. Тепер, якщо ми додамо min, можливий інтервал стає від min до max.

Функція:

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

alert( random(1, 5) );
alert( random(1, 5) );
alert( random(1, 5) );
важливість: 2

Створіть функцію randomInteger(min, max), яка генерує випадкове ціле число від min до max включно.

Будь-яке число з інтервалу min..max повинно з’являтися з однаковою ймовірністю.

Приклади його роботи:

alert( randomInteger(1, 5) ); // 1
alert( randomInteger(1, 5) ); // 3
alert( randomInteger(1, 5) ); // 5

Ви можете використовувати рішення попереднього завдання як основу.

Просте, але неправильне рішення

Найпростішим, але неправильним рішенням буде генерувати значення від min до max і округляти його:

function randomInteger(min, max) {
  let rand = min + Math.random() * (max - min);
  return Math.round(rand);
}

alert( randomInteger(1, 3) );

Функція працює, але вона неправильна. Ймовірність отримати граничні значення min і max в два рази менше, ніж будь-які інші.

Якщо ви запускаєте приклад вище, багато разів, ви легко побачите, що 2 з’являється найчастіше.

Це відбувається тому, що Math.round() отримує випадкові числа з інтервалу 1..3 і округляє їх так:

values from 1    ... to 1.4999999999  become 1
values from 1.5  ... to 2.4999999999  become 2
values from 2.5  ... to 2.9999999999  become 3

Тепер ми можемо чітко бачити, що 1 генерується вдвічі рідше ніж 2. І те саме з 3.

Правильне рішення

Існує багато правильних рішень задачі. Один з них – коригування інтервальних меж. Щоб забезпечити однакові інтервали, ми можемо генерувати значення від 0.5 до 3.5, тим самим додаючи необхідні ймовірності до граничних значеннь:

function randomInteger(min, max) {
  // тепер rand від (min-0.5) до (max+0.5)
  let rand = min - 0.5 + Math.random() * (max - min + 1);
  return Math.round(rand);
}

alert( randomInteger(1, 3) );

Альтернативним способом може бути використання Math.floor для випадкового числа від min до max + 1:

function randomInteger(min, max) {
  // тепер rand від min до (max+1)
  let rand = min + Math.random() * (max + 1 - min);
  return Math.floor(rand);
}

alert( randomInteger(1, 3) );

Тепер усі інтервали відображаються таким чином:

values from 1  ... to 1.9999999999  become 1
values from 2  ... to 2.9999999999  become 2
values from 3  ... to 3.9999999999  become 3

Всі інтервали мають однакову довжину, що робить остаточний розподіл рівномірним.

Навчальна карта

Коментарі

прочитайте це, перш ніж коментувати…
  • Якщо у вас є пропозиції, щодо покращення підручника, будь ласка, створіть обговорення на GitHub або одразу створіть запит на злиття зі змінами.
  • Якщо ви не можете зрозуміти щось у статті, спробуйте покращити її, будь ласка.
  • Щоб вставити код, використовуйте тег <code>, для кількох рядків – обгорніть їх тегом <pre>, для понад 10 рядків – використовуйте пісочницю (plnkr, jsbin, codepen…)