LogicoolのSetPointとVMwareの相性が悪く、マウスボタンに割り当てたコマンドの最後が連続入力される問題

例えばホストのSetPointにて、特定のボタンにWin+Tab(アプリケーションスイッチャ)を割り当てて、VMwareでゲストの操作をしているときにそのボタンを押すと、コマンドはゲストに送信されますが、Win+Tab, Tab, Tab ...のような連続したものになってしまい、とっても悲しいです。

ここで、SetPointにはX1, X2ボタンを割り当てて、それらのボタン入力をマウスジェスチャツールで任意のコマンドに割り当てると、Win10 on Win10の環境ではうまく動くようです。

ホストもゲストもこれでうまく動くので、以前Win8だか8.1だかの頃に話題になった、ソフトウェアが送信したAlt+TabをOSが無視する問題とか、緩和されたんでしょうか?(調べてない)

マウスジェスチャーツール CreviceApp 1.0 をリリースしました

2013年10月上旬に、突如としてかざぐるマウスが消えてしまってから乗り換え先を探していましたが、あまりよいものが見つからなかったので自作しました。先月から実際に使ってバグを潰していましたが、そろそろ安定したかなーという感じなので1.0をリリースします。

rubyu/CreviceApp

特徴

必須要件

良い点

  • ジェスチャの取りこぼしがない
  • CSharp Scriptでジェスチャを定義できる

悪い点

  • GUIがショボい

ジェスチャの取りこぼしがないこと、というのを最重要な要件として開発しました。私の必要としない、例えばGUIでの設定機能とか、ジェスチャ軌跡の描写機能とかはバッサリ切ってます。C#で設定を書けて、ジェスチャの取りこぼしなく、安定して動けばいいかなーと。

機能追加にはあんまり興味がないですが、安定性の向上には興味があります。問題などあれば、報告いただければと思います。

夏休みの宿題「Ankiを捗らせるためのjsビューア」提出しました

AnkiDroidでひながいちにち遊ぶのに、ストレスを感じていました。以前の記事(市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編)の方法で辞書からデッキを生成したためカードのサイズが大きく、頻繁にスクロールのためにタッチ操作をしなければいけなかったためです。Ankiを可能な限り高速に単語とその語義を頭に叩き込むゲームとして考えたとき、このUIには改善の余地があると思いました。幸い、カードはHTMLそのものであり、JavaScriptを読みこませれば、カード閲覧時のUIはカスタマイズ可能です。そこで、いくばくかの時間を割いて、UIの改善を試みることにしました。この改善により学習効率が向上すれば、十分にペイすると考えたからです。

開発中のできごと

当初の方針

当初、DOMを走査してページ位置を割り出せるかな、と見込んでいました。可能なら、それを元に仮想的なページ位置を前後できればビューアが完成するはず…でした。

方針転換

DOMの走査は高コストで、かつ、要素によってはオフセット位置を取れなかったりしました。そこで、CSS3 Multiple Columnsを使って、カラム幅 = ウィンドウ幅に設定し、レンダリングの段階でページごとに描写されるようにしました。この方法だと、単にX方向のスクロール位置をウィンドウ幅ぶん前後させるだけで、ページの前後が実現できます。

生JSを諦める

ひとまず動く、というところまでは生JavaScriptを書きましたが、型がないことに耐えられず、途中でScala.jsに切り替えました。初めて触ったのですが、Scala.jsは思っていたより完成度が高く、十分実用になるのではという印象を持ちました。

AnkiDroidにプルリクを送る

  • Cookieが永続せず消える
  • ユーザーアクションなしでaudio/videoをloadできない

の2点が問題になったため、改善するためのパッチを書いて送りました。無事マージされたので、自前ビルド地獄に落ちずにすみました。Playストアからダウンロードできると楽でいいです。

できあがったもの

全体をスクロールするのではなく、ページ単位の戻り/送りでAnkiのカードを眺めるためのビューアです。シンプルなものですが、これで十分実用になります。

操作方法

操作 動作
左右スワイプ ページ戻り/送り
左右タップ ページ戻り/送り
左右ロングスワイプ チャプター戻り/送り
中央タップ 音声再生
タップ&ホールド 音声リピート再生

成果物

アプリケーションはScalaで記述されており、全部で900行ほど、レポジトリはrubyu/anki-iframe-viewerです。 gitsbtがインストールされている環境であれば、以下の手順でコンパイルできます。

$ git clone https://github.com/rubyu/anki-iframe-viewer
$ cd anki-iframe-viewer
$ sbt prod/dist

現時点で1.0が最新です。リリース一覧からコンパイル済みのjsファイル、cssファイルがダウンロードできます。

  • anki-iframe-viewer-opt.js

ビューア本体。このビューアはページを左右にスライドさせることでページ送りを実現しているので、以下のcssの適用が必要です。

  • anki-iframe-viewer.css

CSS3 multi-column layout をカードに適用するためのCSS#container内にコンテンツが入るようにすると、うまく表示されると思います。

anki-iframe-viewer.cssは標準的な1カラム構成のデザインです。タブレットなどでは、複数カラム構成のデザインのほうが見やすいかもしれません。例えば2カラム構成ではanki-iframe-viewer.cssに続けて、次のコードを適用してください:

#container {
  -webkit-column-width: 50vw;
}

注1: コンテンツの横のグラデーションは、このビューアには関係がない、現在どのあたりを読んでいるのかわかりやすくするためのハックです。適当なHTMLに対し、

<div class="outer">
  <div class="inner"> ... </div>
