漢字データベースを使って漢字ベン図を作問する

漢字ベン図は、QuizKnockがやっていた漢字クイズです。条件が3つ与えられるので、複数の条件に当てはまる漢字を答えていきます。

www.youtube.com

この記事では、漢字情報データベース Mojidata を活用して、漢字ベン図を作問してみようと思います。

github.com

MojidataはSQLiteというデータベースエンジンで使うことができるデータベースになっていて、情報をSQLで取得することができます。

データベースを使う準備

Mojidataを使うには、Node.jsとSQLiteをインストールしてあると楽です。

その後、ターミナルで次のコマンドを実行して、moji.dbをダウンロードし、sqlite3を起動してください。

# 作業用のディレクトリを作る
mkdir kanji-venn
# カレントディレクトリを変更する
cd kanji-venn
# npm パッケージの初期化(node_modulesを作業用ディレクトリに作成するため)
npm init -y
# mojidataパッケージのインストール
npm install @mandel59/mojidata
# SQLiteの起動
sqlite3 node_modules/@mandel59/mojidata/dist/moji.db

SQLiteが起動すると、次のようなプロンプトが表示されます。

SQLite version 3.40.0 2022-11-16 12:10:08
Enter ".help" for usage hints.
sqlite> 

個人的にはSQLiteCLIからだと少し使いづらいと思うので、普段は自作のErqというツールを使っています。これは補完機能が使え、SQLより簡単に書けるErqクエリ言語を使って情報を取得できます。開発途中で、マニュアル等はないのですが、Erqを使ってみたい場合は、こちらもnpmでインストールして使うことができます。次のコマンドでErqをインストールします。

# Erqのインストール
npm install github:mandel59/erq
# Erqの起動
npx erq node_modules/@mandel59/mojidata/dist/moji.db

Erqを起動すると、次のようなプロンプトが表示されます。

Connected to node_modules/@mandel59/mojidata/dist/moji.db
erq> 

他にDuckDBを使って読み込む方法や、GUIのツールを使う方法もあります。

漢字情報を取得してみる

作問するにあたって、漢字の次のような情報が取得したいです。

  • 常用漢字の一覧
  • 漢字の読み
  • 漢字の総画数
  • 漢字の部首
  • 漢字の構造

常用漢字の一覧と読みは、常用漢字表のデータを格納した joyo テーブルに保存されています。また、総画数はMJ文字情報一覧のデータを格納した mji テーブルから、部首は mji_rsindex テーブルから、漢字の構造は ids テーブルから、それぞれ取得できます。

erq> joyo limit 10;;
select * from joyo limit 10
["漢字","音訓","例","備考"]
["亜","ア","[\"亜流\",\"亜麻\",\"亜熱帯\"]",""]
["哀","アイ","[\"哀愁\",\"哀願\",\"悲哀\"]",""]
["哀","あわれ","[\"哀れ\",\"哀れな話\",\"哀れがる\"]",""]
["哀","あわれむ","[\"哀れむ\",\"哀れみ\"]",""]
["挨","アイ","[\"挨拶\"]",""]
["愛","アイ","[\"愛情\",\"愛読\",\"恋愛\"]","愛媛(えひめ)県"]
["曖","アイ","[\"曖昧\"]",""]
["悪","アク","[\"悪事\",\"悪意\",\"醜悪\"]",""]
["悪","オ","[\"悪寒\",\"好悪\",\"憎悪\"]",""]
["悪","わるい","[\"悪い\",\"悪さ\",\"悪者\"]",""]
10 rows (0.011s)

読み情報ビュー kanji_reading を定義して、必要な情報だけを使いやすくします。

erq> view temp.kanji_reading = joyo {k: 漢字, r: 音訓};;
create view `temp`.kanji_reading as with kanji_reading as (select 漢字 as k, 音訓 as r from joyo) select * from kanji_reading
ok (0.002s)
erq> kanji_reading limit 10;;
select * from kanji_reading limit 10
["k","r"]
["一","ひと"]
["一","ひとつ"]
["一","イチ"]
["一","イツ"]
["丁","チョウ"]
["丁","テイ"]
["七","なな"]
["七","ななつ"]
["七","なの"]
["七","シチ"]
10 rows (0.000s)

総画数と部首、構造を取得するビューも定義します。

総画数情報ビュー kanji_strokes の定義

erq> view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};;
create view `temp`.kanji_strokes as with kanji_strokes as (select 実装したUCS as k, 総画数 as s from mji where (漢字施策 = '常用漢字')) select * from kanji_strokes
ok (0.000s)
erq> kanji_strokes limit 10;;
select * from kanji_strokes limit 10
["k","s"]
["一",1]
["丁",2]
["七",2]
["万",3]
["丈",3]
["三",3]
["上",3]
["下",3]
["不",4]
["与",3]
10 rows (0.001s)

部首情報ビュー kanji_radical の定義

erq> view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};;
create view `temp`.kanji_radical as with kanji_radical as (select 対応するUCS as k, 部首漢字 as rad from mji join mji_rsindex on mji.MJ文字図形名 = mji_rsindex.MJ文字図形名 join radicals on mji_rsindex.部首 = radicals.部首 where (漢字施策 = '常用漢字')) select * from kanji_radical
ok (0.000s)
erq> kanji_radical limit 10;;
select * from kanji_radical limit 10
["k","rad"]
["一","一"]
["丁","一"]
["七","一"]
["万","一"]
["丈","一"]
["三","一"]
["上","一"]
["下","一"]
["不","一"]
["与","一"]
10 rows (0.001s)

構造情報ビュー kanji_ids の定義

erq> view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;;
create view `temp`.kanji_ids as with kanji_ids as (select distinct UCS as k, IDS as ids from ids where (UCS in (select 漢字 from joyo))) select * from kanji_ids
ok (0.000s)
erq> kanji_ids limit 10;;
select * from kanji_ids limit 10
["k","ids"]
["一","一"]
["丁","⿱一亅"]
["七","〾⿻乚一"]
["万","⿸丆𠃌"]
["丈","⿻𠂇乀"]
["三","三"]
["上","⿱⺊一"]
["下","⿱一卜"]
["不","⿸丆⿰丨丶"]
["不","⿻丆卜"]
10 rows (0.003s)

クイズを作問する

ここまでできれば、あとは、条件に当てはまる漢字を取得するクエリを作るだけです。先の動画の例題で言えば、「さんずい」「9画」「「せ」から始まる」といった条件は、漢字をxとすれば、SQLとErqではそれぞれ次のように表現できます。

  • さんずい
    • SQL: x in (select k from kanji_ids where ids glob '⿰氵*')
    • Erq: x in kanji_ids[ids glob '⿰氵*']{k}
  • 9画
    • SQL: x in (select k from kanji_strokes where s = 9)
    • Erq: x in kanji_strokes[s = 9]{k}
  • 「せ」から始まる
    • SQL: x in (select k from kanji_reading where r glob 'せ*' or r glob 'セ*')
    • Erq: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}

