モールス符号 — 表が一つ、曖昧さが二つ、細部が三つ
モールス自体は簡単で、難しいのは細部です。空白は誰のものか、全角の点をどう正規化するか、そして「1 単位」がどれだけか。
モールス符号は、見た瞬間に分かった気になり、書き始めると細かい罠が並ぶ類のものです。表は一つ、曖昧さは二つ、そして実装前に決めるべき細部が三つあります。
表 — 英字だけでは足りない
26 文字だけの変換器は玩具です。数字とよく使う記号にもそれぞれ塊があり、さらに略号(prosign)があります。これは英字の組み合わせではなく、独立した意味を持つ連結符号で、...---... は遭難、-...- は段落替えです。略号には文字間隔がないので、復号時に普通の単語のようには分割できません。
表は純ロジックの中に置き、画面の対照表はそこから描画します。事実の写しを二箇所に持たないためです。
曖昧さ 1 — 空白は誰のものか
符号化では、文字の間は空白、単語の間はスラッシュです。
encode('HI ALL').code; // '.... .. / .- .-.. .-..'
復号は逆で、スラッシュが単語、空白が文字を切ります。しかし貼り付けられた符号にはスラッシュがなく空白だけのことがよくあります。その場合それは一つの単語の中の複数の文字であり、所要時間は文字間隔の 3 単位で数えます。単語間隔の 7 単位ではありません。これは不具合ではなく、文書化すべき仕様です。
曖昧さ 2 — 何が点か
Web ページや文書、チャットからコピーした符号には、点や線に似た文字が混ざります。解析で失敗させるより、先に正規化します。
normalizeCode('·−·−'); // '.-.-'
normalizeCode('-_-'); // '---'
点、句点、ビュレット、中黒はすべて . に。ハイフン、en ダッシュ、em ダッシュ、アンダースコアは - に。縦棒は文字の区切りとして扱います。ここまで済めば、解析器が認識する文字は二つだけです。
細部 1 — 時間には規格がある
1 単位 = 点一つ。線は 3 単位、文字内の要素間は 1 単位、文字間は 3 単位、単語間は 7 単位。つまり S は 5 単位、... --- ... は 27 単位です。画面には 20 WPM(1 単位 60 ミリ秒)で換算した合計時間を出します。
最初の版でこれを間違えました。要素の長さだけを足して間隔を忘れ、S が 3 になりました。timingUnits('...') === 5 という assertion を一つ足せば、同じ間違いは二度とできません。
細部 2 — 再生と所要時間は同じ出所から
聴けるようにするなら、再生に使うパルス列と所要時間の計算は同じ定義から出さなければなりません。そうでないと「表示は 0.3 秒、音は 0.36 秒」になります。そこで pulses() が { on, units } の配列を作り、timingUnits() が合計を出し、両者が一致することを assertion で確かめます。二つの関数の間に一本の釘を打つわけです。
音は WebAudio でその場で合成します。620 Hz のサイン波、各パルスの端に短いエンベロープを付けてクリックを防ぎます。音声が拒否された場合は黙って失敗し、ツール自体は使えます。
細部 3 — 変換できなかったものを言う
符号化で表にない文字(たとえば漢字)に当たったとき、黙って捨ててはいけません。unknown の配列を返し、画面に出す。少なくともどの文字が符号にならなかったか分かります。復号側も同じで、不正な符号語は ? で置き換え、元の並びを並べます。
黙って fallback するのは、ツールのコードで最もよくある、最も痛い間違いです。
まとめ
モールスの難しさはアルゴリズムではなく仕様にあります。空白は文字か単語か、何が点か、1 単位はどれだけか。この三つを書き下してテストに固定すれば、あとは描画だけです。

コメント
…