「『出現確率1%のガチャを100回引いても,4割近くの人は全部はずれる。“本当の確率”を読み解いてみよう』を素直に解いてみる」をSqueak Smalltalkで


「1 - 100回引いてハズす確率」ではなく、各回の当たる確率を積算して算出する計算はどうなるか、というお話にからめて、最後に添えられた Ruby 版で、Smalltalk でもおなじみの inject (Smalltalk では inject:into: )が使われていたので Squeak Smalltalk でも書いてみました。

(0 to: 99) inject: 0 into: [:r :n | r+((0.99 raisedTo: n)*0.01)] "=> 0.63396765872677 "


残念ながら Squeak には raisedTo: のエイリアスとして ** が用意されてないのと(Pharo にはあるらしい)、二項メッセージ式に優先順位がないせいで括弧が増えるのがアレですが、よく似ていますね。


なお、Smalltalk でも Squeak や Pharo に限れば、APL 譲り(…とういかワナビ?)の配列計算が使えるので、ちょっと趣を変えて同じようなことをこんなふうにも書くことができます。

((0.99 raisedTo: (0 to: 99)) * 0.01) sum "=> 0.6339676587267705 "


Squeak や Pharo の raisedTo: は、引数に配列を与えれば答えを配列で返すしくみになっています( raisedTo: の返値になぜか生じる丸め誤差が見苦しいので rounded しています)。

(3 raisedTo: (0 to: 4)) rounded "=> #(1 3 9 27 81) "


ではメッセージ raisedTo: 〜 のレシーバーが配列なら、配列の配列が返るかというとそうはならず、対応した各要素について累乗値が返ってきます。

#(3 4 5) raisedTo: #(0 1 2) "=> #(1 4 25) "


したがって、レシーバーと raisedTo: の引数の配列のサイズが違うとエラーになるので要注意です。

#(3 4 5 6) raisedTo: #(0 1 2) "=> Error: otherCollection must be the same size "


余談ですが、整数の累乗なのに配列だと Float に変換されてしまう謎も含め、なぜこのような振る舞いになるかというのは、Number>>#raisedTo: の定義をみると分かります。

Number >> raisedTo: aNumber 
"Answer the receiver raised to aNumber."

aNumber isInteger ifTrue: [
"Do the special case of integer power"
^ self raisedToInteger: aNumber].
aNumber isFraction ifTrue: [
"Special case for fraction power"
^ (self nthRoot: aNumber denominator) raisedToInteger: aNumber numerator ].
self < 0 ifTrue: [
^ ArithmeticError signal: 'Negative numbers can''t be raised to float powers.' ].
0 = aNumber ifTrue: [^ self class one]. "Special case of exponent=0"
1 = aNumber ifTrue: [^ self]. "Special case of exponent=1"
0 = self ifTrue: [ "Special case of self = 0"
aNumber < 0
ifTrue: [^ (ZeroDivide dividend: self) signal]
ifFalse: [^ self]].
^ (aNumber * self ln) exp "Otherwise use logarithms"


なお、レシーバーが配列の場合は、まず Collection>>#raisedTo: が呼ばれるので、レシーバーが整数の場合とは振る舞いが異なってきます。

Collection >> raisedTo: arg
^ arg adaptToCollection: self andSend: #raisedTo:
Collection >> adaptToCollection: rcvr andSend: selector
"If I am involved in arithmetic with another Collection, return a Collection of
the results of each element combined with the scalar in that expression."


rcvr isSequenceable & self isSequenceable ifFalse:
[self error: 'Only sequenceable collections may be combined arithmetically'].
^ rcvr with: self collect:
[:rcvrElement :myElement | rcvrElement perform: selector with: myElement]

平和な動物園を作ろう!をインスタンス特異的メソッドを用いてSqueak Smalltalkで


http://echo.2ch.net/test/read.cgi/tech/1444216746/361 経由で、

あなたは,さいたま動物園の園長に選ばれました.さいたま動物園には全部で10種類の動物たちがいます.あなたの園長としての初仕事は,これらの動物たちをどのオリに入れるかを決めることになりました.

さて,ここで問題なのは,

・動物たちには相性の良し悪しがある.
・相性の悪い動物たちをお互いに近いオリにいれると,みんなが暴れだしてしまう.
・動物たちの不満度が小さくなるようにオリを選んでやる必要がある.

ということです.


動物たちの不満度は,

 (各オリの間の距離) × (各動物の間の相性)の総和

で表されます.


さぁ,地図に示されたオリに動物たちをうまく割り当てて,動物たちの不満度が小さい平和な動物園を作ってください.

平和な動物園を作ろう! ―2次割当て問題って何?― 埼玉大学工学部情報システム工学科池口研究室


手抜きをすべく、Matrix で行あるいは列単位で permutationsDo: を使いたかったのですが、そもそも Matrix は SequenceableCollection のサブクラスではなかったので permutationsDo: は端から使えないことが発覚( permutationsDo: は SequenceableCollection に定義されている。為念)。そこで、配列の配列を使うことにしました。

ただし素朴にデータだけからなる配列の配列では、並べ替えた際に動物との対応が面倒になるので、key に動物名、value に配列を持たせた Association を要素にしました。