</div>
.outer {
  /* berry-juice from http://codepen.io/tumanova/pen/tkvmi?editors=0100 */
  background-image: linear-gradient(to bottom, #c5d4d7 6%, #d6b98d 34%, #c99262 57%, #8c5962 80%, #43577e 100%);
}
.inner {
  margin: 0 0.5rem 0 0;
  padding: 0 0.5rem 0 0.5rem;
  background-color: white;
}

などとすれば、最近のブラウザなら再現できるはずです。

注2: anki-iframe-viewerという名前のアプリケーションですが、 iframe内でしか動作しないわけではありません。後述するiframeデッキに移行する際に導入することを目指して開発を始めたため、この名前になりました。

制約事項

以下の制約は、私が必要としておらず、コスト的にサポートできなかった部分です。

  • 画面をタッチして、文字列を選択したりすることができない
  • 複数のaudio要素が見つかった場合、最初のものだけを再生してしまう

設定方法

Scala.jsにてApplicationを以下のように定義しています。

@JSExport("AnkiIframeViewerApp")
object App extends Logger {
...
  @JSExport def minLongSwipeSize(d: Double)                      = { userMinLongSwipeSize = Option(d); this }
  @JSExport def minSwipeSize(d: Double)                          = { userMinSwipeSize = Option(d); this }
  @JSExport def minLongTouchMillis(d: Double)                    = { userMinLongTouchMillis = Option(d); this }
  @JSExport def maxGestureMillis(d: Double)                      = { userMaxGestureMillis = Option(d); this }
  @JSExport def dispatcherDuplicateEventWindowMillis(d: Double)  = { userDispatcherDuplicateEventWindowMillis = Option(d); this }
  @JSExport def tapCenterRatio(d: Double)                        = { userTapCenterRatio = Option(d); this }
  @JSExport def autoPlayAudio(b: Boolean)                        = { userAutoPlayAudio = Option(b); this }
  @JSExport def holdReplayAudio(b: Boolean)                      = { userHoldReplayAudio = Option(b); this }
  @JSExport def audio(query: String)                             = { userAudioQueries += query; this }
  @JSExport def chapter(query: String)                           = { userChapterQueries += query; this }
...
  @JSExport
  def run(): Unit = {
...
  }
}

よって、カード自体は

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<link rel="stylesheet" type="text/css" href="anki-iframe-viewer.css">
<script type="text/javascript" src="anki-iframe-viewer-opt.js"></script>
<script type="text/javascript">
  AnkiIframeViewerApp()
    .audio("#chapter-00 audio")
    .chapter("#chapter-01")
    .chapter("#chapter-02")
    .run();
</script>
</head>
<body>
<div id="container">
  <div id="chapter-00"> ... </div>
  <div id="chapter-01"> ... </div>
  <div id="chapter-02"> ... </div>
</div>
</body>
</html>

のようなHTMLにすべきです。

ここで、AnkiIframeViewerApp()audio()chapter()をコールして設定を行い、run()でアプリケーションを開始していることに注意してください。anki-iframe-viewer-opt.jsを読み込んだだけではアプリケーションは開始されません。

まずは適当なhtmlファイルを作り、ローカルサーバでビューアが動作することを確認してみてください。正しく動作すれば、それをAnkiに移植してみましょう。

Ankiへの導入方法

Ankiへの導入にはいくつかの問題があり、それらを回避しなければなりません。以下には問題とその対処法を記します。

注1: 何か問題が発生したとき、あるいは何も起こらなかったとき(動きさえしない!)に責任を負うことができないため、実際にデッキにこのビューアを組み込むことは推奨しませんが、なにかの役に立つこともあるだろう、ということで本稿を公開しています。ご注意を。お役に立てたなら嬉しいです。

注2: 2016/07/19現在、このビューアはAnkiDroidでのみ動作確認が取れています。Anki Mobileは不明。Anki Desktopでは動作しません。Anki Desktopについては、組み込んであるPyQt 4.8.4がちょー古いためです。ただし、PyQt 5.5への移行が進行中なので、そのうち動くようになるのではないかと思います。

AnkiDroidとその他の環境で併用したい

動作しない環境では、cssとjsファイルを削除すれば、従来通りのスワイプクロールUIに簡単にフォールバックできます。これは、将来的にこのビューアに問題が発生した場合も、この方法で簡単にフォールバックが行えるということを意味します。

カードのheadをカスタマイズできない

JavaScriptを使って改変できます。カードのテンプレートに、例えば以下のように記述します。

<script>
  var meta = document.createElement("meta");
  meta.setAttribute("name", "viewport");
  meta.setAttribute("content", "width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no");
  document.getElementsByTagName("head")[0].appendChild(meta);
</script>

Ankiのデッキサイズに100MBの制約がある

Collections on AnkiWeb are limited to 100MB, not including media.

Are there limits on file sizes on AnkiWeb? / Anki Ecosystem / Knowledge Base - Anki Support

これは実に絶望的な制約で、例えばCOCA60K(6万単語)コーパスからデッキを生成すると、平均して単語あたり1000文字ほどが上限になってしまいます。このための回避策として、カード自体をhtmlファイルに切り出して、カードからはそのhtmlをiframeとして参照するという方法をなんとか編み出しました。以下の手順で、既存のデッキに適用できます。

準備

まず、次の記事を参考に、データを処理する環境を準備してください: 市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編 - ruby-U's blog

次にhttps://github.com/rubyu/wok-scripts/archive/v0.2.zipをダウンロードして、同様に配置してください。

iframeデッキの構築

以下では、チュートリアルで生成したres2.tsvに手を加えて、カードのデータをhtmlファイルとして切り出します。

> java -jar wok-0.1.0.jar -f wok-scripts-0.2\col-detach.wok -v@str src=1,2,3 -v dst=1 -v@rawstr media=media res2.tsv > res3.tsv
> type res3.tsv
"apple" "<iframe src=""4CC39A56AE79DCFDA45E47D76C6C4D94.html""></iframe>"
"book"  "<iframe src=""1B764C2ED28D05BD3759E0ADAAF56557.html""></iframe>"
"car"   "<iframe src=""8D5A6E213A3F1BC101840112AABE2D0F.html""></iframe>"
> dir *.html /b media
4CC39A56AE79DCFDA45E47D76C6C4D94.html
1B764C2ED28D05BD3759E0ADAAF56557.html
8D5A6E213A3F1BC101840112AABE2D0F.html

stcに複数の列が指定されていることに注意してください。これらの列はまとめて一つのhtmlファイルに変換され、dstでそのhtmlを参照するiframeの出力先の列を指定しています。mediaに出力されたhtmlファイルは以下のようなフォーマットになっています。

<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" type="text/css" href="col-detach.css">
<script type="text/javascript" src="col-detach.js"></script>
</head>
<body>
<div id="detached">
  <div id="detached-00"> ... </div>
  <div id="detached-01"> ... </div>
  <div id="detached-02"> ... </div>
</div>
</body>
</html>
Ankiへの導入

この例の場合、iframeから読み込まれるhtmlは col-detach.jscol-detach.cssを読み込みます。必要であれば、 anki-iframe-viewer-opt.jsにビューアを起動するためのスクリプトを追記したものをcol-detach.jsanki-iframe-viewer.csscol-detach.cssとリネームし、collection.mediaに配置してください。

ひとつのディレクトリに大量のメディアファイルを置くと、メディアスキャンがバッテリードレインを起こすことがある

Anki/AnkiDroidではcollection.media内にファイルを配置することを推奨していますが、その数が膨大になると、パフォーマンス上の問題が発生します。アプリケーションの動作のみならず、端末自体のパフォーマンスが低下することもあるため、Epwingから生成したデッキには以下の手順を適用するといいかもしれません。

バッテリードレインの例:

参考: Issue 37199 - android - android.process.media draining battery at boot - Android Open Source Project - Issue Tracker - Google Project Hosting

tsvファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-tsv.wok -v idx=1 -v depth=2 -v@rawstr prefix=Foo res3.tsv > Foo.tsv
> type Foo.tsv
"apple" "<iframe src=""Foo/4/C/4CC39A56AE79DCFDA45E47D76C6C4D94.html""></iframe>"
"book"  "<iframe src=""Foo/1/B/1B764C2ED28D05BD3759E0ADAAF56557.html""></iframe>"
"car"   "<iframe src=""Foo/8/D/8D5A6E213A3F1BC101840112AABE2D0F.html""></iframe>"
htmlファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-html.wok -v depth=2 -v@rawstr target=media

mediaで指定されたディレクトリ内のhtmlファイルについて、head内にbaseタグを追加し、また、メディアファイル(mp3, png)のパスを書き換えます。 ここで、depthで指定された数だけparentへと遡るbaseタグが設定されることに注意してください。

mediaディレクトリ内の全てのファイルにディレクトリ構造を適用する
> java -jar wok-0.1.0.jar -f wok-scripts-0.2\apply-directory-structure-to-media.wok -v depth=2 -v@rawstr target=media
> dir /b media
0
1
2
...
> dir /b media\4
0
1
2
...
> dir *.html /b media\4\C
4CC39A56AE79DCFDA45E47D76C6C4D94.html

指定のdepthの、ファイル名のprefixから生成されたディレクトリ構造が適用されていることを確認してください。 問題がなければ、mediaディレクトリをFooapply-directory-structure-to-tsv.wokprefixで指定した値)にリネームして、これをcollection.mediaにコピーすれば手順は完了です。Foo.tsvをAnkiに読み込み、動作を確認してください。

