JavaScriptで文字列中の漢数字を算用数字に変換する

## 概要
JavaScriptの学習を１か月前にしたはずが、さっぱり忘れていましたので、復習のために、「文章中の漢数字を算用数字に変換するプログラム」を作ってみました。
次のようにブラウザで表示するもので、変換ボタンを押すと算用数字に変換された文章が表示されます。
なお、'二千十九年１２月三〇日午後６時五十五分' というように算用数字などが混在していても、変換可能です。
<img width="889" alt="スクリーンショット 2019-12-30 16.00.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/551412/87bd702b-4f17-cad4-d6ff-93e1ad70792d.png">
以下に、ソースコードと、簡単な説明（備忘）を掲載します。
学習中で未だ理解が浅いため、コードが冗長であったりして、いまいちな部分が多いと思います。モヤモヤした方は、ご指摘などいただけると幸いです。
## サンプルコード
### 1. JavaScript
```script.js
//定数の設定
const suuji1 = new Set('一二三四五六七八九十百千１２３４５６７８９123456789');  // 数字と判定する文字集合
const suuji2 = new Set('〇万億兆０0,');  // 直前の文字が数字の場合に数字と判定する文字集合
const kans = '〇一二三四五六七八九';
const nums = '０１２３４５６７８９';
const tais1 = '千百十';  // 大数1
const tais2 = '兆億万';  // 大数2

// ●関数(1) '五六七八'または'５６７８'(全角)を'5678'(半角)に単純変換する関数
function Kan2Num(str) {
  let tmp;  // 定数kansまたはnumsを1文字ずつ格納する変数
  for (let i = 0; i < kans.length; i++) {
    tmp = new RegExp(kans[i], "g");  // RegExpオブジェクトを使用（該当文字を全て変換するため）
    str = str.replace(tmp, i);  // replaceメソッドで変換
  }
  for (let i = 0; i < nums.length; i++) {
    tmp = new RegExp(nums[i], "g");  // RegExpオブジェクトを使用（該当文字を全て変換するため）
    str = str.replace(tmp, i);  // replaceメソッドで変換
  }
  return str;
}

// ●関数(2) '九億八千七百六十五万四千三百'を'987654300'に変換する関数（n=1: ４桁まで計算、n=4: 16桁まで計算）
function Kan2NumCnv(str, n) {
  // 変数の宣言（[let ans = poss = 0, pos, block, tais, tmpstr;]とまとめても良い）
  let ans = 0;  // 計算結果を格納する変数（数値型）
  let poss = 0;  // 引数strにおける処理開始位置（数値型）
  let pos;  // 引数strにおける大数（'十','百','千','万'など）の検索結果位置（数値型）
  let block;  // 各桁の数値を格納する変数（数値型）
  let tais;  // 大数を格納（文字列型）
  let tmpstr;  // 引数strの処理対象部分を一時格納する変数（文字列型）

  if (n === 1) {  // n == 1 の場合は４桁まで計算
    tais = tais1;
  } else {  // n == 4 (n != 1) の場合は16桁まで計算（16桁では誤差が生じる）
    n = 4;
    tais = tais2;
  }

  for (let i = 0; i < tais.length; i++) {
    pos = str.indexOf(tais[i]);  // indexOf関数は文字の検索位置を返す
    if (pos === -1) {  // 検索した大数が存在しない場合
      continue;  // 何もしないで次のループに
    } else if (pos === poss) {  // 検索した大数が数字を持たない場合（'千'など）
      block = 1;  // '千'は'一千'なので'1'を入れておく
    } else {  // 検索した大数が数字を持つ場合（'五千'など）
      tmpstr = str.slice(poss, pos);  // sliceメソッドは文字列の指定範囲を抽出する
      if (n === 1) {
        block = Number(Kan2Num(tmpstr));  // 1桁の数字を単純変換（上で作成したKan2Num関数を使用）
      } else {
        block = Kan2NumCnv(tmpstr, 1);  // 4桁の数字を変換（本関数を再帰的に使用）
      }
    }
    ans += block * (10 ** (n * (tais.length - i)));  // ans に演算結果を加算
    poss = pos + 1;  // 処理開始位置を次の文字に移す
  }

  // 最後の桁は別途計算して加算
  if (poss !== str.length) {
    tmpstr = str.slice(poss, str.length);
    if (n === 1) {
      ans += Number(Kan2Num(tmpstr));
    } else {
      ans += Kan2NumCnv(tmpstr, 1);
    }
  }
  return ans;
}

// ●関数(3) '平成三十一年十二月三十日'を'平成31年12月30日'に変換
function TextKan2Num(text) {
  let ans = '';  // 変換結果を格納する変数（文字列型）
  let tmpstr = '';  // 文字列中の数字部分を一時格納する変数（文字列型）
  for (let i = 0; i < text.length + 1; i++) {
    // 次のif文で文字が数字であるかを識別（Setオブジェクトのhasメソッドで判定）
    if (i !== text.length && (suuji1.has(text[i]) || (tmpstr !== '' && suuji2.has(text[i])))) {
      tmpstr += text[i]; // 数字が続く限りtmpstrに格納
    } else {  // 文字が数字でない場合
      if (tmpstr !== '') {  // tmpstrに数字が格納されている場合
        ans += Kan2NumCnv(tmpstr, 4);  // 上で作成したKan2NumCnv関数で数字に変換してansに結合
        tmpstr = '';  // tmpstrを初期化
      }
      if (i !== text.length) {  // 最後のループでない場合
        ans += text[i];  // 数字でない文字はそのまま結合
      }
    }
  }
  return ans;
}

// ●ブラウザの情報取得および操作
window.addEventListener("load", function() {
  // 必要な要素の参照を各変数に格納
  let input = document.getElementById("text-id");
  let ans = document.getElementById("anser-id");
  let cnv_btn = document.getElementById("submit-id");
  let clear_btn = document.getElementById("btn-id");

  // formタグ中の変換ボタン（submit）をクリックした場合の処理
  cnv_btn.addEventListener("click", function(e) {
    let str = input.value;  // textフォームに入力された文字列を取得
    ans.innerText = TextKan2Num(str);  // テキストを変換して表示
    e.preventDefault();  // ブラウザの再読み込みを防ぐ
  });
  
  // buttonタグのクリアボタンをクリックした場合の処理
  clear_btn.addEventListener("click", function() {
    input.value = "";  // textフォームをクリア
    ans.innerText = "";  // 出力結果をクリア
  })
});
```

