「IPAフォントライセンスを巡って」について思うところ 1

IPAフォントライセンスを巡って | 一般社団法人 文字情報技術促進協議会

IPAフォントライセンス1は、Open Source Initiative (OSI) から、The Open Source Definition (OSD) 2に準拠しているという認定を受けているという触れ込みだった。3 だから、ライセンスを理由にMJ明朝体フォントを利用したサービスを差し止めるというのは不可解なことに思える。いったい、これはどういうことなんだろうか。この記事を読んで、次のような疑問が浮かんだ。

  1. IPAmj明朝をWebフォントとして利用することは、IPAフォントライセンス上問題となるのか。
  2. IPAmj明朝をWOFF化するサービスは、IPAフォントライセンス上問題となるのか。
  3. サブセットフォントは包摂基準を変えるのか。

以下では、上記疑問のうち、1と2について考えてみたい。3については、別の記事にする。

以下、IPAフォントライセンスの条項を指す場合は、IPAフォントライセンスの表現に従い、列挙された項目を号ではなく項で表現する。また、OSD条文は八田真行(mhatta)による「オープンソースの定義」の日本語訳4に基づく。

1. IPAmj明朝をWebフォントとして利用することは、IPAフォントライセンス上問題となるのか。

IPAmj明朝が、複製その他の利用をされるケースとして、次の3つがある。

  1. 派生プログラムを再配布する
  2. 許諾プログラムをそのままの状態で改変することなく再配布する
  3. デジタル・ドキュメント・ファイルについて複製その他の利用をする

ケース1は2条4項または2条7項に該当して許諾が付与されるケースで、3条1項の制限を満たす必要がある。ケース2は2条6項に該当するケースで、3条2項の制限を満たす必要がある。ケース3は2条2項、3項および5項に該当するケースで、このケースでは3条1項および2項の制限を満たす必要がない。

Webフォントとしての利用がいずれのケースに該当するにしても、それぞれのケースについて必要な制限を満たしていれば、とうぜんWebフォントとして利用可能だ。

3条3項および4項に示されているのは免責条項なので、以下議論しない。

ケース1 派生プログラムを再配布する。

1のケースに該当する場合は条件が多い。

  • 3条1項(1)「派生プログラムを再配布する際には、下記もまた、当該派生プログラムと一緒に再配布され、オンラインで提供され、または、郵送費・媒体及び取扱手数料の合計を超えない実費と引き換えに媒体を郵送する方法により提供されなければなりません。」該当するデータを提供すればよい。
  • 3条1項(2)「派生プログラムの受領者が、派生プログラムを、このライセンスの下で最初にリリースされた許諾プログラム(以下、「オリジナル・プログラム」といいます。)に置き換えることができる方法を再配布するものとします。かかる方法は、オリジナル・ファイルからの差分ファイルの提供、または、派生プログラムをオリジナル・プログラムに置き換える方法を示す指示の提供などが考えられます。」この条項は、具体的に何を指して「置き換える」と言っているのかがよく分からない。アプリケーション上での表示フォントを置き換える、という意味であれば、サブセット化したWebフォントとローカルにインストールしたIPAmjフォントとで、表示を切り替えることができるようにする機構を入れておけば済むと思われる。
  • 3条1項(3)「派生プログラムを、本契約書に定められた条件の下でライセンスしなければなりません。」ライセンスすればよい。
  • 3条1項(4)「派生プログラムのプログラム名、フォント名またはファイル名として、許諾プログラムが用いているのと同一の名称、またはこれを含む名称を使用してはなりません。」該当箇所を適当にリネームすればよい。
  • 3条1項(5)「本項の要件を満たすためにオンラインで提供し、または媒体を郵送する方法で提供されるものは、その提供を希望するいかなる者によっても提供が可能です。」制約に関する条項なのに、文が「可能です」で終わっているのは不可解だし、意味がとれない。「その提供を希望するいかなる者」というのは、提供を行うことを希望する者なのか、それとも提供を受けることを希望するものなのか、それも分からない。3条1項(1)で示されている提供されなければならないものの提供方法はその提供の主体を問わず、他者から提供を受けることができるようにすればそれで条件を満たしているという但し書きなのだろうか。

