본문 바로가기

Backend/Javascript

[Javascript] - Big Integer

Big Integer (큰 정수)

 

- 자바스크립트에는 64비트 정수가 없다

 

  • int64 형은 최대 9223372036854775807 까지의 정수를 담을 수 있는데, 자바스크립트의 Number.MAX_SAFE_INTEGER 범위보다 세자리나 큼
  • 자바스크립트의 숫자형은 한개라 새로운 숫자형을 추가하는데에도 문제가 있음 -> 단순성을 잃게되고, 잠재적으로 다른 버그를 초래할 가능성이 커짐
  • 만약 64비트 정수형을 추가하더라도 이후 72비트, 96비트, 128비트 등등을 대응할 필요는 없을까? 라는 논쟁이 이어질 수 있습니다.

- 큰 정수를 언어에 추가하는 대신 라이브러리화 하는 것이 적절해 보이며 해당 부분을 이번 내용에서 보도록 하겠습니다.

 

  • 큰 정수는 배열 형태로 저장할 것입니다. (크기가 자유롭기에)
  • 배열의 각 요소는 큰 정수의 일부 비트에 해당하는 값을 가집니다.
  • 배열의 각 요소별로 사용할 비트수에 대해 가장 큰 값은 안전한 양수 범위를 표현할 수 있는 53개지만, 비트 단위 연산자 및 곱하기나 나누기를 고려하면 24비트가 적절합니다.
  • 부호를 표시할 수 있는 크기 표현 방식으로 배열의 0번 요소는 숫자의 부호를 나타내며 "+" 또는 "-" / 아래 예시)
9000000000000000000
  = ["+", 8650752, 7098594, 31974]
  = 8650725 + 7098594 * 16777216 ** 1 + 31974 * 16777216 ** 2 // 16777216 = Math.pow(2, 24)

 

- 위의 설계 부분을 바탕으로 큰 정수 라이브러리를 만들어 보겠습니다. 코드의 양이 꽤 길지만 충분한 주석을 추가했습니다.

 

// 상수 정의
const radix = 16777216; // Math.pow(2, 24);
const radix_squared = radix * radix;
const log2_radix = 24;
const plus = "+";
const minus = "-";
const sign = 0;
const least = 1;

const last = (array) => {
  return array[array.length - 1];
};

const next_to_last(array) => {
  return array[array.length - 2];
}

// 코드 가독성을 위해 추가 (꼭 필요하진 않음)
const zero = Object.freeze([plus]);
const one = Object.freeze([plus, 1]);
const two = Object.freeze([plus, 2]);
const ten = Object.freeze([plus, 10]);
const negative_one = Object.freeze([minus, 1]);

// 큰정수인지 판별하는 술어 함수 (= true or false 를 반환하는 함수)
const is_big_integer = (big) => {
  return Array.isArray(big) && (big[sign] === plus || big[sign] === minus);
};

// 부호가 "-" 인지 판별하는 술어 함수
const is_negative = (big) => {
  return Array.isArray(big) && big[sign] === minus;
}

// 부호가 "+" 인지 판별하는 술어 함수
const is_positive = (big) => {
  return Array.isArray(big) && big[sign] === plus;
};

// 0인지 판별하는 술어 함수
const is_zero = (big) => {
  return !Array.isArray(big) || big.length < 2;
};

// 배열의 마지막 요소가 0인 경우 제거하는 함수 (윗자리에 있는 0은 필요없는 숫자이므로)
const mint = (proto_big_integer) => {
  while(last[proto_big_integer) === 0) {
    proto_big_integer.length -= 1;
  }
  
  if (proto_big_integer.length <= 1) {
    return zero;
  }
  
  if (proto_big_integer[sign] === plus) {
    if (proto_big_integer.length === 2) {
      if (proto_big_integer[least] === 1) {
        return one;
      }
      if (proto_big_integer[least] === 2) {
        return two;
      }
      if (proto_big_integer[least] === 10) {
        return ten;
      }
    }
  } else if (proto_big_integer.length === 2) {
    if (proto_big_integer[least] === 1) {
      return nagative_one;
    }
  }
  return Object.freeze(proto_big_integer); // 더 이상 바꿀 것이 없을 시 동결
};

// 부호 바꾸기 함수
const neg = (big) => {
  if (is_zero(big)) return zero;

  let negation = big.slice();
  negation[sign] = (is_negative(big) ? plus : minus);
  
  return mint(negation);
};