この例の場合、iframeから読み込まれるhtmlは Foo/col-detach.jsFoo/col-detach.cssを参照します。必要であれば、 anki-iframe-viewer-opt.jsにビューアを起動するためのスクリプトを追記したものをcol-detach.jsanki-iframe-viewer.csscol-detach.cssとリネームし、collection.media/Fooに配置してください。 同様に、iframeから参照されるその他のリソースファイルが存在すれば、それらもcollection.media/Fooに配置してください。cssファイルから参照されるフォントなど、間接的な参照もこの場合に含まれることに注意してください。

大量のファイルを含むアーカイブファイルを扱えないファイラーがある

前述の手順で生成されたディレクトリをAndroid上にコピーするには、圧縮したあと、Android上でアーカイブファイルを解凍するという手順が簡単かつ高速なのですが、ESファイルエクスプローラーなど有名なアプリでも、大量のファイルを含むアーカイブファイルを扱えない場合があります。RARというアプリでは問題なく展開できるようなので、このような場合には試してみてください。

実際の例

私はCOCA60Kコーパスから、語義がEpwing辞書なデッキを作り、さらに前述の方法で語義をiframeに変換して、ディレクトリ構造を適用したものを実際に運用しています。以下に設定を記載します。

ノートタイプ

ノートタイプは「Front」「Back」の二つからなる、標準のノートタイプです。

テンプレート

表面のテンプレート
<div id="anki-front">{{Front}}</div>
書式
@font-face { font-family: _myfont00; src: url('_Lucida_Sans_Unicode.ttf'); }
@font-face { font-family: _myfont01; src: url('_yugothic.ttf'); }
@font-face { font-family: _myfont02; src: url('_HanaMinA.ttf'); }
@font-face { font-family: _myfont03; src: url('_HanaMinB.ttf'); }

html {
  font-size: 100%;
}
html body,
html body * {
  padding: 0;
  margin: 0;
}
body {
  overflow: hidden;
}
#outer {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
}
iframe {
  width: 100%;
  height: 100%;
  border: none;
}
#anki-front {
  margin: 1rem;
  display: block;
  font-size: 1rem;
  font-family: _myfont00, _myfont01, _myfont02, _myfont03, sans-serif, serif;
  text-align: left;
  line-height: 1.5rem;
  color: black;
  background-color: white;
}
.win #anki-front {
  font-size: 200%;
}
裏面のテンプレート
<script>
  document.createElement("meta");
  meta.setAttribute("name", "viewport");
  meta.setAttribute("content", "width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no");
  document.getElementsByTagName("head")[0].appendChild(meta);

  document.addEventListener("touchstart", function(event) {
    event.preventDefault();
  }, false);
  document.addEventListener("touchmove", function(event) {
    event.preventDefault();
  }, false);
  document.addEventListener("touchend", function(event) {
    event.preventDefault();
  }, false);
  document.addEventListener("DOMContentLoaded", function() {
    document.getElementsByTagName("iframe")[0].focus();
  }, false);
</script>
<div id="outer">{{Back}}</div>

バイスごとの設定

SO-03G
collection.media/COCA/col-detach.js

anki-iframe-viewer-opt.jsに以下を結合したもの。

AnkiIframeViewerApp()
  .audio("#detached-02 audio")
  .chapter("#detached-03")
  .chapter("#detached-04")
  .chapter("#detached-05")
  .chapter("#detached-06")
  .chapter("#detached-07")
  .chapter("#detached-08")
  .chapter("#detached-09")
  .chapter("#detached-10")
  .run();
collection.media/COCA/col-detach.css
@font-face { font-family: _myfont00; src: url('_Lucida_Sans_Unicode.ttf'); }
@font-face { font-family: _myfont01; src: url('_yugothic.ttf'); }
@font-face { font-family: _myfont02; src: url('_HanaMinA.ttf'); }
@font-face { font-family: _myfont03; src: url('_HanaMinB.ttf'); }

html {
  overflow: hidden;
  font-size: 2vmin;
}

html * {
  font-size: 1rem;
}

