Please support me with,
로또번호추첨기를 만들며 사용하게 된 확률에 따른 항목 추출에 대한 포스팅이다.
Math.random()
자바스크립트에는 Math.random()
이라는 메서드가 있다.
이 메서드를 이용하면 0 이상 1 미만의 임의 난수를 생성할 수 있다.
단순하게 배열에서 랜덤으로 항목을 추출하는 방법은 아래와 같이 간단하게 만들 수 있다.
/**
* Get random index from an array.
* @param array - An array to get index.
* @returns {number} A random index between `0` to `array.length`.
*/
function getRandomIndexFromArray(array) {
return Math.floor(Math.random() * array.length);
}
const fruits = ['Apple', 'Banana', 'Orange', 'Grape'];
// This will return different result between `0` to `fruits.length`.
console.log(getRandomIndexFromArray(fruits));
하지만 이 경우는 사실 모든 항목이 같은 확률로 추첨되는 것이나 마찬가지다.
그렇다면 만약 결과값의 횟수가 사과 > 바나나 > 오렌지 > 포도 순서가 되게 하려면 어떻게 하면 될까?
항목 수에 따른 통제
간단한 편법으로는, 배열에 더 많이 추출하고 싶은 항목을 더 많이 넣으면 된다.
// 4 Apples, 3 Bananas, 2 Oranges, 1 Grape.
// The `getRandomIndexFromArray()` method will pick
// `Apple` more than `Banana`, `Banana` more than `Orange`, `Orange` more than `Grape`.
const fruits = [
'Apple',
'Apple',
'Apple',
'Apple',
'Banana',
'Banana',
'Banana',
'Orange',
'Orange',
'Grape',
];
무식한 방법이지만 아주 간단하게 해결할 수 있다.
만약 사과를 20%의 확률, 바나나를 40%의 확률, 오렌지를 5%의 확률, 포도를 35%의 확률로 추출하고 싶다면 사과 20개, 바나나 40개, 오렌지 5개, 포도 35개를 배열에 때려넣으면 된다.
하지만 이는 분명한 메모리 낭비고 만약 소수점 단위의 확률까지 다루게 된다면 수 천, 수 만 개의 항목을 배열에 때려넣어야 한다.
항목 별 확률에 따른 통제
로또 번호는 다들 알다시피 45개가 존재하고, 내가 만든 사이트는 기존 당첨 내역을 통계삼아 각 번호별로 적절한 확률을 부여해 추출하기 때문에 위와 같은 방법은 적절하지 않았다.
그리하여 생각해낸 방법이 각 항목별로 확률을 입력하고, 그 확률에 따라 번호를 추출하는 것이었다.
/**
* Get random index from an array with percentages.
* @param array - An array to get index.
* @param percentages - An array of percentage for each item in array. The total of every items should be `1`.
* @returns {number} A random index between `0` to `array.length`.
*/
function getRandomIndexFromArrayByPercentage(array, percentages) {
const transformedPercentages = [];
percentages.forEach((percentage, index) => {
const previousPercentage = transformedPercentages[index - 1] || 0;
transformedPercentages.push(previousPercentage + percentage);
});
const random = Math.random();
return transformedPercentages.findIndex((percentage, index) => {
const previousPercentage = transformedPercentages[index - 1] || 0;
return random >= previousPercentage && random < percentage;
});
}
const fruits = ['Apple', 'Banana', 'Orange', 'Grape'];
const percentages = [0.2, 0.4, 0.05, 0.35];
console.log(getRandomIndexFromArrayByPercentage(fruits, percentages));
위의 getRandomIndexFromArrayByPercentage()
함수는 첫 번째 파라미터로
인덱스 값을 추출할 배열을 받고, 두 번째 파라미터로 각 배열 항목에 대한 확률 값을 담고있는 배열을 받는다.
percentages
배열의 모든 값을 더하면 1이 되어야 한다.
const transformedPercentages = [];
percentages.forEach((percentage, index) => {
const previousPercentage = transformedPercentages[index - 1] || 0;
transformedPercentages.push(previousPercentage + percentage);
});
함수 내에서 맨 처음으로 하는 일은 percentages
를 forEach()
메서드로 루프하면서
transformedPercentages
에 값을 저장하는 것이다.
이 때 forEach()
콜백 함수에서는 현재 인덱스 이전의 인덱스를 이용해 previousPercentage
를 가져오고
이 값을 현재 인덱스의 percentage
와 더하여 transformedPercentages
에 저장하는데,
위에서 봤던 [0.2, 0.4, 0.05, 0.35]
값은 [0.2, 0.6000000000000001, 0.6500000000000001, 1]
로 저장된다.
만약 현재 인덱스가 0이라면 이전 값이 없기 때문에 ||
기호를 이용해 previousPercentage
의 기본값을 0으로 지정해준다.
const random = Math.random();
return transformedPercentages.findIndex((percentage, index) => {
const previousPercentage = transformedPercentages[index - 1] || 0;
return random >= previousPercentage && random < percentage;
});
그 다음은 Math.random()
메서드를 이용해 난수를 생성하고
transformedPercentages
배열에서 findIndex()
메서드를 이용해
해당 난수가 포함되는 범위의 인덱스를 찾아내는 것이다.
findIndex()
콜백 함수에서는 난수가 transformedPercentages
의 이전 인덱스 값과 현재 인덱스 값 사이에 있는지 확인하고
만약 그렇다면 true
를 리턴하여 인덱스 값을 얻어내도록 한다.
이때도 역시 현재 인덱스가 0일 때 이전 값이 없기 때문에 previousPercentage
에 기본값 0을 설정해줘야 한다.
마무리
확률에 따라 배열에서 항목을 추출하는 함수를 간단하게 구현해보았다.
그런데 중간에 보면 [0.2, 0.4, 0.05, 0.35]
값이 [0.2, 0.6000000000000001, 0.6500000000000001, 1]
로
변환된다고 한 부분이 있는데, 0.6과 0.65에 붙은 0.0000000000000001은 오타가 아니다.
자바스크립트는 소수점 계산이 정확하지 않기 때문에 발생하는 오차인데,
이 정도는 별 의미가 없기 때문에 너그러이 넘어가도록 하자.
그리고 로또번호추첨기는 추가적인 기능 업데이트를 앞두고 있으니 우리 모두 부자가 되기 위해 자주 방문해보자.