HOME 備忘録 書き込む

関数電卓 JavaScript 版


Apr 19, 2008

Scheme 版Python 版 の関数電卓に続いて今回は JavaScript 版です。

紫藤のページの関数電卓は、

  1. 演算子のオブジェクトを用いることによって、計算の順序と個々のステップで行われる演算 を完全に分離。
  2. 演算子を優先度によってグループ分けすることによって、演算子や関数が追加されても コードを変更する必要がない。(演算子のオブジェクトを作ってそれをリストに追加するだけ)
という特徴があります。 このようなプログラムを書くためには、
  1. 演算をデータとして扱え、
  2. 数値と、演算子オブジェクトを同じリストに入れることができる
プログラミング言語が必要です。 このような条件を満たすプログラム言語としては Lisp と Python があります。 Haskell は最初の特徴を満たすのですが、2 番目を実現するのが面倒です。

実は、JavaScript もこの条件をみたし、Python と同じように関数電卓スクリプトを書くことができます。 JavaScript は安っぽい言語と思われがちですが、実はいけている言語です。 詳しくは

  1. A re-introduction to JavaScript
  2. JavaScript: 世界で最も誤解されたプログラミング言語
を見てください。 スクリプトのソースコードを下に示します。
Firefox で calc.html を読み込むとこういう画面 が表示され、左側のエントリーに式を入力し、'=' ボタンを押すと右側に結果が表示されます。 興味のある人はダウンロードして遊んでみてください。

[calc.js]