body {
  margin: 1rem 0 1rem 0;
  height: calc(100% - 2rem);
  font-family: _myfont00, _myfont01, _myfont02, _myfont03, sans-serif, serif;
  text-align: justify;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: subpixel–antialiased;
  -webkit-text-size-adjust: 100%;
  -webkit-user-select: none;
  cursor: default;
  line-height: 1.4;
  color: black;
  background-color: white;
}

#detached {
  height: 100%;
  -webkit-column-width: 100vw;
  -webkit-column-gap: 0;
}

#horizontal-bar {
  position: fixed;
  left: 0;
  bottom: 0.05rem;
  width: 100vw;
}
#horizontal-bar-icon1-A,
#horizontal-bar-icon1-B,
#horizontal-bar-icon1-C {
  visibility: hidden;
  position: absolute;
  right: 0.5rem;
  bottom: 0.15rem;
  width: 1rem;
  text-align: center;
  vertical-align: bottom;
  font-family: san-serif, selif;
  font-size: 0.5rem;
  line-height: 0.5;
  -webkit-filter: grayscale(100%) opacity(70%);
}
#horizontal-bar-progress-bar {
  position: absolute;
  left: 0.5rem;
  bottom: 0.35rem;
  width: calc(100vw - 2.5rem);
}
#horizontal-bar-progress-total {
  position: absolute;
  top: 0;
  width: 100%;
  border-bottom: 0.05rem solid #f0f0f0;
}
#horizontal-bar-progress-current {
  position: absolute;
  top: 0;
  width: 0;
  border-bottom: 0.05rem solid #505050;
}

#detached-00 {
  display: inline;
  float: left;
  margin: 0 auto 0 0.5rem;
  font-size: 1.2rem;
  line-height: 1;
}
#detached-01 {
  display: inline;
}
#detached-01 table {
  float: right;
  margin: 0 1rem 0.5rem 0.5rem;
  max-width: 50vw;
  border-collapse: collapse;
  border: 0;
}
#detached-01 table * {
  font-weight: normal;
  line-height: 1.1rem;
}
#detached-01 td {
  margin: 2rem 0 2rem 0;
  text-align: center;
  min-width: 10vw;
}
#detached-01 thead th {
  background-color: #f0f0f0;
  font-size: 0.4rem;
}
#detached-01 tbody td {
  font-size: 0.6rem;
}
#detached-01 tbody:nth-child(2n) td {
  background-color: #fefefe;
}
#detached-01 tbody:nth-child(2n+1) td {
  background-color: #f9f9f9;
}
#detached-02 {
  display: none;
}
#detached-03 .ebquery:first-child::before {
  content: "\A";
  white-space: pre;
}
#detached-03:not(:empty),
#detached-04:not(:empty),
#detached-05:not(:empty),
#detached-06:not(:empty),
#detached-07:not(:empty),
#detached-08:not(:empty),
#detached-09:not(:empty),
#detached-10:not(:empty) {
  /* berry-juice from http://codepen.io/tumanova/pen/tkvmi?editors=0100 */
  background-image: linear-gradient(to bottom, #c5d4d7 6%, #d6b98d 34%, #c99262 57%, #8c5962 80%, #43577e 100%);
}
#detached-03 .ebquery:last-child::after,
#detached-04 .ebquery:last-child::after,
#detached-05 .ebquery:last-child::after,
#detached-06 .ebquery:last-child::after,
#detached-07 .ebquery:last-child::after,
#detached-08 .ebquery:last-child::after,
#detached-09 .ebquery:last-child::after,
#detached-10 .ebquery:last-child::after {
  display: block;
  padding-bottom: 0.2rem;
  text-align: right;
  font-size: 0.8rem;
}
#detached-03 .ebquery:last-child::after {
  content: "【ランダムハウス英語辞典】";
}
#detached-04 .ebquery:last-child::after {
  content: "【研究社 新英和大辞典】";
}
#detached-05 .ebquery:last-child::after {
  content: "【リーダーズ・プラス】";
}
#detached-06 .ebquery:last-child::after {
  content: "【英辞郎】";
}
#detached-07 .ebquery:last-child::after {
  content: "【ロングマン現代英英辞典】";
}
#detached-08 .ebquery:last-child::after {
  content: "【オックスフォード現代英英辞典】";
}
#detached-09 .ebquery:last-child::after {
  content: "【斎藤和英大辞典】";
}
#detached-10 .ebquery:last-child::after {
  content: "【ロングマン現代英英辞典】";
}
#detached-03 .ebquery:last-child,
#detached-04 .ebquery:last-child,
#detached-05 .ebquery:last-child,
#detached-06 .ebquery:last-child,
#detached-07 .ebquery:last-child,
#detached-08 .ebquery:last-child,
#detached-09 .ebquery:last-child,
#detached-10 .ebquery:last-child {
  box-shadow: 0 -5rem 1rem -5rem #909090 inset;
}
.ebquery:first-child {
  padding-top: 0.5rem;
}
.ebquery:not(:last-child) {
  padding-bottom: 0.2rem;
}
.ebquery:not(:first-child) {
  margin-top: 0.1rem;
  padding-top: 0.3rem;
}
.ebquery {
  margin: 0 0.5rem 0 0;
  padding: 0 0.5rem 0 0.5rem;
  display: block;
  word-wrap: break-word;
  background-color: white;
}
.ebkw {
  font-weight: bold;
  background-color: #f0f0f0;
}
.ebsb {  vertical-align: sub;  }
.ebsp {  vertical-align: super;  }
.ebec {
  vertical-align: text-bottom;
  height: 1.1rem;
  padding-bottom: 0.25rem;
}
.ebkw,
.pos,
.sense1,
.sense2,
.sense3,
.LDOCE5 .sup {
  padding-left: 0.2rem;
  padding-right: 0.2rem;
  line-height: 1.7;
}
.SRD      .sense1 {
 padding-left: 0.6rem;
}
.SRD      .sense2,
.BODY     .sense1,
.KENE7J5  .sense1,
.KENE7J5  .sense2,
.KENE7J5  .sense3,
.KQNEWEJ6 .sense1,
.KQNEWEJ6 .sense2,
.KQNEWEJ6 .sense3,
.PLUS     .sense1,
.PLUS     .sense2,
.eijiro   .sense1,
.LDOCE5   .sense1,
.LDOCE5   .sense2,
.OALD7    .sense1 {
 padding-left: 1.2rem;
}
.SRD      .pos,
.KENE7J5  .pos,
.KQNEWEJ6 .pos,
.PLUS     .pos,
.eijiro   .pos,
.BODY     .pos {
 background-color: #e0ffe0;
}
.LDOCE5   .sup {
 background-color: #ffead5;
}
.SRD      .sense1,
.KENE7J5  .sense2,
.KQNEWEJ6 .sense1,
.KQNEWEJ6 .sense3,
.PLUS     .sense1,
.eijiro   .sense1,
.BODY     .sense1,
.LDOCE5   .sense1,
.OALD7    .sense1 {
 background-color: #ffe8e8;
}
.SRD      .sense2,
.KENE7J5  .sense3,
.KQNEWEJ6 .sense2,
.PLUS     .sense2,
.LDOCE5   .sense2 {
 background-color: #e8ffff;
}

