Number.MAX_SAFE_INTEGERを気にしておきたい

この記事は、[Business Bank Group Developers Advent Calendar](https://qiita.com/advent-calendar/2018/bbg_developers) 17日目の記事です。

---

こんにちは、ビジネスバンクグループの清水です。
フォームに入力した数値を使用して、計算処理をした時におきた問題について話します。

例は目標と実績の数値を入力させて、目標に達成しているかと達成率を表示する画面です。
（開発環境はAngular, TypeScriptです）
<img width="501" alt="スクリーンショット 2018-12-17 0.37.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/238961/cfe34828-7331-f52f-c1e0-a5f05158c23f.png">

```typescript:計算処理
goal: number; // 目標
result1: number; // 実績1
result2: number; // 実績2

// 実績が目標に達成しているかどうかをチェックする
get achievement(): boolean {
  return this.goal <= (this.result1 + this.result2);
}

// 達成率
get rate(): number {
  return (this.result1 + this.result2) / this.goal * 100;
}
```

このようなフォームに大きな数値を入力すると、**計算結果がおかしくなる** ことがあります。↓達成率の計算が変
<img width="224" alt="スクリーンショット 2018-12-17 9.42.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/238961/725f0e12-b0c0-5070-e9ab-c3e00ba54e0a.png">

これは、JavaScriptの`Number.MAX_SAFE_INTEGER`によりおきている現象でした。

### Number.MAX_SAFE_INTEGER
JavaScriptのNumberオブジェクトには[MAX_SAFE_INTEGER](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER)というプロパティがあります。
これは、JavaScriptで正確に扱える最大整数値を表します。

```typescript
Number.MAX_SAFE_INTEGER // 9007199254740991
```

`9007199254740991` になっているのは、Numberが倍精度浮動小数点型であるためだからだそうです。
正確に扱える最小整数値を表す[MIN_SAFE_INTEGER](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER)もあります。

### 誤差が出てしまう
`Number.MAX_SAFE_INTEGER` `Number.MIN_SAFE_INTEGER`の範囲外の数値で計算処理をすると誤差が生じます。

```typescript
9007199254740991 === 9007199254740992 // false
(9007199254740991 + 1) === (9007199254740991 + 2) // true
9007199254740991 + 1 // 9007199254740992
9007199254740992 + 1 // 9007199254740992
```

parseIntで数値変換しようとした時にも、誤差がでます。

```typescript
parseInt("12345678901234567890", 10) // 12345678901234567000
parseInt("9007199254740992", 10) // 9007199254740992
parseInt("9007199254740993", 10) // 9007199254740992
```
これはJavaScriptの仕様です。
実際に、 `Number.MAX_SAFE_INTEGER` `Number.MIN_SAFE_INTEGER` のような大きな桁数を扱うケースはあまりありませんが
うっかり入力したら表示がおかしくなってしまったり、期待した処理をしなくなったりすることはなるべく防いでおきたいです。

### まとめ
数値系の値を入力させるときは、`Number.MAX_SAFE_INTEGER` `Number.MIN_SAFE_INTEGER`を意識して下記のような制御をフロント側でしてあげると親切だなと思いました。
・`input`タグに`maxlength`を指定して入力自体を制限する
・値入力中またはフォームフォーカスアウト時にフロントでバリデーションチェックをし、正しくない値である可能性を知らせる
また、複数フォームのサマリーなどを別の計算処理に使用する場合などは上限・下限にゆとりを持たせるとより安全です。

### おまけ
今回の数値フォームはAngularMaterialの `MatInputModule` を使用しました。

```html
<p>
  <mat-form-field appearance="outline">
    <mat-label>目標</mat-label>
    <input matInput type="number" [(ngModel)]="goal">
  </mat-form-field>
</p>
<p>
  <mat-form-field appearance="outline">
    <mat-label>実績1</mat-label>
    <input matInput type="number" [(ngModel)]="result1">
  </mat-form-field>
</p>
<p>
  <mat-form-field appearance="outline">
    <mat-label>実績2</mat-label>
    <input matInput type="number" [(ngModel)]="result2">
  </mat-form-field>
</p>

<ng-container *ngIf="achievement; else notAchieved">
  <p class="rate">達成です!!（達成率：{{rate}}％）</p>
</ng-container>
<ng-template #notAchieved><p class="rate">未達成です..（達成率：{{rate}}％）</p></ng-template>
```
`MatInputModule`をインポートすると、[MatFormFieldModule](https://material.angular.io/components/form-field/overview)が使用できるようになります。
フォーカス/未入力/マウスオーバー時のスタイルはこんな感じです。便利でした！
<img width="245" alt="スクリーンショット 2018-12-17 14.04.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/238961/fda71c27-47a2-42fb-741d-192826be02bd.png">



以上です。
明日は、[@bigplantsさん](https://qiita.com/bigplants)の担当です。お願いします！