### 2. HTML
```index.html
<!DOCTYPE html>
<head>
  <meta charset="utf-8" />
  <link rel="stylesheet" href="style.css" />
  <script src="script.js"></script>
</head>
<body>
  <div class="heading">
    <h4>テキスト中の漢数字を算用数字に変換</h4>
  </div>
  <form class="input-form">
    <input type="text" name="input-text" id="text-id" />
    <input type="submit" value="変換" id="submit-id" />
  </form>
  <div class="ans">
    <span class="ans-heading">変換結果：</span>
    <span class="ans-text" id="anser-id"></span>
  </div>
  <div>
    <button id="btn-id">クリア</button>
  </div>
</body>
```

### 3. CSS
```style.css
body {
  margin: 20px;
}

#text-id {
  width: 480px;
  font-size: 14px;
}

#submit-id {
  font-size: 14px;
  background-color: lightblue;
}

.ans {
  height: 50px;
  line-height: 50px;
  font-size: 14px;
}

.ans-text {
  color: blue;
}

#btn-id {
  background-color: lightblue;
  font-size: 14px;
}
```
## 説明（備忘として）
### 1. 変換のアルゴリズムについて
以前、VBAなどで作成したアルゴリズムを使い回しています。考え方は、次の通りです。

#### 1-1. 漢数字変換のアルゴリズム
この内容は、JavaScriptの２つ目の関数[Kan2NumCnv]でコード化しています。

対象文字列の例として「三千百九」を数字に変換する方法を考えます。
まず、千の位は、'千'という文字の前に'三'があるからこれを読み取って'3000'という数値に置き換えます。
次に、百の位は、直前に数値を持たないので、'一百'と読み替えて'100'という数値に置き換えます。
続いて、十の位は存在しないので、'0'という数値に置き換えます。
最後に、一の位は'九'であるので、そのまま'9'という数値に置き換えます。
最後に、各位の数値を合算して、'3109'という数字を演算結果とします。

#### 1-2. 文章中の数字部分だけを変換するアルゴリズム
この内容は、JavaScriptの３つ目の関数[TextKan2Num]でコード化しています。

対象文字列の例として「平成三十年」という文字列を変換する方法を考えます。
方法は、シンプルで、次のように１文字ずつ順に見ていき、順次、結果を入れる変数[ans]に格納していきます。
１文字目の'平'は数字ではないので、そのまま'平'を[ans]に格納します。
２文字目の'成'も数字ではないので、そのまま'成'を[ans]に格納します。
３文字目の'三'は数字を表すので、一旦、変数[tmpstr]に格納しておきます。
４文字目の'十'も数字なので、変数[tmpstr]に格納します（既にある文字と結合して'三十'を格納します。）。
５文字目の'年'は数字ではないので、そのまま'年'を格納しますが、その前に[tmpstr]に溜めていた文字列があるので、これを数字'30'に変換した上で[ans]に格納してから、続いて'年'を[ans]に格納します。
結果として、[ans]に'平成30年'という文字列が格納されたことになります。
### 2. 使用したオブジェクト・メソッドについて
#### 2-1. Setオブジェクトおよびhasメソッド（数字か否かの判定に使用）
数字と判定すべき文字列の集合を定義するために、次のようにSetオブジェクトを使用しました。

```sample.js
const suuji1 = new Set('一二三四五六七八九十百千１２３４５６７８９123456789');  // 数字と判定する文字集合
const suuji2 = new Set('〇万億兆０0,');  // 直前の文字が数字の場合に数字と判定する文字集合
```
以下、Setオブジェクトの備忘です。
###### ＜数値のSetオブジェクト＞
数字のSetオブジェクトは次のように記載します。
集合に含まれているかどうかは、hasメソッドを利用することで判定できます。集合に含まれていれば[true]、含まれていなければ[false]が戻り値となります。