animals := {
   'ライオン' -> #(0 2 6 4 6 2 4 4 2 4).
   'ワニ' -> #(2 0 4 2 2 2 2 2 2 6).
   'ニシキヘビ' -> #(6 4 0 2 6 8 8 6 4 8).
   'オオカミ' -> #(4 2 2 0 4 2 6 6 2 6).
   'トラ' -> #(6 2 6 4 0 2 4 4 2 4).
   'スイギュウ' -> #(2 2 8 2 2 0 6 6 6 8).
   'サイ' -> #(4 2 8 6 4 6 0 6 6 4).
   'カバ' -> #(4 2 8 6 4 6 6 0 6 6).
   'インパラ' -> #(2 2 4 2 2 6 6 6 0 6).
   'ゾウ' -> #(4 6 8 6 4 8 4 6 6 0)}.


こうしておけば動物名も一緒にスワップできるので何かと便利で一件落着…かと思いきや、動物をスワップしたら、その動物との相性を記したデータの対応する位置の要素も連動してスワップさせないといけません。

うーむ、やはり permutationsDo: 相当を書くしかないのかな…と諦めかけたのですが、それだとなんか負けた気(謎)がします。


あらためて SequenceableCollection>>#permutationsDo: 内の処理を眺めてみると、size と swap:with: しか使われていないことが分かります。

SequenceableCollection >> permutationsDo: aBlock
"Repeatly value aBlock with a single copy of the receiver. Reorder the copy
so that aBlock is presented all (self size factorial) possible permutations."

"(1 to: 4) permutationsDo: [:each | Transcript cr; show: each printString]"

self shallowCopy permutationsStartingAt: 1 do: aBlock
SequenceableCollection >> permutationsStartingAt: anInteger do: aBlock
"#(1 2 3 4) permutationsDo: [:each | Transcript cr; show: each printString]"

anInteger > self size ifTrue: [^self].
anInteger = self size ifTrue: [^aBlock value: self].
anInteger to: self size do:
[:i | self swap: anInteger with: i.
self permutationsStartingAt: anInteger + 1 do: aBlock.
self swap: anInteger with: i]


つまり、animals に対して swap:with: で前述の処理(行・列要素のスワップの連動)を行なうようなんとか多態させることさえできれば、permutationsDo: を使って手を抜くという目的は果たせそうです。

とはいえ animals を permutationsDo: するためだけに swap:with: を書き換えてしまうのは、何か違う気がするので(というか、ダメ。ゼッタイ。w)、assureUniClass してインスタンス特異的クラスを作成し、インスタンス特異的メソッドとして swap:with: を再定義することにしました。


…というような腑抜けた方針で書いたのが、このコードです。


| animals cages ans |

animals := {
   'ライオン' -> #(0 2 6 4 6 2 4 4 2 4).
   'ワニ' -> #(2 0 4 2 2 2 2 2 2 6).
   'ニシキヘビ' -> #(6 4 0 2 6 8 8 6 4 8).
   'オオカミ' -> #(4 2 2 0 4 2 6 6 2 6).
   'トラ' -> #(6 2 6 4 0 2 4 4 2 4).
   'スイギュウ' -> #(2 2 8 2 2 0 6 6 6 8).
   'サイ' -> #(4 2 8 6 4 6 0 6 6 4).
   'カバ' -> #(4 2 8 6 4 6 6 0 6 6).
   'インパラ' -> #(2 2 4 2 2 6 6 6 0 6).
   'ゾウ' -> #(4 6 8 6 4 8 4 6 6 0)}.

cages := #(
   (0 3 4 5 8 10 9 6 2 4)
   (3 0 4 4 7 9 9 8 5 9)
   (4 4 0 2 4 7 5 4 4 8)
   (5 4 2 0 3 5 5 5 5 9)
   (8 7 4 3 0 3 5 6 8 12)
   (10 9 7 5 3 0 4 7 10 14)
   (9 9 5 5 5 4 0 3 8 11)
   (6 8 4 5 6 7 3 0 5 8)
   (2 5 4 5 8 10 8 5 0 4)
   (4 9 8 9 12 14 11 8 4 0)).

ans := Set new -> Float infinity.
animals assureUniClass class compile: 'swap: i with: j
   super swap: i with: j.
   self do: [:each | each value swap: i with: j]'.
animals permutationsDo: [:perm |
   | keys values sum |
   keys := perm collect: #key. "keys asString displayAt: 20@20."
   values := perm collect: #value.
   sum := (values * cages) sum sum.
   ans value = sum ifTrue: [ans key add: keys].
   ans value > sum ifTrue: [ans := (Set with: keys) -> sum]].
^ans

"=> a Set(
   an Array1('スイギュウ' 'インパラ' 'ニシキヘビ' 'カバ' 'サイ' 'オオカミ' 'トラ' 'ライオン' 'ゾウ' 'ワニ')
   an Array1('スイギュウ' 'インパラ' 'ニシキヘビ' 'カバ' 'サイ' 'オオカミ' 'ライオン' 'トラ' 'ゾウ' 'ワニ')
)->2160

その後よく考えたら、素直に書いた方がシンプルだし速かったでござるの巻。あと、パラメーターのコピペミスがあったので、結果と共に差し替えました。orz

| animals cages ans |

animals := #(
   (0 2 6 4 6 2 4 4 2 4)
   (2 0 4 2 2 2 2 2 2 6)
   (6 4 0 2 6 8 8 6 4 8)
   (4 2 2 0 4 2 6 6 2 6)
   (6 2 6 4 0 2 4 4 2 4)
   (2 2 8 2 2 0 6 6 6 8)
   (4 2 8 6 4 6 0 6 6 4)
   (4 2 8 6 4 6 6 0 6 6)
   (2 2 4 2 2 6 6 6 0 6)
   (4 6 8 6 4 8 4 6 6 0)).

cages := #(
   (0 3 4 5 8 10 9 6 2 4)
   (3 0 4 4 7 9 9 8 5 9)
   (4 4 0 2 4 7 5 4 4 8)
   (5 4 2 0 3 5 5 5 5 9)
   (8 7 4 3 0 3 5 6 8 12)
   (10 9 7 5 3 0 4 7 10 14)
   (9 9 5 5 5 4 0 3 8 11)
   (6 8 4 5 6 7 3 0 5 8)
   (2 5 4 5 8 10 8 5 0 4)
   (4 9 8 9 12 14 11 8 4 0)).

ans := Set new -> Float infinity.
(1 to: animals size) permutationsDo: [:perm |
   | sum |
   sum := 0.
   perm doWithIndex: [:pi :i |
      perm doWithIndex: [:pj :j |
         sum := ((animals at: pi) at: pj) * ((cages at: i) at: j) + sum]].
   ans value = sum ifTrue: [ans key add: perm copy].
   ans value > sum ifTrue: [ans := (Set with: perm copy) -> sum]].
ans

"=> a Set(#(6 9 3 8 7 4 5 1 10 2) #(6 9 3 8 7 4 1 5 10 2))->2160 "

さらに追記

なんと出題の動物の相性のデータにも対称になっていないという誤りがあったみたいで、

| animals |
animals := #(
   (0 2 6 4 6 2 4 4 2 4)
   (2 0 4 2 2 2 2 2 2 6)
   (6 4 0 2 6 8 8 6 4 8)
   (4 2 2 0 4 2 6 6 2 6)
   (6 2 6 4 0 2 4 4 2 4)
   (2 2 8 2 2 0 6 6 6 8)
   (4 2 8 6 4 6 0 6 6 4)
   (4 2 8 6 4 6 6 0 6 6)
   (2 2 4 2 2 6 6 6 0 6)
   (4 6 8 6 4 8 4 6 6 0)).

