• ベストアンサー

簡素で美しく記述するには・・・・

今与えられた開始日から与えられた終了日までの期間を求めるプログラムを作りました。勿論その中で閏年の計算も行います。 自分の作ったプログラムでは1つだけ気に入らないところがありました。それは閏年の計算部分です。 ■うるう年の判定 ・年が4で割り切れる時はうるう年  従って、2004年はうるう年である ・ただし、100で割り切れる時はうるう年でない  従って、1900年はうるう年ではない ・ただし、400で割り切れる時はうるう年である。  従って、2000年はうるう年である この条件を行うため自分は次のように記述しました for(year = First_Year; year <= End_Year; year++){   if(((year  % 4) == 0) && ((((year % 100) != 0) && ((year % 400) != 0)) || (((year % 100) == 0) && ((year % 400) == 0)))){     閏年の個数を数える    } } for文変数の初期でFirst_Yearを与えていますが、First_Yearが開始年でEnd_Yearが終了年です。 if文がむかつくほど長くなってしまいました。やっていることはyearが4で割り切れかつ100と400で割ったときに両方共に余りが出る、またはyearが4で割り切れかつ100と400で割ったときに両方共に余りが出ない場合に 閏年の個数を数える ようにしています。 かなり強引な質問ではありますが、みなさんならどのように組むでしょうか? 別に自分の考える”美しい”プログラムでなくてもかまいません。色々な考えを聞かせてもらえないでしょうか?

質問者が選んだベストアンサー

  • ベストアンサー
  • thamansa
  • ベストアンサー率40% (95/232)
回答No.3

プログラムを簡素で美しく記述する王道は、 ・何をするのかを日本語で簡潔にまとめる(整理する) → うるう年判定であれば、判定方法を整理します。   (1)4で割れる年をうるう年とする   (2)100で割れる年は平年にする   (3)400で割れる年はうるう年とする ・凝ったことをしようとしない。 → ビット操作とかポインタ操作とかできるのがCの醍醐味ですが、いわゆる「テクニック」を使わないほうが実は簡素で美しくなる傾向にあります。 これを心がけてうるう年判定関数を作ってみました。 bool is_leap(int year){   if(year % 400 == 0) return true;   if(year % 100 == 0) return false;   if(year % 4 == 0) return true;   return false; } 書き方によっては実行速度がはやい方法もあるでしょうけど、うるう年関数なんかチューニングするより、他のアーキテクチャを見直したほうがよっぽど効果的です。 また、この関数であれば、3200年に一度うるう年でないことに対応 させたい(いわゆる仕様追加みたいな場面)ときに   if(year % 3200 == 0) return false; を追加すればよいことが一目瞭然です。 複雑な条件式を書かずに素直に書くと、メンテが楽になりますね。

Mr_tenten
質問者

お礼

返答ありがとうございました。 ・・・・提示してくださったコード、すばらしいと思います。 自分の中では”短いコード”を追求したものが美しいと感じていましたが、そうではないんですよね・・・・・ 提示してくださったコードはプログラムが上から順に実行されていくことをうまく使った方法だと思います。 のちのちのことを考え、他の人が分かりやすく、保守がしやすいコードもまた美しく書く条件の1つになるでしょう。 ありがとうございました。

その他の回答 (9)

  • jacta
  • ベストアンサー率26% (845/3158)
回答No.10

> まぁテーブルで使用されるメモリ容量が気になる場合は、単に0,1のみなので、ビット列としてでも表現できますね。 そうなのですが、除算器を持たないような非力なCPUの場合、大抵シフト演算も遅い(ハード的には1ビット単位でしかシフトできない場合が多い)ので、ビット列にしてしまうと結局パフォーマンスが上がらなくなります。

  • jacta
  • ベストアンサー率26% (845/3158)
回答No.9

> そして最後の記述ですが、ちょっと自分の経験不足で分からないのですが、表引きにするということは、4桁の西暦を上2桁を行番号、下2桁を列番号とし、それぞれにtrueかfalseを入れるといった方法でしょうか? そんな難しい話ではなく、例えば1970年~2099年までしか扱わないのであれば、西暦から1970を引いた値を添え字とするブール値の配列を作るなどすればよいということです。 具体的には、 int is_leap(int year) {  static const char leap_table[130] = { 0,0,0,1,... };  if (yaer < 1970 || 2099 < year) return -1;  return leap_table[year - 1970]; } のようにします。 返却値は、単にうるう年かどうかだけでなく、引数が不正な場合に報告できるように、少なくとも三値を表現できる必要があります。

Mr_tenten
質問者

お礼

返答ありがとうございました。 なるほど・・・基準値を決め、そこから閏年の時だけフラグを立てるという方法ですね。 "少なくとも三値を表現できる必要があります" TRUE、FALSE、-1ということで返り値がintなのですね。 まぁテーブルで使用されるメモリ容量が気になる場合は、単に0,1のみなので、ビット列としてでも表現できますね。 また違った考え方が聞けてよかったと思います。ありがとうございました^^

  • jacta
  • ベストアンサー率26% (845/3158)