3条1項(2), 3条1項(5)の意味に不可解な部分はあるが、満たすことは可能な条件だと思われる。

ケース2 許諾プログラムをそのままの状態で改変することなく再配布する

  • 3条2項(1)「許諾プログラムの名称を変更してはなりません。」
  • 3条2項(2)「許諾プログラムに加工その他の改変を加えてはなりません。」
  • 3条2項(3)「本契約の写しを許諾プログラムに添付しなければなりません。」

「本契約の写しを許諾プログラムに添付」の部分の解釈が微妙だ。これがZIPアーカイブ化であれば、同一アーカイブ内にライセンス本文が含まれていれば、これは確実に添付したと言えるだろう。では、OpenTypeフォントファイルをオリジナルの形式でWebサーバー上にアップロードしてある場合はどうだろうか? フォントファイルをWebフォントとして参照するページ自体に許諾プログラムが記載されていれば、それは添付と言えるだろうか? あるいはライセンス条文へのリンクが記載されている場合はどうか? ここで、アーカイブに含まれているのは添付だが、ライセンス条文へのリンクは添付ではないということになると、それは直感的には不条理な制限だし、OSD 10条「ライセンスは技術中立的でなければならない」に抵触するかもしれない。ここの部分が明確であれば、OpenTypeフォントをサブセット化せずそのままウェブフォントとして使うことは、容量の問題は存在するにせよ、技術的に可能なはずだ。

ケース3 デジタル・ドキュメント・ファイルについて複製その他の利用をする

このケースに該当する場合は、上記3条1項および2項の制限に従う必要がない。

WebアプリケーションにおけるWebフォントとしての利用は、どのケースに該当するのか

ライセンスでは、デジタル・コンテンツおよびデジタル・ドキュメント・ファイルは以下のような定義になっている。

  • 1条4項「「デジタル・コンテンツ」とは、デジタル・データ形式によってエンド・ユーザに提供される制作物のことをいい、動画・静止画等の映像コンテンツおよびテレビ番組等の放送コンテンツ、ならびに文字テキスト、画像、図形等を含んで構成された制作物を含みます。」
  • 1条5項「「デジタル・ドキュメント・ファイル」とは、PDFファイルその他、各種ソフトウェア・プログラムによって製作されたデジタル・コンテンツであって、その中にフォントを表示するために許諾プログラムの全部または一部が埋め込まれた(エンベッドされた)ものをいいます。フォントが「エンベッドされた」とは、当該フォントが埋め込まれた特定の「デジタル・ドキュメント・ファイル」においてのみ表示されるために使用されている状態を指し、その特定の「デジタル・ドキュメント・ファイル」以外でフォントを表示するために使用できるデジタル・フォント・プログラムに含まれている場合と区別されます。」

Webアプリケーションは、定義上は「デジタル・データ形式によってエンド・ユーザに提供される制作物」に該当するから「デジタル・コンテンツ」であるし、それにフォントを埋め込めば、それは「デジタル・ドキュメント・ファイル」となると思われる。しかし、これは本来ライセンスが意図した定義ではないかもしれない。すなわち、Webアプリケーション自体はとうぜん制作物であるにしろ、それによって表示されるテキストはWebアプリケーションに由来するとは限らず、たとえばチャットアプリとして、送信されたテキストをIPAmjフォントで表示するというようなケースでは、アプリに含まれないテキストを表示するためにフォントが使われることになる。そのようなアプリケーションは、制作物でありながらメディアなのだから、IPAフォントライセンス制定者の意図としてはケース3から除外したいもののように推察されるが、それにしては、ライセンス上の定義は不適切だと思われる。

