11/8 簡単なグラフィクス(1)


前回までの補足


前回の課題について


11/1課題問題

分数を扱うクラス Fraction を定義して,四則演算のためのメソッドも書きなさい. その上で,前回の課題の電卓プログラムを分数を扱えるように改造しなさい. ファイルは,自分のホー ムディレクトリの下に java というディレクトリを作成して,その下に Kadai1101.java という名前で作成すること(クラス名もKadai1101という名前 で作成すること).

実行例を示す.人間による入力は下線をつけて表わす.
dell.tanaka.ecc.u-tokyo.ac.jp% java Kadai1101
3/10
3/10
*
3
9/10
+
3/10
6/5
/
-30
-1/25
-
3/100
-7/100
=

解答例(1)
//前回のを使いまわしたので早くできました。
import java.io.*;

class Fraction{
  int nume, deno;
  Fraction(String suji){
    int n=suji.indexOf('/');
    if(n==-1){
      nume=Integer.parseInt(suji);
      deno=1;
    }
    else{
      nume=Integer.parseInt(suji.substring(0,n));
      deno=Integer.parseInt(suji.substring(n+1));
    }
  }
  Fraction(int n,int m){
    nume=n;
    deno=m;
  }
  Fraction add(Fraction p){
    Fraction w =new Fraction(this.nume*p.deno +this.deno*p.nume,this.deno*p.deno
);
    w = w.div();
    return w;
  }
  Fraction mul(Fraction p){
    Fraction w =new Fraction(this.nume*p.nume,this.deno*p.deno);
    w = w.div();
    return w;
  }
  Fraction div(){
    int n=gcd(Math.abs(this.nume),this.deno);
    Fraction w =new Fraction(this.nume/n,this.deno/n);
    return w;
  }
  static int gcd(int n,int m){
    int r=n%m;
    if(r==0) return m;
    else return gcd(m,r);
  }
}
class Kadai1101{ 
  public static int sign(String sign){ 
    if ( sign.equals("=") ) return 0;
    else if ( sign.equals("+") ) return 1;
    else if ( sign.equals("-") ) return 2;
    else if ( sign.equals("*") ) return 3;
    else return 4;
  }
  public static void main(String[] args)throws IOException{
    BufferedReader d=new BufferedReader(new InputStreamReader(System.in));
    Fraction Fra1,Fra2;
    String F1=d.readLine();
    Fra1=new Fraction(F1);
    Fra1=Fra1.div();
    if(Fra1.deno==1){
      System.out.println(Fra1.nume);
    }
    else{
      System.out.println(Fra1.nume+"/"+Fra1.deno);
    }
    for(;;){
      String s=d.readLine();
      int n = sign(s);
      if ( n==0 ) break;
      String F2=d.readLine();
      Fra2 = new Fraction(F2);
      switch ( n ){
        case 2:
          Fra2.nume=-Fra2.nume;
        case 1:
          Fra1=Fra1.add(Fra2);
          break;
        case 4:
          int x = Fra2.nume; int y = Fra2.deno;
          Fra2=new Fraction(y,x);
        case 3:
          Fra1=Fra1.mul(Fra2);
      }
      if(Fra1.deno==1){
        System.out.println(Fra1.nume);
      }
      else{
        System.out.println(Fra1.nume+"/"+Fra1.deno);
      }
    }
  }
}

解答例(2)
import java.io.*;

class Kadai1101{
    public static int gcd(int i, int j){
      int r=i%j;
      if(r==0) return j;
      else return gcd(j,r);}


