4日目

4日目

問題の解答例とその解説

あくまで解答「例」であり、この通りである必要はない。

問題 4-1

曜日を計算するという行為は、ある1つの関数にまとめた方がよいと考えられるので、 最初に曜日を計算する過程を関数として表現することを考えよう。

この関数を youbi_keisan と名付けることにすると、 この関数の引数は「年」、「月」、「日」という整数が3つである。 曜日も、整数で返すので、関数宣言は次のようになる。

int youbi_keisan(int, int, int);

次に関数の本体部分である。 最初に関数の引数や、内部で使用する変数の宣言を行う。 文章に出てくる、ym をそのまま関数の引数としても良いが、 ここでは別に用意することとし、引数は、year, month として受けることにした。 内部で使用する変数は、文章を良く読むと3つ (y, m, d) あるので、つぎのように書ける。

  int youbi_keisan(int year, int month, int day) {
      int y, m, d;

同じ型の変数を複数宣言するときには、, (カンマ)で区切って並べて宣言することができる。

では、順に、書かれた文章を C 言語に翻訳してみよう。

  1. 整数変数 y に年を代入する
  2. 整数変数 m に月を代入する
  3. 整数変数 d に日を代入する

    年は変数 year に、月は month に、日は dayに入っているのであった。 両辺とも変数だが、どちらの値をどちらの変数にいれるのかに気をつければ、次のように表現できる。

       y = year;    /* (1) */
       m = month;   /* (2) */
       d = day;     /* (3) */
      
  4. y から 1600を引く

    細かに日本語を補えば、「y から 1600 を引いた値を y に代入する」という事である。 (はじめに y に 2000 が入っていた場合、この文章の操作を行ったあと、y には400が入っているはずである)

    したがって、この文章は、つぎのように書ける。

       y = y - 1600;
      
  5. 次のような条件に基づく処理を行う。
  6. 条件処理であるから、if を使うのはすぐ想像がつくであろう。 「m が 1 または 2の場合」とは、「m が 1の場合 または m が 2の場合」であることや、 1つ前と同様、「m に 9 を足し」とは、「m に9 を足したものを m に代入し」の意味である事が判れば 文章をそのまま C 言語に直すと次のようになる。

       if (m == 1 || m == 2) {
           m = m + 9;
           y = y - 1;
       } else {
           m = m - 3;
       }
     

    等しいかどうかを判定するのは、== と "=" を2つ並べるのであるが、1つしか書かないという間違いは良くしてしまうので注意。

  7. d に(365.25*y)を加える
  8. d から (y/100) を引く
  9. d に (y/400) を加える
  10. さらに d に 30.6*m + 2.5を加える

    そのまま。順番にひとつづつ書いていくと次のようになる。

        d = d + 365.25 * y;    /* (6) */
        d = d - (y/100);       /* (7) */
        d = d + (y/400);       /* (8) */
        d = d + 30.6*m + 2.5;  /* (9) */
     
  11. 得られた d を7で割った余りは、上で決めたルールにしたがった曜日を表す数字である。

    d を 7 で割った余りは d % 7 であるから、その値を return 文に渡してやり、完成。

         return (d % 7);
     }
     

全部まとめると、youbi_keisan は次のようになる。

/*
 * 与えられた年月日の曜日を計算する
 */
int youbi_keisan(int year, int month, int day)
{
	int y, m, d;

	y = year;
	m = month;
	d = day;

	y = y - 1600;
	if (m == 1 || m == 2) {
		m = m + 9;
		y = y - 1;
	} else {
		m = m - 3;
	}

	d = d + 365.25*y;
	d = d - y/100;
	d = d + y/400;
	d = d + 30.6*m + 2.5;

	return (d % 7);
}

さて、feburary と、youbi_keisan を合わせると、calendar 関数を年、月 を引数に呼べるようになる。 先週作成したmycal5.cmycal6.cという名前でコピーして書き直し作業を行おう。 最初に calendar 関数の前の部分のコメントを修正しよう。 第1引数は、西暦で表した年、第2引数は月とする。

