この記事は、Business Bank Group Developers Advent Calendar 17日目の記事です。
こんにちは、ビジネスバンクグループの清水です。
フォームに入力した数値を使用して、計算処理をした時におきた問題について話します。
例は目標と実績の数値を入力させて、目標に達成しているかと達成率を表示する画面です。
(開発環境はAngular, 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;
}
このようなフォームに大きな数値を入力すると、計算結果がおかしくなる ことがあります。↓達成率の計算が変
これは、JavaScriptのNumber.MAX_SAFE_INTEGER
によりおきている現象でした。
Number.MAX_SAFE_INTEGER
JavaScriptのNumberオブジェクトにはMAX_SAFE_INTEGERというプロパティがあります。
これは、JavaScriptで正確に扱える最大整数値を表します。
Number.MAX_SAFE_INTEGER // 9007199254740991
9007199254740991
になっているのは、Numberが倍精度浮動小数点型であるためだからだそうです。
正確に扱える最小整数値を表すMIN_SAFE_INTEGERもあります。
誤差が出てしまう
Number.MAX_SAFE_INTEGER
Number.MIN_SAFE_INTEGER
の範囲外の数値で計算処理をすると誤差が生じます。
9007199254740991 === 9007199254740992 // false
(9007199254740991 + 1) === (9007199254740991 + 2) // true
9007199254740991 + 1 // 9007199254740992
9007199254740992 + 1 // 9007199254740992
parseIntで数値変換しようとした時にも、誤差がでます。
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
を使用しました。
<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が使用できるようになります。
フォーカス/未入力/マウスオーバー時のスタイルはこんな感じです。便利でした!
以上です。
明日は、@bigplantsさんの担当です。お願いします!