📌

JavaScriptで数字を桁の配列に変換するベンチマーク

2023/09/15に公開

とあるカウンターを作るに当たって、数字を特定桁数(例えば 10 桁)の桁の配列に変換する必要があって、汎用性は微妙で需要零かもしれないけど、いろんな方法を思いついたので、どれが一番速くて簡潔で優雅だろうと思って、色々性能を試してみた。

関数の定義として (m: number, n: number) => (number | null)[]> が形の関数で、数字を一桁ずつ右揃えで書き、未満の桁(前の零)をnullにして、普通の数字(零含む)を一桁の数字にするn要素の配列を返す。

具体的には

f(12, 2) = [1, 2]
f(12, 4) = [null, null, 1, 2]
f(101, 5) = [null, null, 1, 0, 1]

という調子

コードは TypeScript で書かれ、node と bun でそれぞれテストしてみた。Overflow のケースは考慮されていない。

方法一覧

function arrayFrom(m: number, n: number) {
  const str = m.toString();
  return Array.from({ length: n }, (_, i) =>
    i < n - str.length ? null : +str[i - (n - str.length)]
  );
}

function arrayExpand(m: number, n: number) {
  const str = m.toString();
  return [...Array(n - str.length).fill(null), ...str.split('').map(Number)];
}

function arrayFill(m: number, n: number) {
  const str = m.toString();
  return Array(n - str.length)
    .fill(null)
    .concat(...str.split('').map(Number));
}

function arrayConcat(m: number, n: number) {
  const str = m.toString();
  return Array(n - str.length)
    .fill(null)
    .concat(...str.split('').map(Number));
}

function stringPad(m: number, n: number) {
  const str = m.toString();
  return str.padStart(n, '-').split('').map((v) => (v === '-' ? null : +v));
}

function mathExtract(m: number, n: number) {
  const result: (number | null)[] = [];
  while (m > 0) {
    result.unshift(m % 10);
    m = Math.floor(m / 10);
  }
  while (result.length < n) {
    result.unshift(null);
  }
  return result;
};

function recursiveExtract(m: number, n: number): (number | null)[] {
  if (m === 0 && n === 0) return [];
  if (n === 0) return [];

  const prev = recursiveExtract(Math.floor(m / 10), n - 1);
  prev.push(m === 0 ? null : m % 10);
  return prev;
};

テスト結果

結果はすべて四捨五入したもの。node は v16.8.0、bun は v1.0.1。

関数 テスト1 (node) テスト2 (node) テスト3 (bun) テスト4 (bun)
arrayFrom 1.301s 1.310s 0.459s 0.460s
arrayExpand 0.782s 0.804s 0.296s 0.288s
arrayFill 1.030s 1.043s 0.500s 0.493s
arrayConcat 1.018s 1.030s 0.498s 0.484s
stringPad 0.835s 0.874s 0.351s 0.318s
mathExtract 0.963s 0.964s 0.625s 0.599s
recursiveExtract 0.962s 0.967s 0.378s 0.367s

結果を図式化したもの
(一番低い値が一番望ましい)

結果分析

いや~bun君速いっす…しかも TypeScript 直実行…すごすぎ

結論として、一番速い方法は arrayExpand 関数で、しかも結構コードが短いくてキレイなので、まあこれ一択かな!

整理しつつエラー検出も付けるとこんな感じになる

function arrayExpand(m: number, n: number) {
  if (n <= 0) throw new Error('n must be positive');
  if (m < 0) throw new Error('m must be positive or zero');
  if (m === 0) return Array(n - 1).fill(null).concat(0);
  const str = m.toString();
  if (str.length > n) throw new Error('m is too big');
  return [...Array(n - str.length).fill(null), ...str.split('').map(Number)];
}

テスト用コード

テスト用コード(すべて)

function arrayFrom(m: number, n: number) {
  const str = m.toString();
  return Array.from({ length: n }, (_, i) =>
    i < n - str.length ? null : +str[i - (n - str.length)]
  );
}

function arrayExpand(m: number, n: number) {
  const str = m.toString();
  return [...Array(n - str.length).fill(null), ...str.split('').map(Number)];
}

function arrayFill(m: number, n: number) {
  const str = m.toString();
  return Array(n - str.length)
    .fill(null)
    .concat(...str.split('').map(Number));
}

function arrayConcat(m: number, n: number) {
  const str = m.toString();
  return Array(n - str.length)
    .fill(null)
    .concat(...str.split('').map(Number));
}

function stringPad(m: number, n: number) {
  const str = m.toString();
  return str.padStart(n, '-').split('').map((v) => (v === '-' ? null : +v));
}

function mathExtract(m: number, n: number) {
  const result: (number | null)[] = [];
  while (m > 0) {
    result.unshift(m % 10);
    m = Math.floor(m / 10);
  }
  while (result.length < n) {
    result.unshift(null);
  }
  return result;
};

function recursiveExtract(m: number, n: number): (number | null)[] {
  if (m === 0 && n === 0) return [];
  if (n === 0) return [];

  const prev = recursiveExtract(Math.floor(m / 10), n - 1);
  prev.push(m === 0 ? null : m % 10);
  return prev;
};

const FUNCTIONS: Record<string, (m: number, n: number) => (number | null)[]> = {
  arrayFrom, arrayExpand, arrayFill, arrayConcat, stringPad, mathExtract, recursiveExtract
}

// Generate random numbers
const numbers = Array.from({ length: 1e6 }, () => Math.floor(Math.random() * 1e6));

const forTesting = numbers.slice(0, 10);
const testedResult: Record<number, Record<string, (number | null)[]>> = {};

for (const num of forTesting) {
  testedResult[num] = {};
  for (const [name, fn] of Object.entries(FUNCTIONS)) {
    testedResult[num][name] = fn(num, 10);
  }
}

for (const [num, result] of Object.entries(testedResult)) {
  // Assert every result is the same
  const results = Object.entries(result);
  const ref = results[0][1];

  for (const [name, res] of results) {
    console.assert(JSON.stringify(res) === JSON.stringify(ref), name, num);
  }
}

for (const [name, fn] of Object.entries(FUNCTIONS)) {
  console.time(name);
  for (const num of numbers) {
    fn(num, 10);
  }
  console.timeEnd(name);
}

Discussion