2日目 4日目

3日目

目標


課題 3

カレンダプログラムを自分で作る(2)


前回の課題2の最後までくると、カレンダプログラムは次のようになっているはずです。

main () 
{
	int i;
	
	for (i = 0; i < 6; i = i + 1) {
		printf("   ");
	}
	for (i = 1; i <= 31; i = i + 1) {
		printf(" %2d", i);
		if ((i % 7) == 1) {
			printf("\n");
		}
	}
	printf("\n");
}

何をしてたか、覚えてますか?


条件文の復習

先週の最後に駆け足となった条件文の復習からはじめることにしましょう。 カレンダのような形にするには、毎週土曜日の日付を書いたあとだけ、 改行コード "\n" を出力する必要があるのでした。 このような特定の条件が成立したときだけ実行したい場合には、C では if文を使うのでした。 使い方は、

 if (条件1) {
     <条件1が成立した場合の処理>
 } else if (条件2) {
     <条件1が成立せず、条件2が成立した場合の処理>
 } else {
     <どの条件も成立しなかった場合の処理>
 }

という形で使用するということでした。 ここで、条件はどのように指定するかを説明しているところで時間切れになったのでした。 では、気を取り直してはじめることにします。

改行が行われている日付をもう一度確認すると、土曜日である「1, 8, 15, 22, 29」であることが判ります。 いま、日付は変数 iに入っていますから、等値演算子を 使うと

 i == 1
 i == 8
 i == 15
 i == 22
 i == 29

という5個の論理式ができることになります。したがって、

    if (i == 1) {
        printf("\n");
    } else if (i == 8) {
        printf("\n");
    } else if (i == 15) {
        printf("\n");
    } else if (i == 22) {
        printf("\n");
    } else if (i == 29) {
        printf("\n");
    }

と書くことで、目的通り、土曜日の後ろに改行を入れることができます。

でも、上のプログラムは、少しごちゃごちゃしていると思いません? さらに、特別な場合に行っていることがすべて同じです。 このような場合には、論理演算を行うことで条件をまとめることを考えましょう。 今の場合、与えられた条件のうち、どれか一つでも成立すれば改行したいので使用するのは論理和(OR)になります。 というわけで、次のように書くことができる事になります。

    if (i == 1 || i == 8 || i == 15 || i == 22 || i == 29) {
        printf("\n");
    }

だいぶ、すっきりしてきましたが、もう一工夫してみることにしましょう。 条件文に出てくる数字をよぉくにらんでください。共通点はないでしょうか? 隣り合う数字の差は7ですね。 すべて土曜日なのですから、これは1週間の日付の数であり当たり前の事です。 ここで、見方を変えると、この数字達は全員7で割ったときのあまりが等しい事になります。 つまり、「i が、1 または 8 または 15 または 22 または 29 のとき」という代りに、 「i を 7で割った余りが1の時」とまとめることができるのです。 C には割ったあまりを求める「剰余演算子 (%)」があるので、 つぎのように書けることになります。

    if ((i % 7) == 1) {
        printf("\n");
    }

というわけで、カレンダ表示プログラムは一番上に書いたように書き直すことができました。


変わる可能性があるところは変数にする

ここまでで、最初のプログラムよりは、かなりすっきりと見通しが立つようになってきた。 が、これでは、別の月のプログラムに直そうとすると、プログラムのあちこちを変更しなければならない。 変わる可能性があるところは変数にしておくと、あとあと便利である。

カレンダプログラムで変わる部分を探すと次のものが考えられる。

曜日を言葉ではなく、次のような対応で数字で表現すると考えてみる。

日 → 0、月 → 1、火 → 2、水 → 3、木 → 4、 金 → 5、土 → 6

そうすると、「最初に出す空白の数」とは「その月の最初の日の曜日」と みなすことができる。 「改行を出す条件となる数」とは、その月の最初の土曜日であるので、 一週間の日数(7)から最初の日の曜日を引き、7で割ったあまりと求められる。

main() 
{
	int i;
	int youbi;
	int nissu;

	youbi = 6;
	nissu = 31;

	for (i = 0; i < youbi; i = i + 1) {
		printf("   ");
	}

	for (i = 1; i <= nissu; i = i + 1) {
		printf(" %2d", i);
		if ((i%7) == ((7 - youbi)%7)) {
			printf("\n");
		}
	}
	printf("\n");
}

なんども使う可能性があるところは関数にする

これで、月はじめの曜日と月の日数を与えれば、1月分のカレンダが表示できるようになった。 しかし、たとえば、3ヶ月分のカレンダを表示しようとした際に、同じことを3度も書かなければならず、非常に面倒である。 このような場合は、カレンダを書く手続きをひとまとまりとして、関数として定義すると便利である。 関数は次のような形式で定義する。

 関数の型  関数名(引数1, 引数2, ...) {
     関数内部でだけ使用する変数の宣言;

     関数で行う処理1;
     関数で行う処理2;
     ...
     return (関数の返り値);
 }

引数はなくても良い。今までに利用してきた printf やおまじないのように 唱えてきた main も実は関数である。 関数の型を省略した場合は int型が仮定されるが、プログラムを作成する際にはできるだけ明示的に宣言すべきである。

また、関数を利用する前には、その関数の型や引数の種類をあらかじめ宣言しておくのが望ましい。 関数の宣言を行うには、次のように行う

  関数の型 関数名(引数1の型, 引数2の型, ...);

