計算式を評価する
四則演算の計算式となる文字列を渡すと計算結果を返す関数を作りました(コードは文末の「参考リンク」から読めます)。
JavaScript
1evalCalculation('-1*3*.1+14/7*0.65'); // 1
eval()
や JSON.parse()
に倣い、文法違反な文字列を渡すと例外を返します。
JavaScript
1evalCalculation('*1+3'); // SyntaxError: An expression starts with an unexpected token: * 2evalCalculation('1+3/'); // SyntaxError: An expression ends with an unexpected token: / 3evalCalculation('1.1.1+3'); // SyntaxError: Illegal number: 1.1.1
エラーメッセージをDOM上に出力する
この関数は電卓機能の為に作ったものなので早速組み込みました。
しかし、ユーザが電卓で不正な文字を入力したとしても通常はコンソールまで確認しませんのでエラーメッセージに気が付いてもらえません。
そこで、次のように修正する事を検討しました(下記は仮想コードであり、calculator.js
には同コードはありません)。
JavaScript
1function outputErrorMessage (errorMessage) { // 電卓上にエラーメッセージを出力する関数 2 var messages = [ 3 [/^SyntaxError: An expression starts with an unexpected token: ([\s\S]+)$/, '文法違反: "$1" から始まる式は不正です'], 4 [/^SyntaxError: An expression ends with an unexpected token: ([\s\S]+)$/, '文法違反: "$1" で終わる式は不正です'], 5 [/^SyntaxError: Illegal number: ([\s\S]+)$/, '文法違反: "$1" は不正な数値です'] 6 ]; 7 8 for (var i = 0, l = messages.length, message, reg; i < l; ++i) { 9 message = messages[i]; 10 reg = message[0]; 11 12 if (reg.test(errorMessage)) { 13 console.error(errorMessage.replace(reg, message[1])); 14 return; 15 } 16 } 17 18 console.error(errorMessage); 19} 20 21function calc (expression) { 22 try { 23 return evalCalculation(expression); 24 } catch (error) { 25 outputErrorMessage(error.name + ': ' + error.message); 26 return NaN; 27 } 28} 29 30calc('*1+3'); // 文法違反: "*" から始まる式は不正です 31calc('1+3/'); // 文法違反: "/" で終わる式は不正です 32calc('1.1.1+3'); // 文法違反: "1.1.1" は不正な数値です
しかし、これでも出力されるエラーが初めに検出したエラーに限定されるという問題があり、次の状況が考えられます。
- ユーザがクリップボードから
'*1.1.1+2/'
の式を貼り付ける -> エラー「文法違反: "*" から始まる式は不正です」 -
- のエラーを修正し、
1.1.1+2/
とする -> エラー「文法違反: "/" で終わる式は不正です」
- のエラーを修正し、
-
- のエラーを修正し、
1.1.1+2
とする -> エラー「文法違反: "1.1.1" は不正な数値です」
- のエラーを修正し、
-
- のエラーを修正し、
1.1+2
とする ->3.1
が出力される
- のエラーを修正し、
ユーザの事を考えるなら初めから全てのエラーメッセージを出力する設計が親切といえます。
私が考えた打開案
要点
[1] 関数を一つにするか複数に分けるか
[2] 出力値を固定するか、変動するか
[3] 入力値を固定するか、変動するか
[1] は一つの関数の中で「エラーが発生するか、しないか」のような判断要素を関数に持たせて完結させるか、「XMLHttpRequest
と Fetch API」のように根幹となる挙動が同じでも期待する結果が違う考えから関数を分けるか。
[2] は例えば、「エラーが発生した場合にエラーメッセージのリストを返し、エラーが発生なかった場合は Number
型を返す」というように条件に応じて出力値を変えるか、「エラーが発生したら {value: NaN, error: ['エラーメッセージ1', 'エラーメッセージ2']}
を返し、エラーが発生しなかったら {value: 12, error: []}
を返す」のように Number
型への拘りを止めて出力を固定するか。
[3] は例えば、「evalCalculation('*1+2', true);
で関数呼び出ししたなら SyntaxError
例外を発生させずにエラーメッセージリストとなるオブジェクトを返し、evalCalculation('*1+2', false);
または evalCalculation('*1+2');
で関数呼び出ししたなら SyntaxError
の例外を発生させる、というように入力条件に応じて挙動を変更するか、何らかの形で統一的なインターフェースを確立(一例として 2. の出力値を固定する事例が該当)して入力値を evalCalculation('*1+2');
に固定するか。
打開案コード事例
(1) 関数を一つ、入力値を変動、出力値を変動(Number型、Object型)
第二引数に「例外を発生させないフラグ変数」を指定します(規定動作は例外を発生させます)。
JavaScript
1evalCalculation('1+2', true); // 3 2evalCalculation('*1+2/', true); // {value: NaN, errors: ['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']} 3 4evalCalculation('1+2', false); // 3 5evalCalculation('*1+2/', false); // SyntaxError: An expression starts with an unexpected token: * 6 7evalCalculation('1+2'); // 3 8evalCalculation('*1+2/'); // SyntaxError: An expression starts with an unexpected token: *
(2) 関数を一つ、入力値を固定、出力値を固定(Object型)
常に Object
型の値を返しますが、valueOf
関数、toString
関数を書き換える事でプリミティブ値は Number
型と同様の扱いとなります。
下記コードでは分かりやすいようにオブジェクト初期化子で書いていますが、実際にコーディングするなら内部的に new CalculationFormula
のようなコンストラクタを作ることになると思います。
JavaScript
1var number1 = evalCalculation('1+2'), // {valueOf: function () { return 3; }, toString: function () { return this.valueOf().toString(); }, errors: []} 2 number2 = evalCalculation('*1+2/'); // {valueOf: function () { return NaN; }, toString: function () { return this.valueOf().toString(); }, errors: [['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']} 3 4console.log(number1 * 2); // 6 (* 演算子は Number 型に変換して実行する為、number1 は 3 として扱われる) 5document.body.appendChild(document.createTextNode(number1)); // createTextNode で String 型に変換される為、"3" が出力される
(3) 関数を分ける、入力値を固定、出力値を固定(Number型、Promise)
evalCalculation()
は Number 型を返し、evalCalculationPromise()
は Promise を返します。
JavaScript
1/** 2 * evalCalculation() 3 * Number 型を返す 4 */ 5evalCalculation('1+2'); // {valueOf: function () { return 3; }, toString: function () { return this.valueOf().toString(); }, errors: []} 6evalCalculation('*1+2/'); // SyntaxError: An expression starts with an unexpected token: * 7 8/** 9 * evalCalculationPromise() 10 * Promise を返す 11 */ 12function calc (expression) { 13 evalCalculationPromise(expression).then(function (value) { // 成功時には value に Number 型の値が格納される 14 console.log(value); 15 }).catch(function (errors) { // 失敗時には errors にエラーメッセージの配列が格納される 16 for (var i = 0, l = errors.length; i < l; ++i) { 17 console.error(errors[i]); 18 } 19 }); 20} 21 22calc('1+2'); // 成功: 3 23calc('*1+2/'); // 失敗: ['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']
(4) 関数を一つ、入力値を固定、出力値を固定(Number型、Object型、Promise)
(2), (3) の複合型。
JavaScript
1/** 2 * Calculation.eval() 3 * Number 型を返す 4 */ 5Calculation.eval('1+2'); // 3 6Calculation.eval('*1+2/'); // SyntaxError: An expression starts with an unexpected token: * 7 8/** 9 * Calculation() 10 * Number 型を返す 11 */ 12Calculation('1+2'); // 3 13Calculation('*1+2/'); // SyntaxError: An expression starts with an unexpected token: * 14 15/** 16 * new Calculation 17 * Object 型を返す 18 */ 19new Calculation('1+2'); // {valueOf: function () { return 3; }, toString: function () { return this.valueOf().toString(); }, errors: []} 20new Calculation('*1+2/'); // {valueOf: function () { return NaN; }, toString: function () { return this.valueOf().toString(); }, errors: [['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']} 21 22/** 23 * Calculation.evalPromise() 24 * Promise を返す 25 */ 26function calc (expression) { 27 Calculation.evalPromise(expression).then(function (value) { // 成功時には value に Number 型の値が格納される 28 console.log(value); 29 }).catch(function (errors) { // 失敗時には errors にエラーメッセージの配列が格納される 30 for (var i = 0, l = errors.length; i < l; ++i) { 31 console.error(errors[i]); 32 } 33 }); 34} 35 36calc('1+2'); // 成功: 3 37calc('*1+2/'); // 失敗: ['SyntaxError: An expression starts with an unexpected token: *', 'SyntaxError: An expression ends with an unexpected token: /']
所感
直観的には (4) ですが、「名前」や「関数の置き場所」で悩みを抱えています。
- 「Number 型を返す関数」を
Calculation.eval
,Calculation
のどちらにもっていくべきか new Calculation
は必要?Number()
とnew Number()
の関係性に持っていくと統一感が出る?evalPromise
とは何ぞ?Promise
を実行する?
私個人のポリシーとしては ECMAScript や DOM 等の標準 API のインターフェースを参考にして真似る事が多いのですが、同じような設計が見つかりませんでした。
jQuery()
で与えられた引数に応じて挙動を変えるケース(関数を渡せば疑似 DOMContentLoaded のハンドラとなり、文字列を渡せば Selectors API となる)がありますが、ECMAScript を見ると関数の出力値は固定されており、入力値に応じて挙動や出力値が大きく変わるのは筋が良くないように感じています。
エラーメッセージをまとめて出力する為の最適解は?
長くなりましたが、要件をまとめると次のようになります。
- エラーメッセージをまとめて出力したい
- 成功時/失敗時の出力値で最もわかりやすい設計は何か
一人ひとりで設計思想が違うので答えも一つではないと思いますが、皆さんであればどのように設計するでしょうか。
ご意見お聞かせください。
参考リンク

バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2016/11/12 04:23
2016/11/13 03:04