.KENE7J5  .sense1 {
 background-color: #e8e8ff;
}
.KQNEWEJ6 .ebul,
.PLUS     .ebbo,
.PLUS     .ebul,
.PLUS     .ebit,
.eijiro   .ebbo,
.eijiro   .ebit,
.LDOCE5   .ebbo,
.OALD7    .ebbo,
.BODY     .ebbo {
  font-weight: bold;
}

html {
  font-size: 10.5px;
}
Nexus7
collection.media/COCA/col-detach.js

SO-03Gと同じ。

collection.media/COCA/col-detach.css

SO-03Gと以下だけ異なる。

html {
  font-size: 13.5px;
}

#detached {
  -webkit-column-width: 50vw;
}

#detached-00,
#detached-01 {
  display: block;
  width: 100%;
  margin-bottom: 0.2rem;
}
#detached-01 table {
  float: none;
  margin: 0 auto 0 auto;
}
#detached-03 .ebquery:first-child::before {
  content: "";
  white-space: pre;
}
PC
collection.media/COCA/col-detach.js

使用しないので空のファイルを配置する。

collection.media/COCA/col-detach.css

SO-03Gと以下だけ異なる。

html {
  overflow: scroll;
  font-size: 16px;
}

市販のデータを用いてAnkiデッキを生成する方法 Epwing辞書編

以前から、Ankiデッキを効率的に各種のデータから作成する方法を開拓してきたが、ここではEpwing辞書からAnkiデッキを生成する方法を紹介する。

まえがき

Epwingとはかつて、一度データを購入すれば、自身が所有するあらゆる端末上で利用することができるようになるという、夢のような、自由で便利な辞書データの規格(JIS X 4081)であった。 しかし現在では、旧式化、クローズドな規格への切り替えなどにより、ほぼ絶滅しており、オリジナルのEpwing辞書の入手は困難になっている。 一方で、デファクト・スタンダードとして一度覇権を握った影響は大きく、現在でも各種プラットフォーム向けのEpwing辞書ビューアは生き長らえており、各種の辞書ソフトのデータをEpwing形式に変換するようなツールも有志により開発が続けられている。 中でもロゴヴィスタの電子辞書シリーズは、Epwingから派生したフォーマットを使用していることもあり、かなりの辞書がdessedによりEpwing形式に変換可能である。 ITの基礎知識を持つ語学学習者が、これらのEpwing辞書から好きなものを選び、高品質なAnkiデッキを容易に作成できるようになることが、本稿の意図するところである。

準備

おおよそ以下のものが必要である。

  • Javaランタイム

Wokを動作させるために必要になる。多くの環境ではデフォルトでインストールされているため、新たに導入する必要はない。必要があれば、https://java.com/ja/download/ からダウンロードする。

  • Wok

TSVファイルを加工するためのツールhttps://github.com/rubyu/wok/releases/download/v0.1.0/wok-0.1.0.jar をダウンロードする。

Wokが行う処理を記述したファイル。 https://github.com/rubyu/wok-scripts/archive/v0.1.zip からダウンロードする。

前述のEpwing辞書。好みのものを用意すること。

  • 単語のリスト

Ankiデッキに変換する元となる、単語が列挙されたファイル。ASCII、またはUTF-8で記述されていること。

ツアー

以下では次に示されるシンプルな単語のリスト list.txt を出発点として、僅かな手順で、十分に実用になるデッキを生成していく。

> type list.txt
apple
book
car

環境構築

まだ作業を始めたばかり、フォルダの中身はlist.txtだけだ。

> dir /b
list.txt

Java環境は整っているだろうか。確かめてみよう。

> java -version
java version "1.8.0_77"
Java(TM) SE Runtime Environment (build 1.8.0_77-b03)
Java HotSpot(TM) Client VM (build 25.77-b03, mixed mode, sharing)

ではここで、Wok、そしてWok-Scriptsをダウンロードしよう。Wok-Scriptsはzipで圧縮されているので解凍しておく。フォルダの中身はこうだ。

> dir /b
list.txt
wok-0.1.0.jar
wok-scripts-0.1
wok-scripts-0.1.zip

Wok-Scriptsには動作テスト用に、挨拶をするだけのスクリプト hello.wok が含まれている。早速試してみよう。

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\hello.wok
Hello!

フィールドを追加する

list.txtにフィールドを追加することで、Ankiのデッキとして意味を持つTSVファイル(フィールドがタブで句切られているファイル)を構築していく。

ランクフィールド

まず簡単な例を示す。 ある単語が、ある単語リストの中で何番目であるかを示すフィールドを構築するためのスクリプトrank.wokを用いて以下のように:

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\rank.wok -v src=0 -v dst=1 -v grp=1 -v@rawstr list=list.txt -v update=true list.txt
"apple" "1"
"book"  "2"
"car"   "3"

ここで、-v src=0 は処理の元になるフィールドの番号を指定している。list.txtにはまだ1つのフィールドしかない。そのため0と置く。

-v dst=1 は処理の結果を書き込むフィールド番号を指定している。1を指定しているため、2番めのフィールドに処理結果が書き込まれる。

-v grp=1 は何件ごとに1つのランクとするか。ここでは1件ごとにランクは繰り上がる。

-v@rawstr list=list.txt はランクの基準となるリストを指定している。ここでは入力と同じファイルを指定している。

-v update=truedstで指定したフィールドに値があった場合、それを上書きすると指定している。

list.txt を処理するため、一番最後に与えている。