// 절댓값 구하는 함수
const abs = (big) => {
  return (is_zero(big) ? zero : (is_negative(big) ? neg(big) : big));
};

// 부호 추출하는 함수
const signum = (big) => {
  return (is_zero(big) ? zero : (is_negative(big) ? negative_one : one));
};

// 두 큰 정수가 동일한 값을 가지는지 확인하는 함수
const eq = (comparahend, comparator) => {
  return comparahend === comparator || (
    comparahend.length === comparator.length && comparahend.every((element, i) => element === comparator[i])
  );
};

// 큰 정수의 절댓값이 다른 큰 정수 절댓값보다 작은지 판별하는 함수
const abs_lt = (comparahend, comparator) => {
  return (
    comparahend.length === comparator.length ? // 길이가 같으면 모든 배열 원소를 비교
      comparahend.reduce((reduction, element, i) => {
        if (i !== sign) {
          const other = comparator[i];
          if (element !== other) {
            return element < other;
          }
        }
        return reduction;
      }, false)
      :
      comparahend.length < comparator.length // 길이가 더 작으면 절댓값이 작은 것
  );
};

// 부호를 포함한 값이 다른 정수값보다 작은지 판별하는 함수
const lt = (comparahend, comparator) => {
  return (
    comparahend[sign] !== comparator[sign] ?
      is_negative(comparahend)
      :
      (
        is_negative(comparahend) ? abs_lt(comparator, comparahend) : abs_lt(comparahend, comparator)
      )
  );
};

// lt 함수를 활용해 다른 비교함수를 만듬, 이름으로 함수의 기능 파악 가능
const ge = (a, b) => !lt(a, b);
const gt = (a, b) => lt(b, a);
const le = (a, b) => !lt(b, a);

// and 비트 연산 함수
const and = (a, b) => {
  if (a.length < b.length) {
    [a, b] = [b, a]; // 더 짧은 배열을 a 로
  }
  return mint(a.map((element, i) => {
    return (i === sign ? plus : element & b[i]);
  }));
};

// or 비트 연산 함수
const or = (a, b) => {
  if (a.length < b.length) {
    [a, b] = [b, a]; // 더 긴 배열을 a 로
  }
  return mint(a.map((element, i) => {
    return (i === sign ? plus : element | (b[i] || 0));
  }));
};

// xor 비트 연산 함수
const xor = (a, b) => {
  if (a.length < b.length) {
    [a, b] = [b, a]; // 더 긴 배열을 a 로
  }
  return mint(a.map((element, i) => {
    return (i === sign ? plus : element ^ (b[i] || 0));
  }));
};

// 숫자와 큰 정수 값을 쉽게 처리하기 위한 함수
const int = (big) => {
  let result;
  
  if (typeof big === "number") {
    if (Number.isSafeInteger(big)) {
      return big;
    }
  } else if (is_big_integer(big)) {
    if (big.length < 2) {
      return 0;
    }
    if (big.length === 2) {
      return (is_negative(big) ? -big[least] : big[least]);
    }
    if (big.length === 3) {
      result = big[least + 1] * radix + big[least];
      return (is_negative(big) ? -result : result);
    }
    if (big.length === 4) {
      result = (big[least + 2] * radix_squared + big[least + 1] * radix + big[least]);
      if (Number.isSafeInteger(result)) {
        return (is_negative(big) ? -result : result);
      }
    }
  }
};

// 최하위 비트를 삭제해 숫자의 크기를 줄이는 함수
const shift_down = (big, places) => {
  if (is_zero(big)) return zero;
  
  places = int(places);
  
  if (Number.isSafeInteger(places)) {
    if (places === 0) return abs(big);
    if (places < 0) return shift_up(big, -places);
    
    let skip = Math.floor(places / log2_radix);
    places -= skip * log2_radix;
    
    if (skip + 1 >= big.length) return zero;
    
    big = (skip > 0 ? mint(zero.concat(big.slice(skip + 1))) : big);
    
    if (places === 0) return big;
    
    return mint(big.map((element, i) => {
      if (i === sign) return plus;
      
      return ((radix - 1) & ((element >> places) | ((big[i + 1] || 0) << (log2_radix - places))));
    }));
  }
};

