ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Base N(n진법)으로 변환하기
    FrontEnd/알고리즘 2021. 5. 23. 15:44

    깃허브 페이지를 이용해 변환 페이지를 호스팅하고 있다:

    https://hayjo.github.io/Visualization-for-BaseN/


    Intro

    알고리즘 풀이를 하다보면 n진법을 다뤄야 할 때가 있는데, 가끔씩 필요하다보니 그새 원리를 잊어버릴 때가 많다.

    매번 찾아보는 것도 일이라서 이참에 변환해주는 원리를 직접 표현해두기로 했다.

     

    기본 로직

    베이스로 삼은 것은 이쪽 자료. 자세한 설명은 네이버 지식백과 진법 변환 페이지에 잘 나와있다.

    출처: 소수의 진법 변환(https://terms.naver.com/entry.naver?docId=3572374&cid=58944&categoryId=58970)

    오른쪽의 2 * (2  * (2 * (2 * 0 + 1) + 1) + 0) + 1 수식을 단계별로 표현해주기로 한다.

    풀어서 쓰면 이렇다.

     

    (12) + 1

    2 * (6) + 1

    (2 * (2 * 3) + 0) + 1

    (2 * (2 * ((2) + 1) + 0) + 1

    (2 * (2 * (2 * (2 * (0) + 1) + 1) + 0) + 1

     

    나머지가 있으면 다른 항으로 분리하고, 분리 후 남은 몫을 2로 나눈다. 이 작업을 몫이 0이 될 때까지 반복한다.

    이걸 함수로 표현하면 다음과 같다.

    function getNumberByBase(base, number) {
      let remainder = 0;
      let quotient = number;
      let result = '';
    
      while (quotient > 0) {
        remainder = quotient % base;
        quotient = (quotient - remainder) / base;
        result = String(remainder) + result;
        
        // 추가 작업
      }
    
      return result;
    }

     

    숫자 나누기를 HTML 요소로 표현하기

    이제 위의 while 루프 안에서 HTML 요소를 만들어주면 되는데, 필요한 작업은 아래와 같다.

     

    1. 처음 숫자를 입력 받기

    2. 나머지를 분리해서 숫자를 (숫자 - 나머지) + 나머지 형태로 교체

    3. (숫자 - 나머지) 값을 BASE로 나눠서 (BASE * 새로운 몫)으로 교체

    4. 새로운 몫을 대상으로 반복

    *요소를 계속 교체해야 하므로(Node.replaceChild) depth를 맞춰줄 것

     

    이걸 HTML에서 생각해보면,

    1. 처음 숫자를 입력 받기

    <div class="holder">
      <span class="quotient">최초 숫자</span>
    </div>

    2. 나머지를 분리해서 숫자를 (숫자 - 나머지) + 나머지 형태로 교체

    <div class="holder">
      (
      <span class="quotient">숫자 - 나머지</span>
      ) + 
      <span class="remainder">나머지</span>
    </div>

    3. (숫자 - 나머지) 값을 BASE로 나눠서 (BASE * 새로운 몫)으로 교체

    <div class="holder">
      (BASE * 
      <span class="quotient">새로운 몫</span>
      ) + 
      <span class="bold">나머지</span>
    </div>

    4. 새로운 몫을 대상으로 반복

    <div class="holder">
      (BASE * (
      <span class="quotient">숫자 - 나머지</span>
      ) + 
      <span class="remainder">나머지</span>
      ) + 
      <span class="remainder">나머지</span>
    </div>

     

    텍스트 변경을 어떻게 처리할까 고민하다가,

    .quotient 요소의 변경하고, 매번 추가되는 괄호나 + 기호는 createTextNode로 만들어서 quotient 앞뒤로 삽입해주는 방식을 택했다.

    그리고 + 기호 뒤에 .remainder 요소를 삽입해주면 된다.

     

    앞뒤에 삽입하는 방법으로는 Node.insertBefore을 이용했다.

    뒤에 삽입하는 경우에 필요한 insertAfter 메소드는 없지만, 대신에 참조노드로 Node.nextSibling을 지정해서 해결했다.

    .nextSibling은 대상 노드가 마지막 노드면 null을 반환해주고,

    .insertBefore은 참조노드로 null 값이 들어오는 경우 .appendChild처럼 마지막 노드로 추가하기 때문에 의도한 대로 동작한다.

     

    <div class="holder">
      <span class="quotient">최초 숫자</span>
    </div>

    먼저 HTML 뼈대를 만들어주고

     

    const $holder = document.querySelector('.holder');
    const $quotient = document.querySelector('.quotient');

    필요한 요소를 잡아온 다음,

     

    async function getNumberByBase(base, number) {
      let remainder = 0;
      let quotient = number;
      let result = '';
    
      while (quotient > 0) {
        remainder = quotient % base;
        const withoutRemainder = quotient - remainder;
        quotient = withoutRemainder / base;
        result = String(remainder) + result;
    
        separateRemainderElement({
          remainder,
          withoutRemainder,
        });
        await delay(1000);
        replaceQuotientElementWithNewQuotient({
          base,
          quotient,
        });
        await delay(1000);
      }
    
      return result;
    }

    위에서 추가작업으로 표시해둔 부분에 들어갈 작업을 함수로 추가했다.

    시간차를 두고 보여주어야 해서 delay 함수를 추가했다.

    function delay(waitingTime) {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve();
        }, waitingTime);
      });
    }

     

    그리고 표시해둔 작업을 함수로 구현하고

    function separateRemainderElement({ remainder, withoutRemainder }) {
      const $prefix = document.createTextNode('(');
      const $postfix = document.createTextNode(') + ');
      $holder.insertBefore($prefix, $quotient);
      $holder.insertBefore($postfix, $quotient.nextSibling);
      // $postfix는 $quotient 뒤에 추가해야 해서 $quotient의 다음 노드 앞에 삽입하도록 했다.
      
      $quotient.textContent = String(withoutRemainder);
      const $remainder = document.createElement('span');
      $remainder.textContent = String(remainder);
      $remainder.classList.add('remainder');
      $holder.insertBefore($remainder, $postfix.nextSibling);
      // $remainder도 $postfix 뒤에 추가해야 해서 마찬가지로 다음 노드 앞에 삽입
    }
    function replaceQuotientElementWithNewQuotient({ base, quotient }) {
      $quotient.textContent = String(quotient);
      const $multipliedByBase = document.createTextNode(`${base} * `);
      $holder.insertBefore($multipliedByBase, $quotient);
    }

    잘 작동하나 테스트를 해본다.

    잘 나온다.

     

    인풋 / n진법 케이스 추가하기

    이제 유저로부터 인풋을 받을 수 있도록 input form을 만들고, 진법을 직접 선택할 수 있도록 콤보박스를 만든다.

    자바스크립트에서 처리가능한 최대숫자는 Number.Max_SAFE_INTEGER로 얻을 수 있다.

    1 <= user input <= Number.Max_SAFE_INTEGER로 받아서 Number로 변환해야 한다.

     

    <div class="user-input__form-box">
      <form class="user-input__form js__user-input__form">
        <input class="user-input__input-box js__user-input__input-box" type="text" />
      </form>
    </div>

    마찬가지로 먼저 HTML 뼈대를 만들어주고

     

    const $inputFormBody = document.querySelector('.js__user-input__form');
    const $userInput = document.querySelector('js__user-input__input-box');

    요소를 잡아온 다음에

    $inputFormBody.addEventListener('submit', handleUserInput);

    리스너를 달아주고

     

    async function handleUserInput(event) {
      event.preventDefault();
      const inputNumber = Number($userInput.value);
    
      if (Number.isSafeInteger(inputNumber) && inputNumber > 0) {
        await getNumberByBase(2, inputNumber);
      }
    }

    리스너함수에서 입력 받은 숫자를 처리해서 넘기게 한다. 그리고 테스트를 해보면

    의도대로 잘 작동한다. 이제 n진법을 선택할 수 있도록 하면 된다.

    옵션은 주로 사용할 법한 진법들인 2~9진법, 16진법(0~9, A~F), 36(0~9, A~Z)진법으로 하려고 하는데,

    10진법이 넘어가면 나머지가 10 이상으로 표시되는 문제가 있으니

    separateRemainderElement 함수에 값을 넘길 때 remainder를 toString(base)처리를 해서 넘겨주었다.

    separateRemainderElement({
      remainder: remainder.toString(base),
      withoutRemainder,
    });

    36진법이 잘 표시되는지 테스트를 해본다.

     

    <div class="display-base-number">
      현재
      <span class="base-number js__base-number">36</span>
      진법을 사용 중입니다.
    </div>

    HTML에 안내 표시를 넣어주고

     

    const $baseNumber = document.querySelector('.js__base-number');

    요소를 잡아와서

     

    async function handleUserInput(event) {
      event.preventDefault();
      const inputNumber = Number($userInput.value);
    
      if (Number.isSafeInteger(inputNumber) && inputNumber > 0) {
        await getNumberByBase(+$baseNumber.textContent, inputNumber);  // 여기
      }
    }

    base를 넘길 때 값을 가져와서 넘기도록 한다.

    그리고 테스트를 해보면

    잘 나오고 있다. 이제 콤보박스를 만들고 리스너를 달아주면 된다.

     

    <div class="base-number__select-box__holder">
      <span>현재</span>
      <select class="js__base-number__select-box" name="base-number">
        <option value="2" selected>2</option>
        <option value="3">3</option>
        <option value="4">4</option>
        <option value="5">5</option>
        <option value="6">6</option>
        <option value="7">7</option>
        <option value="8">8</option>
        <option value="9">9</option>
        <option value="10">10</option>
        <option value="16">16</option>
        <option value="36">36</option>
      </select>
      <span>진법을 사용 중입니다.</span>
    </div>

    콤보박스에 값을 주고,

    const $selectBoxBody = document.querySelector('.js__base-number__select-box');
    let BASE_NUMBER = 2;

    이제 여기서 값을 받아와야 하니 $baseNumber는 없애고 BASE_NUMBER 변수를 새로 만들었다.

     

    function handleBaseNumberSelecting({ target }) {
      const selectedBase = target.children[target.selectedIndex].value;
      BASE_NUMBER = Number(selectedBase);
    }

    콤보박스 값이 변경되면 BASE_NUMBER 값이 바뀌게 된다.

    그리고 테스트를 해보면

     

    의도대로 잘 작동한다. 이제 여러 번 사용할 수 있게 초기화 부분을 추가해줘야 한다.

     

    초기화 추가하기

    async function handleUserInput(event) {
      event.preventDefault();
      const inputNumber = Number($userInput.value);
    
      if (Number.isSafeInteger(inputNumber) && inputNumber > 0) {
        resetHolder();
        await getNumberByBase(BASE_NUMBER, inputNumber);
      }

    일단 새로 숫자가 들어오면 초기화를 해줘야하니 값을 넘기기 전에 resetHolder 함수를 실행해준다.

    function resetHolder() {
      const temporaryParent = new DocumentFragment();
      temporaryParent.appendChild($quotient);
      $holder.textContent = '';
      $holder.appendChild(temporaryParent);
    }

    $holder 빼고 나머지 자식을 전부 초기화해야하는데 마땅한 방법이 생각나지 않아서 Fragment에 옮겨두는 방법을 썼다.

    초기화가 되기는 하는데 HTML 요소의 값이 초기화될 뿐, getNumberByBase 함수가 중단되는 것은 아니라서

    앞선 getNumberByBase 함수가 실행 중일 때 새로 숫자가 들어오면 이렇게 된다.

     

    이런 경우를 막아야 하니 변환이 진행되고 있는 도중에는 새로 값이 들어오지 않도록 차단해야 한다.

    flag 변수를 만들어서 처리하면 될 것 같아서 기존에 작성했던 throttle 함수를 재활용해서 클로저 형태로 만들어보았다.

     

    function modifiedThrottle(func) {
      let isWaiting = false;
    
      return async (...args) => {
        if (isWaiting) {
          return;
        }
    
        isWaiting = true;
        await func(...args);
        isWaiting = false;
      };
    }
    
    const modifiedGetNumberByBase = modifiedThrottle(getNumberByBase);

    isWaiting 함수를 만들어주고 실행 앞뒤로 토글을 추가해주었다.

    다시 함수가 실행 중일 때 새로 숫자를 넣어보니

     

    실행은 안 끊기고 잘 된다. resetHolder 실행위치만 getNumberByBase 내부로 바꿔주면 되겠다.

     

    자릿수 테이블로 보여주기

    이제 이 값을 자릿수에 맞춰서 정렬해서 보여주는 부분을 추가한다.

    11의 변환값이라면 이런 형태로, 위의 계산이 진행될 때마다 추가되면 좋겠다.

    2^3 2^2 2^1 2^0
    1 1 0 1

     

    getNumberByBase의 while loop마다 추가되어야 하니 우선 함수 위치를 정하고, 지수 값이 될 index를 추가해주었다.

    async function getNumberByBase(base, number) {
      resetHolder();
    
      let remainder = 0;
      let quotient = number;
      let result = '';
      let index = 0;
    
      while (quotient > 0) {
        remainder = quotient % base;
        const withoutRemainder = quotient - remainder;
        quotient = withoutRemainder / base;
        result = String(remainder) + result;
    
        // 숫자 나누기 보여주는 부분
        updatePolynomialTable(base, index, remainder); // 여기 추가
        index++;
      }

     

    이제 테이블이 될 HTML 요소를 만들어주고,

    <div class="mark-polynomial-table__container">
      <table class="mark-polynomial-table js__mark-polynomial-table">
        <thead class="js__mark-polynomial-thead">
          <tr class="mark-polynomial-thead__exponent js__mark-polynomial-thead__exponent"></tr>
        </thead>
        <tbody class="js__mark-polynomial-tbody">
          <tr class="mark-polynomial-tbody__remainder js__mark-polynomial-tbody__remainder"></tr>
        </tbody>
      </table>
    </div>

    thead 부분이 base부분, tbody 부분이 나머지 부분이 된다.

    행은 하나씩만 쓸 거라서 tr을 만들고 거기에 바로 클래스를 주었다.

     

    const $polynomialTableHead = document.querySelector('.js__mark-polynomial-thead__exponent');
    const $polynomialTableBody = document.querySelector('.js__mark-polynomial-tbody__remainder');

    HTML 요소를 잡아와서

     

    function updatePolynomialTable(base, index, remainder) {
      const $multiplierBox = document.createElement('td');
      const $remainderBox = document.createElement('td');
    
      $multiplierBox.textContent = `${base} ** ${index}`;
      $remainderBox.textContent = remainder;
    
      $polynomialTableHead.prepend($multiplierBox);
      $polynomialTableBody.prepend($remainderBox);
    }

    값이 들어오면 테이블에 td로 만들어서 넣어준다.

    앞에서부터 추가해줘야해서 append 대신에 prepend를 썼다.

     

     

    값을 보여주는 elements가 늘었으니 초기화 범위도 늘려줘야 한다.

     

    function resetElements() {
      const temporaryParent = new DocumentFragment();
      temporaryParent.appendChild($quotient);
      $holder.textContent = '';
      $holder.appendChild(temporaryParent);
    
      $polynomialTableHead.textContent = '';
      $polynomialTableBody.textContent = '';
    }

    아까 만들어준 resetHolder 함수를 확장해서 테이블도 초기화해주게 변경했다.

     

    마지막으로 2 ** 4 표기가 보기 좋지 않으니 보기 좋게 변경한다.

    number-box class를 만들고, exponent도 따로 class를 부여해서 작은 숫자로 표기되게 했다.

    .number-box {
      vertical-align: middle;
      box-sizing: border-box;
      display: inline-block;
      min-width: 30px;
      font-size: 45px;
    }
    
    .exponent-box {
      font-size: 20px;
      vertical-align: top;
    }
    function updatePolynomialTable(base, index, remainder) {
      const $multiplierCell = document.createElement('td');
      const $remainderCell = document.createElement('td');
    
      const $multiplier = document.createElement('span');
      const $base = getElementBox(base, ['number-box']);
      const $exponent = getElementBox(base, ['exponent-box']);
      const $remainder = getElementBox(remainder, ['number-box']);
    
      $multiplier.appendChild($base);
      $multiplier.appendChild($exponent);
    
      $multiplierCell.appendChild($multiplier);
      $remainderCell.appendChild($remainder);
    
      $polynomialTableHead.prepend($multiplierCell);
      $polynomialTableBody.prepend($remainderCell);
    }

     

    최종 페이지 데모:

    'FrontEnd > 알고리즘' 카테고리의 다른 글

    Permutation Power Set (순서를 구분하는 멱집합)  (0) 2021.03.06

    댓글

Designed by Tistory.