また、Wokを実行した結果は、リダイレクトを用いて任意のファイルとして保存することができる。ここではres0.tsvとして結果を保存する。

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\rank.wok -v src=0 -v dst=1 -v grp=1 -v@rawstr list=list.txt -v update=true list.txt > res0.tsv

うまく保存できただろうか。確認してみよう。

> type res0.tsv
"apple" "1"
"book"  "2"
"car"   "3"
音声フィールド

ここでは研究社新英和(第7版)・和英(第5版)中辞典(KENE7J5)をデッキ生成に用いる。

Epwing辞書を検索し、音声ファイルのフィールドを構築するためのスクリプトeb-voice-anki.wokを用いて、以下のように:

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\eb-voice-anki.wok -v src=0 -v dst=2 -v idx=0 -v update=true -v@rawstr dic=KENE7J5 -v@rawstr media=media res0.tsv > res1.tsv

-v idx=0 は検索結果の何番目の音声ファイルを使用するかを指定している。(例えば UK、USなどの順で、)複数の音声が含まれている場合に、望ましいインデックス番号を指定すことができる。

-v@rawstr dic=KENE7J5Epwing辞書のパス(CATALOGSを子に持つフォルダ)を指定している。環境に合わせて適切な値を設定すること。

-v@rawstr media=media は音声ファイルを保存するフォルダを指定している。このフォルダが存在しない場合、処理の中で自動的に生成される。

このスクリプトは処理を実行するのに必要なebquery-0.3.1.jarを自動的に保存する。見覚えがないファイルが存在することに驚かないこと。

さて、結果を見てみよう。

> type res1.tsv
"apple" "1"     "[sound:D7E4E20774DDB06FBA354BD6B91FE24F.wav]"
"book"  "2"     "[sound:9669237A5749E31144688F7F24C2C6D9.wav]"
"car"   "3"     "[sound:4C7A322536A21791D5F559F3E199C77B.wav]"

[]で囲まれたAnki形式のsoundタグを持つ3番めのフィールドが追加されている。

> dir /b media
4C7A322536A21791D5F559F3E199C77B.wav
9669237A5749E31144688F7F24C2C6D9.wav
D7E4E20774DDB06FBA354BD6B91FE24F.wav

音声ファイルもきちんとmediaフォルダ内に作られている。

語義フィールド

さて、最後のフィールド。音声フィールドと同様に、研究社新英和(第7版)・和英(第5版)中辞典(KENE7J5)を用いることとする。

このフィールドを作成するために、必要なものがある。それはMapファイルで、ここで必要になるのはKENE7J5.mapEBWin4をインストールすると、%APPDATA%\EBWin4\GAIJIに自動的に生成される。あるいは空のmapファイルを用いてもよい。その場合はtype nul > KENE7J5.mapとして用意する。

念のため、KENE7J5.mapが存在するか確認しておこう。

> dir /b
ebquery-0.3.1.jar
KENE7J5.map
list.txt
media
res0.tsv
res1.tsv
wok-0.1.0.jar
wok-scripts-0.1
wok-scripts-0.1.zip

準備ができれば、Epwing辞書を検索し、語義のフィールドを構築するためのスクリプトeb-html-min.wokを用いて、以下のように:

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\eb-html-min.wok -v src=0 -v dst=3 -v update=true -v@rawstr dic=KENE7J5 -v@rawstr ebmap=KENE7J5.map -v@rawstr media=media res1.tsv > res2.tsv

-v@rawstr ebmap=KENE7J5.map は前述のmapファイルの場所を指定している。

-v@rawstr media=media 音声フィールドのときと同様。ここでは外字を表示するための画像ファイルを保存する。

さて、結果を見てみよう。

> type res2.tsv
"apple" "1"     "[sound:D7E4E20774DDB06FBA354BD6B91FE24F.wav]"  "<span class=""e
bquery KENE7J5""><span class=""ebkw""><img alt=""hA132"" class=""ebec"" src=""40
CB34C31B79B8D7EF231201333C4468.png""><span class=""ebul"">ap繝サple</span> </span
><br>/ヌスpl/竊帝浹螢ー<br>笏・img alt=""zB128"" class=""ebec"" src=""ACD7681805EBF
0F9BD3345E9F56C122F.png""><br><span class=""ebul"">1</span> <span class=""ebul""
~略~

ちょっと見づらいが、4番目のフィールド、HTMLで記述された語義が生成されている。 各フィールドの文字数でもチェックしてみよう。

> java -jar wok-0.1.0.jar -f wok-scripts-0.1\cell-convert.wok -v@rawstr expr=v.size res2.tsv
"5"     "1"     "44"    "1081"
"4"     "1"     "44"    "10742"
"3"     "1"     "44"    "1536"

うまく生成できたようだ。これでデッキは完成。res2.tsvをAnkiで読み込み、mediaフォルダの中のファイルをAnkiのcollection.mediaにコピーすれば、作業は完了となる。

なお、その際、Anki側では適切なノートタイプを予め定義しておく必要がある。ここでは「単語」「ランク」「音声」「語義」からなるデッキを生成したので、ノートタイプも同様に。

おすすめの辞書コンテンツ

dessed にて変換可能なもの

音声あり、中辞典。Weblioを引くと表示されるのはだいたいこれ。

大辞典。英和。

研究社 新英和大辞典第6版
ロゴヴィスタ (2007-02-17)
売り上げランキング: 432

大辞典。和英。

コーパスとして。

コーパスとして。

NEW 斎藤和英大辞典
NEW 斎藤和英大辞典
posted with amazlet at 16.05.02
ロゴヴィスタ (2013-05-24)
売り上げランキング: 11,426

Androidの「メディア」によるバッテリードレインかどうかを確認する方法

Android 6.0にアップデートしたXperia Z4くんがバッテリードレイン病にかかってしまった。とりあえず原因を探り、対処方法を見つけたのでメモしておく。

原因を調べる

まず「設定」→「バッテリー」→「電池使用量」を調べる。 f:id:ruby-U:20160502123522p:plain

この時点で明らかに「メディア」が怪しい。

もう少し詳細に調べる

PCが手元にあれば、adbを使ってtopを見ることができる。 f:id:ruby-U:20160502123738p:plain

ここで、/system/bin/sdcardとあるので、外付けSDカードが原因だ、というわけではないことに注意。これはそういう名前のサービスで、詳しくは Configuration Examples | Android Open Source Project などを参照されたし。

