索引作成

作成日 : 1999-08-27
最終更新日:

連想配列の応用

連想配列の応用として、本の人名索引作成を支援するプログラムを書いてみよう。 たとえば、やじうま入試数学という本がある。 この本には索引がない。 そこで索引のようなものを作ろうと考えた。

さて、ある本の索引を作るためには、キーワードとそのキーワードが出てくるページが必要である。 ここではキーワードは人名と割り切った。 そして、ページとそのキーワードを対にして次のように拾ってみた。 ページが一つでもキーワードは複数あることもあるので、 その場合は半角カンマでつなげている。

    let indekso = { 5 : "コワレフスカヤ", 24 : "コーシー", 25 : "コーシー,久留島義太,アルキメデス"};

ページごとにどんな人名が出てきているか、コードの例は次の通りだ。

let indekso1 = document.getElementById('indekso1'); 
indekso1.innerHTML = "";
for (let idx in indekso) {
    indekso1.innerHTML += `${idx} : ${indekso[idx]} <br />`;
}

実行した結果は次のようになる。

ではこれを人名順に並べて、その人名が出ているページを表示するにはどうすればよいか。 表示は次のようになってほしい。

アルキメデス 25
コワレフスカヤ 5
久留島義太 25
コーシー 24,25

連想配列(ハッシュ)を使って上記のような新たな連想配列(ハッシュ)を得ること、 すなわち、キー (key) と 値 (value) の役割を交換する連想配列(ハッシュ)を得ることは、 一般的にハッシュの反転と言われている。

では文法はお構いなしに、次のように考えてみよう。

/* 動かないスクリプト */
/* キーと値を逆転させて連想配列 nomoj に入れる */
for (let idx in indekso) {
    nomoj[indekso[idx]] = idx
}
/* 連想配列 nomoj を出力する */
let indekso2 = document.getElementById('indekso2');
indekso2.innerHTML = "";
for (let nomo in nomoj) {
    indekso2.innerHTML += `${nomo}:${nomoj[nomo]}<br />`)
}

これは動かないことが予想される。以下の理由が考えられる。

  1. nomoj という新たな連想配列を導入したが、 これはだいたい Array オブジェクトとして認識されているのだろうか。 → let nomoj = {}; という構文が必要。
  2. 連想配列を追加するときに nomoj[key] = value という書き方が可能か? → 可能だが文字列の場合は注意が必要。

ちなみに1.だけ直して修正した。実行結果は次の通りとなる。

これは予想された結果だ。単にもとのデータをひっくり返しただけである。 だから、同じページに複数の人名が出てきたら、それら人名を分けないといけない。 それには、次のように複数の人名を分解する split メソッドを適用する。

/* 動くが意図とは異なる結果が出るスクリプト */
/* 元の値を分解したうえで元のキーを値に、元の値をキーに逆転させて連想配列 nomoj に入れる */
let nomoj = {};
for (let idx in indekso) {
    /* 分解前 */
    /* nomoj[indekso[idx]] = idx */
    /* 分解後 */
    let nm = (indekso[idx]).split(/[, ]/);
    for (let i = 0; i < nm.length; i++) {
        nomoj[nm[i]] = idx;
    }
}

/* 連想配列 nomo を出力する */
let indekso3 = document.getElementById('indekso2');
indekso3.innerHTML = "";
for (let nomo in nomoj) {
    indekso3.innerHTML += `${nomo}:${nomoj[nomo]}<br />`)
}

結果は次の通り。惜しいが予想された通り、意図とは異なる結果が出力される。

意図とは異なる結果とは、意図としては「コーシー」のページが 24,25と出力されてほしいのに、 結果が 25 としか出ないことだ。 これは当然で、最初 24 が入っているが、あとで 25 が上書きされてしまったのだ。さて、どうするか。 普通に考えれば、上書きのところを追加にすればよい。具体的には
nomoj[nm[i]] = idx;
の行を次のようにすればよい。
nomoj[nm[i]] = nomoj[nm[i]] + "," + idx;
この結果は次の通りとなる。

必ず最初に出力される undefined が気持ち悪い。そういうときには、泥縄式だが

nomoj[nm[i]]) = nomoj[nm[i]]) + "," + idx ; 
の行を次のようにすればよい。

if (typeof(nomoj[nm[i]]) === "undefined") {
    nomoj[nm[i]] = idx; 
} else {
    nomoj[nm[i]] = nomoj[nm[i]] + "," + idx;
}

別の方法としては、undefined であることがわかったら空の配列で置き換えてから push する方法がある。 コードは次の通り。

let nomoj = {};
for (let idx in indekso) {
    let nm = (indekso[idx]).split(/[, ]/);
    for (let i = 0; i < nm.length; i++) {
        if (typeof(nomoj[nm[i]]) === "undefined") {
            nomoj[nm[i]] = [];
        }
        (nomoj[nm[i]]).push(idx);
    }
}