animals - ((1 to: animals size) collect: [:idx | animals collect: [:each | each at: idx]])
=> #(
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 -2 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 2 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0)
   (0 0 0 0 0 0 0 0 0 0))


対称となっていない相性の値がそれぞれ 8 の場合と 6 の場合で計算し直すと、結果は次のようになりました。

8 => a Set(
   #(6 9 3 8 7 4 5 1 10 2)
   #(6 9 3 8 7 4 1 5 10 2)
)->2164
6 => a Set(
   #(6 9 3 7 1 5 4 8 10 2)
   #(6 9 3 7 5 1 4 8 10 2)
   #(6 9 3 8 7 4 1 5 10 2)
   #(6 9 3 8 7 4 5 1 10 2)
   #(6 9 7 8 4 1 5 3 10 2)
   #(6 9 7 8 4 5 1 3 10 2)
   #(6 9 8 7 4 1 5 3 10 2)
   #(6 9 8 7 4 5 1 3 10 2)
)->2156

Smalltalk-76(-78)のArrayのAPIが思ったよりSmalltalk-80と違っていて面白かったので、気付いた範囲でいくつか拾ってみた


x .

"at:put:, yourself" ↪(1 2 3) ◦ 2 ← 100; itself "⇒ (1 100 3 ) ".
"atLast:put:" ↪(1 2 3) last ← 100; itself "⇒ (1 2 100 ) ".
"replaceFrom:to:with:" ↪(1 2 3 4 5 6) ◦ (2 to: 3) ← ↪(20 30); itself "⇒ (1 20 30 4 5 6 ) ".
"size" (1 to: 10) length "⇒ 10 ".
"includes:" (1 to: 10) has: 5 "⇒ true ".
"collect:" (1 to: 10) transform▹ x to▹ [x * 2] "⇒ (2 4 6 8 10 12 14 16 18 20 ) ".
"select:" (1 to: 10) all▹ x suchThat▹ [x \ 2 ≡ 0] "⇒ (2 4 6 8 10 ) ".
"detect:" (1 to: 10) find▹ x suchThat▹ [x > 3] "⇒ 4 ".
"inject: 0 into: #+" (1 to: 10) sumTo: 100 "⇒ 155 ".
"asBag sortedCounts" ↪(1 1 2 3 3 3 4 5 5) frequencies "⇒ ((1 4 ) (1 2 ) (2 1 ) (2 5 ) (3 3 ) ) ".

http://squab.no-ip.com/collab/uploads/st80-vs-st76.png