さらに詳細に調べる

端末がrootedなら、adbからsuしてlsofなどを叩くこともできる。詳しくは Finding open files with lsof などを参照されたし。

今回の原因

/storage/sdcard0/DCIM/.thumbnailsを消すと、android.process.mediaの暴走が止まった。思い当たることといえば、

  • 最近Googleの「フォト」アプリを更新した
  • BitTorrentSyncを使用して、DCIMフォルダをPCと同期している
  • 同期したファイルに、破損したものがあった(Lightroomへのインポート時に警告が表示された)

ぐらい。ひとまず問題があれば、怪しいフォルダを探して、PCやクラウドにバックアップした後、top | grep media を眺めながら、サクッと消してみるのがよさそう

Win8タブのコネクト スタンバイ時にBluetoothやWIFIを自動的に無効にする

Miix2くんはゲーム専用機

この間 Lenovo Miix 2 8 を買いまして、美少女紙芝居ゲーム専用機としてぼちぼち設定していたのですが、Bluetoothオーディオレシーバと接続した後、スリープにするとバッテリをものすごい勢いで消費して、全然スリープじゃなかったのでその対策を書いておきます。

接続先のBluetoothオーディオレシーバ

この光デジタル出力をアンプに接続しています。ノベルゲーで遊ぶ限りでは遅延も感じられませんし、(Miix2側の都合で)SBCでの接続になりますが、音も悪く無いです。

ゲーム用のストレージとして。64GBもあれば安心…なはず。

注意

Miix 2 8とBluetoothオーディオレシーバに関係した不具合として、スリープ後にペアリングは維持されているものの、本体側から音が出る、という症状があります。Bluetoothのオンオフで直るのですが、大変煩わしいので、必要であれば、原因である「Lenovo Bluetooth with Enhanced Data Rate Software」を削除してください。

デバイスの有効にしたり無効にしたりするにはDevConが使える

Win8.xで機内モードCUIから操作する方法を見つけられなかったので、DevConを使います。

デバイス マネージャーとして機能する DevCon コマンド ライン ユーティリティ

僕の環境でBluetoothを無効にするには

BT-down.vbs

CreateObject("WScript.Shell").Run "devcon\i386\devcon.exe disable ""BCMBTBUS\BLUETOOTH""", 0

有効にするには

BT-up.vbs

CreateObject("WScript.Shell").Run "devcon\i386\devcon.exe enable ""BCMBTBUS\BLUETOOTH""", 0

ちなみにWIFIを無効にするには

WIFI-down.vbs

CreateObject("WScript.Shell").Run "devcon\i386\devcon.exe disable ""SD\VID_02d0&PID_4324&FN_1""", 0

有効にするには

WIFI-up.vbs

CreateObject("WScript.Shell").Run "devcon\i386\devcon.exe enable ""SD\VID_02d0&PID_4324&FN_1""", 0

みたいな感じです。

タスクを登録する

タスクスケジューラーで、Kernel-Powerの506, 507イベントにスクリプトをフックします。

オンにするフック

f:id:ruby-U:20141104135351p:plain

f:id:ruby-U:20141104135353p:plain

f:id:ruby-U:20141104135352p:plain

f:id:ruby-U:20141104135354p:plain

オフにするフック

f:id:ruby-U:20141104135347p:plain

f:id:ruby-U:20141104135348p:plain

f:id:ruby-U:20141104135349p:plain

f:id:ruby-U:20141104135350p:plain

以上で、コネクト スタンバイ中に自動的にBluetoothデバイスをオフにできるようになるはずです。切断されたBluetoothデバイスは、スタンバイからの復帰後、自動的に再接続されます。

ただし、Bluetoothで何らかのデバイスを使用していると、DevConでの操作が反映されないことがあります。この例だと、Bluetoothオーディオレシーバーで音楽を再生していると、DevConでBluetoothをオフにすることができません。「変更は再起動後に反映されます」というやつです。 ゲームを終了させる→スリープにする という手順が必要になります。

うまくLow Power Activityに落ちるとバッテリ消費量は5%/dayぐらい

このぐらいのバッテリー消費量だと、AndroidタブレットiPadに近い感覚で扱えますね。

f:id:ruby-U:20141104135345p:plain

f:id:ruby-U:20141104135346p:plain

夏休みの自由研究 「女騎士」 提出しました

まえがき

ここ数年ぼちぼちと英単語の暗記に取り組んでいるのですが、暗記カード(紙)やTOEIC対策アプリでの学習に主に効率・自由度などの面で限界を感じたので、よりオープンな暗記用ソフトウェア(SRS; Spaced Repetition learning Systems)を使用することにし、Windows, Mac, Linux, Android, iOSなど主要なプラットフォームに対応し、また定番のソフトウェアでもある Anki を新しい学習ツールとして選択しました。

Ankiはインストールした状態では空っぽで、学習するデータは自分で用意する必要があります。僕が覚えるのは英語で、英語学習に使えるデータは豊富にあります。例えば

単語

語義

  • E-DIC2
  • システムソフト電子辞典(Epwing辞書に変換可能なもの)
  • Epwing辞書

などを組み合わせて「表が単語、裏が語義の暗記カード」を(論理的には)作ることが可能ですが、しかし実際にこれを簡単に行うためのツールが見つからなかったため、時間を作って作成することにしました。

要件

当初考えていたのは

  • AnkiがサポートするCSV, TSVなどを扱えること。
  • 外部のコマンドを叩いて、データを取得できること。

といったふんわりとした要件で、Ankiのデッキを作るツール(概案) のようなものを考えていたのですが、どうもイマイチな気がしていました。そこでさらに調査を進めるうちに驚くべきツールを発見しました。AWKです。非常に有名なツールで、僕もそれまで幾度となく触ったことがあったのにも関わらず、実際には全くこのツールの真の価値を知らなかったわけです。

AWKは正に上記の要件を満たす素晴らしいツールに思えましたが、いくつかの問題があり、結局使用することはできませんでした。しかしAWKのコンセプトは素晴らしいもので、これをそのまま取り入れ、多少の変更を加えたものを実装することにしました。

WOK

AWKはオークと呼ぶらしいです。2014年現在、これと対になるような単語は一つしか思い浮かびません。WOman Knight、すなわち女騎士です。

rubyu/wok