もとの引数 youbi, nissu は、内部で使う変数として宣言しなおし、引数を year, month とすることにしよう。 そして、kaigyo = (7 - youbi) % 7; の文より前で、 year と month から、youbi と nissu を計算してやれば、修正は終りである。

  1. nissu

    nissuは、月に含まれる日の数であった。 したがって、2月以外は「決まっている」ので配列から読み出せばよいし、2月の場合は関数 feburary を使えば良い。

      int mdays[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
    
      /* 目的の月の日数を求める */
      if (month != 2) {
          /* 2月以外ならば決まっているので配列から読み出す */
          nissu = mdays[month];
      } else {
          /* 2月は、年によって異なるので関数 feburary を呼び出す*/
          nissu = feburary(year);
      }
      
  2. youbi

    とっても簡単である。上で作った youbi_keisan関数を呼び出すだけで良い。 月の最初の日の曜日が知りたいのであるから、3つ目の引数には 「1」を指定する。

        youbi = youbi_keisan(year, month, 1);
      

結局、mycal6.c としての解答例は次のようなものになる。 存在しない月はエラーとしたり、先頭部分に何月のカレンダか表示したり、曜日を表すヘッダをつけたりしてみた。 また、mainでの calendar の呼び出し部分もきちんと修正すること。

/* mycal6.c */
#include <stdio.h>

/* 関数宣言 */

int feburary(int);                /* 年         → 2月の日数 */
int youbi_keisan(int, int, int);  /* 年, 月, 日 → 曜日 */
int calendar(int, int);

int main()
{
	calendar(2007,11);  /* 2007年11月 */
	printf("\n");
	calendar(2007,12);  /* 2007年12月 */
	printf("\n");
	calendar(2008,1);  /* 2008年 1月 */
	printf("\n");
}


/*
 * 与えられた年の2月の日数を返す。
 */
int feburary(int year) {
	/*
	 * 閏年かどうかの判別ルール
	 *
	 * 1 西暦年数が4で割りきれなければ平年
	 * 2 西暦年数が400で割りきれると閏年
	 * 3 西暦年数が400では割りきれず、100で割りきれると平年
	 * 4 残りは閏年
	 */

	int mday;

	if ((year % 4) != 0) {
		mday = 28; /* 平年 */
	} else if ((year % 400) == 0) {
		mday = 29; /* 閏年 */
	} else if ((year % 100) == 0) {
		mday = 28; /* 平年 */
	} else {
		mday = 29;  /* 閏年 */
	}

	/* return の引数が呼び出し場所に代入される */
	return(mday);
}

/*
 * 与えられた年月日の曜日を計算する
 */
int youbi_keisan(int year, int month, int day)
{
	int y, m, d;

	y = year;
	m = month;
	d = day;

	y = y - 1600;

	if (m == 1 || m == 2) {
		m = m + 9;
		y = y - 1;
	} else {
		m = m - 3;
	}
	d = d + 365.25*y;
	d = d - y/100;
	d = d + y/400;
	d = d + 30.6*m + 2.5;

	return (d % 7);
}


/*
 * カレンダを表示する。
 * 第1引数は西暦で表した年、第2引数は月

 * 曜日は次のルールで数字に置き換える
 * 日曜日 → 0
 * 月曜日 → 1
 * ...
 * 土曜日 → 6
 */

int calendar(int year, int month)
{
	int i;
	int kaigyo; /* 最初の土曜日を7で割った余 == 改行条件*/

	int youbi; /* 1日の曜日 */
	int nissu; /* その月の日数 */
	/* あらかじめ分かっている各月の日数 */
	int mdays[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };


	if (month > 12 || month < 1) {
		/* そんな月は存在しない */
		printf("Illegal month %d\n", month);
		return (1);  /* カレンダを書けなかった (エラー) */
	}

	/* 目的の月の 1日の曜日を求める */
	youbi = youbi_keisan(year, month, 1);

	/* 目的の月の日数を求める */
	if (month != 2) {
		/* 2月以外ならば決まっているので配列から読み出す */
		nissu = mdays[month];
	} else {
		/* 2月は、年によって異なるので関数 feburary を呼び出す*/
		nissu = feburary(year);
	}

	kaigyo = (7 - youbi) % 7;

	/* いつのカレンダなのかを表示 */
	printf("        %4d/%2d\n", year, month);

	/* 簡単に何曜日かを判るようにヘッダを表示 */
	printf("  S  M Tu  W Th  F  S\n");

	/* 月始めの「空白」 */
	for (i = 0; i < youbi; i = i + 1) {
		printf("   ");
	}

	/* 日付の表示 */
	for (i = 1; i <= nissu; i = i + 1) {
		printf(" %2d", i);

		/* 「土曜日」ならば、改行する */
		if ((i % 7) == kaigyo) {
			printf("\n");
		}
	}

	/* 最後には改行 */
	printf("\n");

	return (0); /* カレンダ表示が成功したことを意味する */
}

問題 4-2

変数に型があったように、 プログラム中に出現するデータにも型が存在する。 同じ型の計算 (int と int など) では問題がないが、異なる型の場合にはルールがなければ計算結果が異なってしまう。 C では、演算を行う場合には、精度の高い方に揃えて演算が行われる。

例えば、次のようなプログラムを考えよう。

   int i, j, k;

   i = 5;
   j = 2;
   k = i/j + i/j

   printf("%d\n", k);

このプログラムでは i, jの両変数とも int 型であるから 変数 k への代入文はすべて整数で演算され printf は 4 と表示する。 (i/j = 5/2 = 2 である)

もし、j が double 型であったらどうなるだろうか。 この場合、 i/j の演算が、int型/double型 であるため、自動的に double型/double型 となるように読替え(cast, 型変換)が行われる。 その結果、i/j は double 型で計算されるため 2.5 となる。 k への代入の右辺は double 型同士の足し算であるため、 2.5 + 2.5 = 5.0 となり、その結果が int 型の変数 k に代入される。 結果的に、j を double 型にすると、printf は 5 と表示することになる。

同様に、曜日計算で示された 6から9の作業を

   d = d + 365.25*y - y/100 + y/400 + (30.6*m + 2.5);

とまとめて書くと、次のように処理が行われることになる。

  1. 計算の優先順位を元に次の各項に分解される

  2. 分解された各項ごとに計算が行われる。 各項の計算が行われる精度は次の通り。

    型表現 計算結果の型
    d int int
    365.25*y double * int double
    y/100 int / int int
    y/400 int / int int
    30.6*m + 2.5 double * int + double double
  3. 各項の計算結果を加減する。 int 型と double 型が混合されているので結果は double 型となる

  4. 得られた double 型の結果を int 型に切り詰め、変数 d に代入する

このように、6 から 9 をまとめて行うと、一部の計算が double 型(小数点以下も有効な形)で行われるため 繰り上がりが発生する可能性があることになる。

なお、あらわに

 (型名) 変換したいもの

という形式で書くことで型変換(cast)を行うことができる。

たとえば、上記の計算をまとめて行いたい場合には次のような形式を用いればよい。

   d = d + (int)(365.25*y) - (y/100) + (y/400) + (int)(30.6*m + 2.5);

このようにすることで、各項の計算が終わった時点ですべて int 型に変換されるので、 まとめて行っても繰り上がりが発生することはない。


  4日目に戻る  

tacha@tack.fukui-med.ac.jp
$Id: 04-2.html,v 1.1 2007/11/08 17:58:00 tacha Exp $