“▹”は白抜きの“:”(オープンコロン。件のエミュレーターでは : を二回タイプして入力できる。ちなみに“◦”は . を二回、↪ は # )。上のテキストを Smalltalk-78 のワークスペース(デスクトップ右クリック → open workspace )などにペーストして使用可能です( ctrl + v 。ホストOS とブラウザの組み合わせによって機能しないことがあります。当方は Win8.1 + Chrome を使用)。変数 x を使用する式は冒頭の | x. から評価してみたい行の最後のピリオドの手前まで(つまり、最後のピリオドを選択範囲に含めない)、変数 x は特に使用しない式なら行の頭からやはりピリオドの手前まで選択して右クリック → doit すると実行、結果の表示ができます。

Ruby にも同一メソッドの別名の一方として採用されている #collect: 、#select: 、#detect: 、#inject:into: などのいわゆる 〜ect系は Smalltalk-76(-78) にはまだなくて、比較的のちの Smalltalk-80 になって取り入れられたものなのですね。


こういう面白い機能も見つけました。UserView の同名メソッドを介して、ウインドウのアクティベート等に使われたようです。のちの MVC の ControlManager では OrderedCollection で remove: してから addFirst: するイデオムに置き換えられていました。


↪(1 2 3 4 5) promote: 3 "⇒ (3 1 2 4 5 ) ".


Smalltalk-72、-76、-80 という三世代の Smalltalk の文法の変化も面白いですが、比較的よく似ている -76(-78) から -80 への API の変化を追いかけるのも楽しそうですね。



▼おまけ

Smalltalk-72
https://pbs.twimg.com/media/Cb5th-0UYAAosCT.png


Smalltalk-76(-78)
https://pbs.twimg.com/media/Cb5tiCWUcAI9jVp.png


Smalltalk-80
https://pbs.twimg.com/media/CcBaiNkUcAAwF-f.png

Squeak5.0で日本語表示をするシリーズ: とりあえずJapaneseEnvironmentを直してStrikeFontで日本語を表示できるようにする

おそらくここらへんは将来的にはなくなる予定の仕組みなのかもしれないのですが、とりあえず淡々と直していきましょう。いったん TrueType のことは忘れて StrikeFont(ビットマップフォント)で日本語を表示できるようにします。


まず最初に、既定のフォントをインストールします。(要ネット接続)

(Locale isoLanguage: 'ja') languageEnvironment installFont


うまくインストールできれば、表示フォントに Accuny あるいは DefaultMultiFont を指定した状態で次のコードを print it すると、「あ」が表示されるはずです。

Character leadingChar: JapaneseEnvironment leadingChar code: 12354 

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix01.png


念のため、Accunyフォントの fontArray を調べるとサイズが 6(JapaneseEnvironment leadingChar + 1)に増えて、その 6 番目めに日本語フォントが新しく入っているのが確認できます。

(TextStyle named: 'Accuny') defaultFont fontArray
"=> {a StrikeFont(Accuny11 12) . nil . nil . nil . nil . a StrikeFont(Japanese10 12)} "


ここで、なぜか無くなっている StrikeFontSet>>#copy を追加しておきます。

| urlStr |
urlStr := 'http://squeak-ja.sourceforge.jp/patches/PatchesJa20111005-4.2.sar'.
SARInstaller new fileInFrom: urlStr asUrl retrieveContents contents asByteArray readStream


以下のスクリプトでデフォルトのフォントを日本語表示可能な Accuny に変えることができます。

| style normalFont windows |
style := TextStyle named: #DefaultMultiStyle.
style defaultFontIndex: 3.
normalFont := style defaultFont.
smallFont := style fontAt: style defaultFontIndex - 2.
Preferences class selectors
   select: [:sel | (sel beginsWith: 'set') and: [sel endsWith: 'FontTo:']]
   thenDo: [:sel | Preferences perform: sel with: normalFont].
Preferences setPaintBoxButtonFontTo: smallFont.
Preferences setBalloonHelpFontTo: smallFont.
BalloonMorph setBalloonFontTo: smallFont.
windows := SystemWindow
   windowsIn: ActiveWorld
   satisfying: [:sw | sw model isKindOf: Workspace].
windows do: [:ws |
   (ws findA: PluggableTextMorph) ifNotNil: [:morph |  morph font: normalFont]]

次に Locale を日本語に変更して、IME からの入力やコピー&ペースト、ファイルの入出力に適切なエンコーディングが使われるようにするしくみ使えるようにします。

Locale currentPlatform: (Locale isoLanguage: 'ja')


しかし、これをするとととたんにいろいろとおかしなことが起こり始めるので、わかる範囲で直していきましょう。コピー&ペーストなどもできなくなることがあるので、適宜 Locale currentPlatform: (Locale isoLanguage: 'en') で元にもどしながら操作するのがよいと思います。なお、以下は Windows 環境での作業ですので、他の OS 向けには適宜読み替えて(あるいは試行錯誤してみて)ください。


▼ LanguageEnvironment class>>#isAlphaNumeric: の追加

この状態で IME から「あ」などと入力してみて最初に気づくのは MessageNotUnderstood: JapaneseEnvironment class>>isAlphaNumeric: エラーです。

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix02.png


とりあえず、JapaneseEnvironment class のスーパークラスの LangageEnvironment class に、すでにある #is〜: メソッド群を参考に self charsetClass に委譲する #isAlphaNumeric: メソッドを新しく追加します。

どのように定義してもよいのですが、ここでは次のようにします。まず、 #isDigit: を implementors of it (alt/cmd + m) するなどして定義を呼び出します。

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix03.png


ここで最初の Digit を選択して AlphaNumeric をタイプするなどして置き換え、続けて alt/cmd + j (again) して次のも同様に置き換えます。

http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix04.png
http://squab.no-ip.com/collab/uploads/Sq5JapaneseEnvFix05.png


置き換えたら accept (alt/cmd + s) します。すると、#isAlphaNumeric: の定義はいったん消えて元の #isDigit: の定義に戻ってしまいますが、これは当該ウインドウが #isDigit: の implementors of it で開かれたからで、LanguageEnvironment を brorse it (alt/cmd + b) してブラウザをクラスメソッド一覧に切り替えるか、isAlphaNumric: についてあらためて implementors of it (alt/cmd + m) してやれば、#isAlphaNumric: がきちんと定義されているのが確認できるはずです。

これで、もしノーティファイアがそのままなら Proceed して消せますし、以後、IME 経由での日本語の入力時に同様のノーティファイアが現われることもないはずです。


▼スクロールホイールの操作が反応しなくなっているのを直す

手元の Win環境では、Locale を ja に切り替えると、ブラウザなどでスクロールをしようとしてスクロールホイールを回しても、ペインの中がフラッシュするだけでうまく動作しないようです。これは JapaneseEnvrironment class>>#inputInterpreterClass で Win 向けなどに指定されている UTF32JPInputInterpreter で、スクロールホイールのイベントうまく取得できていないのが原因のようです。そこで、次の赤字の部分を付け足して accept (alt/cmd + s) します。

UTF32JPInputInterpreter >> nextCharFrom: sensor firstEvt: evtBuf
    | keyValue mark |
    keyValue := evtBuf at: 6.
    keyValue = 0
        ifTrue: [keyValue := evtBuf at: 3].
    mark := self japaneseSpecialMark: keyValue.
    mark notNil
        ifTrue: [^ mark].
    keyValue < 256
        ifTrue: [^ (Character value: keyValue) squeakToIso].
    ^ Character leadingChar: JapaneseEnvironment leadingChar code: keyValue


▼コピー&ペーストがうまくいかないのを直す

JapaneseEnvironment >> clipboardInterpreterClass
    | platformName osVersion |
    platformName := Smalltalk platformName.
    osVersion := Smalltalk osVersion.
    (platformName = 'Win32'
            and: [osVersion = 'CE'])
        ifTrue: [^ NoConversionClipboardInterpreter].
    platformName = 'Win32'
        ifTrue: [^ UTF8ClipboardInterpreter].
        ifTrue: [^ WinShiftJISClipboardInterpreter].
    platformName = 'Mac OS'
        ifTrue: [^ MacShiftJISClipboardInterpreter].
    ^ platformName = 'unix'
        ifTrue: [(ShiftJISTextConverter encodingNames includes: X11Encoding getEncoding)
                ifTrue: [MacShiftJISClipboardInterpreter]
                ifFalse: [UnixJPClipboardInterpreter]]
        ifFalse: [NoConversionClipboardInterpreter]


この修正のあと、JapaneseEnvironment clearDefault してあげます。


▼ファイルの出力エンコードを UTF8 にする

JapaneseEnvironment >> systemConverterClass
    | platformName osVersion encoding |
    platformName := Smalltalk platformName.
    osVersion := Smalltalk osVersion.
    platformName = 'Win32'
    (platformName = 'Win32'
            and: [osVersion = 'CE'])
        ifTrue: [^ UTF8TextConverter].
    platformName = 'ZaurusOS'
    (#('Win32' 'ZaurusOS' ) includes: platformName)
        ifTrue: [^ ShiftJISTextConverter].
    platformName = 'Mac OS'
        ifTrue: [^ ('10*' match: osVersion)
                ifTrue: [UTF8TextConverter]
                ifFalse: [ShiftJISTextConverter]].
    platformName = 'unix'
        ifTrue: [encoding := X11Encoding encoding.
            encoding
                ifNil: [^ EUCJPTextConverter].
            encoding = 'utf-8'
                ifTrue: [^ UTF8TextConverter].
            (encoding = 'shiftjis'
                    or: [encoding = 'sjis'])
                ifTrue: [^ ShiftJISTextConverter].
            ^ EUCJPTextConverter].
    ^ MacRomanTextConverter

やはり修正の後、JapaneseEnvironment clearDefault してあげます。


あと、個人的な好みですが、(FileDirectory oldFileNamed: 'text.txt') edit を do it (alt/cmd + d) して FileList をエディタモードで開いたときなどに便利なように、拡張子が .txt ならデフォルトで UTF8 で開くようにも細工してしまいます。

FileList >> defaultEncoderFor: aFileName
    "This method just illustrates the stupidest possible implementation of
    encoder selection."
    | l |
    l := aFileName asLowercase.
    "((l endsWith: FileStream multiCs) or: [
    l endsWith: FileStream multiSt]) ifTrue: [
    ^ UTF8TextConverter new.
    ]
    "
    ((l endsWith: FileStream cs)
            or: [l endsWith: FileStream st])
        ifTrue: [^ MacRomanTextConverter new].
    (l endsWith: '.txt')
        ifTrue: [^ UTF8TextConverter new].
    ^ Latin1TextConverter new