    public static void main(String[] args) throws IOException {
      BufferedReader d=new BufferedReader(new InputStreamReader(System.in));
      int g,s,c=0,b,l,an,ad,sumn=0,sumd=1;
    for(s=0;s<1;s=s+0){
      String t=d.readLine();
    if(t.equals("+")){ 
      c=4;
      continue;}
    if(t.equals("-")){
      c=1;
      continue;}
    if(t.equals("*")){
      c=2;
      continue;}
    if(t.equals("/")){ 
      c=3;
      continue;}
    if(t.equals("=")){ 
      break;}
      b=t.indexOf("/");
    if(b==-1){
      an=Integer.parseInt(t);
      ad=1;
      }
    else{
      l=t.length();
      an=Integer.parseInt(t.substring(0,b));
      ad=Integer.parseInt(t.substring(b+1,l));}
    if(c==4){
      sumn=sumn*ad+an*sumd;
      sumd=sumd*ad;
      }
    if(c==1){
      sumn=sumn*ad-an*sumd;
      sumd=sumd*ad;
      }
    if(c==2){
      sumn=sumn*an;
      sumd=sumd*ad;
    }
    if(c==3){
      sumn=sumn*ad;
      sumd=sumd*an;
      }
    if(c==0){
      sumn=an;
      sumd=ad;
    }
    if(b==-1){
      an=Integer.parseInt(t);
      ad=1;
      }
    g=gcd (sumn,sumd);
    if(sumd==g){
      System.out.println(sumn/g);}
    else{
    System.out.println(sumn/g+"/"+sumd/g);}
    }}}


講評

解答例(1)は,Fractionの四則演算を作らずに減算,除算は負数の加算,逆数 の乗算でやっているところが面白いので紹介しました.

解答例(4)はプログラムとしてはあっていますが,残念ながら「分数を扱うク ラス Fraction」という条件は満たしていません.

全般的な注意ですが,適切な字下げ(インデント)をしていない人がいました が,バグをみつけにくくする原因の一つなので注意してください.あと,同じ ようなコードが繰り返し出現する場所ではメソッドにまとめられないか検討し て見てください.


オプション課題

上の課題だけでは満足できない人は,例外処理を入れて,間違った入力を入 れた時に,的確なメッセージを出して実行を続けられるようにプログラムを改 造して(構文を変更してもかまわない), 11/6(月)の21:00までに,lectures.g00.cp1-ktanaka-W-Wed-5 のニュースグ ループに投稿すること.

解答例(1)

最初の課題で思いのほか時間をくってしまったので
ついでということでさっさとオプションまでやってみました。
実際に例外処理が必要なのは数値代入の部分だけで、
演算子に関しては簡単な処理で終わらせられたので
けっこうすぐにできました。

上記の理由もあり、ソースは通常課題のものと
ほとんど変わりません。説明もそのままつけて
おきました。
前回のオプションはやらなかったので、時間があれば
これをちょっとだけ高機能化してみようかと思います。


//全体の処理の流れは前回とあまり変わっていませんが、
//教官の指摘にしたがってラベルつきのbreakを使ったところ
//少しすっきりしたプログラムになったように思います。

//演算結果は括弧つきで表示されます。

//続いて分数クラスFractionについて。
//メソッドは加減乗除のほか、文字列解釈/代入処理と
//共通で必要と思われる通分/約分処理を用意しました。
//このおかげか、加減乗除のメソッドは各3行で
//済ますことができました。

//通分は最小公倍数を探すやり方でも良かったのですが、
//こっちのほうが分かりやすく早いということで
//単純に分母を掛け合う形にしました。
//でもオーバーフローの可能性を考えると
//最小公倍数を使った方がいいのかもしれませんね。

//また、符号は必ず分子につけるようにしました。
//ごちゃ混ぜだと見にくいですから。

