指定dotの空白を生成する

趣味でJavaScriptを使ってAAエディタを作っているのだが、そこで使う指定dotの空白を生成する関数を作った。絶対間違ってそうなのでAdvent Calenderとして晒し上げる。
基準は本来16pxのMS Pゴシックだが、WebkitだとDirectWriteの影響でAAが表示されないため計測には[Saitamaar](https://github.com/keage/Saitamaar)を使う。

## AAで使う空白
### Shift-JISの空白
Shift-JISでは、空白は以下のものしかない。

|名前|記号|dot数|
|:--|:--|:--|
|全角スペース|``[　]``|11dot|
|半角スペース|``[ ]``|5dot|


さらに2ちゃんねる系掲示板では以下の制約がある

- 半角スペースの行頭禁止(自動的に削除される)
- 半角スペースの連続禁止(自動的に1個にされる)

当然この条件では自由な空白が作れない。そこでピリオド``.``(3px)なども使いある程度自由に作っていたが、現在はやる夫系掲示板の多くでは容量と引き換えにUnicode空白の使用が可能である。

### Unicode空白
Unicodeでは、以下の空白が使用可能である。

|名前|記号|ドット数|
|:--|:--|:--|
|EM SPACE|``[ ]``  |16dot|
|FIGURE SPACE|``[ ]`` |10dot|
|EN SPACE|``[ ]``|8dot|
|THREE-PER-EM SPACE|``[ ]``|5dot|
|FOUR-PER-EM SPACE|``[ ]``|4dot|
|SIX-PER-EM SPACE|``[ ]``|3dot|
|THIN SPACE|``[ ]``|2dot|
|HAIR SPACE|``[ ]``|1dot|

ただし、Unicodeは通常は掲示板に送信できないので、``& #x2006;``という形の文字参照で送る。すると、1文字につき12byteほど使うことになる。となると、1レスのbyte数に引っかかったりするので、出来るだけUnicode空白は少なくしたい。

## 条件
- 半角スペースは5dot、全角スペースは11dot。
- 半角スペースの連続禁止。
- 半角スペースが頭に来るの禁止。
- Unicode空白使用可能。ただし可能な限り少なくすること。
- できれば速度。レイヤー処理とか書くと何回も呼びだすので。

## 実装
```javascript:lib/space.js
// 大分省略したもの。全体はhttps://github.com/Duct-and-rice/misaki-editor/blob/master/src/lib/space.js。
export function generateSpaceFromAH (a, h) {
    if (a === 0 && h === 1) {
        return HALF_SPACE
    }
    if (a < h || a < 0 || h < 0) {
        throw new Error('a:' + a + ',h:' + h)
    }
    return DOTS_TO_SPACE[11].str.repeat(a - h) +
        (DOTS_TO_SPACE[11].str + HALF_SPACE).repeat(h) // a - h個の全角空白とh個の全角空白半角空白の連続文字列を並べる。
}

export function oneDotReduce (ah) {
    ah = (() => {
        if (ah.a - 1 >= ah.h + 2) {
            return {a: ah.a - 1, h: ah.h + 2, adj: ah.adj}
        } else if (ah.adj >= 1) {
            return {a: ah.a, h: ah.h, adj: ah.adj - 1}
        } else if (ah.h >= 1) {
            return {a: ah.a, h: ah.h - 1, adj: ah.adj + 4}
        } else if (ah.a >= 1 && ah.adj < 6) {
            return {a: ah.a - 1, h: ah.h + 1, adj: ah.adj + 5}
        }
    })()
    if (ah.h >= 1 && ah.adj <= 6) {
        ah.h--
        ah.adj += 5
    }
    return ah
}

export default function widthSpace (sp) {
    if (typeof sp !== 'number') {
        throw new TypeError()
    }
    const mod = sp % 11
    let a = 0 // 全角スペースの数
    let h = 0 // 半角スペースの数
    let adj = 0 // 調整用dot
    switch (mod) {
    case 0:
        a = Math.floor(sp / 11)
        break
    case 1:
    case 2:
    case 3:
    case 4:
        a = Math.floor(sp / 11)
        h = 1
        break
    case 5:
        if (sp === 5) {
            return DOTS_TO_SPACE[5].str
        }
        a = Math.floor(sp / 11)
        h = 1
        break
    case 6:
    case 7:
    case 8:
    case 9:
    case 10:
        a = Math.ceil(sp / 11)
        break
    }

    if (mod !== 0 && mod !== 5) {
        while (a * 11 + h * 5 + adj !== sp) {
            const ah = oneDotReduce({a, h, adj})
            a = ah.a
            h = ah.h
            adj = ah.adj
        }
        const ah = adjToAH({a, h, adj})
        a = ah.a
        h = ah.h
        adj = ah.adj
    }

    return generateSpaceFromAH(a, h) + adjustWithUnicode(adj)
}
```

まず指定dotから少し多めに全角スペースと半角スペース1or0だけで軽く見積る。
たとえば100dotの場合、全9、半1で``11 * 9 + 5 * 1 = 104``となる

次に、それを1dotずつ減らしていく。減らすには全角スペースを1つ減らし半角スペースを2つ増やす。すると``-11 * 1 + 5 * 2 = -11 + 10 = -1``で一つ減る。この処理は上の``oneDotReduce``関数で行なっている。

当然、全2半1など対応できないケースが出てくるので、その時に前述のUnicode空白を用いて調整した。どこか間違っているかも……

あとはそれらを半角スペースの制約に引っかからないように文字列化する。全角スペースをA、半角スペースをHとすると、``AHAHAHAH``というように半角スペースを配置すれば制約に引っかからないので、``AAAAHAHAHAH``というようなスペースを目指す関数が``generateSpaceAH``である。

あとは最後に調整用Unicode空白を付けて終わり！閉廷！

## やばそうなところ
- ``oneDotReduce``がちゃんと動くか
- 速度(多分チューニングしてもJSの壁にぶちあたるので出来ればWASM化したい)
- この記事を書いていて気づいたのだが5dotの場合行頭がそうでないかで場合分けが必要では？

## コード全体
https://github.com/Duct-and-rice/misaki-editor/blob/master/src/lib/space.js

## 参考文献
1. 『Orinrin Editor』紹介　第02回　『Orinrin Editor』で「ユニコード空白」を使ってみよう - やる夫まとめもZ - http://matomemo3.blog.fc2.com/blog-entry-5263.html