// 최하위 위치에 0을 넣어 숫자를 증가시키는 함수
const shift_up = (big, places) => {
  if (is_zero(big)) return zero;
  
  places = int(places);
  
  if (Number.isSafeInteger(places)) {
    if (places === 0) return abs(big);
    
    if (places < 0) return shift_down(big, -places);
    
    let blanks  = Math.floor(places / log2_radix);
    let result = new Array(blanks + 1).fill(0);
    
    result[sign] = plus;
    places -= blanks * log2_radix;
    
    if (places === 0) return mint(result.concat(big.slice(least)));
    
    let carry = big.reduce((accumulator, element, i) => {
      if (i === sign) return 0;
      
      result.push(((element << places | accumulator) & (radix - 1));
      
      return element >> (log2_radix - places);
    }, 0);
    
    if (carry > 0) result.push(carry);
    
    return mint(result);
  }
};

// 값이 1인 비트를 지정한 갯수만큼 가진 큰 정수를 만드는 함수
const mask = (bits) => {
  bits = int(bits);
  
  if (bits !== undefined && bits >= 0) {
    let mega = Math.floor(bits / log2_radix);
    let result = new Array(mega + 1).fill(radix - 1);
    result[sign] = plus;
    let leftover = bits - (mega * log2_radix);
    
    if (leftover > 0) result.push((1 << leftover) - 1);
    
    return mint(result);
  }
};

// 모든 비트의 보수를 만드는 not 함수
const not = (a, bits) => xor(a, mask(i));

// 임의의 큰 정수를 생성하는 함수
const random = (bits, random = Math.random) => { // 보안을 신경 써야 한다면 Math.random 보다 더 강력한 난수 생성기를 인잘 전달
  const ones = mask(bits);
  
  if (ones !== undefined) {
    return mint(ones.map((element, i) => {
      if (i === sign) return plus;
      
      const b = random();
      
      return ((b * radix_squared) ^ (b * radix)) & element;
    }));
  }
};

// 더하기
const add = (augend, addend) => {
  if (is_zero(augend)) return addend;
  if (is_zero(addend)) return augend;
  if (augend[sign] !== addend[sign]) return sub(augend, neg(addend)); // 부호가 다르면 뺄셈
  
  if (augend.length < addend.length) [addend, augend] = [augend, addend];
  
  let carry = 0;
  let result = augend.map((element, i) => {
    if (i !== sign) {
      element += (addend[i] || 0) + carry;
      if (element >= radix) {
        carry = 1;
        element -= radix;
      } else {
        carry = 0;
      }
    }
    return element;
  }); // 더 긴 정수쪽에 map 을 쓴 다음 || 연산자를 통해 짧은 쪽에 존재하지 않는 요소에 0을 적용해 덧셈
  
  if (carry > 0) result.push(carry);
  
  return mint(result);
};

// 뺄셈
const sub = (minuend, subtrahend) => {
  if (is_zero(subtrahend)) return minuend;
  if (is_zero(minuend)) return neg(subtrahend);
  
  let minuend_sign = minuend[sign];
  
  if (minuend_sign !== subtrahend[sign]) return add(minuend, neg(subtrahend)); // 부호가 다르면 더하기
  
  // 큰 수에서 작은 수를 뺌
  if (abs_lt(minuend, subtrahend)) {
    [subtrahend, minuend] = [minuend, subtrahend];
    minuend_sign = (minuend_sign === minus ? plus : minus);
  }
  let borrow = 0;
  
  return mint(minuend.map((element, i) => {
    if (i === sign) return minuend_sign;
    
    let diff = element - ((subtrahend[i] || 0) + borrow);
    
    if (diff < 0) {
      diff += 16777216;
      borrow = 1;
    } else {
      borrow = 0;
    }
    return diff;
  }));
};

// 곱셈 -> 각 요소의 곱은 48비트가 될 수 있지만 24비트밖에 담을 수 없으므로 초과할 시 자리올림수 처리
const mul = (multiplicand, multiplier) => {
  if (is_zero(multiplicand) || is_zero(multiplier)) return zero;
  
  let result = [mutiplicand[sign] === multiplier[sign] ? plus : minus]; // 두 수의 부호가 같으면 결과는 양수
  
  // 자리올림수를 계속 전달하면서 multiplicand 의 각 요소와 multiplier 의 각 요소를 곱함
  multiplicand.forEach((multiplicand_e, i) => {
    if (i !== sign) {
      let carry = 0;
      multiplier.forEach((multiplier_e, j) => {
        if (j !== sign) {
          let at = i + j - 1;
          let product = (multiplicand_e * multiplier_e) + (result[at]) || 0) + carry);
          result[at] = product & 16777215;
          carry = Math.floor(product / radix);
        }
      });
      if (carry > 0) result[i + multiplier.length - 1] = carry;
    }
  });
  return mint(result);
};