こうやっていろいろと日本語の入出力が可能になるように直しているうちに、最初に適用した StrikeFontSet>>#copy を含め、umejavaさんのパッチ群に同じものがあることに今更ですがようやく気がついてきたので、まずはこのパッチ群をそのまま使わせていただいて、それを基に手を加えていった方がよさそうな気がしてきました。ということで、いつになるかわかりませんが次回は、試しに4.4J向けのパッチ群を5.0に導入したらどうなるかトライしてみようかと思います。

Squeak5.0で日本語表示をするシリーズ: (寄り道編)ワークスペース内容をHTMLで保存する機能追加

Squeak5.0の日本語フォント表示周りをぼちぼち直していこうかと件のTTなんたらクラスの動きをちまちま調べている途中、ワークスペースにメソッド内容などをコピペしてメモを作ったりしているうちに、シンタックスハイライトできれいに色づけされた文字属性も保持しつつ保存したくなってきたので、思い切ってそういう機能を追加することにしました。そこで、トリビアルなテクニックなどをご紹介しつつ、作業内容を記したいと思います。


もともとワークスペースの内容は、ウインドウメニュー(以前はマウスの第三ボタンでしたが、今はウインドウタイトルバー右手に設置された青いメニューボタンでプルダウン)から save contents to file... を選択したり、黄ボタン(通常は右クリック)メニューからも more... 経由でポップアップできるシフト黄ボタン(同、右クリック)メニューから同名メニュー項目を選択する(ワークスペースを含む TextMorphForEditView なら可能)ことで保存自体はできるようになっています。

http://squab.no-ip.com/collab/uploads/saveContentsInFile01.png


ちなみにワークスペースでは、内容の保存のつもりで accept (alt/cmd + s) しても、これはビューなど UI コンポーネントでの編集内容がモデル(a Workspace)に反映されるだけで、ファイルへの保存がされているわけではないという点に注意を要します。やっかいなことに、こうしてモデルにビューの内容が反映されて一致した状態になってしまうと、両者に不一致がある状態のままインドウを閉じようとした場合に出る警告(Changes have not been saved. Is it OK to cancel those changes?)が出なくなり、当該ウインドウを容易に閉じてしまうことができるというワナももれなくついてくるのでこれまた要注意です。(もっとも、うっかり閉じてしまったとしても、ガベコレされる前なら、Workspace allInstances を inspect it (alt/cmd + i) して、閉じてしまったと思われるワークスペースのモデルをその内容(contents)からなんとか探し、それに openLabel: 'Workspace' などのメッセージを送ってやれば復活は可能です。念のため。)


さて、ワークスペースの内容の保存に話を元に戻すと、たしかに save contents to file... でファイルとして保存することは可能なのですが、保存されるのは文字列のみで、カラー情報などは抜け落ちてしまいます。そこで、保存時の拡張子を .html にしたときにカラー情報などを保持したまま HTML ファイルとしてはき出す細工をします。


まず、メニューから save contents to file... を選んだときにコールされるメソッド(当該機能の実体)を探します。本来であれば StringHolder(ワークスペースの実体)のソースを読むべきなのですが、操作のトリガーとなるメニュー項目をとっ捕まえて訊ねるのが一番手っ取り早いので、ここではそうします。

適当なワークスペースのウインドウメニューかシフト黄ボタンメニューをポップアップさせて、alt/cmd + シフトキーを押しながら当該メニュー項目をクリックしてモーフ(オブジェクト)として選択します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile02.png


他のモーフ同様、メニュー項目オブジェクト(a MenuItemMorph)も選択すると、ハロー(小さな丸いボタン)に囲まれるので、右手にある灰色のデバッグハローをシフトボタンを押しながらクリック(あるいはただクリックしてポップアップするメニューから inspect morph )して選択されたメニュー項目のインスペクターを呼び出します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile03.png


左側のペインからインスタンス変数である selector をクリックして内容を確認すると、#saveContentsInFile であることがわかります。“#”を含めずに save〜以下を選択して implementors of it (alt/cmd + m) すると、同名メソッドの一覧を呼び出せるので、その中から関連がありそうな TextEditor>>#saveContentsInFile をクリックして選択してそのコードを呼び出します。

http://squab.no-ip.com/collab/uploads/saveContentsInFile04.png


最後のところを下に示す青字の部分を赤字のようにちょっと手直しして置き換え、accept (alt/cmd + s) します。いつもどおり、初回の accept でイニシャルを求められたら教えてあげてください。

TextEditor >> saveContentsInFile
    "Save the receiver's contents string to a file, prompting the user for a
    file-name. Suggest a reasonable file-name."
    | fileName stringToSave parentWindow labelToUse suggestedName |
    stringToSave := paragraph text string.
    stringToSave size = 0
        ifTrue: [^ self inform: 'nothing to save.'].
    parentWindow := model dependents
                detect: [:dep | dep isKindOf: SystemWindow]
                ifNone: [].
    labelToUse := parentWindow
                ifNil: ['Untitled']
                ifNotNil: [parentWindow label].
    suggestedName := nil.
    #(#('Decompressed contents of: ' '.gz' ) )
        do: [:leaderTrailer | | lastIndex | "can add more here..."
            (labelToUse beginsWith: leaderTrailer first)
                ifTrue: [suggestedName := labelToUse copyFrom: leaderTrailer first size + 1 to: labelToUse size.
                    (labelToUse endsWith: leaderTrailer last)
                        ifTrue: [suggestedName := suggestedName copyFrom: 1 to: suggestedName size - leaderTrailer last size]
                        ifFalse: [lastIndex := suggestedName
                                        lastIndexOf: $.
                                        ifAbsent: [0].
                            (lastIndex = 0
                                    or: [lastIndex = 1])
                                ifFalse: [suggestedName := suggestedName copyFrom: 1 to: lastIndex - 1]]]].
    suggestedName
        ifNil: [suggestedName := labelToUse , '.text'].
    fileName := UIManager default request: 'File name?' initialAnswer: suggestedName.
    fileName isEmptyOrNil
        ifFalse: [(fileName endsWith: '.html')
                ifTrue: [FileStream
                        newFileNamed: fileName
                        do: [:file | paragraph text printHtmlOn: file]]
                ifFalse: [FileStream
                        newFileNamed: fileName
                        do: [:file | file nextPutAll: stringToSave]]]
        ifFalse: [(FileStream newFileNamed: fileName) nextPutAll: stringToSave;
                 close]


