4.3 配列

(1/1)
1ヵ月カレンダー
これまで学んできた知識を総動員し、配列という仕組みを学びながら、1ヵ月カレンダーを表示するプログラムを作る。

目次

サンプル・プログラム

カレンダーを作るのに必要な関数

1ヵ月カレンダー
プログラムを作るには、まず目的を明確化しよう。
今回は、上図のように、指定した年月の1ヵ月カレンダーを表示することを目的にする。
次に、手作業でカレンダーを作ることを想像しながら、必要なプロシージャを洗い出していく。
1ヵ月カレンダー
手作業でカレンダーを作ろうとするなら、上図のようなカレンダーのフレームを用意するだろう。横方向の列は7マス(1週間分)、縦方向の行は5マス用意しておけばいい。便宜上、列・行ともに、0から始まる番号を付けておく。
1ヵ月カレンダー
このフレームを使って2021年6月のカレンダーを作ってみよう。
まず、6月1日の曜日を調べる。身近にあるカレンダーやネットで調べれば、6月1日は火曜日(列番号2)であることがわかる。
そこで、0行2列を1日として、右方向へ数字を並べていく。土曜日(列番号6)になったら折り返し、次の行に書いてゆく。
6月は30日まであるので、30まで書き続ける。
この作業を通じ、プログラムで必要になるプロシージャが明らかになる。すなわち
  1. 1日の曜日(番号)を求める
  2. 月の日数を求める
  3. 上記1から2まで、ループ文を使って日数を加算しながらカレンダー・フレームを埋めてゆく

曜日を求める関数

1日の曜日を求めるプロシージャだが、汎用性を考え、引数に西暦年、月、日の3つを指定し、曜日(番号)を戻り値として返す関数 getWeekNumber として作ることにする。

ネットを検索すると「ツェラーの公式」という計算式が見つかる。Wikipediaにある公式を参考に、「日曜日=0~土曜日=6」となる下記の計算式を選んだ。

\( C = \lfloor \frac{y}{10} \rfloor \)
\( Y = y \quad mod \quad 7 \)
\( h = (5C + Y + \lfloor \frac{y}{4} \rfloor + \lfloor \frac{C}{4} \rfloor + \lfloor \frac{26(m + 1)}{10} \rfloor + d) \quad mod \quad 7 \)
\( y \):西暦年,\( m \):月,\( d \):日

※ただし、\( m \)が1,2の時は、13月、14月として扱い、\( y \)を1だけ減じる。
\( \lfloor x \rfloor \):xを超えない最大の整数(Math.floorメソッド)