「デジタル・コンテンツ」および「デジタル・ドキュメント・ファイル」の定義がそもそも不適切なので、フォントが埋め込まれたWebアプリケーションはおよそケース3に該当してしまうという解釈はできる。しかし、そう解釈しないにしても、本来想定されていたであろう外部のテキストの表示にフォントを用いない、真に「デジタル・ドキュメント・ファイル」と呼べるWebアプリケーションも存在する。そもそも、HTMLというのはアプリケーション以前に、文書を表現するためのフォーマットであるはずなのだから、Webアプリケーションのうち、真に「デジタル・ドキュメント・ファイル」であるものは存在する。

WebフォントがHTMLファイルの外部に保存されていて、そのファイルにリンクされている場合は、条文中の「エンベッド」に該当しない可能性も残ってはいる。しかし、Webフォントをdata URLの形でHTMLファイル中に埋め込むことも可能であり、この場合は「エンベッド」に該当することに争う余地はないだろう。

(そもそも細かいことを言えば、リンクの場合はエンベッドに該当しないという解釈を取ることは、どうなのだろう? HTMLファイルにdata URLをを埋め込むことと、単なるリンクにすることの間で、フォント抽出の難しさはほとんど差がなく、別の文書での再利用も簡単だ。ここで、data URLの場合は埋め込みで、リンクの場合は埋め込みではないということにする合理性はどれほど存在するだろうか?)

2. IPAmj明朝をWOFF化するサービスは、IPAフォントライセンス上問題となるのか。

IPAmj明朝のWOFF化は、ケース1に該当するのだから、3条1項の条件を満たせば、IPAmj明朝をWOFF化するサービスは可能だと思われる。そのサービスから提供されたWOFFフォントをWeb文書に埋め込んで利用する場合、これはケース3に該当し、3条1項および2項の条件によらず、利用できる。このとき、単にWOFFファイルへのリンクを埋め込む形では文書への埋め込みに該当しないという解釈がありうるが、バックエンドサーバーでWOFFサブセット化サーバーからフォントを取得して文書に埋め込むという処理を行うことでより確実にケース3に該当する形でWebフォントを利用できる。そもそも、今のブラウザーの仕組みではセキュリティ上の問題で、異なるサイト5 6間ではもはやWebフォントのキャッシュは共有されない7 8のだから、リンクを使ったWebフォント提供サービスというのは、もはやキャッシュ効率の点では価値がないので、その文書・そのページで使われるフォントを直接HTMLに埋め込む(インライン化する)という選択を取ることもあるだろうと思う。

MJ文字情報一覧 部首・内画数の誤りについて

MJ文字情報一覧の部首・内画数に誤りがあるので、現時点で気づいているものをここにメモしておく。(部首が異体字の部首になっているもののうち、内画数と総画数が一致しているものを含む。内画数に-が指定されているものは含んでいない。)

MJ000026
㐭
亠部6画
广部8画

MJ000499
㙄
土部7画
阜部10画

MJ000946
㠯󠄁
人部5画
己部2画

MJ001318
㦲
口部8画
戈部4画

MJ003904
䑑
人部18画
臣部12画

MJ003925
䑨
木部11画
舟部5画

MJ006832
倐󠄂
人部9画
犬部11画

MJ007269
兩󠄂
人部6画
入部8画

MJ007496
刦
刀部5画
力部7画

MJ007952
厩󠄊
厂部12画
广部14画

MJ009348
壐󠄂
玉部17画

MJ010288
𡭗
小部2画
爻部1画

MJ010857
帰󠄄
刀部8画
巾部7画
彑部7画
止部10画

MJ013046
斋
文部6画
示部10画

MJ016562
熔󠄃
火部10画
金部14画

MJ016795
牕󠄂
片部11画
穴部15画

MJ019072
稉󠄂
禾部7画
米部12画

MJ019186
穤
禾部14画
米部19画

MJ019768
籠󠄃
竹部16画
而部16画