回答No.8

3200年や80000年に関しては、シミュレーションなどでは必要になる可能性がありますし、遠未来を扱ったSFのゲームなどでも関係してくるかもしれませんね。逆に、グレゴリオ暦ができる(1582年)以前を、または国によって扱うべき暦法を変える必要があるなら、それも考慮する必要があります。 ここ100年か200年だけを扱う場合でも、#3の回答だけでは不十分で、引数の有効範囲をチェックする必要があるかと思います。最低限、assertを仕込んでおくぐらいは必要でしょう。 ところで、ここ100年か200年を扱う場合で、かつCPUが非力な環境(特に除算器を持たないCPU)であれば、表引きにするのが一番効率がよいかと思います。ただし、その分メモリを食いますので、どうするかは総合的に判断する必要があります。

Mr_tenten
質問者

お礼

返答ありがとうございました。 確かに有効範囲をチェックする場合は#3の回答では不十分です。 問題は有効範囲までif文を追加するか、または有効範囲をチェックするかの方法ですが、それに関してもメンテはしやすいと思います。 まぁassertを仕込むだけでも一行で終わりですからね。 そして最後の記述ですが、ちょっと自分の経験不足で分からないのですが、表引きにするということは、4桁の西暦を上2桁を行番号、下2桁を列番号とし、それぞれにtrueかfalseを入れるといった方法でしょうか?

  • thamansa
  • ベストアンサー率40% (95/232)
回答No.7

> ところで、3200で割れる年が平年と言うのははじめて聞きましたが、 私も気になったのでgoogleしてみたら、 http://www.water.sannet.ne.jp/kazuya-ai/14/week.html によると、3200で平年、また80000年でうるう年だそうです。 原理的には、その先も何百万年とか何億年単位で例外があると思いますが、3200年後がうるう年かどうかは実用上問題ないので、一般には無視されるという話を聞いたことがあります。 でも、3200年後がうるう年かどうかがシステムに無関係であることを確認したうえで無視するべきで、盲目的に無視したら2000年問題と同じですね。

Mr_tenten
質問者

お礼

返答ありがとうございました。 そしてわざわざ調べていただきありがとうございます。 80000年でうるう年ですか・・・・・・天文学的な数字になってきましたね。 たしかにそこまでがシステムに関係なかったら無視するべきで、関係するシステムもそんなにあるわけではないだろうし、無視してもよいでしょうね。

回答No.6

aに年数が入っているときに、 閏年なら0、そうでないとき0以外を返す処理 if (!(a % 100)) a = a /100; return a % 4; これは、閏年を論理和、論理積を使わずに判定せよと言う課題の 解答です。 ところで、3200で割れる年が平年と言うのははじめて聞きましたが、 本当かしら。

Mr_tenten
質問者

お礼

返答ありがとうございました。 そしてすばらしいコードを提示していただきありがとうございます。 自分は論理和、論理積 と このような計算により算出する場合とで、どちらのほうが速度が速いか分かりませんが、このコードも理解しやすいと思います。 つまり100年がいくつ含まれているかを判定し、その個数が4の倍数かどうかを判定していますね。 しかし最後におっしゃられた3200年ももし閏年であっても if(!(a % 3200)) a = a/3200; if(!(a % 100)) a = a/100; return a % 4; 一応上のように条件をただ付け足すだけですよね。 ありがとうございました

  • MrBan
  • ベストアンサー率53% (331/615)
回答No.5

カリカリに速度チューニングが必要(保守性よりも優先)なケースでもなければ、 ソースレビューしたら私も#3の方の方針を支持します。 「凝ったことをしようとしない。」←これ大事。 「簡素で美しい」コードは、「解説」を必要としないシンプルなコードだと思います。

Mr_tenten
質問者

お礼

返答ありがとうございました。 ”「凝ったことをしようとしない。」←これ大事。” 確かに計算やアルゴリズムに依存して、プログラムのコードも複雑になるケースは沢山ありますが、解釈の仕様によってとてもシンプルなコードになりますよね。 こったことをしようとせず、そのままを頭の中で整理し、コードを作成してみる。すると思ったより分かりやすく、かつシンプルなコードが出来上がることはままあるものですよねw しかし時々あることを重要と考えずに通り過ぎてしまっている。 それはご指摘されたようにこったことをしないことはとても大事だということですね。 そして新しい考え方を聞けてとても感謝しています。 ”簡素で美しい」コードは、「解説」を必要としないシンプルなコード” これもまた簡素で美しいコードの条件の一つに含まれると思いますし、誰もが納得するでしょう。

  • tatsu99
  • ベストアンサー率52% (391/751)
回答No.4

全然回答ではありませんが、#3のかたに一票です。 プログラムは、他人がみて最もわかりやすいものが、簡素で美しい記述になります。自分一人で趣味で作る範囲では、どのようなものでもかまいませんが、業務用の場合は、そのプログラムが、次のひとに引き継がれていきます。 従って、 他人がみてわかりやすい。 仕様変更が容易にできる。 ことが必要です。 #3のかたの回答は、その意味で、満点です。