常用漢字 x についてそれぞれ判定し、複数の条件に当てはまるものを表示すれば作問ができそうです。Erqでクエリを作ってみます。

/* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
/* 各漢字について、条件を判定する */
{
  x,
  `さんずい`: x in kanji_ids[ids glob '⿰氵*']{k},
  `9画`: x in kanji_strokes[s = 9]{k},
  `「せ」から始まる`: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
}
/* 複数の条件にあてはまる漢字のみ残す */
[`さんずい` + `9画` + `「せ」から始まる` >= 2]
/* あてはまる条件でグループ化 */
{ `さんずい`, `9画`, `「せ」から始まる` => group_concat(x) }
;;

これを入力すると:

erq> /* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
...> /* 各漢字について、条件を判定する */
...> {
...>   x,
...>   `さんずい`: x in kanji_ids[ids glob '⿰氵*']{k},
...>   `9画`: x in kanji_strokes[s = 9]{k},
...>   `「せ」から始まる`: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
...> }
...> /* 複数の条件にあてはまる漢字のみ残す */
...> [`さんずい` + `9画` + `「せ」から始まる` >= 2]
...> /* あてはまる条件でグループ化 */
...> { `さんずい`, `9画`, `「せ」から始まる` => group_concat(x) }
...> ;;
select `さんずい`, `9画`, `「せ」から始まる`, group_concat(x) from (select x, x in (select k from kanji_ids where (ids glob '⿰氵*')) as `さんずい`, x in (select k from kanji_strokes where (s = 9)) as `9画`, x in (select k from kanji_reading where (r glob 'せ*' or r glob 'セ*')) as `「せ」から始まる` from (select distinct 漢字 as x from joyo) where (`さんずい` + `9画` + `「せ」から始まる` >= 2)) group by (`さんずい`), (`9画`), (`「せ」から始まる`)
["さんずい","9画","「せ」から始まる","group_concat(x)"]
[0,1,1,"宣,専,政,施,星,染,泉,牲,狭,省,窃,背"]
[1,0,1,"清,潜,瀬"]
[1,1,0,"洋,洞,津,洪,活,派,浄,海"]
[1,1,1,"洗,浅"]
4 rows (0.017s)

コマンドラインからクエリを実行する

先ほどは手でクエリを入力していましたが、毎回同じように手で入力するのは面倒なので、次のクエリを kanji-venn.erq ファイルに保存しておき、コマンドで実行してみます。

view temp.kanji_reading = joyo {k: 漢字, r: 音訓};;
view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};;
view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};;
view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;;
/* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
/* 各漢字について、条件を判定する */
{
  x,
  p1: x in kanji_ids[ids glob '⿰氵*']{k},
  p2: x in kanji_strokes[s = 9]{k},
  p3: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
}
/* 複数の条件にあてはまる漢字のみ残す */
[p1 + p2 + p3 >= 2]
/* あてはまる条件でグループ化 */
{p1, p2, p3 => group_concat(x)}
;;
npx erq node_modules/@mandel59/mojidata/dist/moji.db < kanji-venn.erq
$ npx erq node_modules/@mandel59/mojidata/dist/moji.db < kanji-venn.erq            
Connected to node_modules/@mandel59/mojidata/dist/moji.db
view temp.kanji_reading = joyo {k: 漢字, r: 音訓};;
view temp.kanji_strokes = mji[漢字施策='常用漢字']{k: 実装したUCS, s: 総画数};;
view temp.kanji_radical = mji[漢字施策='常用漢字'] -:MJ文字図形名:> mji_rsindex -:部首:> radicals {k: 対応するUCS, rad: 部首漢字};;
view temp.kanji_ids = ids[UCS in joyo{漢字}]{k: UCS, ids: IDS} distinct;;
/* 常用漢字を x という名前で取り出す */
joyo {x: 漢字} distinct
/* 各漢字について、条件を判定する */
{
  x,
  p1: x in kanji_ids[ids glob '⿰氵*']{k},
  p2: x in kanji_strokes[s = 9]{k},
  p3: x in kanji_reading[r glob 'せ*' or r glob 'セ*']{k}
}
/* 複数の条件にあてはまる漢字のみ残す */
[p1 + p2 + p3 >= 2]
/* あてはまる条件でグループ化 */
{p1, p2, p3 => group_concat(x)}
;;
create view `temp`.kanji_reading as with kanji_reading as (select 漢字 as k, 音訓 as r from joyo) select * from kanji_reading
ok (0.024s)
create view `temp`.kanji_strokes as with kanji_strokes as (select 実装したUCS as k, 総画数 as s from mji where (漢字施策 = '常用漢字')) select * from kanji_strokes
ok (0.000s)
create view `temp`.kanji_radical as with kanji_radical as (select 対応するUCS as k, 部首漢字 as rad from mji join mji_rsindex on mji.MJ文字図形名 = mji_rsindex.MJ文字図形名 join radicals on mji_rsindex.部首 = radicals.部首 where (漢字施策 = '常用漢字')) select * from kanji_radical
ok (0.000s)
create view `temp`.kanji_ids as with kanji_ids as (select distinct UCS as k, IDS as ids from ids where (UCS in (select 漢字 from joyo))) select * from kanji_ids
ok (0.000s)
select p1, p2, p3, group_concat(x) from (select x, x in (select k from kanji_ids where (ids glob '⿰氵*')) as p1, x in (select k from kanji_strokes where (s = 9)) as p2, x in (select k from kanji_reading where (r glob 'せ*' or r glob 'セ*')) as p3 from (select distinct 漢字 as x from joyo) where (p1 + p2 + p3 >= 2)) group by (p1), (p2), (p3)
["p1","p2","p3","group_concat(x)"]
[0,1,1,"宣,専,政,施,星,染,泉,牲,狭,省,窃,背"]
[1,0,1,"清,潜,瀬"]
[1,1,0,"洋,洞,津,洪,活,派,浄,海"]
[1,1,1,"洗,浅"]
4 rows (0.032s)

数列の内包的記法について

公理的集合論についてほとんどなにも知らないからこれから書くことは間違っているのかもしれないけど、古典的なクラスや集合の記法は公理的集合論の元では複数の公理に対応していると理解している。ZF公理系の分出公理に対応する記法として

\left\lbrace x \in X \,\middle|\, P(x) \right\rbrace

というような記法が考えられる。これがふつうは内包的記法と呼ばれているんだけど、別の置換公理に根拠を持つ記法も考えられて、

\left\lbrace f(x) \,\middle|\, x \in X \right\rbrace

