カメリアの記事

意味があることやないことを綴ります

JavaScript 自由研究 - 最頻値を求めるコード

javascript 配列の最頻値を求める | mebee」にあった以下のコードについて研究しました。

'use strict'
  
  const arr = ["a", "a", "a", "b", "c", "d", "d", "e", "e", "e", "e"];
  
  const c = (x, i, v) => (x[i] ? x[i].add(v) : x[i] = new Set(v), i);
  
  const result = arr.reduce(function (x, v) { return (this.set(v, c(x, (this.get(v) + 1 || 1), v)), x); }.bind(new Map), []).pop();
  
  console.log([...result]); // ['e']

概要

概要は下のとおりです。なかなかややこしいので 3 ステップに分けて解説します。 ‘use strict’ はなくても動くようなので省略しています。画像は拡大して表示することができます。

ステップ 1

  1. arr を参照します。
  2. 二つある this のうち前の this が arr を参照します。
  3. this は bind(new Map) によって初期化され Map オブジェクトになります。この時点で this からは重複が取り除かれます。
  4. この this を後ろの this が参照します。
 
POINT: this を「これ」だと思うと理解が阻害されます。「親を参照する」と考えると僕としては分かりやすいです。 前の this では親筋にあたる function(x, v) は関数ですので参照すべき親がありません。レキシカルスコープをたどって遡上するとメソッド reduce() があります。メソッドには親がいますので、この親である arr に行き着きます。 後ろの this も同様に関数 c のレキシカルスコープをたどってメソッド set() の親である、前の this に行き着きます。
 
  1. メソッド reduce() の引数である x と v が各所に配置されます。配置方法がこのコードの妙と言えましょう。後に関数 c の構造を見ていくと何が行われているのか分かるようになります。
  2. メソッド reduce() の引数 x の初期値が設定されます。

ステップ 2

  1. 関数 c の詳細です。関数 c は引数 x について引数 i と v を用いて処理をします。処理をしますが、戻り値はコロン演算子で区切られた最後の値 i となります。
  2. 引数 i と v が配置されます。
  3. 下のようになります。 A. x[i] が存在すれば、 B. Set オプジェクト x[i] に v を add() します。 A. x[i] が存在しなければ、 C. new Set(v) で初期化します。

ステップ 3

  1. 関数 c の戻り値として i が、メソッド Set() の第 2 引数として渡されます。
  2. 関数 function(x, v) の戻り値はコロン演算子の最後の値である x が return されます。それを受け取った reduce() は再び function(x, v) の引数 x としてリサイクルします。
  3. メソッド reduce() が arr について一通りの処理を行った結果を pop() します。 reduce() の返す最終的な配列 x ( x[i] の中身は Set )の最終項目が最頻値となります。
  4. result として結果が返されます。
  5. スプレッド演算子「 … 」によって Set である result を解体します。解体したものを [ ] を使って配列にして返します。

JavaScript で最頻値を求めたら敗北した!

下のようなコードを考えました。

let array = [1, 2, 2, 3, 4, 5]
let add = {}
for (let i in array) {
  if (add[array[i]]) {
    add[array[i]]++
  }
  else {
    add[array[i]] = 1
  }
}
let max = 0
for (let i = 1; i < Object.keys(add).length; i++) {
  if (add[i] > add[i - 1]) {
    max = Number(i)
  }
}
console.log(max)

個人的にはちょっとトリッキーで面白いな、と思っていました。で、既知の方法をググったわけですが「 javascript 配列の最頻値を求める | mebee 」にはすっかり敗北です。難しすぎて解読することができません。その上最頻値が複数あっても求められるとのこと。僕はそんなこと考えもしませんでした。とりあえずウェブクリップしましたよ。

JavaScript の FileReader() で複数のファイルを受け取る

UI から得たファイルを受け取るには FileReader() を使うらしいのですが、複数を処理するにはちょっとコツが必要なようです。

具体的には下のようなコードになります。

function getFiles(event) {
  let work = []
  let collection = event.dataTransfer.files
           // or = event.target.files
  let fr = new FileReader()
  return new Promise(resolve => {
    let i = 0
    fn()
    function fn() {
      fr.readAsText(collection[i])
      fr.onload = () => {
        work[i] = fr.result
        if (i < collection.length - 1) {
          i++
          fn()
        }
        else {
          resolve(work)
        }
      }
    }
  })
}

event から file collection を受け取る

まずは受け取ります。画面上にドロップされたファイルは evetn.dataTransfer.files で、ファイル選択画面から入力されたファイルは event.target.files で受け取ります。

FileReader()

FileReader() をセットします。データの取り出しは繰り返し処理で何度も行いますが、 FileReader() は 1 回だけ使います。

再帰処理

ファイルの数だけ繰り返し処理です。 for 文を使うと FileReader() のデータ吸いだしが同時に発生して怒られるので再帰処理をします。再帰処理の部分だけ抜き出すと下のようになります。

return new Promise(resolve => {
  let i = 0
  fn()
  function fn() {
    // 処理
    if (i < collection.length - 1) {
      i++
      fn()
    }
    else {
      resolve(work)
    }
  }
})

readAsText()

テキストファイルを受け取るには readAsText() を使います。受け取るファイルに応じて適宜変更が必要です。

onload

データを読むのに時間がかかるみたいです。 onload のイベントが発生するのでこれを待って処理を行います。

result

読み込みが終わったら result で結果を受け取ります。

終わりに

FileReader() を 1 回だけ作るのではなく fr[i] = new FileReader() などしていれば再帰処理ではなくfor 文が使えたのかもしれないな、と今さら思っています。どうなんでしょうね。

JavaScript の module で JSON を読み込んでみた

fetch するのは大げさだな、という気がしていたのです。内部的にどうなっているのか知りませんが、 module で読み込めたら素敵だと思いした。早速「 JSON Modules が使えるようになった」を参考に下のようにやってみました。

import test from "./test.json" assert {type: "json"}

相対パスで指定して「 ./ 」の明示は必須のようです。そうしたら――

Uncaught SyntaxError: Cannot use import statement outside a module

――「モジュールの外部で import ステートメントを使用することはできません」あれ? 2 時間悩んだのですが、見落としでした。見落としていたのは下の部分です。

<script type="module">

module であることを HTML で宣言しないといけないんですね。

で、このときはスクリプトを HTML 内に書くつもりだったのでこれでよかったんですが、気が変わって別ファイルにすることにしました。それでまた同じ問題が発生したのです。え、書いているコードが module であることをどうやって宣言するんでしょう。さらに 1 時間悩んで得た答えが下です。

<script src="test.js" type="module"></script>

ダサっ。いちいち HTML で宣言しないと module になれないみたいです。自分だけで完結しないなんて。そんなの fetch するほうがよっぽどいいじゃないですか。なんだこれ? 僕は何か見落としているのでしょうか。サーバサイドで書くときはまた違ってくるのかなぁ。今回 import は使わないことにしました。

Promise.all() は Promise の配列ならなんでもいいっぽい

以前「 for 文で Promise.all() 」という記事を書いたんですが、いろいろやってみると Promise.all() としては Promise の配列ならなんでもいいっぽいです。下のようなものも可です。

Promise.all(
  array.map(rly => fetch(rly))
)
.then(rly => {
  // 処理
})

上述は map() を例にしていますが forEath() でもよさそうですし。