MJ019851
粦󠄂
火部12画
米部6画

MJ021301
臯
白部12画
自部6画

MJ021531
芔󠄂
十部9画
艸部3画

MJ023019
藁󠄂
禾部18画
艸部14画

MJ024216
覊󠄂
网部25画
襾部19画

MJ024997
貟
口部9画
貝部2画

MJ026090
遡
水部14画
辵部10画

MJ026250
邨󠄃
木部7画
邑部4画

MJ028027
鞸
革部11画
韋部20画

MJ028062
韌󠄂
革部12画
韋部3画

MJ029194
鯖󠄃
宀部16画
魚部8画
鹿部8画

MJ029360
鱒
魚部12画
麻部12画

MJ029862
麿󠄃
魚部7画
麻部7画

MJ030124
龍󠄇
立部0画
龍部0画

MJ033043
𡌛󠄂
里部10画

MJ059404
𭅼
邑部10画

MJ059587
𫝸󠄀
彡部9画
立部4画

MJ059875
𤏁󠄂
火部13画
日部13画

MJ060005
𮄂
水部13画

MJ060014
𮄿
立部30画

MJ060316
𫕅
阜部8画
足部11画

㋿のヒストリー

ハックとデジタル社会

コンピューターで作られたシステムに問題があって、困っているとしよう。システムを作った人はここにいないし、一から作り直す時間はなかったり、そもそも触る権限がなかったりする。そういうシステムを、どうにかうまく使えるようにすることを、ハックという。

ここでいう「ハック」は、コンピューターを壊すとか、システムに侵入するという意味ではないし、単純にコンピューターに精通するというニュアンスでもない。責任の境界を超越して、システムをうまく動くようにしてしまう行為のことを言っている。いいかえれば、他人に依頼して直してもらうめんどくささを回避して、自分でシステムを直してしまうことだ。

たとえば、macOSの標準ブラウザーSafariには「サイト固有のハックを無効にする」という開発者向けの設定項目がある。これは、どういう意味だろうか?

世の中のWebサイトは、コンテンツを配信するWebサーバーと、それを受け取って表示するブラウザーの組み合わせでできている。Webサーバーとブラウザーでどうやってやりとりするかは取り決めがあって、それに従うことで、システムは機能している。だけど、ばあいによっては、Webサーバーとブラウザーのどちらかに問題があって、Webサイトが壊れる場合がある。

もしサーバーに問題があるとしても、Safariを使っている人はSafariが壊れていると思うだろうし、Safariを作っている人たちが世の中のWebサーバーを直してまわるわけにもいかない。手っ取り早く、サイトごとに特殊な取り扱いを追加することで直してしまうことを、ここでは「サイト固有のハック」と呼んでいるのだ。

Safariの開発者向け設定画面。「互換性:サイト固有のハックを無効にする」という設定項目がある。(オレンジ色で囲って強調した部分)

人間社会は責任の境界がある。そういう社会でシステムを直す「まっとうな方法」は、責任をもっている人に対してうまく交渉を行って、直してもらうということだろう。でも、世の中には、誰が責任を持っているのか分からないシステムや、誰も責任を持っていないシステムというものがある。交渉しても、失敗して目的が達成できないかもしれない、という不安がある。でも、そこを迂回して、「ハック」を行ってしまえば話は簡単になる。だれか他人の顔色をうかがうことなく、目的を達成できてしまうので、楽だし、効率的で、安くつく。むかし、コンビニのコーヒーメーカーのデザインが分かりづらすぎて、テプラの修正まみれになったこともあった。つまり、そういうことである。

ソフトウェアのハックには、ソースコードを共有する文化も重要だ。テプラがあるから簡単に製品の文言を修正できるのと同じように、ソースコードがあるから、ソフトウェアを簡単に修正できる。ソースコードがなければないでうまくやる方法はあるけど、難しくなってくる。ソースコードオープンソースの形で公開することは、私の了承など取らずとも自由にハックしてくださいという表明で、このようにしてハックを自由にやらせることが、結果的には有用であることを経験的に知っている。そして、ハックを自由にさせる文化は、いろいろな価値観と結びついている。

