課題4に戻る

問題 4 解答

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

問題 4-1

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

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

   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. 次のような条件に基づく処理を行う。

    条件処理であるから、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つしか書かないという間違いは良くしてしまうので注意。

  6. d に(365.25*y)を加える
  7. d から (y/100) を引く
  8. d に (y/400) を加える
  9. さらに 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) */
        
  10. 得られた 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);
}
  

さて、leap_year と、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月の場合は、28 に関数 leap_year()の 返り値(平年なら 0, 閏年ならば 1であった)を足せば良いし、 2月以外ならば「決まっている」ので配列から読み出せばよい。

            int mdays[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
    
    	/* その月の日数 */
    	if (month == 2) {
    		/* 2月は 28 または 29 */
    		nissu = mdays[2] + leap_year(year);
    	} else {
    		/* 決まりきっているので配列から読み出す */
    		nissu = mdays[month];
    	}
      
  2. youbi

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

        youbi = youbi_keisan(year, month, 1);
      

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

/* prob4-1.c */
#include <stdio.h>

/* 関数宣言 */
int calendar(int, int);
int youbi_keisan(int, int, int);
int leap_year(int);

int main()
{
	calendar(2008, 4);  /* 2008年 4月 */
	printf("\n");
	calendar(2008, 5);  /* 2008年 5月 */
	printf("\n");
	calendar(2008, 6);  /* 2008年 6月 */
	printf("\n");
}

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

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

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

	/* 各月の日付を保持する配列 */
	/* 用意される最大要素の添字を12にするには要素数は13 */
	/*                    1   2   3   4   5   6   7   8   9  10  11  12 */
	int mdays[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};


	/* 有り得ない月が与えられたら警告を表示して、
	 * カレンダを表示しない (書けないのだから)
	 */
	if (month < 1 || month > 12) {
		printf("不正な月です。(%d)\n", month);
		return (1); /* 異状であったことが判るように正常時と異なる数字 */
	}

	/* タイトル行に「年月」が出せるようになった  */
	printf("        %4d/%2d\n", year, month);
	printf("  S  M Tu  W Th  F  S\n");


	/* year と month から youbi と nissu を求める */
	/* その月の1日の曜日 */
	youbi = youbi_keisan(year, month, 1);

	/* その月の日数 */
	if (month == 2) {
		/* 2月は 28 または 29 */
		nissu = mdays[2] + leap_year(year);
	} else {
		/* 決まりきっているので配列から読み出す */
		nissu = mdays[month];
	}


	/* ここ以降は、もともと mycal5.c で youbi と nissu が
	 * 与えられたらカレンダが書けるようになっていたので、
	 * 変更しない。
	 */

	kaigyo = (7 - youbi) % 7;

	/* 月始めの「空白」 */
	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);  /* カレンダ表示が成功したことを意味する */

}


/*
 * 閏年かどうかの判定を行う
 */

/* 問題 3
 *
 * カレンダをつくる場合、考える年が閏年であるかどうかを判断する事は重要である。
 * 次に示す閏年の判別ルールを用いて、与えられた年が閏年かどうかを判別するような
 * 関数 leap_year を作成せよ。
 *
 *  1. 西暦年数が4で割りきれなければ平年
 *  2. 西暦年数が400で割りきれると閏年
 *  3. 西暦年数が400では割りきれず、100で割りきれると平年
 *  4. 残りは閏年
 *
 * なお、与えられた年が閏年である場合は返り値 1、閏年でない場合は返り値 0 とせよ。
 */
int leap_year(int year) {
	int ret; /* 答を入れる変数 */

        if((year % 4) != 0) {           /* 1) 西暦年数が4で割りきれなければ */
                ret = 0;                /*    平年 */
        } else if ((year % 400) == 0) { /* 2) 西暦年数が400で割りきれると*/
                ret = 1;                /*    閏年 */
        } else if ((year % 100) == 0) { /* 3) 西暦年数が400では割りきれず、100で割りきれる */
                ret = 0;                /*    平年 */
        } else {                        /* 4) 残りは */
                ret = 1;                /*    閏年 */
        }
	return ret;
}


/*
 * 与えられた年月日の曜日を計算する (1600年以降)
 */

/* 問題 4-1
 *
 *  1. 整数変数 y に年(西暦)を代入する
 *  2. 整数変数 m に月を代入する
 *  3. 整数変数 d に日を代入する
 *  4. y から 1600を引く
 *  5. 次のような条件に基づく処理を行う。
 *         * m が 1 または 2の場合は、m に 9を足し、y から1を引く
 *         * それ以外の場合は、m から 3を引く
 *  6. d に(365.25*y)を加える
 *  7. d から (y/100) を引く
 *  8. d に (y/400) を加える
 *  9. さらに d に 30.6*m + 2.5を加える
 * 10. 得られた d を7で割った余りは、
 *     日曜日が 0, 月曜日が 1, ...土曜日が 6 という
 *     ルールにしたがった曜日を表す数字である。
 */
int youbi_keisan(int year, int month, int day)
{
	int y, m, d;

	y = year;       /* 1) */
	m = month;      /* 2) */
	d = day;        /* 3) */

	y = y - 1600;   /* 4) */

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

	d = d + 365.25*y;        /* 6) */
	d = d - y/100;           /* 7) */
	d = d + y/400;           /* 8) */
	d = d + 30.6*m + 2.5;    /* 9) */

	return (d % 7);          /* 10) */
}

問題 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 に戻る 6日目に戻る

tacha@tack.fukui-med.ac.jp
$Id: kadai04-ans.html,v 1.1 2008/05/16 03:32:27 tacha Exp tacha $