printf等の標準的に備わっている関数も本来は宣言を行って使用するべきであるが、 いちいち、書くのは面倒であるため、あらかじめそのような宣言を行っているファイルが用意されている。 その宣言ファイルは stdio.h という名前で /usr/include 直下に存在している。 プログラム中にこれを読み込むのには #include文を利用して次のように書く。 # は行の先頭に書くようにすること。(先頭にない場合には、エラーとなる処理系も存在する。)

#include <stdio.h>

カレンダを書くためには、月はじめの曜日と、その月の日数が必要だったので、引数は2つとなる。 関数の名前として、calendarを選ぶと、カレンダプログラムは次のように変更できることになる。 ついでに、10月、11月、12月の3ヶ月分表示するようにもしてみた。 これで、コンパイル時の警告も消えるはずである。(mycal5.c)

#include <stdio.h>

int calendar(int, int );

int main() 
{
	calendar(6, 31);
	calendar(2, 30);
	calendar(4, 31);
}

int calendar(int youbi, int nissu) 
{
	int i;

	for (i = 0; i < youbi; i = i + 1) {
		printf("   ");
	}
	
	for (i = 1; i <= nissu; i = i + 1) {
		printf(" %2d", i);
		if ((i%7) == ((7 - youbi)%7) {
			printf("\n");
		}
	}
	printf("\n");
	return (0); /* カレンダ表示が成功したことを意味する */
}

あらかじめ月の日数ってわかってるよね (配列)

これで、だいぶ使い回せるような形に近づいてきた。 しかし、月のはじめの曜日や日数をいちいち与えるのはめんどうである。 できれば、年月を指定すると勝手にコンピュータが月のはじめの曜日や日数を考えてくれる方が利用者には便利である。

良ぉく考える(調べる)と、2月の日数が閏年か平年かで変化する以外は、何月かが分かればその月の日数は判る。 このような場合には、月の日数を配列に入れておいて参照すると便利である。

配列とは、同じ型の同じような内容のデータを順に並べておいたものである。 宣言の際に、変数名の後ろに数字をカギ括弧("[" と "]")でくくって宣言すると配列となる。 たとえば int a[5];と宣言すると次のように、a[0], a[1], a[2], a[3], a[4] の5つの変数が用意される。

括弧でくくられた部分は添字(subscript)と呼ばれる。 数学で出てきた数列などを表現する a0, a1 の小さい右下の文字と同じ意味と思って良い。 添字は 0 から始まるので、用意される配列の最後は宣言の時の数字より1小さいことに注意しなければならない。

月の日数を保持する配列として、int mdays[13] を用意し値を代入するプログラムは次のようになる。

   int mdays[13]; /* 用意される最大要素の添字を12にするには要素数は13 */
   mdays[1] = 31;
   mdays[2] = 28;
   mdays[3] = 31;
   mdays[4] = 30;
   mdays[5] = 31;
   mdays[6] = 30;
   mdays[7] = 31;
   mdays[8] = 31;
   mdays[9] = 30;
   mdays[10] = 31;
   mdays[11] = 30;
   mdays[12] = 31;

宣言と同時に初期化を行う場合にのみ、次のようにも書ける。

   /* 用意される最大要素の添字を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};

このようにすると、何月かが変数 mに入っているとすると、 その月の日数はmdays[m]で求められる。 問題は年によって変化する 2月のデータであるが、 次の閏年かどうかの判別ルール

  1. 西暦年数が4で割りきれなければ平年
  2. 西暦年数が400で割りきれると閏年
  3. 西暦年数が400では割りきれず、100で割りきれると平年
  4. 残りは閏年

ifを使うと解決できるはずである。

ヒント
「割りきれる」とは「割ったあまりが 0」の事である。


問題 3-1

2月の日数を求めるようなプログラムを考えてみよ。


曜日って計算できないのかな?

閏年の判別方法が判るということは、年と月が判れば、どのような場合でもその月の日数が判ることになる。 ということは、年月日をすべて指定すると、ある基準日からの延べ日数が判るはずである。 さらに、曜日は7で割ったあまりで表現することができたのであるから、結局曜日も計算できることになる。

曜日を計算する方法は様々なものがあるが、ここでは、次の方法を使うことにしよう。 次の方法は西暦1600年以降の年と月が与えられた場合のその月の最初の日の曜日を求めるものである。

  1. 整数変数 y に年を代入する
  2. 整数変数 m に月を代入する
  3. y から 1600を引く
  4. 次のような条件に基づく処理を行う。
  5. 整数変数 d に(365.25*y)を代入する
  6. d から (y/100) を引く
  7. d に (y/400) を加える
  8. さらに d に 30.6*m + 3.5を加える
  9. 得られた d を7で割った余りは、上で決めたルールにしたがった曜日を表す数字である。

問題 3-2

上の手法をプログラムとして表現し、関数calendarの引数を 「年」と「月」に変更して mycal6.c として保存せよ。 できれば、「2005/10」の用にどの月のカレンダかを表示したり、 何曜日か判るようにしてみよう。

問題 3-3

5から8をまとめて行うと、正しい答えを出さない場合がある。なぜだろうか?


2日目 表紙 4日目

tacha@tack.fukui-med.ac.jp
$Id: 03.html,v 1.1 2005/11/03 17:10:46 tacha Exp $