ハッカー文化は「私たちは問題に対して、責任の境界を気にせず直接的に取り組むことができる」という感覚に支えられている。その一方で、「問題の解決方法は、元の責任者に対してフィードバックされるべきだ」という感覚も兼ね備えている。無秩序なハックはさらなる追加のハックを困難にしてしまうし、ソフトウェア更新のたびに、ハックを適用するのは大変なので、最終的には、オープンソースソフトウェアの開発で「アップストリーム」と呼ばれる、元の開発責任者に対して、自分の行なった変更を適用してもらうように依頼する。テプラの例えでいえば、テプラを貼ってそれでよしとするのではなく、もとのコーヒーメーカーのデザインに反映してもらうような感じだ。その過程で、結局は交渉が必要になってくるんだろうけど、交渉して他人に直してもらうのと、自分で直した後に反映してもらえるよう交渉するのとだったら、後者の方が簡単な場合も多い。「直して」ではなく、「直しました」という形で、提案を行うのだ。何かを取り決めてから課題に取り組むのではなく、課題に取り組んでから取り決めるという順序の逆転を許容することが、より迅速で効果的な課題解決につながっている。

インターネットの根幹はハックに支えられている。世の中の無数に存在する、責任者の不明なシステムどうしをうまく繋げて通信できるようにする必要があるわけで、その技術は上から下まで、最初から考えられて設計されているわけではなく、テプラの修正のような後付けの技術にあふれている。もちろん、インターネットにもいろいろな標準規格が存在しているけど、規格で決めたこと以上に実際に動くことが大事だから、規格に合わせてシステムを直すという力だけではなく、システムに合わせて規格に直すという力が働くことになる。ASCIIしか想定していない古いシステムを置き換えずに世界中の文字を扱いたいから、UTF-8だとか、パーセントエンコーディングだとか、そういう技術が生み出されてくるし、そういうものが、規格になっていく。インターネットの世界で使われる規格というのは、国や団体が議論を尽くしてキレイな規格を定めたから広まるというよりは、既存のエコシステムに適応した、優秀なハックであるかどうかという点が有利に働くようにみえる。

そういうハックが支える技術や文化が背景知識としてあると、登大遊の「けしからん」「超正統派インチキ」というキーワードや、オードリー・タンがレナード・コーエンのAnthemを引用していう "There is a crack, a crack in everything. That's how the light gets in." というフレーズについての理解も、深まるのではないだろうか。責任境界の超越を問題とせず、自由に課題に取り組めないことを問題とする思想。問題がいつまでも解決されず絶望するのではなく、問題を自分で解決につなげられるという希望。システムを壊さないよう人間を管理するのではなく、人間が自発的にシステムを直せるよう自由にするという思考。そういったことを言っているのであり、そういった、ハックに親和的な考えを企業や社会に取り入れる重要性がそれぞれの考え方に表れているように、私は思う。

デジタル社会の実現には、技術に精通した優秀なハッカーが必要なわけではない。必要なのは、技術への精通ではなく、課題へのアクセシビリティだ。技術への精通は、課題に取り組み、ハックした結果として得られるものであって、ハックする前に必要なのではない。ハックは、最初は身近で簡単なところからはじめていけばよい。

デジタル社会を形成するハックという考え方は、そのままでほかの分野には適用できないかもしれない。リスクが高い領域だから自由にさせられないとか、システムは入札で調達するから取り決める前に取り組ませるのは難しいとか、いろいろと課題はあると思う。しかし、そういった課題だって、解決する方法は存在するのではないだろうか? ハックという行為を社会に組み込むというメタな課題について、ハックしてみるべきかもしれない。

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

漢字ベン図は、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āṃ

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