import java.io.*;
//分数のクラス
class Fraction
{
  public int nume, deno; //分子,分母
  public Fraction(int n, int d) //コンストラクタ
  {
    nume = n;
    deno = d;
  }
  public void let(String str) //代入処理
  {
    if(str.indexOf('/')==-1) //入力が分数でない場合
    {
      nume = Integer.parseInt(str);
      deno = 1;
      return;
    }
    nume = Integer.parseInt(str.substring(0,str.indexOf('/')));
    deno =
Integer.parseInt(str.substring(str.indexOf('/')+1,str.length()));
    if(deno<0)//分母にマイナス符号はつけない
    {
      nume *= -1;
      deno *= -1;
    }
  }
  public void tuubun(Fraction frct) //通分処理
  {
    //わざわざ分母の最小公倍数を探したりせず
    //単純に互いの分母を掛け合って通分
    int tmpd;
    tmpd = deno;
    nume *= frct.deno;
    deno *= frct.deno;
    frct.nume *= tmpd;
    frct.deno *= tmpd;
  }
  public void yakubun() //約分処理
  {
    //素直に互除法
    int a, b, r;
    a = Math.abs(nume);
    b = Math.abs(deno);
    while(b>0)
    {
      r = a%b;
      a=b;
      b=r;
    }
    nume /= a;
    deno /= a;
    if(deno<0) //分母にマイナス符号はつけない
    {
      nume *= -1;
      deno *= -1;
    }   
  }
  public void plus(Fraction frct) //加算
  {
    this.tuubun(frct);
    nume += frct.nume;
    this.yakubun();
  }
  public void minus(Fraction frct) //減算
  {
    this.tuubun(frct);
    nume -= frct.nume;
    this.yakubun();
  }
  public void multiple(Fraction frct) //乗算
  {
    nume *= frct.nume;
    deno *= frct.deno;
    this.yakubun();
  }
  public void divide(Fraction frct) //除算
  {
    nume *= frct.deno;
    deno *= frct.nume;
    this.yakubun();
  }
}