WOKはAWKPython互換のCSVモジュールと、JavaとそしてScalaの豊富なライブラリを追加したものになります。

女騎士 vs オーク

Running a script

$ wok 'print("hello!")'
$ awk 'BEGIN { print "hello!" }'

Running a program file

$ wok -f program-file input
$ awk -f program-file input

Variables

$ wok -v name=value
$ awk -v name=value

Operating fields

In { _ foreach { row => 
  println(row(0)) 
}}
{ print $1 }

Filtering data

In { _ foreach {
  case row if row exists ("pattern".r.findFirstIn(_).isDefined) => 
    println(row: _*)
  case _ =>
}}
/pattern/ { print $0 }

Printing to a file

val path = "list" !<
In { _ foreach { _ =>
  path.println(NF)
}}
{ print NF > "list" }

Executing a system command

In { _ foreach { row =>
  val res = Seq("echo", row(0)) #| Seq("grep", "angel") !>
  println(res.string)
  println(res.code)
}}

女騎士の拡張された部分

Typed variables

# The following command is equivalent to 
# var name = `value`
$ wok -v@char name=value

# The following command is equivalent to 
# var name = "value"
$ wok -v@str name=value

# The following command is equivalent to 
# var name = """value"""
$ wok -v@rawstr name=value

Quoting

// Setting Quote(mode=Min, quote='"') to Reader
FQ = Quote Min 

// Setting Quote(mode=All, quote='"', escape='\\') to Writer
OFQ = Quote All Q('"') E('\\')

Encoding

// Setting Codec to Reader
CD = Codec("UTF-8")

// Setting Codec to Writer
OCD = Codec("UTF-16")

サンプルスクリプト

rubyu/wok-scripts より

指定した列を元に、Epwingから辞書引きした列を追加する

eb-html.wok

#!/usr/bin/wok -f

/** eb-html.wok
  * 
  * Usage:
  *   java -jar wok-0.1.0.jar -f eb-html.wok -v src=0 -v dst=1 -v@rawstr dic=path_to_epwing -v@rawstr ebmap=path_to_ebmap < input.tsv > output.tsv
  *
  * Option:
  *   -v src:           the index number of the source field
  *   -v dst:           the index number of the destination field
  *   -v@rawstr dic:    path to a directory containing Epwing's CATALOGS file
  *   -v@rawstr ebmap:  path to a EBWin3/4 map file corresponds to a EPWING dictionary specified by option `dic`
  */

FS = '\t'
FQ = Quote.Min
OFS = '\t'
OFQ = Quote.All

val eb = "ebquery-0.3.1.jar"

if (eb nonExistent) 
  eb write Resource.fromURL("https://bitbucket.org/rubyu/ebquery/downloads/ebquery-0.3.1.jar").bytes

def query(s: String) = Seq("java", "-Dfile.encoding=UTF-8", "-jar", eb, "-d", dic, "-f", "html", "-m", "tx,sb,sp,ec,ls", "--ebmap", ebmap, s).!>.string

In { _ 
  .filter (_ isDefinedAt src)
  .map (_.padTo(dst+1, ""))
  .map (row => row.updated(dst, query(row(src))))
  .foreach (row => println(row: _*))
}

list1.txt

a
abacus
abalone

ここでlist1.txtを単語のリストとして、以下のコマンドを実行できます。

> java -jar wok-0.1.0.jar -f eb-html.wok -v src=0 -v dst=1 -v@rawstr dic=C:\dic\KENE7J5 -v@rawstr ebmap=C:\dic\KENE7J5.MAP list1.txt > list2.txt

list2.txt

word definition
a a1, A1 /éı/→音声 ( as, a's, As, A's ...
abucus ab・a・cus /ǽbəkəs/ ( 〜・es, ―ci...
abalone ab・a・lo・ne /æ̀bəlóʊni/ 〔貝〕 ...

指定した列に、外部の頻度順データ等でのランキング列を追加する

rank.wok

#!/usr/bin/wok -f

/** rank.wok
  * 
  * Usage:
  *   java -jar wok-0.1.0.jar -f rank.wok -v src=0 -v dst=1 -v grp=1 -v@rawstr ranking=list.txt < input.tsv > output.tsv
  *
  * Option:
  *   -v src:               the index number of the source field
  *   -v dst:               the index number of the destination field
  *   -v grp:               the unit size of grouping the rank value; this value must be larger than zero
  *   -v@rawstr ranking:    path to a ranking file
  */

FS = '\t'
FQ = Quote.Min
OFS = '\t'
OFQ = Quote.All

val rank = In.from(ranking) { _
  .zipWithIndex
  .map { case (row, i) => row(0) -> i }
  .toMap
}

In { _
  .filter (_ isDefinedAt src)
  .map (_.padTo(dst+1, ""))
  .map { row =>
    val k = row(src)
    rank.get(k) match {
      case Some(v) => row.updated(dst, s"${v / grp + 1}")
      case None => row
    }
  }
  .foreach (row => println(row: _*))
}

ranking.txt

a
aa
ab
aah
aardvark
abucus
abaca
abalone

ここでlist2.txtを単語のリストとして、以下のコマンドを実行できます。

> java -jar wok-0.1.0.jar -f rank.wok -v src=0 -v dst=2 -v grp=1 -v@rawstr ranking=ranking.txt list2.txt > list3.txt

list3.txt

word definition rank
a a1, A1 /éı/→音声 ( as, a's, As, A's ... 1
abucus ab・a・cus /ǽbəkəs/ ( 〜・es, ―ci... 6
abalone ab・a・lo・ne /æ̀bəlóʊni/ 〔貝〕 ... 8

あとがき

我々には夏休みに何かを作ろうとする習性が刷り込まれているのではないか、そしてそれは義務教育が我々にもたらす最も素晴らしいものの一つではないか、などと思ったりします。

さて、夏休みどころか、さらに数ヶ月をまるまる投じて制作した本ソフトウェアですが、起動時にScalaファイルをコンパイルするためメモリを大量に消費する、同じくコンパイルのために数秒の待ち時間が発生する、記述が冗長になるなどの様々な短所はありますが、一方で、クォートが扱える、コンパイル時に型エラーがないことが保証される、ScalaのCollectionフレームワークに乗っかれるなどの長所もあります。

英語学習用デッキを生成するためのツールとして十分な機能を備えてはいますが、CUI操作に慣れていなければ難しくもあります。このあたりは今後の課題とします。