のような書き方ができる。この両者を内包的記法と呼ぶのは紛らわしいから、分出公理に基づく記法を分出記法、置換公理に基づく記法を置換記法を置換記法と呼ぶことにする。(x \in Xは命題であると考えれば、命題が左側に来る分出記法は元来のクラスの記法のルールを破っているように思えるが、左辺に来るx \in Xは分出記法の一部として扱われる。それと同様に、置換記法の右辺に来るx \in Xも命題ではなく、置換記法の一部として扱われる。)

ところで、数列があったとして、その数列の各項を2倍した数列をどう書けばいいだろうか。

まあふつうに

a = \left( a_n \right)_{n \in \mathbb{N}}

b = \left( 2 a_n \right)_{n \in \mathbb{N}}

でもよいのだが、内包的記法のように、簡単に書きたい。Pythonであれば、

a = [1, 2, 3]
b = [2*x for x in a]

と書くのだから、数式でも

b = \left( 2 x \,\middle|\, x \in a \right)

とするかとも思うが、しかし、x \in aというのは気持ちが悪いと言うか、aは数列であり、数列は写像なのだから、x \in aと書いたときのxは、aの数列としての項ではなくaの集合としての元であり、aの集合としての元とは添え字と項のペアであるのでは、という気持ちがある。そうすると

b = \left( 2 x \,\middle|\, (k, x) \in a \right)

と書くのか。ここまでするのなら、いっそ

b = \left\lbrace (k, 2 x) \,\middle|\, (k, x) \in a \right\rbrace

と書いてしまって、ふつうに集合の置換記法で書いてしまってもいい気がする。しかし、操作したいのは数列なのに、これでは数列を表す写像を表す集合を操作していることになってしまう。x \in aの気持ち悪さが問題であれば、矢印にしてしまって

b = \left( 2 x \,\middle|\, x \leftarrow a \right)

でもいいかなあ。わかりづらいか。

Tutorial DとErqの比較

Tutorial DとErqは部分的に似た文法を持っているが、その目的は異なっている。Tutorial Dは、その目的が数学的により純粋な関係代数を実現することであるのに対し、ErqはSQLのセマンティクスを保ったまま文法を異なるものにしている。

この記事では、Tutorial DとErqを簡単に比べる。なお、この記事でTutorial Dとして例示するもは、Project:M36 Relational Algebra Engineが実装している記法である。

(Project:M36を実際に動かして試したかったが、手元の環境でビルドに失敗して試せていない。)

セマンティクスの違い

純粋な関係か、多重関係か

Tutorial Dは関係代数に忠実なセマンティクスを持っている。そこで扱われる関係はタプルの集合であって、重複したタプルを持たないし、属性の順番は重要ではないし、関係に含まれるタプルの間に暗黙の順序はない。

一方でErqのセマンティクスは基本的にはSQLと同じであって、扱われるテーブルは純粋な関係とは限らず、重複したレコードを許すし、カラムの順番は重要で、暗黙の順序を持っている。

NULL・三値論理を採用するか

SQLはTRUE/FALSE/UNKNOWNの三値論理を採用している。(SQLiteの場合、UNKNOWNの代わりにNULLを使う。)そのせいで、値にNULLが絡んできた場合の対応が面倒なことになっている。

演算の比較

Tutorial DとErqで、個々の演算を比較してみる。データとして、TutorialD Tutorial for Project:M36で使われているものと同じ、Chris Dateのサンプル関係データを用いる。

関係(テーブル)s (suppliers) の内容

s#,sname,status,city
S3,Blake,30,Paris
S4,Clark,20,London
S5,Adams,30,Athens
S1,Smith,20,London
S2,Jones,10,Paris

関係(テーブル)p (products) の内容

p#,pname,color,weight,city
P6,Cog,Red,19,London
P5,Cam,Blue,12,Paris
P1,Nut,Red,12,London
P4,Screw,Red,14,London
P3,Screw,Blue,17,Oslo
P2,Bolt,Green,17,Paris

関係(テーブル) sp (supplierProducts) の内容

s#,p#,qty
S1,P1,300
S1,P2,200
S1,P3,400
S1,P4,200
S1,P5,100
S1,P6,100
S2,P1,300
S2,P2,400
S3,P2,200
S4,P2,200
S4,P4,300
S4,P5,400

Erq/SQLでは値にNULLが入る場合があるが、それを考慮すると複雑になってしまうので、ここではNULLが入らない場合だけについて考えることにする。

関係自体の表示

Tutorial DもErqも、関係それ自体を表すのに余計なキーワードを必要としない。関係(テーブル) p を表示したいのであれば、単に p をクエリすればよい。

属性の改名

Tutorial Dの場合は属性(カラム)の改名のシンタックスが存在して、

s rename {city as town}

のようにすると、属性cityをtownに改名できる。

Erqは、少なくとも現状ではカラムの改名の記法は存在しないので、ブレース記法で残りのカラムを選択する必要がある。

s{`s#`,sname,status,town: city}

射影

Tutorial DとErqで、射影の記法は似ている。どちらもブレースを使って、属性を選択することができる。

p{color,city}

しかしTutorial Dは関係代数に忠実であるのに対し、ErqはSQLと同じセマンティクスを持っている。すなわち、Tutorial Dの場合はタプルの重複は除去されるので、この結果は4件になる一方で、Erqの場合は重複が除去されず、結果は6件になる。

Erqで重複タプルを除去するには、明示的にdistinctをつける必要がある。

p{color,city} distinct

結合

Tutorial Dではjoinは自然結合のこと。

s join sp

ErqではSQL同様natural joinを使う。

s natural join sp

射影の略記

s join sp から関係 s に含まれる属性すべての射影をとるとき、Tutorial Dでは {all from s} と書く。Erqでは {s.*} と書く。

(s join sp){all from s}
s natural join sp {s.*} distinct

拡張

Tutorial Dではこう書く。@は属性を表す。

s:{status2:=add(10,@status)}

Erqでは射影と区別せず、同じブレース記法を使えばよい。

s{s.*, status2: status + 10}

ユニオン

Tutorial Dではunion演算子を使う。

s union s

Erqでは ;SQLのunion all相当になる。

s; s

distinctを最後につけると、unionになる。

s; s distinct

s minus s

Erqには現状SQLのexcept相当の構文が存在しないが、データにnullが入っていなければ not in を使って対処できる。

s[{`s#`, sname, status, city} not in s];;

セミジョイン

Tutorial D の s semijoin sp(s join sp){all from s} と同じ。

s semijoin sp

Erqにセミジョインの記法はないが、ブラケット記法(where句)とin演算子セミジョインを表現できる。

s[{`s#`} in sp{`s#`}]

ただし、多重集合を許すErq/SQLのセマンティクス上では、x natural join y {x.*} distinctx[{c} in y{c}] は同じ結果になるとは限らない。

アンチジョイン

s のタプルのうち、セミジョイン s semijoin sp に含まれないものからなる関係が s antijoin sp