001:   /**
002:    * 電卓スクリプト
003:    * JavaScript 版
004:    * **/
005:   
006:   
007:   
008:   // map を定義 
009:   if (!Array.map) {
010:     if (!Array.prototype.map) {
011:       Array.prototype.map = function (callback, thisObject) {
012:         var length = this.length;
013:         var result = new Array(length);
014:         for (var i = 0; i < length; i++)
015:           result[i] = callback.call(thisObject, this[i], i, this);
016:         return result;
017:       };
018:     }
019:     Array.map = function (array, callback, thisObject) {
020:       return Array.prototype.map.call(array, callback, thisObject);
021:     };
022:   }
023:   
024:   
025:   
026:   
027:   // 演算子の優先度
028:   P_BPM=0;
029:   P_BMD=1;
030:   P_UPM=2;
031:   P_POW=3;
032:   P_FUN=4;
033:   P_LHB=5;
034:   P_CST=6;
035:   
036:   
037:   
038:   
039:   function Operator(name, fun, priority)
040:   /**
041:    * 演算子クラスのコンストラクタ
042:    * **/
043:   {
044:        this.name=name;
045:        this.fun=fun;
046:        this.priority=priority;
047:   }
048:   
049:   
050:   // 文字列に変換
051:   Operator.prototype.toString=function(){return this.name};
052:   
053:   // 関数を呼び出す
054:   Operator.prototype.call=function(){
055:        if (this.priority==P_CST) return this.fun;
056:        else if(this.is_unary()) return this.fun(arguments[0]);
057:        else return this.fun(arguments[0], arguments[1]);
058:   }
059:   
060:   // this が other より優先度が大きいとき true を返す
061:   Operator.prototype.is_higher=function(other){
062:        return other==null || this.priority > other.priority || (this.is_reverse() && this.priority==other.priority)
063:        };
064:   
065:   // 定数か?
066:   Operator.prototype.is_const=function(){return this.priority == P_CST};
067:   
068:   // 関数か?
069:   Operator.prototype.is_func=function(){return this.priority == P_FUN};
070:   
071:   // 単項演算子か?
072:   Operator.prototype.is_unary=function(){return this.priority == P_UPM || this.priority == P_FUN};
073:   
074:   // fact か?
075:   Operator.prototype.is_lhb=function(){return this.priority == P_LHB};
076:   
077:   // 逆順の演算子か?
078:   Operator.prototype.is_reverse=function(){
079:        return this.priority == P_FUN ||
080:             this.priority == P_POW ||
081:                  this.priority == P_UPM
082:                  };
083:   
084:   
085:   function fact(n)
086:   {
087:        for( var m=i=1; i<n; m*=++i);
088:        return m;
089:   }
090:   
091:   
092:   function permutation(m0, n0)
093:   /**
094:    * 順列
095:    * **/
096:   {
097:        for (var a=i=m0, n=m0-n0; i>n; a*=--i);
098:        return a;
099:   }
100:   
101:   
102:   function combination(m,n)
103:   /**
104:    * 組み合わせ
105:    * **/
106:   {
107:        return permutation(m,n)/fact(n);
108:   }
109:   
110:   
111:   
112:   // 演算子のリスト
113:   LS_OP = new Object();
114:   LS_OP['+'] = new Operator('+', function (x,y){return x+y;}, P_BPM);
115:   LS_OP['-'] = new Operator('-', function (x,y){return x-y;}, P_BPM);
116:   LS_OP['*'] = new Operator('*', function (x,y){return x*y;}, P_BMD);
117:   LS_OP['/'] = new Operator('/', function (x,y){return x/y;}, P_BMD);
118:   LS_OP['%'] = new Operator('%', function (x,y){return x%y;}, P_BMD)
119:   LS_OP['<<'] = new Operator('<<', function(x,y){return x*Math.pow(2,y);}, P_POW); 
120:   LS_OP['>>'] = new Operator('>>', function(x,y){return x/Math.pow(2,y);}, P_POW); 
121:   LS_OP['@+'] = new Operator('@+', function (x){return x;}, P_UPM);
122:   LS_OP['@-'] = new Operator('@-', function (x){return -1*x;}, P_UPM);
123:   LS_OP['**'] = new Operator('**', Math.pow, P_POW);
124:   LS_OP['^'] = LS_OP['**'];
125:   LS_OP['exp']= new Operator('exp', Math.exp, P_FUN);
126:   LS_OP['log']= new Operator('log', Math.log, P_FUN);
127:   LS_OP['log10']= new Operator('log10', function(x){return Math.log(x)/Math.log(10)}, P_FUN);
128:   LS_OP['sin']= new Operator('sin', Math.sin, P_FUN);
129:   LS_OP['cos']= new Operator('cos', Math.cos, P_FUN);
130:   LS_OP['tan']= new Operator('tan', Math.tan, P_FUN);
131:   LS_OP['asin']= new Operator('asin', Math.asin, P_FUN);
132:   LS_OP['acos']= new Operator('acos', Math.acos, P_FUN);
133:   LS_OP['atan']= new Operator('atan', Math.atan, P_FUN);
134:   LS_OP['!']= new Operator('!', fact, P_LHB);
135:   LS_OP['P']= new Operator('P', permutation, P_BMD);
136:   LS_OP['C']= new Operator('C', combination, P_BMD);
137:   LS_OP['pi']= new Operator('pi', Math.PI, P_CST);
138:   LS_OP['e']= new Operator('e', Math.E, P_CST);
139:   
140:   
141:   
142:   function add_backslash(s)
143:   /**
144:    * 必要に応じてバックスラッシュを挿入します。
145:    * **/
146:   {
147:        var s1='';
148:        var ls=s.split('');
149:        for (var i=0, n=ls.length; i<n; ++i){
150:             var c=ls[i];
151:             s1+= (c.match(/W/)? '\' : '') + c;
152:        }
153:        return s1;
154:   }
155:   
156:   
157:   function make_regstr_each(s0)
158:   /**
159:    * Operator から正規表現用の文字列を作ります
160:    * **/
161:   {
162:        var op=LS_OP[s0];
163:        return add_backslash(s0) +
164:             (op.is_const() ? '(?=\W|$)'   :
165:              op.is_func()  ? '(?=\s|\()' : '');
166:   }
167:   
168:   
169:   
170:   
171:   
172:   
173:   // 式の要素の正規表現, 演算子のリストから自動生成。無名関数を作ってすぐに呼び出す。
174:   ROBJ= function()
175:   {
176:        var ls= new Array();
177:        for (var s in LS_OP)
178:             ls.push(s);
179:        ls.sort(function(x,y){return y.length - x.length})
180:             
181:             return new RegExp('(\()|(\d+(?:\.\d+)?)|(' + ls.map(make_regstr_each).join('|') + ')'   );
182:   }();
183:   
184:   
185:   
186:   function find_corresponding(s0)
187:   /**
188:    * 対応する閉じカッコの位置を返す
189:    * **/
190:   {
191:        var count=0;
192:        for (var i=0, n=s0.length; i<n; ++i){
193:             var c=s0.charAt(i);
194:             if(c=='(') ++count;
195:             else if(c==')') --count;
196:             if(count==0) return i;
197:        }
198:        //return -1;
199:        throw Error('cannot parse input');
200:   }
201:   
202:   
203:   
204:   function parse(s0)
205:   /**
206:    * 数式の文字列を解析して数値と演算子からなるリストを返す
207:    * **/
208:   {
209:        var ls=[];
210:   
211:        while(1){
212:             s0=s0.replace(/^s+|s+$/, '');
213:             if (! s0) break;
214:             var mobj=s0.match(ROBJ);
215:             if (! mobj) throw Error('cannot parse input');
216:             if(mobj[1]){
217:                  var idx=find_corresponding(s0);
218:                  //    print (idx);
219:                  ls.push(parse(s0.slice(1, idx)));
220:                  s0=s0.substring(idx+1);
221:             }else{
222:                  if(mobj[2])
223:                       ls.push(parseFloat(mobj[2]));
224:                  else{
225:                       var sop=mobj[3];
226:                       if ((sop=='+' || sop=='-') && 
227:                           (ls.length==0 || 
228:                            (ls[ls.length-1].constructor==Operator && 
229:                             ! ls[ls.length-1].is_lhb())))
230:                            sop='@'+sop;
231:                       
232:                       ls.push(LS_OP[sop]);
233:                  }
234:                  s0 = s0.substring(mobj[0].length);
235:             }
236:        }
237:        return ls;
238:   }
239:   
240:   
241:   
242:   function find_op(ls)
243:   /** 
244:    * 次に計算する演算子の位置を返す
245:    * **/
246:   {
247:        var op=null;
248:        var idx=-1;
249:        for(var i=0, n=ls.length; i<n; ++i){
250:             var item=ls[i];
251:             if(item.constructor == Operator && item.is_higher(op)){
252:                  idx=i;
253:                  op=item;
254:             }
255:        }
256:        return idx;
257:   }
258:   
259:   
260:   function append_result(ls_before, result, ls_after)
261:   {
262:        ls_before.push(result);
263:        return ls_before.concat(ls_after);
264:   }
265:   
266:   
267:   function calc(ls)
268:   /**
269:    * ls を計算して結果を返す
270:    * **/
271:   {
272:        switch(ls.constructor){
273:        case Number: return ls;
274:        case Operator: return ls.call();
275:        case Array:
276:             if(ls.length==1)
277:                  return calc(ls[0]);
278:             else if(ls.length>1){
279:                  var op_idx=find_op(ls);
280:                  var op=ls[op_idx];
281:                  if(op.is_const())
282:                       return calc(append_result(ls.slice(0,op_idx), op.call(), ls.slice(op_idx+1)));
283:                  
284:                  else if(op.is_lhb() && op_idx > 0)
285:                       return calc(append_result(ls.slice(0,op_idx-1), 
286:                                                op.call(calc(ls[op_idx-1])), 
287:                                                ls.slice(op_idx+1)));
288:                  
289:                  else if(op.is_unary() && op_idx < ls.length-1)
290:                       return calc(append_result(ls.slice(0,op_idx), 
291:                                                 op.call(calc(ls[op_idx+1])),
292:                                                 ls.slice(op_idx+2)));
293:                  
294:                  else if(op_idx>0 && op_idx<ls.length-1)
295:                       return calc(append_result(ls.slice(0,op_idx-1), 
296:                                                 op.call(calc(ls[op_idx-1]), calc(ls[op_idx+1])),
297:                                                 ls.slice(op_idx+2)));
298:                  else
299:                       throw Error('cannot calculate!');
300:             }
301:        default:
302:             throw Error('funny');
303:        }
304:   }
305:   
306:   
307:   
308:   function calculate()
309:   /**
310:    * 文字列を電卓風に eval して返します
311:    * **/
312:   {
313:        var ans_el=document.getElementById('answer');
314:        try{
315:          var ans_val=calc(parse(document.form1.edit1.value));
316:        }catch(e){
317:          var ans_val="式が不正です";
318:        }
319:        var ans_node=document.createTextNode(ans_val.toString());
320:        if(ans_el.firstChild)
321:            ans_el.replaceChild(ans_node, ans_el.firstChild);
322:        else
323:            ans_el.appendChild(ans_node);
324:        
325:        // submit 処理を途中で中断するため
326:        return false;  
327:   }
328:   
329:   
330:   function show_operators()
331:   /**
332:    * 演算子や定数の一覧を出力します
333:    * **/
334:   {
335:        var ls= new Array();
336:        for (var s in LS_OP) ls.push(s);
337:        document.write( ls.join(', '));
338:   }
[calc.html]
000:   <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
001:   
002:   <html lang="ja">
003:   <head>
004:   <meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS">
005:   <link rel="icon" href="../images/shido.png" type="image/png">
006:   <script type="text/javascript" src="calc.js">
007:   </script>
008:   <title>JavaScript 電卓</title>
009:   </head>
010:   <body style='margin:30pt'>
011:   
012:   <h1 style='font-size:120%'>JavaScript 電卓</h1>
013:   
014:   <form name='form1' onsubmit='return calculate()'>
015:   <table>
016:   <tr>
017:     <td>
018:       <input type='text' size=40 name='edit1' id='edit' style='font-size:120%;font-family:monospace'>
019:     </td>
020:     <td>
021:       <span>
022:         <input type="button" value="=" onclick='calculate()'>
023:       </span>
024:     </td>
025:     <td>
026:       <span style='margin-left:15pt;' id='answer' style='font-size:120%;font-family:monospace'>  </span>
027:     </td>
028:   </tr>
029:   </table>
030:   </form>
031:   <div style='margin-top:20pt'>
032:   <pre>
033:   使える演算子: 
034:   <script type="text/javascript">
035:   show_operators();
036:   </script>
037:   </pre>
038:   </div>
039:   </body></html>
040:   
041: