こういうのに心当たりがある人は、まぁいつも「ちゃんとした」テストを書こうとする人なんだろうなと思う。
ホワイトボックスであれブラックボックスであれ、「入力のバリエーション」を生成するためには、「機械的な規則でパターンを網羅する」のと、もう一つ、ワタシがやってるようなヤツだと「本物データ」がすぐに手に入るので、「手っ取り早く本物データを持ってきちゃえ」の2つがあって、まぁどっちもやるわけだね。
で、「本物データ」を使うと、すぐに現実にフィット出来る代わりに、「動機付け(理論付け)のない収集」になるので、「網羅してんの、してへんの」なんてのはもうわからない。「なんかすごそー」と思うだけのことだ。(要するに見掛け倒し。)
というわけで、最初はとにかくインチキに「持ってるヤツ全部」でやってた。けど、「論理的なパターンとしては全く一緒のもの」が大量に収集出来てしまっているので、「網羅できてない可能性が高い上に滅茶苦茶量が多い」という、大層無駄なことになる。
テスト対象のコードはこんな感じ:
1 MalUrlBuilder.prototype.calc_cname = function(name, cname, context) {
2 if (cname) {
3 return cname;
4 }
5 // if cname is undefined, cname can be calculated by name.
6 function _conv(s) {
7 return s
8 .replace(new RegExp("[: +/=]", "g"), "_")
9 .replace(/&/g, "_")
10 .replace(/"/g, "")
11 // TODO: "&", "#", ";"
12 .replace(/&#[0-9]+;/g, "")
13 .replace(/[!\[\].,()@?*%~$]/g, "")
14 ;
15 }
16 var res;
17 if (context == "anime") {
18 res = _conv(name);
19 } else {
20 var spl = name.split(", ");
21 if (spl.length == 1 || spl.length > 2) {
22 res = _conv(name);
23 } else {
24 // "Arisugawa, Otome" to "Otome_Arisugawa"
25 res = _conv(spl[1]) + "_" + _conv(spl[0]);
26 }
27 }
28 return res;
29 }
「name」から「link canonical name」を計算しているわけなんだけれど、この「変形」のバリエーションが結構あるわけよね。この生成仕様から手作りでデータを作ったほうが早いことも多いんだけど、まぁ「持ってるしなぁ」てこともあるし、それに「本当の本物にフィット出来なくなる」のは困る(つまり「現実」に対してテスト出来ている、という自信の方を失う)というのもあって、実際のデータを使ってた。こんなテストデータにしていた:
1 {
2 "anime": [
3 [
4 "Accel World: Infinite∞Burst",
5 "Accel_World__Infinite∞Burst"
6 ],
7 [
8 "Active Raid: Kidou Kyoushuushitsu Dai Hachi Gakari 2nd",
9 "Active_Raid__Kidou_Kyoushuushitsu_Dai_Hachi_Gakari_2nd"
10 ],
11 [
12 "Active Raid: Kidou Kyoushuushitsu Dai Hachi Gakari",
13 "Active_Raid__Kidou_Kyoushuushitsu_Dai_Hachi_Gakari"
14 ],
15 [
16 "Ai Tenshi Densetsu Wedding Peach DX",
17 "Ai_Tenshi_Densetsu_Wedding_Peach_DX"
18 ],
19 [
20 "Ai Tenshi Densetsu Wedding Peach",
21 "Ai_Tenshi_Densetsu_Wedding_Peach"
22 ],
23 [
24 "Aikatsu! Movie",
25 "Aikatsu_Movie"
26 ],
27 [
28 "Aikatsu! Music Award: Minna de Shou wo MoracchaimaShow!",
29 "Aikatsu_Music_Award__Minna_de_Shou_wo_MoracchaimaShow"
30 ],
31 [
32 "Aikatsu!",
33 "Aikatsu"
34 ],
35 [
36 "Aikatsu!: Nerawareta Mahou no Aikatsu! Card",
37 "Aikatsu__Nerawareta_Mahou_no_Aikatsu_Card"
38 ],
39 [
40 "Air Master",
41 "Air_Master"
42 ],
43 // ...
44 [
45 "3-gatsu no Lion",
46 "3-gatsu_no_Lion"
47 ]
48 ],
49 "not anime": [
50 [
51 "Aculeasis",
52 "Aculeasis"
53 ],
54 [
55 "Adachi, Tooru",
56 "Tooru_Adachi"
57 ],
58 [
59 "Aida, Rikako",
60 "Rikako_Aida"
61 ],
62 [
63 "Aihara",
64 "Aihara"
65 ],
66 [
67 "Aikawa, Chocolat",
68 "Chocolat_Aikawa"
69 ],
70 [
71 "Akagami no Shirayuki-hime",
72 "Akagami_no_Shirayuki-hime"
73 ],
74 [
75 "Akagi, Miria",
76 "Miria_Akagi"
77 ],
78 // ...
79 ]
80 }
ひとめで無駄だとわかるわけである。実際かなりのデータ量なので、読むのも非常に疲れるし、もしもここから「手作業で」刈り込みを行うと考えると、もうヘコみますわ。
というわけで、「そんじゃぁ、ここから意味のあるバリエーションだけ抜き出しちゃいますか」と。「網羅してないかもしれんぞ」は削ったあとで考えましょうか、つぅわけだわな。
javascript なアプリケーションなので node.js ワールドのままやってもいいんだけれども、javascript はループの記述がダルかったり、必要インフラが色々欠けてたりで面倒くさいので、うーん、Python の Counter 活用してやっちまおうかと。まぁ Python にしたらしたで今度はエンコーディングの問題が鬱陶しかったりもするけど、そんなんワタシは慣れてるのでどうでもいい。
というわけで:
1 # -*- coding: utf-8 -*-
2 import io
3 import json
4 import string
5 from collections import Counter
6
7 fn = "known_cnames.json" # 元のデータ
8 ofn = "known_cnames-reduced.json" # 削減後データ
9
10 data = json.loads(io.open(fn, encoding="utf-8").read())
11 newdata = {"anime": [], "not anime": []}
12 for k in ["anime", "not anime"]:
13 dejav = set() # 同一パターン管理の set
14 for row in data[k]:
15 cnter = Counter()
16 # Counter で管理したいのは「ascii 文字がいるかどうか」と「非 ascii なら
17 # 各々の出現回数」。つまり、「abc def」と「a bcdef」は同一パターン、
18 # 「ab c def」は新パターンとして認識する。同じ要領で、「ab-c」が初なら
19 # 「ascii がいて、ハイフンが一個」というパターンとして認識する。
20 for c in row[0]:
21 if c in string.ascii_letters:
22 cnter["ascii"] = 1 # manage just whether if exists or not
23 else:
24 cnter[c] += 1
25 djk = repr([(k, cnter[k]) for k in sorted(cnter.keys())])
26 if djk not in dejav:
27 dejav.add(djk)
28 newdata[k].append(row)
29
30 with io.open(ofn, "wb") as fo:
31 dsredtowr = json.dumps(
32 newdata, indent=2, ensure_ascii=False).replace("\r", "").encode("utf-8")
33 fo.write(dsredtowr)
唯一ダルかったのが hashable の件だけね。あと Python 3 だと辞書の文字列表現が実行のたんびに揺れるんで、そこだけがダルいちゃぁダルイ。たぶん10分くらいで作った。
で、うん、1/4 に減った。まぁまずはこんなもんかな。
なお、「非 ASCII の各コードごとの出現数」としているのは、"Aikatsu! Music Award: Minna de Shou wo MoracchaimaShow!"
みたいなのが人間にはわかりにくい振る舞いなんだよね。一番上のテスト対象コードを注意深く読んでもらうとちょっとわかるかも。「_」に「変換したりしなかったりする」というデザインらしくて、結構キワドい。
誰のためにこれを書いたかって? そりゃぁもちろん「オレのため」が第一である。上のデータ削減コードは、もう捨てる。だってもう使わないもん。だから捨てる前にここの載せとく、てのが第一。
もう一つはやはり「魔法なんてない」ということを知って欲しい、ということだわな。ずーっと後輩と接して思ってたことなんだけど、若い子はなぜだか「何かステキな魔法があるに違いない」と考えがちなんだよね。だけどね、「なんであれものづくりをする際には、血と汗をかく」ということを、「誰もが」やってるんだよ、ということ。それを人に見せる見せないが人によって違うだけのことであって、皆が見えないところでこういう地道なことを、コツコツやっておるのです。