これを関数に書くと、次のようになる。
前半で、引数が計算可能な値であることを調べるバリデーションを行っている。計算可能な値でなければ、曜日番号としてはあり得ない -1 を返す。

  79: /**
  80:  * 指定した年月日の曜日番号を求める.
  81:  * ツェラーの公式を利用する.
  82:  * @param   Number year  西暦年
  83:  * @param   Number month 月
  84:  * @param   Number day   日
  85:  * @return  Number 曜日番号(0:日曜日, 1:月曜日...6:土曜日)/(-1):引数エラー
  86: */
  87: function getWeekNumber(year, month, day) {
  88:     //引数のバリデーション
  89:     if (! Number.isInteger(year|| ! Number.isInteger(month||
  90:         ! Number.isInteger(day||
  91:         month < 1 || month > 12) {
  92:         return (-1);
  93:     }
  94:     maxDay = getDaysInMonth(year, month);
  95:     if (day < 1 || day > maxDay) {
  96:         return (-1);
  97:     }
  98: 
  99:     //ツェラーの公式の変形
 100:     if (month <2) {
 101:         month +12;
 102:         year--;
 103:     }
 104:     c = Math.floor(year / 100);
 105:     y = year % 100;
 106:     h = (5 * c + y + Math.floor(y / 4+ Math.floor(c / 4+ Math.floor(26 * (month + 1) / 10+ day - 1% 7;
 107: 
 108:     return h;
 109: }

月の日数を求める関数

次に、西暦年と月を引数として、その月の日数を求める関数 getDaysInMonth を作る。
3.2 switch~case文」で学んだプロシージャをベースに、うるう年対応にして関数化する。

2月の月の日数が可変となるため、switch~case文では扱いが難しい。
そこで、配列を利用する。
配列
配列は、上図のように、1つの変数の中に仕切り枠があり、複数の値を代入できる仕組みである。仕切りの枠番号のことを添字と呼び、JavaScriptの場合、原則として添字は0からスタートする。各々の添字に代入する値を要素と呼ぶ。

ここでは添字を月とみなして、あらかじめ月の日数を代入しておく。

  70:     //月の日数
  71:     let daysInMoth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

配列に値を代入するには、
x = [a, b, ...];
のように書く。配列xの添字0に要素aが、添字1に要素bが代入されてゆく。
配列xの添字0の要素を参照するには x[0] と書けばいい。添字には変数を利用することができる。

配列xは変数であるから、うるう年の時は、2月に29を代入すればいい。うるう年の計算は、前回作ったユーザー定義関数 isleap を用いる。

  57: /**
  58:  * 指定した年月の日数(末日までの日数)を求める.
  59:  * @param   Number year  西暦年
  60:  * @param   Number month 月
  61:  * @return  Number 月の日数/(-1):引数エラー
  62: */
  63: function getDaysInMonth(year, month) {
  64:     //引数のバリデーション
  65:     if (! Number.isInteger(year|| ! Number.isInteger(month||
  66:         month < 1 || month > 12) {
  67:         return (-1);
  68:     }
  69: 
  70:     //月の日数
  71:     let daysInMoth = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
  72: 
  73:     //うるう年判定
  74:     daysInMoth[2] = isLeapYear(year? 29 : 28;
  75: 
  76:     return daysInMoth[month];
  77: }

カレンダー・フレームを日付で埋める

3番目がカレンダー・フレームを日付で埋めるプロシージャだ。
1ヵ月カレンダー
ここで再びカレンダー・フレームを見ておこう。
このフレームをいきなり画面に表示するのではなく、7列×5行の配列に日付を代入していくことを考えることにする。
添字が列・行の2方向に及ぶが、こういう配列を2次元配列を呼ぶ。たとえば、2021年6月1日は火曜日なので、0行目の2列目に1を代入する。以後、3列目に2を、4列目に3を‥‥6列目に5を代入したら折り返して、1行目の0列目に6を代入していく。
JavaScriptでは、2次元配列を x[a][b] のように書く。

 138:     //カレンダー配列の初期化
 139:     let calendarArray = new Array(MAX_ROW);
 140:     for (let row = 0row < MAX_ROWrow++) {
 141:         calendarArray[row] = new Array(MAX_COL);
 142:         for (let col = 0col < MAX_COLcol++) {
 143:             calendarArray[row][col] = 0;
 144:         }
 145:     }

カレンダー・フレームとなる2次元配列は calendar だ。まず、この配列を初期化(すべての要素を日付としてあり得ない値0で埋める)する。行番号の最大値は定数 ROW_MAX に、列番号の最大値は定数 COL_MAX に、あらかじめ代入しておく。

先ほどは、days = [0, 31, 28, 31, 30,... のようにして1次元配列を初期化したが、今回のようにループ文を使って値を代入していく場合には Array オブジェクトを生成する必要がある。詳しいことはコラム欄に譲るが、ここでは、配列の初期化のお作法として、こういう書き方があると覚えておいてほしい。

 167:         //配列calendarArrayに日付を格納
 168:         day = 1;
 169:         col = startWeekNumber;
 170:         row = 0;
 171:         do {
 172:             calendarArray[row][col] = day;
 173:             day++;
 174:             if (++col >MAX_COL) {
 175:                 col = 0;
 176:                 row++;
 177:             }
 178:         } while (day <goalDay);

2次元配列 calendar に日付を代入するために、変数として、行番号 row、列番号 col、日付 day、関数 getWeekNumber で求めた1日の曜日番号 start。関数 getDaysInMonth で求めた月の日数 day_max を用意する。
これを doループ で回し、配列 calendar に日付を代入していく。

表示用HTMLタグを生成する

2次元配列 calendar が埋まったら、今度は、その内容を表示用HTMLタグに変換する。ここではTABLEタグを使うことにする。

 180:         //配列calendarArrayを表形式に展開
 181:         for (let row = 0row < MAX_ROWrow++) {
 182:             if ((row >= (MAX_ROW - 1)) && (calendarArray[row][0] === 0))    break;              //最後が空行ならbreak
 183:             html +="<tr>\n";
 184:             for (let col = 0col < MAX_COLcol++) {
 185:                 if (calendarArray[row][col> 0) {
 186:                     html +=`
 187: <td class="${WEEK_LIST[col]}">${calendarArray[row][col]}</td>
 188: `;
 189:                 } else {
 190:                     html +=`
 191: <td>&nbsp;</td>
 192: `;
 193:                 }
 194:             }
 195:             html +="</tr>\n";
 196:         }
 197:         //結果を表示
 198:         document.getElementById('calendar').innerHTML = html;

これで1ヵ月カレンダーが完成した。
最後に、カレンダー作成を行う関数 dispMonthlyCalendar を通して見ておこう。

 111: /**
 112:  * テキストボックスから年月を取得し,1ヵ月カレンダーを表示する.
 113:  * @param   なし
 114:  * @return  なし
 115: */
 116: function dispMonthlyCalendar() {
 117:     //カレンダーの行列サイズ
 118:     const MAX_ROW = 6;
 119:     const MAX_COL = 7;
 120:     //曜日の種類
 121:     const SUNDAY   = 'sunday';
 122:     const SATURDAY = 'saturday';
 123:     const WEEKDAY  = 'weekday';
 124:     //曜日表
 125:     const WEEK_LIST = [SUNDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, SATURDAY];
 126: 
 127:     //年月を取得
 128:     let year  = parseInt(document.getElementById('year').value, 10);
 129:     let month = parseInt(document.getElementById('month').value, 10);
 130: 
 131:     //月の日数,1日の曜日番号
 132:     goalDay         = getDaysInMonth(year, month);
 133:     startWeekNumber = getWeekNumber(year, month, 1);
 134: 
 135:     //エラーメッセージをクリア
 136:     document.getElementById('error').innerHTML = '';
 137: 
 138:     //カレンダー配列の初期化
 139:     let calendarArray = new Array(MAX_ROW);
 140:     for (let row = 0row < MAX_ROWrow++) {
 141:         calendarArray[row] = new Array(MAX_COL);
 142:         for (let col = 0col < MAX_COLcol++) {
 143:             calendarArray[row][col] = 0;
 144:         }
 145:     }
 146: 
 147:     //カレンダーの1行目
 148:     html =`
 149: <tr>
 150: <th class="sunday">日</th>
 151: <th class="weekday">月</th>
 152: <th class="weekday">火</th>
 153: <th class="weekday">水</th>
 154: <th class="weekday">木</th>
 155: <th class="weekday">金</th>
 156: <th class="saturday">土</th>
 157: </tr>
 158: `;
 159: 
 160:     //バリデーション
 161:     if ((goalDay === (-1)) || (startWeekNumber === (-1))) {
 162:         //エラーを表示
 163:         document.getElementById('error').innerHTML = 'エラー:正しい月を入力してください.';
 164: 
 165:     //カレンダー作成
 166:     } else {
 167:         //配列calendarArrayに日付を格納
 168:         day = 1;
 169:         col = startWeekNumber;
 170:         row = 0;
 171:         do {
 172:             calendarArray[row][col] = day;
 173:             day++;
 174:             if (++col >MAX_COL) {
 175:                 col = 0;
 176:                 row++;
 177:             }
 178:         } while (day <goalDay);
 179: 
 180:         //配列calendarArrayを表形式に展開
 181:         for (let row = 0row < MAX_ROWrow++) {
 182:             if ((row >= (MAX_ROW - 1)) && (calendarArray[row][0] === 0))    break;              //最後が空行ならbreak
 183:             html +="<tr>\n";
 184:             for (let col = 0col < MAX_COLcol++) {
 185:                 if (calendarArray[row][col> 0) {
 186:                     html +=`
 187: <td class="${WEEK_LIST[col]}">${calendarArray[row][col]}</td>
 188: `;
 189:                 } else {
 190:                     html +=`
 191: <td>&nbsp;</td>
 192: `;
 193:                 }
 194:             }
 195:             html +="</tr>\n";
 196:         }
 197:         //結果を表示
 198:         document.getElementById('calendar').innerHTML = html;
 199:     }
 200:     calendarArray = null;
 201: }

読みやすいプログラム:変数名

今回紹介した、月の日数を求める関数 getDaysInMonth や、曜日番号を求める関数 getWeekNumber前回紹介した関数の命名規則に従っている。
今回は、これらの関数の中で使っている変数の命名規則を紹介する。

●変数の命名規則
  1. 変数の内容を名詞で表現する。
  2. 並べる順序は形容詞+名詞
  3. 発音しやすい英単語を並べる。
  4. 単語はローワーキャメルケースで並べる。
  5. 単語は最大3つ程度まで。
  6. 配列は複数形またはListやArrayを負荷する。
year, month, day は、変数名と内容が一致するので分かりやすいだろう。getDaysInMonth で取得した月の日数は、最大のdayと考えて、maxDay と命名した。
getDaysInMonth の中で定義している、月の日数を表す配列は、daysInMoth と複数形にした。
カレンダー配列は、カレンダーが複数あるわけではなくカレンダーの構造を配列で表していることから、calendarArray と命名した。

関数名と変数名は、ともにローワーキャメルケースだ。
PHPでは、変数名の頭文字はドルマーク $ ではじまるので区別しやすいが、JavaScriptではそういうわけにはいかない。動詞ではじまるのが関数という命名規則によって両者を識別する。

読みやすいプログラム:定数名

 117:     //カレンダーの行列サイズ
 118:     const MAX_ROW = 6;
 119:     const MAX_COL = 7;
 120:     //曜日の種類
 121:     const SUNDAY   = 'sunday';
 122:     const SATURDAY = 'saturday';
 123:     const WEEKDAY  = 'weekday';
 124:     //曜日表
 125:     const WEEK_LIST = [SUNDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, WEEKDAY, SATURDAY];
 126: 

dispMonthlyCalendar の中でconst宣言している定数 MAX_ROW, MAX_COL, WEEK_LIST などの命名規則は下記の通り。

●定数の命名規則
  1. 変数の内容を名詞で表現する。
  2. 並べる順序は形容詞+名詞
  3. 発音しやすい英単語を並べる。
  4. 単語はアッパースネークケースで並べる。
  5. 単語は最大3つ程度まで。
定数はアッパースネークケースで書くことにする。

読みやすいプログラム:マジックナンバーを使わない

プログラム中に数値や文字列を直接書くことをマジックナンバーと呼ぶ。
読みやすいプログラムでは、マジックナンバーを使わず、定数などに置き換える。
こうすることで、プログラムの仕様変更の時、定数の変更だけで対応できるようになる。

コラム:JavaScriptの配列

多くのプログラミング言語に配列が用意されているが、その性質や振る舞いは、言語によってやや異なる。JavaScriptの配列は、後述するオブジェクトの一種である。

例題で紹介した days = [0, 31, 28, 31, 30,... は、じつは days というオブジェクトを生成している。
2次元配列を初期化するのに calendar = new Array(ROW_MAX) と書いているのは、配列がオブジェクトであることを端的に表している。
JavaScriptの配列は、添字に0から始まる整数しか使えないという制約がある。PHPやPythonのような連想配列には対応していない。しかし、要素として代入できるデータ型に制約はない。

そして、JavaScriptの配列は1次元しか実在しておらず、2次元配列は calendar[0]に別の1次元配列(オブジェクト)を代入している。
前述の2次元配列を初期化の際、2次元目の要素を初期化するのに calendar[row] = new Array(COL_MAX); と書いているのが、そのことをよく表している。
これにより、3次元、4次元‥‥という多次元配列も実現できる。
2次元配列
したがって、2次元目の添字が一定ではない、こういう形の2次元配列を定義することもできる。
(この項おわり)
header