これで #saveContentsInFile で色情報などを保ったまま HTML での保存が可能になります。上のは実は TextEditor>>>#saveContentsInFile の versions の diff(下のペインの内容)を当該メソッドを用いて HTML保存し、それをコピペしたものです。と、書いていて気がついたのですが打ち消し線とか抜け落ちてしまうのですね。残念。


さて、スタイルを保ったまま保存はできましたが、これを読み込んでワークスペースとして表示できないとうれしさは半減です。そこで、ファイルリストをちょっといじって読み込みもできるようにしましょう。

Squeak 組み込みのファイラであるファイルリスト(Tools メニュー → File List などで呼び出し可能)には、ファイル名のリストにあるファイル(たとえば .html ファイル)を右クリックして選択すると同時に黄ボタンメニューを呼び出すとそこに「workspace with contents」という項目が見つかります。

http://squab.no-ip.com/collab/uploads/saveContentsInFile05.png


これを選ぶと選択したテキストファイルの内容を新しいワークスペースとして開けます。ただ、この機能で注意しないといけないのは、ワークスペースとして表示されたのはあくまでファイルの中身のコピーであり、これを編集して accept したからといって、元ファイルに変更が保存されるわけではない、ということです。なお、ファイルの中身を編集して更新したければ、ファイルリストの下のペインか、spawn (alt/cmd + o) して1ペインのエディタスタイルのウインドウを開いて編集し、accept → overwrite that file する必要があります。


話を戻して、workspace with contents ですが、先の方法でこのメニュー項目のインスペクターを開いて selector を見ても #performServiceFor: とあるだけで、今回は残念ながら機能の実体のメソッド名らしき情報を得ることができません。こういう場合は arguments とか target も順に見ていくとよいです。すると target に SimpleServiceEntry: (a FileList --- viewContentsInWorkspace) とあり、セレクター(メソッド名の実体のシンボル)ではありませんが、それっぽい viewContentsInWorkspace というメソッド名らしき情報を得ることができます。

http://squab.no-ip.com/collab/uploads/saveContentsInFile06.png


試しに、選択して implementors of it すると、はたして興味の対象であるファイルの内容をワークスペースとして表示する機能の実体である FileList>>#viewContentsInWorkspace のコードを呼び出すことができます。

http://squab.no-ip.com/collab/uploads/saveContentsInFile07.png


これを次のように編集して accept してコンパイルします。

FileList >> viewContentsInWorkspace
    
"View the contents of my selected file in a new workspace"
    
    
| aStringOrText aName |
    
directory readOnlyFileNamed: self fullName do: [:file |
        
file setConverterForCode.
        
aStringOrText := (file localName endsWith: '.html') ifFalse: [
            
file contentsOfEntireFile
        
] ifTrue: [
            
(HtmlReadWriter on: file) nextText
        
].
        
aName := file localName.
    
].
    
UIManager default edit: aStringOrText withSqueakLineEndings label: 'Workspace from ', aName


この後、ファイルリストで .html を選択し workspace with contents すると、元のカラー属性などを保ったままワークスペースに内容を復元できるはずです。

Squeak5.0で日本語表示をするシリーズ: FontImporterTool を使う


ご多分に漏れず1バイト文字圏に自由気ままに振る舞われているのと、将来的には EncodingTag/leadingChar とかの仕組みが変わるような話も耳にして、5.0 までになった Squeak ではもはや日本語表示は無理なのかなぁ…4.3J とか 4.4J を使い続けるかなぁ…と、はなからあきらめて試してすらいなかったのですが、id:phaendal さんが http://phaendal.hatenablog.com/entry/2015/11/08/024055 の方法であっさり日本語表示できたの目の当たりにして、改心してちょっと調べてみました。


いろいろと眺めてみたところ、破壊されているのは確かですが、想像していたような壊滅的状況というわけではなく、根気強く直していけばなんとかなりそうな印象でした。しかし、それにつけても TrueType の表示に関わる TTなんちゃらクラス群は似たような名前のクラスが入り交じっていて、TrueType フォント周りはまったく無知な私が自力でなんとかするにしても、状況を把握する段階でかなり時間がかかりそうなので、とりあえず日本語を表示して遊びたいという向けの暫定的情報として FontImporterTool というのを見つけたのでこれだけ書いておきます。


このツールは Appsメニュー → Font Importer 等、あるいは、FontImporterTool open を do it (alt/cmd + d) することで起動できます。その名の通り、TrueType フォントをシステムにリンク、もしくはグリフデータごと読み込んでイメージ(環境)への組込みが可能な機構を持っているようです。

http://squab.no-ip.com/collab/uploads/FontImporterTool01.png


ただ相変わらずの Squeak クオリティで、デフォではいろいろと壊れているのでちょこちょこ直さないとうまく動きません。たとえば現状では、左側で選択したフォントが右側のプレビューで表示されないというなんだかなぁ…なバグがあります。

http://squab.no-ip.com/collab/uploads/FontImporterTool02.png


TrueType フォントファイル(.ttf、.ttc)によっては日本語フォントを提供していても、今の Squeak5.0 では日本語の表示ができないものもあるのでこれは不便です。そこで、とりあえず、FontImportTool>>#currentSelection: をさくっと一カ所修正(#contents を #previewText に変えて accept (alt/cmd + s)。イニシャルを求められたら適当に入れてあげてください)すれば、期待された動作をするようになるはずです。


http://squab.no-ip.com/collab/uploads/FontImporterTool03.png

http://squab.no-ip.com/collab/uploads/FontImporterTool04.png


これで選択したフォントの表示例が(なんとか)出るようになるので、この欄に 'あ' などと入力すれば選択したフォントで日本語表示が可能かどうか手軽に判断できるようになります。