s antijoin sp

すなわち

s minus (s semijoin sp)

と同じ。

Erqでは

s[{`s#`} not in sp{`s#`}]

と書けばよい。

制限

Tutorial D

s where lt(@status, 30)

Erq

s[status < 30]

グループ・アングループ

Tutorial Dでは、グループ化するとサブリレーションを値に持った属性が作られる。Aggregate Queries

s group ({s#,sname,status} as subrel)

サブリレーションに対してアングループを行うと元に戻る。

s group ({s#,sname,status} as subrel) ungroup subrel

(個人的には、グループ化の基準になるcityが陽に指定せず、都市以外の属性を列挙することになるのが気になる。{all but city}と書けるから別にいいのだろうか。)

Erq/SQLiteではサブリレーションは存在しない。代わりにjson_group_arrayを使ったグループ化が行える。

s{city => subrel: json_group_array(json_array(`s#`, sname, status))}

JSONからのアングループも、長くなるが、一応可能となっている。

s{city => subrel: json_group_array(json_array(`s#`, sname, status))} join j: json_each(subrel) {`s#`: j.value->>0, sname: j.value->>1, status: j.value ->> 2, city}

集約

Tutorial Dではリレーション関数が用意されているので、グループ化した後に、リレーション関数を適用した属性を追加すればよい。

s group ({s#,sname,status} as subrel):{citycount:=count(@subrel)}

Erqでは、集約関数を使う。

s{city => citycount: count(*), subrel: json_group_array(json_array(`s#`, sname, status))}

簡単関係照会言語 Erq で快適なデータベース分析生活を送る

Erq(アーク)は、SQLの代わりにアドホックなデータ分析に用いることを主目的とした、新しいデータベース言語です。リレーショナルデータベースは便利ですが、アドホックなデータ分析を行う上で、SQLの文法は面倒なものです。Erqは、SQLのセマンティクスは極力そのままに異なる文法を採用することで、簡単にクエリを書けるようになっています。

SQLクエリの実例

私はSQLiteデータベースに漢字の文字情報を入れて、複雑な検索や分析ができるようにしているのですが、実際にそのデータベースを使ったクエリ例を見てみましょう。使っているMojidataデータベースは、次のリポジトリからビルドできます。

まず、漢字の読みを集めたmji_readingテーブルの内容を全部表示するために、SQLで次のように照会します。(末尾のセミコロン ; は、SQLite CLIにおける文の終端記号です。)

select * from mji_reading;

データは全部で122148件あるのですが、冒頭のデータはこんな感じになっています。MJ文字図形名は、文字情報基盤における図形番号です。

"MJ文字図形名","読み"
MJ000001,"おなじ"
MJ000001,"くりかえし"
MJ000001,"のま"
MJ000002,"しめ"
MJ000004,"キュウ"
MJ000004,"おか"
MJ000005,"テン"
MJ000006,"キ"
MJ000006,"よろこぶ"
MJ000007,"カ"

ここで、簡単な分析として、読みごとに件数をカウントし、多い順に10件表示してみましょう。

select 読み, count(*) from mji_reading group by 読み order by count(*) desc limit 10;
"読み",count(*)
"コウ",2775
"ショウ",1985
"ソウ",1732
"シ",1730
"トウ",1675
"キ",1536
"カン",1515
"セン",1476
"キョウ",1437
"ケン",1279

カラムを追加し、読みに対応する漢字の例をいくつか表示してみましょう。mji_readingに格納されているのはUnicodeではなくMJ文字図形名なので、Unicodeの漢字を表示するには、別のテーブル mji と結合して照会する必要があります。UnicodeとMJ文字図形名は1対多対応なので、重複するUnicodeを排除するために、select句にdistinctキーワードを使います。また、表示する漢字を最大5つに制限するために、サブクエリを二重に使って、limit句で制限をかけたデータに対してgroup_concat()集約関数で集約を行うことにします。そうすると、クエリはこのようになります。

select
  読み,
  count(*),
  (
    select group_concat(c)
    from (
      select distinct 対応するUCS as c
      from mji
      natural join mji_reading as r
      where r.読み = mji_reading.読み
      limit 5
    )
  ) asfrom mji_reading
group by 読み
order by count(*) desc
limit 10;
"読み",count(*),"例"
"コウ",2775,"㐬,㒶,㓂,㓚,㓛"
"ショウ",1985,"㐮,㐮,㐼,㑱,㒉"
"ソウ",1732,"㐮,㑿,㒎,㔌,㔿"
"シ",1730,"㑥,㒋,㒾,㓨,㓼"
"トウ",1675,"㑽,㓊,㓱,㓸,㔁"
"キ",1536,"㐂,㑧,㑶,㒫,㔳"
"カン",1515,"㒈,㓧,㔋,㔶,㖤"
"セン",1476,"㑒,㒄,㒨,㒰,㔊"
"キョウ",1437,"㐩,㓋,㓏,㓙,㕳"
"ケン",1279,"㐸,㒽,㓩,㓺,㔓"

上記の例は単純ですが、SQLの冗長性・煩雑性がよく表れています。

  • select句とgroup by句やorder by句に重複して書くことになる。
  • select句はクエリの先頭、group by句やorder by句はクエリの末尾にあるので、カーソル移動が面倒くさい。
  • サブクエリにも都度selectキーワードを書くので、多重のサブクエリは記述量がすごく多くなってしまう。
  • 処理の流れ上は後にくるselect句が先頭にあるので、処理の流れがクエリ上で行ったり来たりしてしまう。
  • テーブル名やカラム名の別名を式の後に書くので、後から読むとき、特に長い式の場合に、見づらい。

Erqクエリの実例

今度は同じ分析をErqで行ってみましょう。テーブルの全件取得は、Erqではテーブル名を書くだけです。(Erq CLIでは、文の終端記号に";;"を使っています。)

mji_reading;;

読みごとに件数をカウントし、多い順に10件表示するには、次のように書きます。

mji_reading {読み => count(*) desc} limit 10;;

ブレース・アロー記法 { ... => ... } はErqにおける集約クエリの書き方で、アローの左側にグループに使うカラムを、アローの右側に集約関数のカラムを書きます。また、カラムの後に asc/desc を指定することもできます。この記法によって、SQLのselect句・group by句・order by句の指定を一度に行えるので、Erqでは集約を書くのが簡単になっています。

サブクエリはどうでしょうか。SQLのときと同様に、漢字の例のカラムを追加してみます。

mji_reading
{
  読み =>
  count(*) desc,
  例:
    from mji
    natural join r: mji_reading
    [r.読み = mji_reading.読み]
    {c: 対応するUCS}
    distinct
    limit 5
    {group_concat(c)}
}
limit 10;;
  • Erqでは、サブクエリの先頭にfromを書きます。サブクエリを括弧で括る必要はありません。(トップレベルのクエリにもfromをつけて良いのですが、省略できます。サブクエリではテーブル名とカラム名の区別のため、基本的にはfromキーワードが必要です。)
  • カラム名やテーブル名の別名は、式の前に書きます。
  • ブラケット記法 [...] はwhere句・having句に相当します。
  • ブレース記法 {...} はselect句に相当しますが、from句の後に書きます。
  • distinctキーワードは、Erqでは独立したdistinct句です。
  • ブラケット記法やブレース記法は、クエリに複数書いても問題ありません。

Erqのこれらの特徴により、SQLでは二重のサブクエリとして書いていたクエリを、すっきりとした直列的なサブクエリとして記述できました。

そのほかのクエリ例

他にもいくつかクエリ例を載せてみます。Erq CLIではErqクエリから変換されたSQLを出力するので、どういう変換が行われるか分かるようになっています。

ブラケット記法がhaving句に変換される例

erq> unihan_variant[property='kTraditionalVariant']{s: UCS => t: group_concat(value, '')}[count(*)>1] limit 10;;
select UCS as s, group_concat(value, '') as t from unihan_variant where (property = 'kTraditionalVariant') group by (UCS) having (count(*) > 1) limit 10
["s","t"]
["䴘","鷈鷉"]
["䴙","鷿鸊"]
["么","幺麼麽"]
["云","云雲"]
["伪","偽僞"]
["余","余餘"]
["冲","沖衝"]
["出","出齣"]
["历","曆歷"]
["发","發髮"]
10 rows (0.015s)

共通テーブル式とユニオン

erq> with t(a, b) as (`kdpv_cjkvi/non-cognate`{subject, object}) (t{a, b}; t{b, a}) join unihan_kTotalStrokes on a = UCS {a, b, s: cast(value as integer) asc}[s = 1];;
with t(a, b) as (select subject, object from `kdpv_cjkvi/non-cognate`) select a, b, cast(value as integer) as s from (select a, b from t union all select b, a from t) join unihan_kTotalStrokes on a = UCS where (s = 1) order by (cast(value as integer)) asc
["a","b","s"]
["乀","乁",1]
["乀","乁",1]
["乁","乁",1]
["乙","𠃉",1]
["乁","乀",1]
["𠃉","乙",1]
["乁","乀",1]
["乁","乁",1]
8 rows (0.015s)

共通テーブル式を使った再帰クエリ

erq> with g(i) as ({i: 1}; g{i + 1} limit 10) g;;
with g(i) as (select 1 as i union all select i + 1 from g limit 10) select * from g
["i"]
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
[9]
[10]
10 rows (0.000s)

再帰クエリを使ってグラフを辿る

erq> with v(a, b) as (mjsm natural join mji {対応するUCS, 縮退UCS})
...> with w(a, b) as (v; v{b, a})
...> with g(a, b) as ({null, '刈'}; g join w on g.b = w.a {w.a, w.b} distinct)
...> g {a => group_concat(b)};;
with v(a, b) as (select 対応するUCS, 縮退UCS from mjsm natural join mji), w(a, b) as (select * from v union all select b, a from v), g(a, b) as (select distinct null, '刈' union select distinct w.a, w.b from g join w on g.b = w.a) select a, group_concat(b) from g group by (a)
["a","group_concat(b)"]
[null,"刈"]
["㓼","刹"]
["㔑","刹"]
["䒳","䒳,朵,朶,𣎾,𣎿,𣏻"]
["䓭","刹,苅"]
["刈","刈,苅,𠚫,𠛄,𭃅,𭃆"]
["刴","刹,朶"]
["刹","㓼,㔑,䓭,刴,刹,剎,𠛴,𠞻"]
["剎","刹"]
["朵","䒳,朶"]
["朶","䒳,刴,朵,朶,𣎾,𣎿,𣏻"]
["苅","䓭,刈,苅,𠛄,𫟌"]
["𠚫","刈"]
["𠛄","刈,苅"]
["𠛴","刹"]
["𠞻","刹"]
["𣎾","䒳,朶"]
["𣎿","䒳,朶"]
["𣏻","䒳,朶"]
["𫟌","苅"]
["𭃅","刈"]
["𭃆","刈"]
22 rows (0.123s)

in演算子とorder by句の例

erq> mji natural join mji_reading[対応するUCS in joyo{漢字}]{漢字: 対応するUCS => 読み: group_concat(distinct 読み)} order by count(distinct 読み) desc limit 10;;
select 対応するUCS as 漢字, group_concat(distinct 読み) as 読み from mji natural join mji_reading where (対応するUCS in (select 漢字 from joyo)) group by (対応するUCS) order by count(distinct 読み) desc limit 10
["漢字","読み"]
["明","メイ,ミョウ,ミン,ベイ,ボウ,あかり,あかるい,あかるむ,あからむ,あきらか,あける,あく,あくる,あかす,ひかり"]
["生","セイ,ショウ,ソウ,いきる,いかす,いける,うまれる,うむ,おう,はえる,はやす,き,なま,うぶ"]
["行","コウ,ギョウ,アン,ゴウ,カン,ガン,いく,ゆく,おこなう,まさに,みち,めぐる,やる,ゆくゆく"]
["上","ジョウ,ショウ,うえ,うわ,かみ,あげる,あがる,のぼる,のぼせる,のぼす,たっとぶ,たてまつる,ほとり"]
["下","カ,ゲ,ア,した,しも,もと,さげる,さがる,くだる,くだす,くださる,おろす,おりる"]
["白","ハク,ビャク,ベ,ハ,ヒャク,シ,ジ,しろ,しら,しろい,しらげる,しらむ,もうす"]
["薄","ハク,ヘキ,ホ,うすい,うすめる,うすまる,うすらぐ,うすれる,せまる,すすき,バク,ビャク,ブ"]
["重","ジュウ,チョウ,ジュ,ズ,トウ,シュウ,シュ,え,おもい,かさねる,かさなる,おもんじる,はばかる"]
["反","ハン,ホン,タン,ヘン,ベン,そる,そらす,かえす,かえって,かえる,そむく,たん"]
["懐","カイ,エ,ふところ,なつかしい,なつかしむ,なつく,なつける,いだく,おもい,こころ,おもう,ふところにする"]
10 rows (0.021s)

ローバリュー演算

erq> with u(s, t) as (unihan_variant[property='kTraditionalVariant']{UCS, value})
...> u[{s, t} not in tghb_variants{规范字, 繁体字}] limit 10;;
with u(s, t) as (select UCS, value from unihan_variant where (property = 'kTraditionalVariant')) select * from u where ((s, t) not in (select 规范字, 繁体字 from tghb_variants)) limit 10
["s","t"]
["㐷","傌"]
["㐹","㑶"]
["㐽","偑"]
["㑈","倲"]
["㑔","㑯"]
["㑩","儸"]
["㑺","儁"]
["㓥","劏"]
["㔉","劚"]
["㖊","噚"]
10 rows (0.006s)

Erq実装について

現状はNode.js/JavaScriptSQLiteのErqクライアントを実装し、個人的に利用しています。

将来的にはRustなどで実装しなおすかもしれませんが、現状でもそれなりに便利に使えています。リポジトリには公開していないので、GitHubからインストールしてください。次のコマンドを実行すると、erqコマンドがインストールされます。

npm install -g github:mandel59/erq

UnicodeのSmall Kana Extensionに関する文書

Small Kana Extension - Wikipedia に記載のない文書も追加。

  • L2/10-468R2/N3987 Lunde, Ken (2011-02-09), Proposal to add two kana characters
  • L2/16-334 Sim, Cheon Hyeong (2016-11-04), Hiragana and Katakana (Small Letters)
  • L2/16-354 Yamaguchi, Ryusei (2016-11-07), Proposal to add Kana small letters
  • L2/16-358R/N4803 Lunde, Ken (2016-11-22), L2/16-334 & L2/16-354 Feedback (small kana)
  • L2/16-325 Moore, Lisa (2016-11-18), "C.14 Kana", UTC #149 Minutes
  • L2/16-381 Suignard, Michel (2016-12-08), Additional repertoire for ISO/IEC 10646:2016 (5th ed.) Amendment 1.2
  • L2/17-016 Moore, Lisa (2017-02-08), "Consensus 150-C18", UTC #150 Minutes
  • N4523 The Japan National Body (2017-04-01), Japanese National Body Contribution on Small Kana Characters
  • N4953 "M66.07i", Unconfirmed minutes of WG 2 meeting 66, 2018-03-23
  • L2/17-353 Anderson, Deborah; Whistler, Ken (2017-10-02), "N.1. Small Kana Extension code block and code point changes", WG2 Consent Docket
  • L2/17-362 Moore, Lisa (2018-02-02), "Consensus 153-C13", UTC #153 Minutes

UnicodeのSmall Kana Extensionに関する文書

Small Kana Extension - Wikipedia に記載のない文書も追加 - L2/10-468R2/N3987 Lunde, Ken (2011-02-09), Proposal to add two kana characters - L2/16-334 Sim, Cheon Hyeong (2016-11-04), Hiragana and Katakana (Small Letters) - L2/16-354 Yamaguchi, Ryusei (2016-11-07), Proposal to add Kana small letters - L2/16-358R/N4803 Lunde, Ken (2016-11-22), L2/16-334 & L2/16-354 Feedback (small kana) - L2/16-325 Moore, Lisa (2016-11-18), "C.14 Kana", UTC #149 Minutes - L2/16-381 Suignard, Michel (2016-12-08), Additional repertoire for ISO/IEC 10646:2016 (5th ed.) Amendment 1.2 - L2/17-016 Moore, Lisa (2017-02-08), "Consensus 150-C18", UTC #150 Minutes - N4523 The Japan National Body (2017-04-01), Japanese National Body Contribution on Small Kana Characters - N4953 "M66.07i", Unconfirmed minutes of WG 2 meeting 66, 2018-03-23 - L2/17-353 Anderson, Deborah; Whistler, Ken (2017-10-02), "N.1. Small Kana Extension code block and code point changes", WG2 Consent Docket - L2/17-362 Moore, Lisa (2018-02-02), "Consensus 153-C13", UTC #153 Minutes

キー番号と調号の決定に関するメモ

MIDIのノート番号は音のピッチ(音高)を表現している。ノート番号0はC-1の音を表していて、番号が1増えると、音高は半音上がる。

このような、ピッチを使った音の表記は、しかし、キーを表現することができないという問題がある。ノート番号はピッチを表現しているため、異名同音のD♯、E♭、F𝄫を区別することはできない。

そこで、ここではピッチではなく、キーに対して番号を振る方法を考えてみる。五度圏を考えると、キーは次のように並んでいる:

... A𝄫 E𝄫 B𝄫 F♭ C♭ G♭ D♭ A♭ E♭ B♭ F C G D A E B F♯ C♯ G♯ D♯ A♯ E♯ B♯ F𝄪 C𝄪 G𝄪 ...

そこで、五度圏に並んだ順で、音名Cのキー番号を0とし、そこから完全5度上昇するごとに番号を1ずつ増やしたものを、キー番号とする。

ノート番号とキー番号の関係

オクターブの差や異名同音を無視して考えると、キー番号が1増えれば、音高は完全五度上昇、すなわち、7半音上昇する。逆に、音高が半音上昇すれば、キー番号は7増える。そして、ノート番号もキー番号も、12増えた場合は同音になる。

C-1 のノート番号・キー番号がそれぞれ0となるように定めたので、ある音のノート番号nとキー番号kの間には

 k \equiv 7n \pmod{12}

 n \equiv 7k \pmod{12}

の関係があることになる。

調号の決定

五度圏を使ってキー番号を定義したことからも分かるが、キー番号は調号と直接的に対応している。長調においては、主音のキー番号が、そのまま調号におけるシャープ記号の数となる。(負数の場合は、フラット記号を付ける。)また、Aのキー番号が3であることから、短調では主音のキー番号から3を引けばシャープ記号の数になることがわかる。

調号の変化記号(シャープ記号またはフラット記号)の数は最大で7個なので、長調においては主音のキー番号k-7 \le k \le 7となるように定める必要がある。同様に、短調においては主音のキー番号を-4 \le k \le 10の範囲で定める必要がある。従って、-7 \le k \le -5の場合(主音がC♭, G♭, D♭の場合)は長調に対して同主短調が記法上存在せず、8 \le k \le 10の場合(主音がG♯, D♯, A♯の場合)は短調に対して同主長調が記法上存在しない。

このことから、同主調を必要とするならば、キー番号は-4 \le k \le 7の範囲にある12個(A♭, E♭, B♭, F, C, G, D, A, E, B, F♯, C♯)に決めるべきだとわかる。C♭ major, G♭ major, D♭ majorはそれぞれB major, F♯ major, C♯ majorが同主短調の存在する異名同音調となる。また、G♯ minor, D♯ minor, A♯ minorはそれぞれE♭ minor, B♭ minor, F minorが同主長調の存在する異名同音調となる。

調の主音のノート番号がnとして、

k = \left ( \left (7n + 4 \right ) \bmod 12 \right ) - 4

と定めることで、調のキー番号k-4 \le k \le 7の範囲で決定できる。

2種類の悉曇文字ryaとUnicodeに関するメモ

悉曇文字のデータ化に関する諸問題 —大蔵経テキストデータベース化に伴う悉曇文字作成をめぐって—によれば、悉曇文字のryaは、大蔵経中に2種類の字体が使われている。一つは、raの切継上半体にyaを継いだもの、もう一つはraの切継上半体にyaの切継下半体を継いだものだ。しかし、Unicodeの規格上、ryaの字体をどのように実装するかということは明記されていない。これは、Unicode悉曇文字フォントを実装する上で問題になる。

このryaの字体の曖昧性は、悉曇文字だけの問題ではなく、デーヴァナーガリーベンガル文字、カンナダ文字といった他のインド系文字にも存在しており、The Unicode Standard にも Alternative Forms of Cluster-Initial RA という題で言及されている。

Alternative Forms of Cluster-Initial RA. In addition to reph (rule R2) and eyelash (rule R5a), a cluster-initial RA may also take its nominal form while the following consonant takes a reduced form. This behavior is required by languages that make a morphological distinction between “reph on YA” and “RA with reduced YA”, such as Braj Bhasha. To trigger this behavior, a ZWJ is placed immediately before the virama to request a reduced form of the following consonant, while preventing the formation of reph, as shown in the third example below.

Similar, special rendering behavior of cluster-initial RA is noted in other scripts of India. See, for example, “Interaction of Repha and Ya-phalaa” in Section 12.2, Bengali (Bangla), “Reph” in Section 12.7, Telugu, and “Consonant Clusters Involving RA” in Section 12.8, Kannada.

https://www.unicode.org/versions/Unicode14.0.0/ch12.pdf#page=19&zoom=auto,-40,350

悉曇文字にも他のインド系文字の規則を応用すれば、raの切継上半体にyaを継いだもの(“reph on YA” に相当する)は ra + virama + ZWJ + ya、raの切継上半体にyaの切継下半体を継いだものは(“RA with reduced YA” に相当する)は ra + ZWJ + virama + ya として表現されるべきものであるように思われるが、悉曇文字に関しては、そのような挙動は明示的に記述されていないし、それを実装しているフォントもなさそうだ。

また、ZWJを使わない ra + virama + ya をどちらで表示するかという問題もある。

  • ra + virama + ZWJ + ya <U+115A8 U+115BF U+200D U+115A7> 𑖨𑖿‍𑖧
  • ra + ZWJ + virama + ya <U+115A8 U+200D U+115BF U+115A7> 𑖨‍𑖿𑖧
  • ra + virama + ya <U+115A8 U+115BF U+115A7> 𑖨𑖿𑖧

TypeScriptでESNextを使う場合のSequelizeの書き方

TypeScriptでSequelizeを使おうとして、マニュアル(Manual | Sequelize)にある通りに、null assertionを使ってモデルのフィールドを定義してやると、うまく動かなかった。

import { DATEONLY, INTEGER, STRING, Model, Sequelize } from "sequelize";

export const sequelize = new Sequelize("sqlite::memory:");

export class User extends Model {
    id!: number;
    name!: string;
    birth!: string;
}

User.init({
    id: {
        type: INTEGER,
        primaryKey: true,
        autoIncrement: true,
    },
    name: {
        type: STRING,
        allowNull: false,
    },
    birth: {
        type: DATEONLY,
        allowNull: false,
    }
},
    { sequelize }
);

export async function main() {
    await sequelize.sync({ force: true });
    const user = await User.create({
        name: "Albert Einstein",
        birth: "1879-03-14"
    });
    console.log(user.name, user.birth);
}

main();

コンパイラオプションのターゲットがESNextに設定されている場合、上のコードを実行すると undefined undefined と出力される。これは、最新版のTypeScriptが TC39 Stage 3 提案のクラスフィールド に対応しており、ターゲットがESNextになっている場合は、useDefineForClassFieldsコンパイラオプションが有効になって、現行の意味論である「定義」意味論が使われるからだ。(「定義」意味論については、Babelプラグインの順序とallowDeclareFieldsの妙を参照してほしい。)

この問題は、以下のいずれかで解決する:

  • useDefineForClassFieldsオプションをfalseに設定する。
  • null assertion の代わりに declare プロパティ修飾子を使う。
export class User extends Model {
    declare id: number;
    declare name: string;
    declare birth: string;
}

新規のコードを書くときは declare 修飾子を使うようにした方がいいだろう。

参考文献

IDSデータベースリスト

macOS Big Sur では梵字が表示できる

support.apple.com

macOS Big Sur に組み込まれているフォント一覧を見ると、Noto Sans Siddhamがあることが分かる。これは悉曇文字、つまり寺院などで目にする梵字を収録したフォントだ。Font Bookやアプリのフォント一覧には表示されず、フォントフォールバック機能を介して使うことが前提になっているみたいだ。

フォントが入っていない人は https://github.com/googlefonts/noto-fonts/tree/main/hinted/ttf/NotoSansSiddham からフォントを入手できる。(NotoフォントはSIL Open Font Licenseの条件下で公開されている)

試しに、この記事にもいくつか梵字を載せておこう。(他にも 种子字 - 维基百科,自由的百科全书 にたくさん載っている。)

𑖭𑖾 saḥ

この字は東スポで記事になってた。(馬というか、勢至菩薩の種字で、午年の守り本尊なんだってさ) www.tokyo-sports.co.jp

𑖂 i

𑗘 i

𑗙 i

𑖮𑖳𑖽 hūṃ

𑖮𑗝𑖽 hūṃ

𑖏 kha

𑖭𑖿𑖝𑖿𑖪𑖽 stvaṃ

𑖮𑖿𑖨𑖱𑖾 hrīḥ

𑖮𑖿𑖮𑖳𑖼 hhūṃ

𑖮𑖿𑖦𑖿𑖦𑖯𑖼 hmmāṃ

梵字をよく知らないので、残念ながら書体のクオリティについてはコメントできない。

電子発行された生命保険料控除証明書を読む

皆さんは確定申告は済みましたか? 私はまだです。2月中旬にやろうとしたけど、年末の携帯電話料金の引き落としの決済が完了していなかったんですよね……それで今日着手したんですけど、生命保険料控除証明書が必要になりました。10月ごろに郵送されて届いているらしい? けど覚えていません。でも、電子発行も可能なみたいで、大丈夫そうでした。

で、電子発行したんですけど、ファイルの形式がXML。会計ソフトに金額を入力しなきゃいけないから読みたいと思ったんですけど、テキストエディタで開いて中をみてもタグ名がWCE00370みたいな識別子で、さっぱり項目が分かりません。

じゃあ、どうすれば読めるのか。方法を2つ見つけました。

方法その1 QRコード付証明書等作成システムを使う

www.e-tax.nta.go.jp

QRコード付証明書等作成システムに、生命保険料控除証明書のXMLファイルをアップロードすれば、PDFに変換されて、人間が読めるようになります。専門知識が要らないので、これが楽です。自分の環境はMacFirefoxの推奨環境外ですけど、ふつうに使えました。

ただ、使うのはあまり難しくないにしても、リモートのサービスに依存しないと書類が読めないというのはちょっと不満があります。システムをオープンソースにしてほしい。

方法その2 XMLをウェブブラウザーで開く

XMLの基幹的な技術のひとつに、スタイルシートというものがあります。これは、XMLファイルを、人間が読める形に変換してくれる技術です。スタイルシートとしてはCSSが有名ですが、XMLスタイルシートとしてはXSLというものが使われることが多いです。いまどきのWeb界隈はXMLを扱わないんでしょうが、ウェブブラウザーはXSLにもちゃんと対応しているので、開けるはずです。早速ブラウザーで開いてみましょう。

f:id:mandel59:20210301103913p:plain
XMLファイルを開いた結果

……ひどいですね。これはFirefoxで表示されたエラー画面ですけど、Google Chromeではエラーさえ表示されず、真っ白な画面が表示されます。開発ツールのウェブコンソールを確認すると、セキュリティ関係のエラーが出ています。(ローカルに保存したXMLファイルが、リモートにあるスタイルシートを参照しているので、エラーになっています。)

このエラーを回避する手順は:

  1. http://xml.e-tax.nta.go.jp/xsl/1.0/CMTEG800-001.xsl からスタイルシートをダウンロードする。
  2. XMLファイル中のスタイルシートのパスを、ダウンロードしてきたスタイルシートを指すように書き換える。
  3. 開発用ウェブサーバーを立ち上げ、XMLスタイルシートをサーバーで提供する。
  4. ウェブブラウザーから開発用ウェブサーバーにアクセスしてXMLファイルを開く。

たいへんですね。専門知識がないとできません。こんなハックじみた方法じゃないと開けないのでは、スタイルシートの意義がないんじゃないでしょうか。 ウェブを取り巻く環境が変わって、セキュリティの仕組みがウェブブラウザーに入ってきたことに、対応できていないんですね。

むすび

いかがでしたか? この記事で皆さんもXMLに興味が湧いてきたんじゃないでしょうか。

ここには書いてない読み方として、XMLスキーマをダウンロードしてきて自力で解読するという方法も思いつきましたが、試そうとしたところ国税庁ホームページがメンテナンスに入ってしまったんで試せませんでした。あとで誰か試してみてください。

個人的には、そもそもスタイルシートがなくても読めるようにしておくべきなのでは?という点が不満に感じました。人間に読めることを考えないんだったらJSONでいいじゃんって思うし、だからみんなJSONを使うようになっちゃったんでしょうね。どうあるのが理想なんでしょうか。

小ネタ

f:id:mandel59:20210301110012p:plain
XSLで変換された後のDOM

oncontextmenu="return false" はやめてください

mandel59.hateblo.jp

異体字セレクタを使うと「禰󠄀」の字を出せるよって話を前に書いたけど、アニメ公式の人物情報では「煉󠄁獄杏寿郎」の「煉󠄁」の字、「鬼舞辻󠄀無惨」の「辻󠄀」の字では異体字セレクタを使っていることがわかった。なんで禰󠄀の字だけフォントの変更で対応していたのかな?

MIDIデータを可視化するやつを作った

Midivis screenshot

音楽でどういう音が鳴っているのかを知りたいと思ったので、MIDIデータを可視化するやつ、Midivisを作りました。

https://midivis.ryusei.dev/

Web MIDI APIが動くブラウザーGoogle ChromeMicrosoft Edge)で使うことができます。Mozilla FirefoxはWeb MIDI APIをサポートしていないので動きません。(プラグインを使えば動かせるかもしれませんが、確認していません。)

ノートは、横方向は半音、縦方向は完全4度の音程で並んでいます。ノートは横に12列並んでいるので、これだと同じ音が重複して表示されることになりますが、このように表示することで、コードのパターンが分かりやすくなります。たとえば、次の動画ではカノン進行 D-A-Bm-F♯m-G-D-G-A を演奏していますが、D→AやBm→F♯m、G→Dのような4度下行のパターンは下に、D→Gのような4度上行は上に、A→BmやF♯m→G、G→Aといった2度の上行は右に動いて感じるかと思います。そうすると、カノン進行全体ではパターンはどんどん右に動いていきますが、循環コードなので最後には元の配置に戻ります。こういう表現ができるのは音を重複して表示させているからで、これを横幅5列にして重複をなくしてしまうと、パターンが途切れてうまくアニメーションしなくなってしまいます。

www.youtube.com

(動画ではF♯はG♭と表示されていますが、Midivis最新版では音名の表示にシャープを使うよう切り替えることができます。)

おまけですが、コードネームを表示する機能もつけています。よく使われるコードは表示できると思いますが、対応していないコードは代替表示になります1異名同音も無視されるので、あくまでおまけ機能です。


  1. たとえばC, D♭, G, Bから構成されるコードがC{1,m2,5,M7}のような表示になります。

「不愉快の峠」仮説

マイノリティとマジョリティの間には、不愉快の峠があるのではないかと思う。

マイノリティ文化がマジョリティ文化に認知される過程を考えると、情報の少ない接触初期段階では、どうしても表現が歪み、不正確になってしまう。理解の深い人間の目には、この不正確で偏見混じりの描写はいかにも不愉快なものに映る。偏見をなくせ、正しく理解しろと憤る。

しかし、マイノリティ文化を最初から正確に描写することはできない。マイノリティ文化は異文化で、まず、正確に表現するための語彙さえ、マジョリティ文化には存在していない。マジョリティ文化の人間にマイノリティのことを正確に説明しようとすればするだけ、迂遠な表現で浅薄な解釈を回避することになるし、正確だが無味な描写は、結局はマジョリティ文化においては無視されることになる。偏見を許さない態度を強めれば強めるだけ、マイノリティ文化への理解が進展しなくなってしまう。そうであるとすれば、マイノリティがマジョリティに認知され、受容されるためには、少なくとも当分は、不愉快を飲んで相手の好奇心を刺激する描写に甘んじるしかないのではないか。

このような法則が、マイノリティの種別を問わず、普遍的にあるのではなかろうか。これは仮説でしかないのだけれども、明らかに不条理な仮説だ、と思う。マイノリティが無視される社会と、マイノリティが偏見にさらされる社会は、どちらも苦しい。マジョリティからの能動的な攻撃に晒されないだけ、無視されていた方がマシだ、という考えもあるだろう。しかし、無視されている状態からは、理解は進むはずがない。異文化間の不和を解消するには、不愉快の峠を乗り越え、その先へ進むしかないはずだ。

では、偏見を受け入れるべきなのか。部分的にはそうだ。しかし、極力無害な偏見を選ぶことは、できるかもしれない。致命的でない、許容できる不愉快さの偏見の峠を見つければ、不愉快さを耐えた先で、より深い相互理解を得られた未来に至ることができるかもしれない。