class Kadai1101_opt
{
  public static void main(String[] args) throws IOException
  {
    BufferedReader buf = new BufferedReader(new
InputStreamReader(System.in));
    Fraction x , y;
    String a;
    String s[] = {"+","-","*","/","="};
    int type = 0;
    x = new Fraction(0,1);
    y = new Fraction(0,1);

    for(;;) //数値入力
    {
      System.out.println("数値を入力してください");
      try
      {
        x.let(buf.readLine());
        break;
      } catch(java.lang.NumberFormatException e)
      {
        System.out.println("数値入力が正しくありません");
        System.out.println("入力は (整数)/(整数) または (整数) の形式で
す");
        continue;
      }
    }

    toploop: for(;;)
    {
      System.out.print("[ "+x.nume);
      //分子が1でないときのみ分子を表示
      if(x.deno!=1) System.out.print("/"+x.deno);
      System.out.println(" ]");
      
      loop1: for(;;) //演算子入力
      {
        System.out.println("演算子を入力してください");
        a = buf.readLine();
        for(int i=0;i<5;i++)
        {
          if(a.equals(s[i]))
          {
            type=i;
            if(type==4) break toploop; //終了
            break loop1;
          }
        }
        System.out.println("入力された演算子は正しくありません");
        System.out.println("+ - * / =(終了) のいずれかを入力してください
");
      }
      for(;;) //数値入力
      {
        System.out.println("数値を入力してください");
        try
        {
          y.let(buf.readLine());
          break;
        } catch(java.lang.NumberFormatException e)
        {
          System.out.println("数値入力が正しくありません");
          System.out.println("入力は (整数)/(整数) または (整数) の形式
です");
          continue;
        }
      }
      switch(type) //演算子の種類によって分岐
      {
        case 0:
          x.plus(y);
          break;
        case 1:
          x.minus(y);
          break;
        case 2:
          x.multiple(y);
          break;
        case 3:
          x.divide(y);
          break;
      }
    }
  }
}
// 分数電卓のオプション課題、ゼロ除算と分母ゼロ代入の
// エラー処理を忘れていました......

解答例(2)
11月1日のオプション課題  0XXXXXX XXXX

基本的な機能は一切変えずにエラー処理のみを完璧(?)にした(つもりの)プロ
グラムをつくりました。

エラー処理は次の点に関して行った:
 数値でないものの入力(数値入力時)
 演算子でないものの入力(演算子入力時)
 0による除算
 0を分母とする分数の入力

オーバーフローなどは処理していません。

=====以下、コード (OptKadai1101.java)

import java.io.*;

abstract class FractionException extends Exception // 分数の例外をまとめて扱う
{
  abstract public String toString();
}

class ZeroDenomException extends FractionException // 例外クラス
{
  public String toString()
  {
    return "分母を0にすることはできません。";
  }
}

class FractionDivByZeroException extends FractionException // 例外クラス
{
  public String toString()
  {
    return "0で除算をすることはできません。";
  }
}

class OptFraction // 分数クラス
{
  int n, d; // 分子、分母

  /*
    OptFraction(int num, int den) throws FractionException // コンストラクタ
    // 実際には使っていない
    {
      n = num;
      d = den;
      if (d == 0) // 分母に0は困る
        {
          throw new ZeroDenomException();
        }
      reduce();
    }
    */

  OptFraction() // コンストラクタ(0に初期化)
  {
    n = 0;
    d = 1;
  }

  OptFraction(String str) throws FractionException
  // コンストラクタ(文字列から変換)
  {
    int i = str.indexOf('/');
    if (i == -1) // '/'が含まれていないときは整数
      {
	n = Integer.parseInt(str);
	d = 1;
      }
    else // 分数は'/'の前後を切り出して読む
      {
	n = Integer.parseInt(str.substring(0, i));
	d = Integer.parseInt(str.substring(i+1, str.length()));
	if (d == 0) // 分母に0は困る
	  {
	    throw new ZeroDenomException();
	  }
      }
    reduce();
  }

  public static OptFraction parseFraction(String str) throws FractionException
  // 文字列からの変換
  {
    return new OptFraction(str);
  }

  public String toString() // 表示用に文字列に変換する
  {
    if (d == 1) // 整数のときは
      {
	return String.valueOf(n); // 分子のみ表示
      }
    else
      {
	return n + "/" + d;
      }
  }

  public OptFraction add(OptFraction f) // 加算
  {
    OptFraction r = new OptFraction();
    r.n = n*f.d + d*f.n;
    r.d = d*f.d;
    r.reduce();
    return r;
  }

  public OptFraction sub(OptFraction f) // 減算
  {
    OptFraction r = new OptFraction();
    r.n = n*f.d - d*f.n;
    r.d = d*f.d;
    r.reduce();
    return r;
  }

  public OptFraction mul(OptFraction f) // 乗算
  {
    OptFraction r = new OptFraction();
    r.n = n*f.n;
    r.d = d*f.d;
    r.reduce();
    return r;
  }

  public OptFraction div(OptFraction f) throws FractionDivByZeroException
  // 除算
  {
    if (f.n == 0) // 0除算
      {
	throw new FractionDivByZeroException();
      }
    OptFraction r = new OptFraction();
    r.n = n*f.d;
    r.d = d*f.n;
    r.reduce();
    return r;
  }

  void reduce() // 約分する内部手続き。n==0でも正常に動作し、d=1にする。
  {
    int g = gcd(Math.abs(n), Math.abs(d));
    n /= g;
    d /= g;

    if (d < 0) // 符号は常に分子に含める
      {
	n = -n;
	d = -d;
      }
  }

  static int gcd(int a, int b) // 最大公約数を求める内部手続き
  {
    int c = a % b;
    if (c == 0)
      {
	return b;
      }
    else
      {
	return gcd(b, c);
      }
  }
}

class OptKadai1101
{
  static String readLineOrExit(BufferedReader br) throws IOException
  // 1行読み込み、"="だったら終了する
  // このメソッドを使うことにより、読み込み時にいちいちチェックしなくて済む
  {
    String s = br.readLine();
    if (s.equals("="))
      {
	System.exit(0);
      }
    return s;
  }

  static OptFraction readFraction(BufferedReader br) throws IOException
  // エラー処理つき分数入力
  {
    OptFraction r;
    while (true)
      {
	try
	  {
	    r = OptFraction.parseFraction(readLineOrExit(br));
	    // 最初の数値を読み、アキュムレータを初期化
	    break; // エラーなくたどり着いたらwhileループを抜ける
	  }
	catch (NumberFormatException nfe)
	  {
	    System.err.println("分数または整数を入力してください。");
	  }
	catch (FractionException fe)
	  {
	    System.err.println(fe);
	    System.err.println("分数または整数を入力してください。");
	  }
      }
    return r;
  }

  public static void main(String[] args) throws IOException
  {
    BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
    String inputOp;
    OptFraction inputNum; // 入力された数値
    OptFraction acc;  // 累算器 (アキュムレータ)

    acc = readFraction(br);
    // 最初の数値を読み、アキュムレータを初期化
    System.out.println(acc);

    while (true) // 無限ループ
      {
	inputOp = readLineOrExit(br); // 演算子を読み込む
	if (inputOp.equals("+")) // "+"なら
	  {
	    inputNum = readFraction(br);
	    acc = acc.add(inputNum); // 数値を読み、加える。以下同様
	    System.out.println(acc);
	  }
	else if (inputOp.equals("-"))
	  {
	    inputNum = readFraction(br);
	    acc = acc.sub(inputNum);
	    System.out.println(acc);
	  }
	else if (inputOp.equals("*"))
	  {
	    inputNum = readFraction(br);
	    acc = acc.mul(inputNum);
	    System.out.println(acc);
	  }
	else if (inputOp.equals("/"))
	  {
	    while (true)
	      {
		inputNum = readFraction(br);
		try // 0除算の可能性あり
		  {
		    acc = acc.div(inputNum);
		    break;
		  }
		catch (FractionException fe)
		  {
		    System.err.println(fe);
		    System.err.println("分数または整数を入力してください。");
		  }
	      }
	    System.out.println(acc);
	  }
	else // +, -, *, / のいずれでもないならエラー
	  {
	    System.err.println("演算子を入力してください。");
	  }
      }
  }
}

=====終り

-- 

解答例(3)
エラー処理の他に、とりあえず、
100/4 + 2*(3 - 2000)
みたいに式を1行に書けるようにしました。("="は不要。というか書くとエラー)
それから、"quit"と入力するまで何度でも連続して計算ができます。
扱う数が有理数だけで、sin() とか log() とかを考えないで済むので、意外と楽でした。

以下、コードです。

----------------------------------------------------

import java.io.*;


class Calculator {
  static BufferedReader d;
  static char[] expChr; // 入力された式を解析するための配列
  
  public static void main(String args[]) throws IOException {
    String s;
    Fraction result;
    
    d = new BufferedReader(new InputStreamReader(System.in));
    
    for (;;) {
      s = d.readLine();
      if (s.compareTo("quit") == 0) {
	break;
      }
      else {
	try {
	  result = evalFraction(s);
	}
	catch(Exception e) {
	  System.err.println(" = E");
	  continue;
	}
	/* 結果表示 */
	System.out.println(" = " + result.toString());
      }
    }
    
    return;
  }
  
  static Fraction evalFraction(String expStr) throws Exception {
    StringBuffer buf = new StringBuffer(expStr);
    int depth = 0;
    boolean balance = true;
    
    // ()の対応チェック、およびスペースの除去
    for(int i=buf.length()-1; i >= 0; i--) {
      switch(buf.charAt(i)) {
      case ' ':
      case '\n':
	buf.deleteCharAt(i); break;
      case ')':
	depth++; break;
      case '(':
	depth--;
	if (depth < 0)
	  balance = false;
	break;
      }
    }
    if (depth != 0 || !balance) {
      System.out.println("Unbalanced '(' ')'");
      throw new Exception();
    }
    expChr = buf.toString().toCharArray();
    // 再帰開始
    return evalFraction(0, expChr.length);
  }
  static Fraction evalFraction(int l, int r) {
    int op[] = new int[] {-1, -1}; // op[0]は"*,/"用、op[1]は"+,-"用
    int i, j;
    char c;
    
    while (expChr[l]=='(' && expChr[r-1]==')') { // ()でくくられていればそれをとる
      l++;
      r--;
    }
    // 結合順位の一番低い演算子を探す(結合順序は左から)
    for (i=r-1; i>=l; i--) { // 右から探す。")"に当たるか、+, - が見つかればループを抜ける。
      c = expChr[i];
      if (c == ')')
	break;
      else if (op[0] < 0 && (c=='*' || c=='/'))
	op[0] = i;
      else if (c=='+' || c=='-') {
	op[1] = i;
	break;
      }
    }
    if (op[1] < 0) { // 右から探して +, - が見つからなかった場合
      for (j=l; j 数字のみ
	return new Fraction(new String(expChr, l, r-l));
      }
      else {
	switch(expChr[op[0]]) {
	case '*':
	  return evalFraction(l, op[0]).mltp(evalFraction(op[0]+1, r));
	case '/':
	  return evalFraction(l, op[0]).div(evalFraction(op[0]+1, r));
	}
      }
    }
    else { // +, - がある
      if (op[1] == l) { // 単項の +, -
	switch (expChr[op[1]]) {
	case '-':
	  return evalFraction(op[1], r);
	case '+':
	  return evalFraction(op[1]+1, r);
	}
      }
      else {
	switch(expChr[op[1]]) {
	case '+':
	  return evalFraction(l, op[1]).plus(evalFraction(op[1]+1, r));
	case '-':
	  return evalFraction(l, op[1]).sub(evalFraction(op[1]+1, r));
	}
      }
    }
    return new Fraction("0"); // <- これは実行されることはない
  }
}


class Fraction {
  int n;  // 分子
  int d;  // 分母
  
  Fraction(int numerator, int denominator)  {
    n = numerator;
    d = denominator;
    if (d == 0) System.err.println("Zero denominator.");
    reduction();
  }
  
  Fraction(String s) {
    int i = s.indexOf('/');
    if (i < 0) {
      n = Integer.parseInt(s);
      d = 1;
    }
    else {
      n = Integer.parseInt(s.substring(0, i));
      d = Integer.parseInt(s.substring(i+1, s.length()));
      if (d == 0) System.err.println("Zero denominator.");
    }
    reduction();
  }
  
  public String toString() {
    String s;
    if (n == 0 || d == 1)
      s = new String(n+"");
    else
      s = new String(n+"/"+d);
    return s;
  }
  
  public void reduction() {  // 通分
    int p, sign=1;
    if (n != 0) {
      // マイナスはあとで分子にまとめる
      if (n < 0) { sign *= -1; n = -n; }
      if (d < 0) { sign *= -1; d = -d; }
      // p = 分母と分子の最大公約数
      p = gcd(Math.max(n, d), Math.min(n, d));
      n /= p;
      d /= p;
      n *= sign;
    }
  }
  
  public Fraction inverse() {  // 逆数をとるメソッド
    return new Fraction(d, n);
  }
  
  int gcd(int a, int b) {  // a > b でなければならない。
    int q = a % b;
    if (q == 0) return b;
    else return gcd(b, q);
  }
  
  public Fraction plus(Fraction f) {  // 足し算
    return new Fraction(n*f.d + f.n*d, d*f.d);
  }
  public Fraction plus(int i) {
    return new Fraction(this.n + i*this.d, this.d);
  }
  
  public Fraction sub(Fraction f) {  // 引き算
    return plus( f.mltp(-1) );
  }
  public Fraction sub(int i) {
    return plus(-i);
  }
  
  public Fraction mltp(Fraction f) {  // 掛け算
    return new Fraction(this.n*f.n, this.d*f.d);
  }
  public Fraction mltp(int i) {
    return new Fraction(this.n*i, this.d);
  }
  
  public Fraction div(Fraction f) {  // 割り算
    return mltp(f.inverse());
  }
  public Fraction div(int i) {
    return new Fraction(this.n, this.d*i);
  }
}

----------------------------------------------------


追記: 四則演算のintによるオーバーロードは、結局使わなかった。

講評

解答例(1)は,素直に手堅く書いてあります.それに対して解答例(2)はプロ好 みのお金の取れるプログラムですね.解答例(3)は,数式のparse(パース)とい う難しい課題にチャレンジしてくれました.parseのアルゴリズムは自然な発 想に基づくものではありますが,対象とする式が大きく成っていった時(ある いは,プログラムのparseに適用しようとした時)には効率が悪いので,別のア ルゴリズムが使われています.この講義でも後半に時間が余ったら,parserに ついてちょっと触れたいと思います.

今日の課題

上のURLは,今日(11/8)の17:00 までは
Forbidden

You don't have permission to access /~ktanaka/programming00/kadai1108.html on this server.
とうメッセージが出てアクセ スできないはずである.17:00以降にも同様のエラーが出る時は,Shiftキーを 押しながら,再読み込み(Reload)を押してみること.
次へ進む