// 나누기 (몫과 나머지 모두 반환)
const divrem = (dividend, divisor) => {
  if (is_zero(dividend) || abs_lt(dividend, divisor)) return [zero, dividend];
  if (is_zero(divisor)) return undefined;
  
  let quotient_is_negative = dividend[sign] !== divisor[sign];
  let remainder_is_negative = dividend[sign] === minus;
  let remainder = dividend;
  dividend = abs(dividend);
  divisor = abs(divisor);
  
  /*
   * 최상위 비트가 1이 될 때까지 왼쪽으로 시프트
   * 나눠지는 수도 똑같이 길이만큼 시프트
   * 시프트 횟수를 알아내기 위해 첫번째 0을 찾음
   */
  let shift = Math.clz32(last(divisor)) - 8; // clz32 는 32비트내에서 찾는 함수라 8을 뺌 (24비트)
  
  dividend = shift_up(dividend, shift);
  divisor = shift_up(divisor, shift);
  let place = dividend.length - divisor.length;
  let dividend_prefix = last(dividend);
  let divisor_prefix = last(divisor);
  
  if (dividend_prefix < divisor_prefix) {
    dividend_prefix = (dividend_prefix * radix) + next_to_last(dividend);
  } else {
    place += 1;
  }
  divisor = shift_up(divisor, (place - 1) * 24);
  let quotient = new Array(place + 1).fill(0);
  quotient[sign] = plus;
  
  while(true) {
    let estimated = Math.floor(dividend_prefix / divisor_prefix);
    
    // 추정값이 너무 큰 경우 결과가 음수면 줄인 다음 다시 시도, 너무 작은 경우는 없음
    if (estimated > 0) {
      while(true) {
        let trial = sub(dividend, mul(divisor, [plus, estimated]));
        
        if (!is_negative(trial)) {
          dividend = trial;
          break;
        }
        estimated -= 1;
      }
    }
    
    // quotient에 저장하되 마지막 공간이면 다음으로 넘어감
    quotient[place] = estimated;
    place -= 1;
    
    if (place === 0) break;
    
    if (is_zero(dividend)) break;
    
    dividend_prefix = last(dividend) * radix + next_to_last(dividend);
    divisor = shift_down(divisor, 24);
  }
  
  quotient = mint(quotient);
  remainder = shift_down(dividend, shift);
  
  return [
    (quotient_is_negative ? neg(quotient) : quotient),
    (raminder_is_negative ? neg(reminader) : remainder)
  ];
};

// 몫만 반환하는 함수도 제공
const div = (dividend, divisor) => {
  let temp = divrem(dividend, divisor);
  
  if (temp) return temp[0];
};

// 거듭제곱 (제곱과 곱하기를 사용)
const power = (big, exponent) => {
  let exp = int(exponent);
  
  if (exp === 0) return one;
  if (is_zero(big)) return zero;
  if (exp === undefined || exp < 0) return undefined;
  
  let result = one;
  
  while(true) {
    if ((exp & 1) !== 0) result = mul(result, big);
      
    exp = Math.floor(exp / 2);
      
    if (exp < 1) break;
      
    big = mul(big, big);
  }
  return mint(result);
};

// 기약분수로 만들는 함수
const gcd = (a, b) => {
  a = abs(a);
  b = abs(b);
  
  while(!is_zero(b)) {
    let [ignore, remainder] = divrem(a, b);
    a = b;
    b = remainder;
  }
  return a;
};

/*
 * 숫자형이나 문자열을 큰 정수로 변환 후 반대로도 변환할 수 있는 함수가 필요
 * digitset 문자열은 숫자를 문자로 매핑시킬 때 사용
 * charset 객체는 반대로 문자를 숫자로 매핑
 */
const digitset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ*~$=";
const charset = (object) => {
  digitset.split("").forEach((element, i) => {
    object[element] = i;
  });
  return Object.freeze(object);
}(Object.create(null)));