// 連想配列 nomoj をソート後出力する */
// 連想配列のソート結果を保存する変数を用意する
let sortednomoj = {};
 
// キー配列を取得
let forsort = Object.keys(nomoj);

// キー配列をソート
forsort.sort();
 
// ソートされたキー配列でforループ
for(let i = 0; i < forsort.length; i++ ) {
    // ソートされたキーで元の連想配列から値を取得し、ソート後連想配列に代入
    sortednomoj[forsort[i]] = nomoj[forsort[i]];
}
for (let nomo in sortednomoj) {
    indekso5.innerHTML += `${nomo}:${sortednomoj[nomo]}<br />`;
}

結果は次の通り。

アイウエオ順の索引

こんどは、アイウエオ順に項目を並べてみることを考える。 対象は、「現代数学への招待」である。 今までは索引を表わす元データは、ページがキーで項目が値であるハッシュを使っていたが、 今回は読みがなを表わすために 2 次元配列にしてみた。

let indeksokana = [ 
	[16, "数直線", "スウチョクセン"],
	[18, "弧度", "コド"],
	[33, "k次元実空間", "kジゲンジツクウカン"],
	[35, "k次元球面", "kジゲンキュウメン"],
	[64, "葉層構造", "ヨウソウコウゾウ"],
	[85, "開集合", "カイシュウゴウ"],
	[101, "開集合", "カイシュウゴウ"],  
];

この indeksokana のソーティングでは、各配列の要素がさらに配列となっている。 この場合は、各要素を配列とみたときのその配列の第 3 要素にしたがってソーティングすればいい。

この場合、k のようなアルファベットはあえてよみがなにせずそのまま残している。 英字はさくいんでも別になることが多いと判断したからだ。なお、読みがなをカタカナとしたのは、 ヴの読みがなに対応したかったためだ。

さて、この indeksokana に関する索引生成のプログラムは次のようになる。

  indeksokana.sort((a, b) => a[2] > b[2]);
  let terminoj = Array.from(new Set(indeksokana.map(indekso => indekso[1])));
  indekso.innerHTML = "";
  for (let i = 0; i < terminoj.length; i++) {
    const rezultoj = indeksokana.filter(indekso => indekso[1] === terminoj[i]);  
    const pagxoj = rezultoj.map(rezulto => rezulto[0]);
    indekso.innerHTML += `${terminoj[i]} : ${pagxoj} <br />`;
  }

行ごとにコメントする。
まず第 1 行は各要素を配列とみたときのその配列の第 3 要素、つまりカタカナでソーティングしたことを表わす。
第 2 行は、次の順で処理する。最初に、indeksokana の各要素を配列とみたとき、 その配列の第 2 要素である indekso[1] 、つまり用語の部分を取り出して配列とする。これが、 indeksokana.map(indekso => indekso[1]) の部分である。 次に、この配列を引数に、新たな集合型のデータを作る。これが、 new Set(...) の部分である。最後に、この集合型を再度配列型にする。これが Array.from(...) の部分である。なぜ一度集合型にするかというと、配列型から集合型を作るときに、重複を省いてくれるからである。 そして集合型から配列型に戻せば重複が消えている。こうして索引の最初の文字列 terminoj が無事できた。
第 3 行は索引に書き出す文字列の初期化である。
第 4 行は terminoj ごとに処理するループである。ここは、for 文を使わずに terminoj.forEach(termino => {...}); のような forEach を使うか、 さらにやはり map や filter のようなメソッドを使うべきなのだろう。 しかし、ここでは自分がなじんでいる for 文を使ってしまった。反省点である。
第 5 行は、termino[i] という用語が indeksokana の第 2 要素と一致する indeksokana の要素をすべて集める。この例でいくと、terminoj[i] が "開集合" であるとき、 rezultoj には [[85, "開集合", "カイシュウゴウ"],[101, "開集合", "カイシュウゴウ"]] という 2 次元配列が入っている。
第 6 行は、rezultoj から第 1 列を抜き出す処理で、結果 pagxoj には [85,101] という配列ができる。
第 7 行は得られた索引の文字列を書き出す処理である。

以上、怪しいところが 2 点ある。1 点めは、配列から集合に戻して再度配列にするときの順序である。 集合型を使って重複を省くのはいいが、 最初にソーティングしていた配列の順序が保たれているかどうかについては保証されていない。 結果としては最初のソーティングの結果が保存されているのでこのままにする。 2 点めは、複数のページがある場合にページが若い順から並んでいることの保証がないことである。 これも結果として最初のページを並べた順になっているので、これでいいことにする。いい加減だな。

まりんきょ学問所JavaScript 手習い > 索引作成


MARUYAMA Satosi