Mr_tenten
質問者

お礼

返答ありがとうございました。 自分も#3様に一票ですw 確かにおっしゃられるとおり、とても分かりやすくシンプルで、保守もしやすいと思います。 ぱっと見、これ以上そぎ落とすものがなく、条件の追加が容易で分かりやすい。 自分もこのようなコードを書けるよう日々努力しようと思います。

  • Oh-Orange
  • ベストアンサー率63% (854/1345)
回答No.2

★アドバイス ・うるう年の判定はあまり複雑に考えなくても判定できますよ。  そこで昔、作成したマクロ関数と関数を載せておきます。 /* 閏年判定のマクロ関数 */ #define MacroIsLeapYear(n) ((!((n) % 4) && ((n) % 100) && ((n) % 3200)) || !((n) % 400)) /* 西暦から閏年の判定 */ bool FuncIsLeapYear( int year ) {  if ( !(year % 4) && (year % 100) && (year % 3200) ){   return( true );  }  return( !(year % 400) ); } /* ●解説 (1)4で割れる年をうるう年とする (2)ただし、100で割れる年は平年にする (3)ただし、400で割れる年は閏年とする (4)ただし、3200で割れる年は平年とする ※まとめると4年に1回は閏年、でも 400 年に3回は閏年ではない事になる。 */ その他: ・開始日から終了日の期間の計算は西暦1年1月1日からの通算日数を求めた後で引き算をした方が  for 文でうるう年を数えるよりも早く計算できると思います。まずは通算日数を計算する関数を  作成して下さい。もちろん、この計算にもうるう年を含めた計算を追加します。 ・つまり、4 年に1回はうるう年なので『西暦 / 4』を通算日数に加算します。  でも 400 年に3回は平年になるため『西暦 / 400 * 3』を通算日数から引きます。  これで西暦1年1月1日からの通算日数が求まります。 ・その後に『終了日』から『開始日』を単純に引くことで期間の日数が計算できます。  下に1月1日からの通算日数を求める関数を紹介します。 /* 年月日から1月1日からの日数を取得 */ int FuncGetDays( int year, int month, int day ) {  static int table[] = { /* 0 */ 0, /* 1 */ 0, /* 2 */ 31, /* 3 */ 31 + 28, /* 4 */ 31 + 28 + 31, /* 5 */ 31 + 28 + 31 + 30, /* 6 */ 31 + 28 + 31 + 30 + 31, /* 7 */ 31 + 28 + 31 + 30 + 31 + 30, /* 8 */ 31 + 28 + 31 + 30 + 31 + 30 + 31, /* 9 */ 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31, /* 10 */ 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30, /* 11 */ 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31, /* 12 */ 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30,  };    // 2月以降で閏年なら+1日  if ( (month >= 3) && FuncIsLeapYear(year) ){   day++;  }  return (table[month] + day); } /* ●解説 ※1月1日からの日数を返す。(1-366) */ 最後に: ・開始日を Y1/M1/D1、  終了日を Y2/M2/D2 とすると  days1 = ((Y1 * 365) + (Y1 / 4) - (Y1 / 400 * 3)) + FuncGetDays(Y1,M1,D1);  days2 = ((Y2 * 365) + (Y2 / 4) - (Y2 / 400 * 3)) + FuncGetDays(Y2,M2,D2);  diff = (days2 - days1);   ↑  この diff が開始日から終了日の期間の日数です。 ・以上。

Mr_tenten
質問者

お礼

返答ありがとうございました。 /* ●解説 (1)4で割れる年をうるう年とする (2)ただし、100で割れる年は平年にする (3)ただし、400で割れる年は閏年とする (4)ただし、3200で割れる年は平年とする ※まとめると4年に1回は閏年、でも 400 年に3回は閏年ではない事になる。 */ 3200で割れる年が平年だとは知りませんでした。 そして解釈の仕様によってとてもシンプルなアルゴリズムになりますね。”4年に1回は閏年、でも 400 年に3回は閏年ではない”と解釈の仕方をすることができなかったため、自分のようなソースになってしまいました。 自分が作ったプログラムは配列を13個用意し、添え字が1から12までの各要素を月として、その月の最大日数を格納して計算していました。 しかしOh-Orange様が示されたように、1月を0として、そのつきの前の日数を足して配列に格納していけば、たとえば2月22日といった場合には添え字2の場所には1月分の日数が入っているわけだから、添え字2に格納されている数字と22を足すだけでいいですね。 こういう方法もあるんだと関心です。

  • koko_u_
  • ベストアンサー率18% (459/2509)
回答No.1

bool is_leap(int year); みたいな関数にする (End_Year / 4 ) - (First_Year / 4) みたいにすれば、間にある 4の倍数を数えることも易しいけど、関数にした方が意味を把握しやすいでしょう。

Mr_tenten
質問者

お礼

返信ありがとうございます。 関数にしてみたところ確かにすっきりしました。 すばやい回答に感謝します。

関連するQ&A