// 숫자나 문자열, 기수값을 받아 큰 정수로 반환하는 함수
const make = (value, radix_2_37) => {
  let result;
  
  if (typeof value === "string") {
    let radish;
    
    if (radix_2_37 === undefined) {
      radix_2_37 = 10;
      radish = ten;
    } else {
      if (!Number.isInteger(radix_2_37) || radix_2_37 < 2 || radix_2_37 > 37) {
        return undefined;
      }
      radish = make(radix_2_37);
    }
    
    result = zero;
    let good = false;
    let negative = false;
    
    if (value.toUpperCase().split("").every((element, i) => {
      let digit = charset[element];
      
      if (digit !== undefined && digit < radix_2_37) {
        result = add(mul(result, radish), [plus, digit]);
        good = true;
        return true;
      }
      if (i === sign) {
        if (element === plus) return true;
        if (element === minus) {
          negative = true;
          return true;
        }
      }
      return digit === "_";
    }) && good) {
      if (negative) result = neg(result);
      
      return mint(result);
    }
    return undefined;
  }
  
  if (Number.isInteger(value)) {
    let whole = Math.abs(value);
    
    result = [(value < 0 ? minus : plus)];
    
    while(whole >= radix) {
      let quotient = Math.floor(whole / radix);
      result.push(whole - (quotient * radix));
      whole = quotient;
    }
    
    if (whole > 0) result.push(whole);
    
    return mint(result);
  }
  
  if (Array.isArray(value)) return mint(value);
};

// 큰 정수 값을 자바스크립트 수로 바꿈 (값이 안전한 정수 범위내에 있을때만 유효)
const number = (big) => {
  let value = 0;
  let the_sign = 1;
  let factor = 1;

  big.forEach((element, i) => {
    if (i === 0) {
      if (element === minus) the_sign = -1;
    } else {
      value += element * factor;
      factor *= radix;
    }
  });
  return the_sign * value;
};

// 큰 정수값을 문자열로 변환하는 함수
const string = (a, radix_2_thru_37 = 10) => {
  if (is_zero(a)) return "0";
  
  radix_2_thru_37 = int(radix_2_thru_37);
  
  if (!Number.isSafeInteger(radix_2_thru_37) || radix_2_thru_37 < 2 || radix_2_thru_37 > 37) {
    return undefined;
  }
  const radish = make(radix_2_thru_37);
  const the_sign = (a[sign] === minus ? "-" : "");
  
  a = abs(a);
  let digits = [];
  
  while(!is_zero(a)) {
    let [quotient, remainder] = divrem(a, radish);
    digits.push(digitset[number(remainder)]);
    a = quotient;
  }
  digits.push(the_sign);
  return digits.reverse().join("");
};

// 32비트 정수에서 값이 1인 비트의 개수 반환
const population_32 = (int32) => {
  int32 -= (int32 >>> 1) & 0x55555555;
  
  int32 = (int32 & 0x33333333) + ((int32 >>> 2) & 0x33333333);
  
  int32 = (int32 + (int32 >>> 4) & 0x0F0F0F0F;
  
  int32 = (int32 + (int32 >>> 8) & 0x001F001F;
  
  return (int32 + (int32 >>> 16)) & 0x0000003F;
};

// 큰 정수에서 값이 1인 비트개수를 세어서 반환하는 함수 (해밍거리를 계산할 때 유용)
const population = (big) => {
  return big.reduce((reduction, element, i) => {
    return reduction + (i === sign ? 0 : population_32(element));
  }, 0);
};

// 앞쪽의 0을 제외한 전체 비트 수 세어서 반환하는 함수
const significant_bits = (big) => {
  return (
    big.length > 1
    ?
    make((big.length - 2) * log2_radix + (32 - Math.clz32(last(big))))
    :
    zero
  );
};

export default Object.freeze({
  abs,
  abs_lt,
  add,
  and,
  div,
  divrem,
  eq,
  gcd,
  is_big_integer,
  is_negative,
  is_positive,
  is_zero,
  lt,
  make,
  mask,
  mul,
  neg,
  not,
  number,
  or,
  population,
  power,
  random,
  shift_down,
  shift_up,
  significant_bits,
  signum,
  string,
  sub,
  ten,
  two,
  one,
  xor,
  zero
});

 

- 상당히 긴 코드였지만 이렇게 해서 big_integer 객체를 export 시켰습니다.

 

import big_integer from './big_integer';

 

- 위와 같이 해당 모듈을 임포트해서 사용할 수 있으며, 추후에 해당 모듈의 각 함수를 사용한 예시도 다뤄보도록 하겠습니다.

'Backend > Javascript' 카테고리의 다른 글

[Javascript] - Object  (0) 2020.10.31
[Javascript] - Array  (0) 2020.10.24
[Javascript] - Boolean  (0) 2020.10.17
[Javascript] - Number  (0) 2020.10.03
[Javascript] - Name  (0) 2020.09.26