```sample.js
const numbers = new Set([1, 2, 3]);

console.log(numbers.has(1));
// 表示結果 true

console.log(numbers.has(5));
// 表示結果 false
```

●参考にした記事
[MDN Web Docs - 標準ビルトインオブジェクト Set](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Set)

###### ＜文字列のSetオブジェクト＞
文字列のSetオブジェクトは、シングルクォーテーションまたはダブルクォーテーションで文字列を括れば、同様に使用できます。

```sample.js
const kans = new Set(['一', '二', '三']);

console.log(kans.has('一'));
// 表示結果 true

console.log(kans.has('四'));
// 表示結果 false
```

なお、要素が１文字ずつであれば、カンマ区切りをしなくても、次のように記述することで、Setオブジェクトの作成が可能でした。

```sample.js
const kans2 = new Set('一二三四五');

console.log(kans2.has('一'));
// 表示結果 true

console.log(kans2.has('二三'));
// 表示結果 false

console.log(kans2.has('六'));
// 表示結果 false
```

#### 2-2. replaceメソッド（正規表現オブジェクトを使用して全ての対象文字を置換）
replaceメソッドを使用した変換例は次の通りです。

```sample.js
str = '三六三八';

// 単純にreplaceメソッドを使用すると最初の１文字だけ変換
console.log(str.replace('三', '3'));
// 表示結果 '3六三八'

// 正規表現でグローバルマッチのフラグ(g)を使用すると対象の全ての文字を変換
console.log(str.replace(/三/g, '3'));
// 表示結果 '3六3八'

// 変数を使用する場合は、RegExpオブジェクトを使用
tmpStr = new RegExp('三', "g");  // RegExpオブジェクトを使用
console.log(str.replace(tmpStr, '3'));
// 表示結果 '3六3八'
```
単純にreplaceメソッドを使用すると、最初の１文字しか変換できないため、正規表現でグローバルマッチのフラグ(g)を利用する必要があります。
これも、やり方が分からず最初は結構悩みました。コードの記載にあたっては、次の記事を参考にさせていただきました。

●参考にした記事
[replaceに変数を使ってグローバルマッチさせる2つの方法](https://qiita.com/naoyashiga/items/75bce8cabbbab29b0ccb)
[MDN Web Docs - String.prototype.replace()](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/String/replace)

#### 2-3. indexOfメソッド
文字の検索にあたっては、indexOfメソッドを使用しました。
使い方は、次の通りです。

```sample.js
str = '千六百五';
console.log(str.indexOf('千'));
// 表示結果 0  1文字目の場合の戻り値は 0

console.log(str.indexOf('百'));
// 表示結果 2  3文字目の場合の戻り値は 2

console.log(str.indexOf('十'));
// 表示結果 -1  文字が見つからない場合の戻り値は -1
```

戻り値は、検索された文字の位置となりますが、0 からカウントする点に注意が必要です。
なお、検索対象の文字が見つからない場合の戻り値は -1 となります。

●参考にした記事
[JavaScriptでindexOfメソッドを使って文字列を検索する方法](https://techacademy.jp/magazine/14759)

#### 2-4. sliceメソッド
文字の切り出しについては、sliceメソッドを使用しました。
使い方は、次の通りです。

```sample.js
str = '一二三四五六七八九';

console.log(str.slice(0, 1));
// 表示結果 '一'

console.log(str.slice(2, 5));
// 表示結果 '三四五'

console.log(str.slice(5, 5));
// 表示結果 ''
```

第一引数に切り出しの開始位置、第二引数に切り出しの終了位置を指定することで、必要な文字列が抽出できます。

#### 2-5. Numberオブジェクト
文字列型の変数を数値型の変数に変換する方法として、Numberオブジェクトを使用しました。

```sample.js
numStr = '23'

console.log(5 + numStr);
// 表示結果 523  文字列として結合される

console.log(5 + Number(numStr));
// 表示結果 28　数値として加算される
```

変数に格納されている値が文字列として認識されていると四則演算等の計算ができませんので、数値に変換する必要があります。

●参考にした記事
[JavaScriptのparseInt()とNumber()の違い](https://qiita.com/peg/items/8bfa34979da64f15035c)

#### 2-6. ブラウザの情報取得および操作について
ここで使用している addEventListener や getElementById などの説明は、ここでは割愛します。
下記の記事などを参考としていただければと思います。

●参考となると思われる記事
[MDN Web Docs - ドキュメントの操作](https://developer.mozilla.org/ja/docs/Learn/JavaScript/Client-side_web_APIs/Manipulating_documents)

## おわりに
Qiitaの記事を初めて書きました。

今回の記事は、自分自身の備忘を兼ねて書きましたので、焦点のボケた内容となっていますが、何らかの役に立つことがあれば幸いです。

●Qiitaの記事作成で参考にした記事
[Qiitaに投稿する記事の書き方に望むこと](https://qiita.com/riocampos/items/43b8c47e14d63dc6f6a4)
[Qiitaアカウント作成方法、記事の書き方、投稿手順](https://qiita.com/predora005/items/342b50859e2aeafc39b6)