http://squab.no-ip.com/collab/uploads/FontImporterTool05.png


フォントが決まったらインポートなのですが、ここで注意しないといけないのは、日本語表示用のフォントを下のボタンで Import しても表示できないということです。とんでもないワナですね。^^; Import ボタンを押したくなるのをぐっとこらえて、左側の枠の表示できるようにしたいフォント名のところで黄ボタン(通常は右ボタン)クリックし、 Link Font を選ぶ必要があります。

http://squab.no-ip.com/collab/uploads/FontImporterTool06.png


これで、ワークスペースなどの別ウインドウでもフォント選択ツール(FontChooserTool。alt/cmd + k)でフォントを選択すれば表示が可能になります。

http://squab.no-ip.com/collab/uploads/FontImporterTool07.png


システムフォントに設定するなどすれば日本語文字列の表示が可能になるはずです。手元の Windows では日本語入力も IME 経由で可能そうですが、他の OS ではいろいろといじる必要があるかもしれません。他にも、日本語フォントは MultiTTCFont でインポートされなければならないのに TTCFont になっているとか(したがって cache を再構築したりすると死にます)、もろもろの不都合があるので仕組みを勉強しつつ、暇をみて直していければと思っています。

“サイコロの展開図”問題を正規表現がデフォで使える Pharo Smalltalk で(正規表現を使わずに Squeak でも)


Pharo では正規表現が使えるとのことなので、ちょうどそれ向きのお題ということもあり、久しぶりに Pharo で、Squeak 向けのコードの動作確認の範疇をこえて、書き下ろしていろいろ遊んでみました。

| patterns solveDiceDev |

patterns := #(
     '12S/453'
     '1T6/D45'
     '146/53D'
     '15S/3D4'
     '215/T64'
     '2S5/41T'
) inject: OrderedCollection new into: [:sum :pat |
     | alt |
     alt := (pat last: 3), '/', (pat first: 3).
     sum addAll: {pat. pat reversed. alt. alt reversed}; yourself
].

solveDiceDev := [:probStr |
   | boxes regex found |
   boxes := probStr allRegexMatches: '(w|x|y|z)'.
   regex := (probStr copyWithRegex: '(w|x|y|z)' matchesReplacedWith: '(.)') asRegex.
   found := OrderedCollection new.
   patterns do: [:pat |
      (regex matches: pat) ifTrue: [
         found add: (String streamContents: [:ss |
            (1 to: boxes size) do: [:idx |
               ss nextPutAll: (boxes at: idx), '=', (regex subexpression: idx+1)
            ] separatedBy: [ss nextPutAll: ',']
         ])
      ]
   ].
   found size caseOf: {
      [0] -> ['none'].
      [1] -> [found first].
   } otherwise: ['many']
].

#(
   '0' 'Tx4/5yz' 'x=1,y=S,z=2'
   '1' '14S/xyz' 'none'
   '2' '1w6/xyz' 'many'
   '3' '4w3/12S' 'w=5'
   '4' '4w3/S51' 'w=D'
   '5' '15S/wD4' 'w=3'
   '6' '54D/6Tw' 'w=1'
   '7' 'S21/35w' 'w=4'
   '8' 'w2x/354' 'w=S,x=1'
   '9' 'wx1/54D' 'w=6,x=T'
   '10' '45w/12x' 'w=3,x=S'
   '11' '5w2/x14' 'w=S,x=T'
   '12' 'Dw5/x41' 'w=3,x=6'
   '13' 'w4x/1y6' 'w=D,x=5,y=T'
   '14' '15w/xy4' 'w=S,x=3,y=D'
   '15' 'D35/wxy' 'w=6,x=4,y=1'
   '16' '4wx/51y' 'w=6,x=T,y=2'
   '17' 'wTx/D4y' 'w=1,x=6,y=5'
   '18' 'wxy/z3D' 'w=1,x=4,y=6,z=5'
   '19' 'wx5/1yz' 'w=D,x=4,y=T,z=6'
   '20' 'w53/xyz' 'w=4,x=1,y=2,z=S'
   '21' 'wx1/yzD' 'w=6,x=T,y=5,z=4'
   '22' 'wxS/3yz' 'w=1,x=5,y=D,z=4'
   '23' 'wx2/y1z' 'w=5,x=S,y=T,z=4'
   '24' '4wx/2yz' 'w=1,x=T,y=S,z=5'
   '25' 'T6w/xyz' 'w=4,x=2,y=1,z=5'
   '26' 'Swx/yDz' 'w=5,x=1,y=4,z=3'
   '27' 'wDx/yzS' 'w=3,x=4,y=1,z=5'
   '28' 'wxy/5Sz' 'w=T,x=1,y=4,z=2'
   '29' 'wSx/4yz' 'w=2,x=5,y=1,z=T'
   '30' 'wxS/y5z' 'w=1,x=2,y=4,z=3'
   '31' 'wxy/35z' 'w=S,x=2,y=1,z=4'
   '32' 'wxy/T6z' 'w=2,x=1,y=5,z=4'
   '33' 'wxD/yz1' 'w=5,x=4,y=6,z=T'
   '34' '1wx/yz5' 'w=T,x=6,y=D,z=4'
   '35' 'wx3/y5z' 'w=4,x=D,y=S,z=1'
   '36' '6wx/y3z' 'w=4,x=1,y=D,z=5'
   '37' '5wx/4yz' 'w=1,x=2,y=6,z=T'
   '38' 'wx4/Syz' 'w=3,x=5,y=2,z=1'
   '39' 'w3D/xyz' 'w=5,x=1,y=4,z=6'
   '40' 'w3x/6yz' 'w=D,x=5,y=4,z=1'
   '41' 'wxy/z12' 'w=4,x=6,y=T,z=5'
   '42' '1wS/xyz' 'many'
   '43' 'wxy/Dz5' 'many'
   '44' '3w4/xyz' 'many'
   '45' 'wxy/5zD' 'many'
   '46' 'wxy/Tz4' 'many'
   '47' '5wD/xyz' 'many'
   '48' 'wDx/y5z' 'many'
   '49' 'wxy/3z4' 'many'
   '50' 'wxy/5z2' 'many'
   '51' 'Dyz/S1x' 'none'
   '52' 'w1z/xyS' 'none'
   '53' '15x/T6y' 'none'
   '54' 'zy4/5x6' 'none'
   '55' '2xy/4Tz' 'none'
   '56' 'xzS/y1w' 'none'
   '57' 'Syx/4z5' 'none'
   '58' 'xwS/Tzy' 'none'
   '59' 'D5z/xwy' 'none'
   '60' 'yxD/z35' 'none'
) groupsOf: 3 atATimeDo: [:data | self assert: [(solveDiceDev value: data second) = data third]]


