【なでしこさん】令和の順不同表比較機

[なでしこさん](https://nadesi.com/doc3/index.php?FrontPage)で**順不同表比較機**をリメイクしてみた。

# 順不同表比較機

**順不同表比較機**は、昔の自分が作成したツールである。
改行区切りの表2個を入力とし、「2個の表に共通である項目」と「それぞれの表にしか無い項目」にわけてくれる。
同じ項目が1個の表に複数含まれる場合も、数を考慮した処理を行う。

![順不同表比較機](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/cc2e2488-7179-4142-5e5f-220abd4fa75c.png)

しかし、このツールには

* 遅い (40項目の表同士の比較で10秒くらいかかった。画面が固まらないようにわざとウェイトを入れてるかも)
* リストの最後に改行を入れると結果がおかしくなる
* 終了時に謎のエラーダイアログが出る

![順不同表比較機 終了時のエラー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/ef0737f8-1fa3-63b8-d358-37181fdff204.png)

という欠点があった。
そこで、せっかくなのでなでしこさんで作り直してみることにした。

# アルゴリズム

もともとの順不同表比較機がどのようなアルゴリズムだったかはよく覚えていない。
(表示を見ると、一方の表の各項目について他方の表から線形探索し、見つかったら削除している？)
今回は、辞書型変数 (以下、「辞書」) を用いて処理を行う。

1. 表B用の辞書を空で初期化する。
2. 表Bの各項目について、それをキーとして表B用の辞書の対応する値に1を加える。すなわち、各項目の出現回数を数える。
3. 表Aの各項目について、同様に出現回数を数えながら、以下の処理を行う。
   * 表B用の辞書上のその項目の出現回数が正であれば、両方の表に含まれる項目として追加し、出現回数から1を引く。
   * 出現回数が0以下、または表B用の辞書に存在しなければ、表Aにしか含まれない項目として追加する。
4. 表Bの各項目について、表A用の辞書を用いて同様の処理を行う。ただし両方の表に含まれる項目への追加は行わない。(両方の表に含まれているので、既に表Aの処理で完成している)

# 実装

実装を貯蔵庫に置いた。
[令和の順不同表比較機](https://n3s.nadesi.com/id.php?1946)

# ハマったポイント

## 「DOM属性設定」でのテキストエリアの「readonly」の設定が効かない

テキストエリアを読み取り専用にするために「readonly」属性を「[DOM属性設定](https://nadesi.com/v3/doc/index.php?plugin_browser%2FDOM%E5%B1%9E%E6%80%A7%E8%A8%AD%E5%AE%9A&show)」で設定しようとしたところ、なぜか効かなかった。
[`setAttribute()`](https://developer.mozilla.org/ja/docs/Web/API/Element/setAttribute)を「[JSメソッド実行](https://nadesi.com/v3/doc/index.php?plugin_system%2FJS%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%AE%9F%E8%A1%8C&show)」で直接呼び出すことで、読み取り専用にはできた。

```
入力欄Aは「」のテキストエリア作成。
入力欄Aの「readonly」に「readonly」をDOM属性設定。

入力欄Bは「」のテキストエリア作成。
入力欄Bの「setAttribute」を["readonly", "readonly"]でJSメソッド実行。
```

![「DOM属性設定」でのテキストエリアの「readonly」の設定が効かない](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/7909c710-8a35-700d-bf56-496f758f9c15.png)

開発者ツールでチェックしても、「DOM属性設定」で設定したはずの `readonly` 属性が無いことがわかる。

## UIから取得したテキストを連文で関数に渡せないことがある

```
入力欄は「ほげ{改行}ふが」のテキストエリア作成。

「テスト」のボタン作成。
それをクリックした時には
　リストは入力欄のテキスト取得して改行で区切る。
　リストを表示。
ここまで。
```

![関数テスト 1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/4abd4286-ea81-4ebf-3bf0-2fc0d24ba5d8.png)

このプログラムは、期待通りの結果になった。
この「改行で区切る」処理を関数として切り出す。
(実際のプログラムでは、改行で区切った後末尾の改行に対処する処理をしている)

```
●(テキストを)改行区切りとは
　テキストを改行で区切って戻す。
ここまで。

入力欄は「ほげ{改行}ふが」のテキストエリア作成。

「テスト」のボタン作成。
それをクリックした時には
　リストは入力欄のテキスト取得して改行区切り。
　リストを表示。
ここまで。
```

![関数テスト 2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/2fa46877-b397-93c5-c050-228e88bbeafd.png)

すると、なぜか結果が `[object Object]` となってしまった。
要素のテキストを取得するところを含めて関数化すると、期待する結果が得られた。

```
●(要素の)改行区切り取得とは
　要素のテキスト取得して改行で区切って戻す。
ここまで。

入力欄は「ほげ{改行}ふが」のテキストエリア作成。

「テスト」のボタン作成。
それをクリックした時には
　リストは入力欄の改行区切り取得。
　リストを表示。
ここまで。
```

![関数テスト 3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/9aa55b3f-2b3e-2204-1cf9-7c4cdf24ff11.png)

結果が `[object Object]` となる時の引数を `console.log` でチェックしたところ、長い文字列を値とするプロパティ `lastJSCode` などのプロパティを持つオブジェクトであった。

```
●(テキストを)改行区切りとは
　「console.log」を[テキスト]でJS関数実行。
　テキストを改行で区切って戻す。
ここまで。

入力欄は「ほげ{改行}ふが」のテキストエリア作成。

「テスト」のボタン作成。
それをクリックした時には
　リストは入力欄のテキスト取得して改行区切り。
　リストを表示。
ここまで。
```

![関数テスト 4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/8d0c6b3e-73d7-54be-1349-cca22303d9d1.png)

同じく「を」を用いて引数を1個だけとる関数の「[空白除去](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E7%A9%BA%E7%99%BD%E9%99%A4%E5%8E%BB&show)」であっても、結果が `[object Object]` となることがあった。
連文がうまく使えないことがあるのは自作関数を用いる時だけではないようである。

```
入力欄は「ほげ{改行}ふが」のテキストエリア作成。

「テスト」のボタン作成。
それをクリックした時には
　リストは入力欄のテキスト取得して空白除去。
　リストを表示。
ここまで。
```

![関数テスト 5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/dbf88474-cd86-dbd7-9cef-65440f158c83.png)


## 辞書型変数や配列の要素に対する「減らす」「増やす」が効かない

変数に対して「[減らす](https://nadesi.com/v3/doc/index.php?%E6%96%87%E6%B3%95%2F%E5%A2%97%E6%B8%9B%E6%96%87&show)」を用いると、複合代入演算子 `-=` のような処理を行うことができる。

```
ほげは10。
ほげを表示。
ほげを1だけ減らす。
ほげを表示。
```

このプログラムを実行すると、期待通り「10」と「9」が表示された。

![減らすテスト 1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/1b3e5d02-f80d-8945-7d00-45c46de355d0.png)

しかし、この「減らす」は、辞書型変数の要素に対しては効かないようである。

```
ほげは空ハッシュ。
ぽよは「ぽよ」。
ほげ[ぽよ]は10。
ほげ[ぽよ]を表示。
ほげ[ぽよ]を1だけ減らす。
ほげ[ぽよ]を表示。
```

このプログラムを実行すると、値が減らず、「10」が2回表示された。

![減らすテスト 2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/c91a9c55-bfa7-1fcb-79ba-618395004248.png)

「減らす」のかわりに「[引く](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E5%BC%95&show)」と代入を用いると、辞書型変数の要素の値を更新できた。

```
ほげは空ハッシュ。
ぽよは「ぽよ」。
ほげ[ぽよ]は10。
ほげ[ぽよ]を表示。
ほげ[ぽよ]はほげ[ぽよ]から1を引く。
ほげ[ぽよ]を表示。
```

このプログラムを実行すると、期待通り「10」と「9」が表示された。

![減らすテスト 3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/406d4735-74ec-601c-76fe-22bb3502fcbf.png)

ちなみに、配列の要素に対しても「減らす」は効かないようである。

```
ほげは[10, 20]。
ほげ[0]を表示。
ほげ[0]を1だけ減らす。
ほげ[0]を表示。
```

このプログラムを実行すると、値が減らず、「10」が2回表示された。

![減らすテスト 4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/82d645e6-2ce7-9011-1c38-46c3519f2c88.png)

同様のプログラムで検証した結果、「減らす」だけでなく「増やす」も配列やハッシュの要素に対しては効かなかった。

## 対応する値が偽とみなされる辞書型変数のキーを削除できない

「[ハッシュキー削除](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%AD%E3%83%BC%E5%89%8A%E9%99%A4&show)」で削除したはずの辞書型変数のキーがなぜか復活するような挙動がみられた。

```
テストは{"hoge": 1}。
["hoge", "hoge", "hoge"]を反復
　もし、テストに対象がハッシュキー存在ならば
　　「存在」を表示。
　　テスト[対象]はテスト[対象]から1を引く。
　　もし、テスト[対象]が0以下ならば
　　　「削除」を表示。
　　　テストから対象をハッシュキー削除。
　　ここまで。
　違えば
　　「無い」を表示。
　ここまで。
ここまで。
```

「削除」が表示されるのでキーを削除しているはずであるにもかかわらず、再び「存在」が表示され、キーが復活しているようである。
しかも、2回目の削除の後は復活しなかった。

![ハッシュキー削除テスト](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99656/7586a4f8-b569-62a3-3f45-ed6b9805afed.png)

「[ハッシュキー存在](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%AD%E3%83%BC%E5%AD%98%E5%9C%A8&show)」「ハッシュキー削除」のかわりに、「[辞書キー存在](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E8%BE%9E%E6%9B%B8%E3%82%AD%E3%83%BC%E5%AD%98%E5%9C%A8&show)」「[辞書キー削除](https://nadesi.com/v3/doc/index.php?plugin_system%2F%E8%BE%9E%E6%9B%B8%E3%82%AD%E3%83%BC%E5%89%8A%E9%99%A4&show)」を用いても、同じ結果が得られた。

さらなる実験の結果、「削除したキーが復活」しているわけではなく、対応する値が偽とみなされるものの場合、回数にかかわらず「辞書キー削除」によるキーの削除が効かない可能性があることがわかった。

```
テストは{
　"A": 1, "B": 0, "C": -1,
　"D": "", "E": "hoge",
　"F": 1=1, "G": 1=2,
　"H": [], "I": {},
　"J": NULL
}。
キーリストは["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"]。
3回繰り返す
　「反復開始」を表示。
　キーリストを反復
　　もし、テストに対象が辞書キー存在ならば
　　　「{対象}は存在し、{テスト[対象]}」を表示。
　　　テストから対象を辞書キー削除。
　　違えば
　　　「{対象}は無い」を表示。
　　ここまで。
　ここまで。
ここまで。
```

このプログラムを実行すると、以下が表示された。

```
反復開始
Aは存在し、1
Bは存在し、0
Cは存在し、-1
Dは存在し、
Eは存在し、hoge
Fは存在し、true
Gは存在し、false
Hは存在し、
Iは存在し、[object Object]
Jは存在し、null
反復開始
Aは無い
Bは存在し、0
Cは無い
Dは存在し、
Eは無い
Fは無い
Gは存在し、false
Hは無い
Iは無い
Jは存在し、null
反復開始
Aは無い
Bは存在し、0
Cは無い
Dは存在し、
Eは無い
Fは無い
Gは存在し、false
Hは無い
Iは無い
Jは存在し、null
```

JavaScriptで偽とみなされる `0`、`""`、`false`、`null` は削除されず、その他の値を持つキーは削除された。

「値が偽とみなされるものの時は削除されない」などという仕様はマニュアルには載っていない上、今回のような「数を減らしていって0になったら削除する」という実用的な処理の妨害となるため、初見殺しの不親切な仕様だという印象を持った。
(まあ、このような仕様であるとわかってしまえば削除の前に適当な真とみなされる値を入れればいいだけか…)

# 記事タイトルの元ネタ

[令和のデ・ジ・キャラット #01「おひさしぶりぶりアキハバラ～」 - ニコニコ動画](https://www.nicovideo.jp/watch/so41172760)