10/26レポート講評
問題
以下のような簡単な電卓プログラムを作る.
- 入力は数の行と四則演算子(+, -, *, /)の行を交互に入れる.数の行を
入れた直後にそれまでの計算結果を表示する.最後に四則演算子の代りに= を
入れると終了する.
- 演算子の優先順位などは考慮せず,入力順に計算していく.
実行例を示す.人間による入力は下線をつけて表わす.
dell.tanaka.ecc.u-tokyo.ac.jp% java Calc
10
10.0
*
3
30.0
+
4
34.0
/
4
8.5
-
0.5
8.0
=
以下の(1)-(4)の部分を埋めてプログラムを完成させなさい.
// 学生証番号: XXXXXXX
// 名前: XXXXXX
//
import java.io.*; // 入力に関するクラスを使う時は必要
class Calc{
// 1行入力して実数に変換するメソッド
// throws IOException で内部で入出力エラーが起きる可能性があることを示す
static double getNumber(BufferedReader d) throws IOException{
// 1行入力して,実数に変換
String s=d.readLine();
double val;
// sを実数に変換してvalに代入
(1) ;
return val;
}
// throws IOException で内部で入出力エラーが起きる可能性があることを示す
public static void main(String[] args) throws IOException{
// 入力をするためには,System.inからBufferedReaderを作る
BufferedReader d=new BufferedReader(new InputStreamReader(System.in));
double num=getNumber(d);
for(;;){
System.out.println(num);
String s=d.readLine();
// "="だけの行が入力されたら終了
if(s.equals("="))
(2) ;
// 数を入力
double num1=getNumber(d);
if(s.equals("+")){
// numにnum1を足す
num=num+num1;
}
else if(s.equals("-")){
// numからnum1を引く
(3) ;
}
else if(s.equals("*")){
// numにnum1をかける
num=num*num1;
}
else if( (4) ){
// numをnum1で割る
num=num/num1;
}
}
}
}
講評
- 49名が提出
- 入力する数はdoubleなのに,Double.parseDouble()ではなく
Integer.parseInt()を使っている人が多くいた.
- メソッド名をスペルミス(Double.ParseDouble)している解答があった.プログラムはコンパイル実行できるのを確認してから提出すること.
解答例とコメント
// 学生証番号: XXXXXX
// 名前: XXXXX
//
import java.io.*; // 入力に関するクラスを使う時は必要
class Calc{
// 1行入力して実数に変換するメソッド
// throws IOException で内部で入出力エラーが起きる可能性があることを示す
static double getNumber(BufferedReader d) throws IOException{
// 1行入力して,実数に変換
String s=d.readLine();
double val;
// sを実数に変換してvalに代入
val=Double.parseDouble(s);
return val;
}
// throws IOException で内部で入出力エラーが起きる可能性があることを示す
public static void main(String[] args) throws IOException{
// 入力をするためには,System.inからBufferedReaderを作る
BufferedReader d=new BufferedReader(new InputStreamReader(System.in));
double num=getNumber(d);
for(;;){
System.out.println(num);
String s=d.readLine();
// "="だけの行が入力されたら終了
if(s.equals("="))
break ;
// 数を入力
double num1=getNumber(d);
if(s.equals("+")){
// numにnum1を足す
num=num+num1;
}
else if(s.equals("-")){
// numからnum1を引く
num=num-num1 ;
}
else if(s.equals("*")){
// numにnum1をかける
num=num*num1;
}
else if( s.equals("/") ){
// numをnum1で割る
num=num/num1;
}
}
}
}
穴埋めなので,ほとんどの人がこれと同じ解答を書いてくれました.
オプション課題
この課題ではもの足りない人は,以下のような拡張をした電卓プログラムに
挑戦してみると良いだろう
- 1行に式すべてを書ける(この場合は最後の=は指定しなくても良い).
- 括弧付きの式を扱える.
- 四則演算の優先順位に従って括弧をつけなくても適切な順序で計算する.
- 関数(sin, cos, log)を扱える(入力形式は任意).
講評
- 4名が解答
-
- 今回紹介しない解答でも同じレベルまで達している解答は加点の対象にする.
解答例(1)
/*
11/2追記
御指摘のあった「3(2*-1)」ですが、括弧の前に数字が続けて書いてた場合は"*"
が省略されてるものとして計算するようにしました。また、*の後に負の数が続く
場合は確かに「2*(-1)」と表記するのが正しいですが、これはあえて無視します。
理由としては、対処が難しい上に内部処理的に意味があまりない事もありますが、
それ以上に普段手で書く上で 2 * -1 と普通に書く人もいると言う事を拠り所とし
たいと思います。子供の言い訳みたいですが。
関数電卓プログラム
現在実行可能な事(11/1時点)
1.四則演算
2.括弧の使用
3.三角関数の使用 ( sin( 〜 ) cos( 〜 ) tan( 〜 ) と入力)
4.πの使用 ( PI or pi と入力) <- 一応"pI"や"Pi"も認識する
5.exp、lnの使用 ( exp( 〜 ) ln( 〜 ) と入力)
6.平方根の使用 ( sqrt( 〜 ) と入力)
7.累乗計算 ( x^y )
8.メモリー機能:一つ前の計算の結果を使える(ans or Ansと入力)
9.絶対値の使用( abs( 〜 ) と入力)
10.逆三角関数の使用( asin( 〜 ) acos( 〜 ) atan( 〜 ) と入力)
11.乱数の使用( rnd と入力)
*/
import java.io.*;
class F_Calc{
// メモリ機能用グローバル変数
static double Memory=0;
// 組み込み関数群
static double c_sin(String Str){
return Math.sin(calc(Str));
}
static double c_cos(String Str){
return Math.cos(calc(Str));
}
static double c_tan(String Str){
return Math.tan(calc(Str));
}
static double c_exp(String Str){
return Math.exp(calc(Str));
}
static double c_ln(String Str){
return Math.log(calc(Str));
}
static double c_sqrt(String Str){
return Math.sqrt(calc(Str));
}
static double c_abs(String Str){
return Math.abs(calc(Str));
}
static double c_asin(String Str){
return Math.asin(calc(Str));
}
static double c_acos(String Str){
return Math.acos(calc(Str));
}
static double c_atan(String Str){
return Math.atan(calc(Str));
}
//主計算
static double calc(String Str){
//長いなぁ 部分部分ごとにメソッドに分けた方が良かっただろうか
StringBuffer StrBuf=new StringBuffer(Str);
boolean flag=true;
int ind[]=new int[2];
ind[0]=0;ind[1]=0;
int count=0;
int i;
char n_char=' ';
String P_Str="";
// 定数の処理 文字を定数に変える
for(i=0;i< StrBuf.length();i++){
n_char=StrBuf.charAt(i);
if(n_char=='P'|| n_char=='p'){
if(StrBuf.charAt(i+1)=='i'||StrBuf.charAt(i+1)=='I'){
StrBuf.replace(i,i+2,Double.toString(Math.PI));
}
}
if(n_char=='A'|| n_char=='a'){
if(StrBuf.charAt(i+1)=='n' && StrBuf.charAt(i+2)=='s'){
StrBuf.replace(i,i+3,Double.toString(Memory));
}
}
if(n_char=='r'){
if(StrBuf.charAt(i+1)=='n' && StrBuf.charAt(i+2)=='d'){
StrBuf.replace(i,i+3,Double.toString(Math.random()));
}
}
}
// ( と ) の処理 再帰的
for(i=StrBuf.length()-1;i>=0;i--){
n_char=StrBuf.charAt(i);
P_Str=StrBuf.substring(0,i);
if(flag){
if(n_char==')'){
flag=false;
ind[1]=i;
count+=1;
}
if(n_char=='('){
count=-1;
break;
}
}else{
if(n_char==')')count+=1;
if(n_char=='(')count-=1;
if(count==0){
flag=true;
if(StrBuf.charAt(i-1)=='0'||StrBuf.charAt(i-1)=='1'||StrBuf.charAt(i-1)=='2'||StrBuf.charAt(i-1)=='3'||
StrBuf.charAt(i-1)=='4'||StrBuf.charAt(i-1)=='5'||StrBuf.charAt(i-1)=='6'||StrBuf.charAt(i-1)=='7'||
StrBuf.charAt(i-1)=='8'||StrBuf.charAt(i-1)=='9'){
StrBuf=StrBuf.insert(i,'*');
ind[1]++;
ind[0]=i+1;
}else{
ind[0]=i;
}
if(P_Str.endsWith("asin")){
StrBuf.replace(ind[0]-4,ind[1]+1,Double.toString(c_asin(StrBuf.substring(ind[0]+1,ind[1]))));
i-=4;
}else if(P_Str.endsWith("acos")){
StrBuf.replace(ind[0]-4,ind[1]+1,Double.toString(c_acos(StrBuf.substring(ind[0]+1,ind[1]))));
i-=4;
}else if(P_Str.endsWith("atan")){
StrBuf.replace(ind[0]-4,ind[1]+1,Double.toString(c_atan(StrBuf.substring(ind[0]+1,ind[1]))));
i-=4;
}else if(P_Str.endsWith("sin")){
StrBuf.replace(ind[0]-3,ind[1]+1,Double.toString(c_sin(StrBuf.substring(ind[0]+1,ind[1]))));
i-=3;
}else if(P_Str.endsWith("cos")){
StrBuf.replace(ind[0]-3,ind[1]+1,Double.toString(c_cos(StrBuf.substring(ind[0]+1,ind[1]))));
i-=3;
}else if(P_Str.endsWith("tan")){
StrBuf.replace(ind[0]-3,ind[1]+1,Double.toString(c_tan(StrBuf.substring(ind[0]+1,ind[1]))));
i-=3;
}else if(P_Str.endsWith("abs")){
StrBuf.replace(ind[0]-3,ind[1]+1,Double.toString(c_abs(StrBuf.substring(ind[0]+1,ind[1]))));
i-=3;
}else if(P_Str.endsWith("sqrt")){
StrBuf.replace(ind[0]-4,ind[1]+1,Double.toString(c_sqrt(StrBuf.substring(ind[0]+1,ind[1]))));
i-=3;
}else if(P_Str.endsWith("exp")){
StrBuf.replace(ind[0]-3,ind[1]+1,Double.toString(c_exp(StrBuf.substring(ind[0]+1,ind[1]))));
i-=4;
}else if(P_Str.endsWith("ln")){
StrBuf.replace(ind[0]-2,ind[1]+1,Double.toString(c_ln(StrBuf.substring(ind[0]+1,ind[1]))));
i-=2;
}else{
StrBuf.replace(ind[0],ind[1]+1,Double.toString(calc(StrBuf.substring(ind[0]+1,ind[1]))));
}
}
}
}
//System.out.println(StrBuf.substring(0));
if(count!=0){
System.out.println("Error count="+count+":The number of'(' doesn't equal that of ')'.");
System.exit(0);
}
// 以下、四則演算部
double result=0;
ind[0]=0;ind[1]=0;flag=true;
double num[]=new double[(StrBuf.length()+1)/2+1];
char sn[]=new char[StrBuf.length()/2+1];
int las=0;
//式を数値と演算子に分ける
for(i=1;i< StrBuf.length()-1;i++){
n_char=StrBuf.charAt(i);
//"+","-"は符号やDouble型の桁数を表す場合もあるので注意した
if(n_char=='-'){
if(flag){
flag=false;
num[ind[0]]=Double.parseDouble(StrBuf.substring(las,i));
sn[ind[1]]=n_char;
ind[0]++;ind[1]++;
las=i+1;
}
}else if(n_char=='+'){
if(flag){
flag=false;
num[ind[0]]=Double.parseDouble(StrBuf.substring(las,i));
sn[ind[1]]=n_char;
ind[0]++;ind[1]++;
las=i+1;
}
}else if(n_char=='*' || n_char=='/' || n_char=='^'){
flag=false;
num[ind[0]]=Double.parseDouble(StrBuf.substring(las,i));
sn[ind[1]]=n_char;
ind[0]++;ind[1]++;
las=i+1;
}else if(n_char=='E'){
flag=false;
}else{
flag=true;
}
}
num[ind[1]]=Double.parseDouble(StrBuf.substring(las));
//最初に累乗の処理
i=0;
while(true){
if(sn[i]=='^'){
num[i]=Math.pow(num[i],num[i+1]);
for(int j=i;j< ind[0]-1;j++){
sn[j]=sn[j+1];
num[j+1]=num[j+2];
}
ind[0]--;
}else{
i++;
}
if(i>=ind[0])break;
}
//次に、*と/の処理を先に行う
i=0;
while(true){
if(sn[i]=='*'){
num[i]*=num[i+1];
for(int j=i;j< ind[0]-1;j++){
sn[j]=sn[j+1];
num[j+1]=num[j+2];
}
ind[0]--;
}else if(sn[i]=='/'){
num[i]/=num[i+1];
for(int j=i;j< ind[0]-1;j++){
sn[j]=sn[j+1];
num[j+1]=num[j+2];
}
ind[0]--;
}else{
i++;
}
if(i>=ind[0])break;
}
//最後に+と−の計算を行う
result+=num[0];
for(i=0;i< ind[0];i++){
if(sn[i]=='-'){
result-=num[i+1];
}else{
result+=num[i+1];
}
}
return result;
}
public static void main(String[] args) throws IOException{
//mainだけど、F_calcの中では主役じゃないw
BufferedReader d=new BufferedReader(new InputStreamReader(System.in));
String Str="";
while(true){
System.out.println("Please Input Expression");
Str=d.readLine();
Memory=calc(Str);
System.out.println("Ans: "+Double.toString(Memory));
System.out.println("agein? (y-n)");
Str=d.readLine();
if(Str.equals("n")||Str.equals("N"))break;
}
}
}
計算結果を文字列の置き換えで実行するというのは豪快で面白いですね.
解答例(2)
/*
電卓プログラム ファイルその1
XXXXX XXXXX
動作にはほかにCCalc.java,CTreeElement.javaが必要です。
申し訳ありませんがShiftJISでかかれているのでコンパイル時にオプション -encoding SJISでエンコードをお願いします。
特徴:
一行に式を書き込むことで計算できます。
(末尾に=は入れないこと。)
四則演算、括弧使用可。
使える関数はsin,cos,tan,asin,acos,atan,exp,log,abs,sqrt
定数としてpi,eも使えます。
入力例:log(e*e)+sin(pi/4)*sqrt(cos(pi/8))+5*(3/2-8)
*/
import java.io.*;
import java.math.*;
class CMainLoop
{// メインループとなるクラス
public static void main(String[] args) throws IOException
{
BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
System.out.println("終了するにはexitとタイプしてください。");
CCalc calcmachine = new CCalc();
String calcstring= buff.readLine();
while(!calcstring.equals("exit"))
{//少数部分は丸めた方が少しは表示がきれいかも
BigDecimal val = new BigDecimal(calcmachine.Calculate(calcstring));//計算結果を格納
String st_val =(val.setScale(val.scale(),BigDecimal.ROUND_HALF_UP)).toString();//四捨五入で丸め
int pos_dot = st_val.indexOf('.');
if((pos_dot>=0)&&(st_val.length()-pos_dot>12))
{//小数点12桁以下を切る
String st_val_ = st_val.substring(0,pos_dot+12);
System.out.println("answer = "+st_val_);
}
else
{//あまり小数部の数が長くない
System.out.println("answer = "+st_val);
}
calcstring= buff.readLine();
}
}
}
/*
電卓プログラム ファイルその2
XXXXX XXXXXX
動作にはほかにCMainLoop.javaとCTreeElement.javaが必要です。
申し訳ありませんがShiftJISでかかれているのでコンパイル時にオプション-encoding SJISでエンコードをお願いします。
*/
import java.io.*;
import java.math.*;
class CCalc
{/*渡された式の文字列から計算を行うクラス
・主な計算の流れ
入力された文字列を数字、記号、文字(関数および定数)に切り分ける。
文字列配列を適当に補正したあと
解析木を作り計算する。
特徴として関数を計算上は演算子として扱うようにした。(関数がどれも一つしか引数を持たない
ものばかりだったのでそうした方が実装が楽だと判断した為。引数が複数あるようなものを実装するなら
大掛かりな変更が必要となる。(手軽には実装できなくなるかも?))
*/
///(文字の種類)///////////////////////////////////////////////////////
final int NUMBER = 0;
final int SIGN = 1;// +,-,*,/のこと。ただし以下のプログラムではsinなどの関数も演算子として扱う。
final int LETTER = 2;// 文字(関数,定数)
final int LEFTPAREN = 3;// 左括弧
final int RIGHTPAREN = 4;// 右括弧
//////////////////////////////////////////////////////////
public String Calculate(String string)
{
int stringlength = string.length();
if(stringlength == 0){return "0.0";}//渡された文字列が空
String[] calcstring = CutIntoElement(string);//式の文字列から識別子ごとに切り出し
if(calcstring[0] == ""){return "0.0";}//CutIntoElementで構文にエラーを発見
String[] revisedcalcstring = ReviseElements(calcstring);//識別子列を計算しやすいように適当に補正
if(revisedcalcstring[0] == ""){return "0.0";}//ReviseElementsで構文にエラーを発見
return CreateTreeAndCalc(revisedcalcstring);//識別子列から解析木を作り計算をする
}
protected int CheckCharType(char c)
{// この関数で数字かどうかを調べるときに文字列の末尾の文字を渡しているのは数字が'-'で始まることもあるため
if((c == '0')||(c == '1')||(c == '2')||(c == '3')||(c == '4')
||(c == '5')||(c == '6')||(c == '7')||(c == '8')||(c == '9')||(c == '.'))
{return NUMBER;}
if((c == '+')||(c == '-')||(c == '*')||(c == '/'))
{return SIGN;}
if(c == '(')
{return LEFTPAREN;}
if(c == ')')
{return RIGHTPAREN;}
return LETTER;
}
protected String[] VoidStrings()
{//エラー時の空の文字配列を返す
String[] ret_st = new String[1];
ret_st[0] = "" ;
return ret_st;
}
protected String[] CutIntoElement(String string)
{ /*
stringを識別子ごとに区切ってcalcstringに入れる。
識別子:
数字,+,-,/,*,一番外側の()とその中の文字列,関数名または特定の定数(と思われる文字列)(ここではまだ関数名のチェックは行わない)
なお、ここで()の数が合っているかのチェックを行う。
また、スペースやタブをはじく。
*/
int stringlength = string.length();
String[] calcstring = new String[stringlength];// 最悪必要となる最大長を確保
int elementnumber = 0;// calcstringに何個文字列が入っているか
int parencounter = 0;// 括弧の数をチェック。下のループを抜けたときこの値が0なら正常
String tempstring = new String();//ここに次の文字列を作っていく
tempstring = "";// 初期化は勝手にされているようだが気持ち悪いので一応
int firstcharindex = 0;//スペースやタブでない最初の文字のインデックス
while((firstcharindex<=stringlength-1)
&&((string.charAt(firstcharindex)==' ')||(string.charAt(firstcharindex)==' ')))
{
firstcharindex++;
}
if(firstcharindex>=stringlength){return VoidStrings();}//すべてタブかスペース(indexが範囲を超えている)
int oldstate = CheckCharType(string.charAt(firstcharindex));// 直前の文字の種類(一文字目は必ずtempstringに追加される)
boolean inparen = false;// 括弧内かどうか
for(int i = 0;i< stringlength;i++)
{
if((string.charAt(i) == ' ')||(string.charAt(i) == ' ')){continue;}//スペースやタブなら無視
if(inparen)
{// 括弧内なので()のなかを纏めて文字列に。括弧も文字列に含める
if((CheckCharType(string.charAt(i))==RIGHTPAREN)&&(parencounter == 1))
{// 文字列中の一番外の括弧の末尾の処理
if(tempstring.equals("("))
{
System.out.println("空の括弧があります。");
return VoidStrings();
}
inparen = false;
parencounter--;
}
else{// 括弧の中に括弧があるときも括弧の数を調べる
if(CheckCharType(string.charAt(i))==RIGHTPAREN){parencounter--;}
if(CheckCharType(string.charAt(i))==LEFTPAREN){parencounter++;}
}
tempstring += string.charAt(i);// 括弧内文字列は種類を気にせずつなげていく
}
else
{// 括弧の中ではない
if(CheckCharType(string.charAt(i))==oldstate)
{//前の文字と同じ種類
tempstring += string.charAt(i);
}
else
{// 前の文字と種類が違うのでここで一度切る
calcstring[elementnumber] = tempstring;
elementnumber++;
tempstring = "";
tempstring += string.charAt(i);
}
if(CheckCharType(string.charAt(i))==RIGHTPAREN)
{// 一番外側の右括弧だが、そもそもここに来るということは構文エラー(右括弧は本当はinparen==trueのときにのみ現れるはずだから。)
parencounter--;
}
if(CheckCharType(string.charAt(i))==LEFTPAREN)
{// 一番外側の左括弧
inparen = true;
parencounter++;
}
}
oldstate = CheckCharType(string.charAt(i));
}
calcstring[elementnumber] = tempstring;// 最後に残っているのを入れる
elementnumber++;
if(parencounter != 0)
{// 括弧の数をチェック
System.out.println("括弧の数が正しくありません。");
return VoidStrings();
}
if((elementnumber==1)&&calcstring[0].equals(".")){return VoidStrings();}
/* "."一文字しかないときだけは全体のルーチンのチェックから漏れてしまうのではじく
('.'は数字扱いなので関数名チェックや数字と演算子の位置のチェックは通ってしまうが
"."ではdouble変換時にエラーになるため。)
*/
String[] ret = new String[elementnumber];// nullのStringを取り除いた配列を作る
for(int j = 0;j< lementnumber;j++)
{ret[j] = calcstring[j];}
return ret;
}
protected String[] ReviseElements(String[] calcstring)
{
/*
解析木を作るときに渡す識別子列には以下のような条件がある
・必ず数字、演算子、数字、演算子…、数字という文字列になっていること
・数字には( )を使って纏めたものは認めない
この条件に合うように少し識別子列を加工する。
1.sin,cosなども以降 演算子として扱うので、(例:sin(value)は dummyvalue sin value の式と等価として扱う)
3+sin(...)などとできるように、sinなどの前にダミーデータを挿入
つまり3 + dummy sin ()として識別子列の順序を守る
ダミーデータは計算時には無視されるのでどのような値でも構わないがとりあえず1を入れておく
2.()内はその文字列を再びCalculateに渡して返り値をデータに格納
3.pi,eは定数として数字に置換
*/
String[] revisedcalcstring = new String[600];
//配列の大きさは適当。異様に長く打ち込まれるとまずいかも。
//calcstringを読み込んで正確に個数を決定することもできなくは無いが…
int originalelementnumber = calcstring.length;
int revisedelementnumber = 0;// 補正されたデータの要素数
for(int i = 0;i< originalelementnumber;i++)
{
if(CheckCharType(calcstring[i].charAt(0))==LETTER)// 関数,定数
{
if(calcstring[i].equals("pi")){// piを定数に置換
revisedcalcstring[revisedelementnumber] = Double.toString(StrictMath.PI);
revisedelementnumber++;
}
else
{
if(calcstring[i].equals("e"))
{// eを定数に置換
revisedcalcstring[revisedelementnumber] = Double.toString(StrictMath.E);
revisedelementnumber++;
}
else
{// pi,e以外の文字なら関数
revisedcalcstring[revisedelementnumber] = "1";// dummy
revisedcalcstring[revisedelementnumber+1] = calcstring[i];
revisedelementnumber+=2;
}
}
}
else
{
if(CheckCharType(calcstring[i].charAt(0))==LEFTPAREN)
{// 括弧を読み込んだ
String temp_st = new String();//先頭・末尾の括弧を取り除く
temp_st ="";// 一応初期化
for(int j = 1;j< calcstring[i].length()-1;j++)
{
temp_st += calcstring[i].charAt(j);
}
revisedcalcstring[revisedelementnumber] = Calculate(temp_st);//括弧内を計算した返り値を格納
revisedelementnumber++;
}
else
{// 関数(に見える演算子)や定数、括弧など以外の、特別な処理が必要無いもの
revisedcalcstring[revisedelementnumber] = calcstring[i];
revisedelementnumber++;
}
}
}
/*
この段階で識別子列は数字、演算子、数字、演算子…となっていなければならないが
例えば6sin(pi)などとすると6 1(<-dummy) sin piとなり
数字が二つ連続することがあるのでそれをチェック
(数字と演算子のどちらから始まるかはまだ不明)
*/
boolean elementisnumber;//読み込んだ識別子が数字であるかどうか
if(CheckCharType(revisedcalcstring[0].charAt(revisedcalcstring[0].length()-1))==NUMBER){elementisnumber = true;}
else{elementisnumber = false;}
for(int j = 1;j< revisedelementnumber;j++)
{
if(CheckCharType(revisedcalcstring[j].charAt(revisedcalcstring[j].length()-1))==NUMBER)
{
if(elementisnumber)
{// 一つ前の識別子と同じ
System.out.println("演算子が足りません。");
return VoidStrings();
}
elementisnumber = true;
}
else
{
if(!elementisnumber)
{
System.out.println("演算子が足りません。");
return VoidStrings();
}
elementisnumber = false;
}
}
//一番初めが "-"で始まると、演算子から始まるので具合が悪い
//ややこしいが、ひとつの識別子内で-valueとなっているのは問題ない(括弧内の計算での返り値はそうなることがある)
//ここで問題となるのは-sin()など、 - がひとつの文字列として入っている場合。
//このとき識別子列の先頭に"0"をつけて数字から始まるようにする。
int offset;
if(revisedcalcstring[0].equals("-"))
{
offset = 1;//先頭に文字列"0"を追加する分
}
else
{
offset = 0;
}
String[] ret = new String[revisedelementnumber+offset];//返り値のString[] でnullが無いものを作る
for(int j = offset;j< revisedelementnumber+offset;j++)
{ret[j] = revisedcalcstring[j-offset];}
if(revisedcalcstring[0].equals("-"))
{
ret[0] = "0";
}
if((CheckCharType(ret[0].charAt(ret[0].length()-1))!=NUMBER)
||(CheckCharType(ret[ret.length-1].charAt(ret[ret.length-1].length()-1))!=NUMBER))
{//先頭・末尾は必ず数字になることを確認
System.out.println("数字が足りません。");
return VoidStrings();
}
return ret;
}
protected String CreateTreeAndCalc(String[] revisedcalcstring)
{
/*以下の処理で行うこと
1.revisedcalcstringから演算子の優先順位に従って「解析木」(?)を作る
2.木の中で一番優先度の低いところまで移動
3.計算
---------------------------------------------------------------------------
注意:ネットで調べてみると逆ポーランド記法というのに変換してから
計算していくものも多いようなのですがここではそうしていません。
(プログラムを書いている途中で気づいたので。)
もし逆ポーランド記法を採用するならこの関数を書き換えれば動くとは思うのですが。
---------------------------------------------------------------------------
解析木…CTreeElementで構成される。それぞれのエレメントは親への手と右手、左手を持っていて
ほかのCTreeElementとつながることができる
木を組み立てる手順
以下では必ずrevisedcalcstringが数字、演算子、数字、演算子…、数字となっていることが前提。
一度に数字、演算子を読み込み、その直前に読み込んだ演算子との優先度(ID)を比較して
・読み込んだ演算子の優先度が前に読み込んだもの以上なら
読み込んだ演算子を前の演算子の右手につけ、その左手に読み込んだ数字をつける。
・読み込んだ演算子の優先度が前に読み込んだものよりも小さいなら
前の演算子の右手に読み込んだ数字をつけ、前の演算子の親をたどっていって
優先度がたどっていった演算子のそれより小さくなったらそこに挿入
(親が無いところまできたら親にする)
要するに階層が深くなるほど演算子の優先度が上がり、
演算子との対応が崩れないように木の末端に数字をつなげて木を作る。
計算については一番優先度の低いところからCTreeElement.Calurate()を実行してやると
深いところまで勝手に辿ってくれるようになっている
詳しくはCTreeElement.Calculate()を参照。
*/
int loopnumber= revisedcalcstring.length;
if(loopnumber == 1){return revisedcalcstring[0];}
// 数字一個だけのときは例外的に扱う
CTreeElement numberelement_0 = new CTreeElement(revisedcalcstring[0]);// 0番目の数字
CTreeElement currentsignelement = new CTreeElement(revisedcalcstring[1]);//前に読み込んだ演算子
numberelement_0.m_Parent = currentsignelement;
currentsignelement.m_LeftHand = numberelement_0;
for(int i = 2;i< loopnumber-2;i+=2)
{
CTreeElement tempnumberelement = new CTreeElement(revisedcalcstring[i]);
CTreeElement tempsignelement = new CTreeElement(revisedcalcstring[i+1]);//新しく読み込んだ演算子
if(currentsignelement.CheckSignType()<=tempsignelement.CheckSignType())
{//読み込んだ演算子の優先度が前に読み込んだもの以上
currentsignelement.m_RightHand = tempsignelement;
tempsignelement.m_Parent = currentsignelement;
tempsignelement.m_LeftHand = tempnumberelement;
tempnumberelement.m_Parent = tempsignelement;
}
else
{//読み込んだ演算子の優先度が前に読み込んだものよりも小さい
currentsignelement.m_RightHand = tempnumberelement;
tempnumberelement.m_Parent = currentsignelement;
while((currentsignelement.m_Parent!=null)&&((currentsignelement.CheckSignType())>(tempsignelement.CheckSignType())))
{// 演算子の親をたどっていく
currentsignelement = currentsignelement.m_Parent;
}
if(currentsignelement.m_Parent != null)
{// エレメントをエレメント同士の間に挿入する必要があるとき
if((currentsignelement.m_Parent).m_LeftHand == currentsignelement)
{//currentsignelementは親の左手についている
(currentsignelement.m_Parent).m_LeftHand = tempsignelement;
}
else{// 右手
(currentsignelement.m_Parent).m_RightHand = tempsignelement;
}
tempsignelement.m_Parent = currentsignelement.m_Parent;
}
currentsignelement.m_Parent = tempsignelement;
tempsignelement.m_LeftHand = currentsignelement;
}
currentsignelement = tempsignelement;
}
// revisedcalcstringの最後に残っている数字をつなげる
currentsignelement.m_RightHand = new CTreeElement(revisedcalcstring[loopnumber-1]);
(currentsignelement.m_RightHand).m_Parent = currentsignelement;
///木ができたので一番優先度の低いもの(ルート)に移動////////////////////////////////////////////////////////////////
while(currentsignelement.m_Parent !=null)
{
currentsignelement = currentsignelement.m_Parent;
}
///////////////////計算//////////////////////////////////////////////////////////////////////////////////////////////
return Double.toString(currentsignelement.Calculate());
}
}
/*
電卓プログラム ファイルその3
XXXXXX XXXXX
動作にはほかにCMainLoop.javaとCCalc.javaが必要です。
申し訳ありませんがShiftJISでかかれているのでコンパイル時にオプション-encoding SJISでエンコードをお願いします。
*/
import java.math.*;
import java.io.*;
class CTreeElement
{// CCalc.CreateTreeAndCalcで使われるクラス。解析木?を作る。
///(文字の種類)///////////////////////////////////////////////////////
final int NUMBER = 0;
final int SIGN = 1;// +,-,*,/,関数(プログラムでは見かけ上演算子として扱う)
///演算子の種類(IDになるとともに、その演算子の優先度としても扱われる(数字が大きいほど優先))////////
public static final int MINUS = 1;
public static final int PLUS = 2;
public static final int SLASH =3;
public static final int CROSS = 4;
public static final int SIN = 5;
public static final int COS = 6;
public static final int TAN = 7;
public static final int ASIN = 8;
public static final int ACOS = 9;
public static final int ATAN = 10;
public static final int SQRT = 11;
public static final int EXP = 12;
public static final int LOG = 13;
public static final int ABS = 14;
public static final int NONE = 0;// 不正な関数など
////////////////////////////////////////////////////////////////////////////////
public CTreeElement m_Parent,m_LeftHand,m_RightHand;
public String m_Body;
public CTreeElement(String body)
{
m_Body = body;
}
public double Calculate()
{
if(CheckIfNumber())
{
return Double.parseDouble(m_Body);
}
else
{/*m_RightHandやm_LeftHandのCalculateを実行することで
それらよりも階層が深い(優先度が高い)ものがイモヅル式に先に計算されて帰ってくるので
本体は自分の計算に専念するだけでよい*/
switch(CheckSignType())
{// それぞれのcase は必ずreturnを含むのでbreakは不要
case PLUS:{return m_LeftHand.Calculate()+m_RightHand.Calculate();}
case MINUS:{return m_LeftHand.Calculate()-m_RightHand.Calculate();}
case SLASH:{double temp_slash = m_RightHand.Calculate();
if(temp_slash==0.0){ System.out.println("0による除算が含まれています。");return 0.0;}
else{return m_LeftHand.Calculate()/temp_slash;}
}
case CROSS:{return m_LeftHand.Calculate()*m_RightHand.Calculate();}
// これ以降の演算子(見かけは関数)では左手のデータはダミーなので無視して構わない
case SIN:{return StrictMath.sin(m_RightHand.Calculate());}
case COS:{return StrictMath.cos(m_RightHand.Calculate());}
case TAN:{return StrictMath.tan(m_RightHand.Calculate());}
case EXP:{return StrictMath.exp(m_RightHand.Calculate());}
case LOG:{double temp_log = m_RightHand.Calculate();
if(temp_log>0){return StrictMath.log(temp_log);}
else{System.out.println("logの引数が0以下です。");return 0.0;}
}
case SQRT:{return StrictMath.sqrt(m_RightHand.Calculate());}
case ABS:{return StrictMath.abs(m_RightHand.Calculate());}
case ASIN:{
double temp_asin = m_RightHand.Calculate();
if(StrictMath.abs(temp_asin)<=1){return StrictMath.asin(temp_asin);}
else{System.out.println("asinの引数の大きさが1よりも大きくなっています。");return 0.0;}
}
case ACOS:{
double temp_acos = m_RightHand.Calculate();
if(StrictMath.abs(temp_acos)<=1){return StrictMath.acos(temp_acos);}
else{System.out.println("acosの引数の大きさが1よりも大きくなっています。");return 0.0;}
}
case ATAN:{return StrictMath.atan(m_RightHand.Calculate());}
default:{
System.out.println("不正な演算子または関数が含まれています。");
return 0.0;
}
}
}
}
protected boolean CheckIfNumber()
{//m_Bodyが数字かどうか調べる
char c = m_Body.charAt(m_Body.length()-1);//m_Bodyの末尾の文字から判断
if((c == '0')||(c == '1')||(c == '2')||(c == '3')||(c == '4')
||(c == '5')||(c == '6')||(c == '7')||(c == '8')||(c == '9')||(c == '.'))
{return true;}
return false;
}
public final int CheckSignType()
{
if(m_Body.equals("+")){return PLUS;}
if(m_Body.equals("-")){return MINUS;}
if(m_Body.equals("/")){return SLASH;}
if(m_Body.equals("*")){return CROSS;}
if(m_Body.equals("sin")){return SIN;}
if(m_Body.equals("cos")){return COS;}
if(m_Body.equals("tan")){return TAN;}
if(m_Body.equals("exp")){return EXP;}
if(m_Body.equals("log")){return LOG;}
if(m_Body.equals("sqrt")){return SQRT;}
if(m_Body.equals("abs")){return ABS;}
if(m_Body.equals("asin")){return ASIN;}
if(m_Body.equals("acos")){return ACOS;}
if(m_Body.equals("atan")){return ATAN;}
return NONE;
}
}
きちんと字句解析をして構文木まで作って計算しているのにはびっくりしました