Pharo も 4 まで来てだいぶマシになったように思うのですが、4 になって改めて、ifTrue: ifFalse: のイッパツ挿入とか、advance argument(shift + alt + A)とか、これなくして Pharo 使いはいったいどうやってコードを書いているの?といったような、いにしえの Smalltalk-80 〜 Squeak 時代の(かろうじて Pharo 3 までは使えた)便利機能をぶっ壊したまま放置されているところが多々あり、相変わらずいろいろダメなやつですね。

もちろん、今回の正規表現とかコード補完とか、今日日の処理系や IDE にはあってあたりまえの機能があたりまえのようにデフォで用意されている点に関しては Squeak は完全に置いてきぼりにされた感があるので(だが、古き良き機能をぶっ壊すくらいならそれでいい…)、Squeak を知らない世代は Pharo でぜんぜんオッケー、あえて Squeak を使う意味がきっとわからないのでは?とも思いますが。^^;



そんなわけでやっぱりしばらくは Squeak 使いなので Squeak でも動く正規表現を使わない版も書きました。

| boxChars patterns solveDiceDev |

boxChars := 'wxyz'.

patterns := #(
   '12S/453'
   '1T6/D45'
   '146/53D'
   '15S/3D4'
   '215/T64'
   '2S5/41T'
) inject: OrderedCollection new into: [:sum :pat |
   | alt |
   alt := (pat last: 3), '/', (pat first: 3).
   sum addAll: {pat. pat reversed. alt. alt reversed}; yourself
].

solveDiceDev := [:probStr |
   | found |
   found := OrderedCollection new.
   patterns do: [:pat | [:exit |
      found add: (String streamContents: [:ss |
         pat with: probStr do: [:a :b |
            (a = b or: [(boxChars includes: b) and: [ss nextPutAll: {b. $=. a. $,}. true]]) ifFalse: [exit value]
         ].
         ss skip: -1
      ])
   ] valueWithExit].
   found ifEmpty: ['none'] ifNotEmpty: [found size = 1 ifTrue: [found first] ifFalse: ['many']]
].

#(
   '0' 'Tx4/5yz' 'x=1,y=S,z=2'
   '1' '14S/xyz' 'none'
   '2' '1w6/xyz' 'many'
   '3' '4w3/12S' 'w=5'
   '4' '4w3/S51' 'w=D'
   '5' '15S/wD4' 'w=3'
   '6' '54D/6Tw' 'w=1'
   '7' 'S21/35w' 'w=4'
   '8' 'w2x/354' 'w=S,x=1'
   '9' 'wx1/54D' 'w=6,x=T'
   '10' '45w/12x' 'w=3,x=S'
   '11' '5w2/x14' 'w=S,x=T'
   '12' 'Dw5/x41' 'w=3,x=6'
   '13' 'w4x/1y6' 'w=D,x=5,y=T'
   '14' '15w/xy4' 'w=S,x=3,y=D'
   '15' 'D35/wxy' 'w=6,x=4,y=1'
   '16' '4wx/51y' 'w=6,x=T,y=2'
   '17' 'wTx/D4y' 'w=1,x=6,y=5'
   '18' 'wxy/z3D' 'w=1,x=4,y=6,z=5'
   '19' 'wx5/1yz' 'w=D,x=4,y=T,z=6'
   '20' 'w53/xyz' 'w=4,x=1,y=2,z=S'
   '21' 'wx1/yzD' 'w=6,x=T,y=5,z=4'
   '22' 'wxS/3yz' 'w=1,x=5,y=D,z=4'
   '23' 'wx2/y1z' 'w=5,x=S,y=T,z=4'
   '24' '4wx/2yz' 'w=1,x=T,y=S,z=5'
   '25' 'T6w/xyz' 'w=4,x=2,y=1,z=5'
   '26' 'Swx/yDz' 'w=5,x=1,y=4,z=3'
   '27' 'wDx/yzS' 'w=3,x=4,y=1,z=5'
   '28' 'wxy/5Sz' 'w=T,x=1,y=4,z=2'
   '29' 'wSx/4yz' 'w=2,x=5,y=1,z=T'
   '30' 'wxS/y5z' 'w=1,x=2,y=4,z=3'
   '31' 'wxy/35z' 'w=S,x=2,y=1,z=4'
   '32' 'wxy/T6z' 'w=2,x=1,y=5,z=4'
   '33' 'wxD/yz1' 'w=5,x=4,y=6,z=T'
   '34' '1wx/yz5' 'w=T,x=6,y=D,z=4'
   '35' 'wx3/y5z' 'w=4,x=D,y=S,z=1'
   '36' '6wx/y3z' 'w=4,x=1,y=D,z=5'
   '37' '5wx/4yz' 'w=1,x=2,y=6,z=T'
   '38' 'wx4/Syz' 'w=3,x=5,y=2,z=1'
   '39' 'w3D/xyz' 'w=5,x=1,y=4,z=6'
   '40' 'w3x/6yz' 'w=D,x=5,y=4,z=1'
   '41' 'wxy/z12' 'w=4,x=6,y=T,z=5'
   '42' '1wS/xyz' 'many'
   '43' 'wxy/Dz5' 'many'
   '44' '3w4/xyz' 'many'
   '45' 'wxy/5zD' 'many'
   '46' 'wxy/Tz4' 'many'
   '47' '5wD/xyz' 'many'
   '48' 'wDx/y5z' 'many'
   '49' 'wxy/3z4' 'many'
   '50' 'wxy/5z2' 'many'
   '51' 'Dyz/S1x' 'none'
   '52' 'w1z/xyS' 'none'
   '53' '15x/T6y' 'none'
   '54' 'zy4/5x6' 'none'
   '55' '2xy/4Tz' 'none'
   '56' 'xzS/y1w' 'none'
   '57' 'Syx/4z5' 'none'
   '58' 'xwS/Tzy' 'none'
   '59' 'D5z/xwy' 'none'
   '60' 'yxD/z35' 'none'
) groupsOf: 3 atATimeDo: [:data | self assert: [(solveDiceDev value: data second) = data third]]