基本情報技術者 2015年 秋期 午後 問09
次のCプログラムの説明及びプログラムを読んで、設問1,2に答えよ。
〔プログラムの説明〕
入退室が制限されているエリア(以下、制限エリアという)の入退室の状況を記録したファイルを読み込んで、入退室状況を印字するプログラムである。
X社では、ICカードを用いた入退室システムを導入して、制限エリアの入退室を管理している。図1に、X社の制限エリアのレイアウトを示す。ドアは5か所(ドア番号11、12、21、22、31)あり、各ドアの内外にカードリーダが設置されている。入室時と退室時には、カードリーダにICカードをかざしてドアを開錠する。各ドアのカードリーダにICカードをかざす都度、その入退室の状況を記録した1行の入退室レコードが入退室ログに書き込まれる。
制限エリアには、機密度に応じたレベルが設定されている。レベルは、外部からその部屋へ入るまでに開錠して通過する必要があるドアの数で表す。ドア番号は、10の位がレベルを表す。入退室できるレベルは、ICカードごとに設定されている。

(1) 入退室レコードは33桁の固定長の文字列で、次に示す項目から成る。

① カードIDは、ICカードを識別するIDであり、英数字から成る。
② 日付と時刻は、カードリーダにICカードをかざした日付と時刻である。
③ 入退は、“I”(入室)又は“O”(退室)である。
④ 可否は、“A”(開錠)又は“R”(拒否)である。
⑤ 名前は、ICカードの使用者を識別する名前であり、英数字と空白から成る。
⑥ レコードの終端には、改行文字が付いている。
(2) 入退室ログから印字したい日付・時間帯の入退室レコードを抽出し、カードID、日付及び時刻(先頭の18桁)で昇順に整列したファイルをAccess.Logとする。図2に、Access.Logのレコードの例を示す。

(3) Access.Logから読み込んだ1レコードごとに、カードID、名前、日時、ドア番号、入退、可否から成る1行を、図3に示す様式で印字する。日時以降の項目は、ICカードをかざしたドアのレベルに応じて19, 39, 59桁目のいずれかから印字する。

① カードID及び名前は、同一カードIDが続くとき、先頭の行だけに印字する。
② 日時は、“月-日 時:分”の形式で印字する。
③ 入退は、“I”なら“IN”と、“O”なら“OUT”と印字する。
④ 可否は、“A”なら何も印字せず、“R”なら“IN”又は“OUT”の直前に“(R)”を印字する。
(4) ライブラリ関数strcmp(s1, s2)は、文字列s1とs2を比較し、s1 < s2のとき負の値を、s1 = s2のとき0を、s1 > s2のとき正の値を、それぞれ返す。また、ライブラリ関数strcpy(s1, s2)は、文字列s2をs1に複写する。

設問1:
問題文を見る プログラム中のに入れる正しい答えを、解答群の中から選べ。
a, bに関する解答群
ア:cardID = lastID
イ:lastID = cardID
ウ:logEOF = 0
エ:logEOF = EOF
オ:strcpy(cardID, " ")
カ:strcpy(cardID, lastID)
キ:strcpy(lastID, " ")
ク:strcpy(lastID, cardID)
cに関する解答群
ア:door[0] - '0' - 1
イ:door[0] - '0'
ウ:door[0] - '0' + 1
エ:door[0] - '3'
模範解答
a:エ、b:ク、c:ア
解説
解答の導き方
このプログラムは、Access.Logからレコードを1件ずつ読み込み、カードIDごとにまとめながら印字します。空欄aはファイルの終わりを知らせる処理、空欄bは直前のカードIDを記録する処理、空欄cはドアのレベルに応じて印字位置を調整する処理です。
まず、レコードを読み込む流れを確認します。プログラムは最初にgetRecordを実行し、1件目を読み込みます。その後、logEOFがEOFでなければputRecordで印字し、再びgetRecordで次のレコードを読み込みます。ファイルの終わりに達したとき、次の繰返しを止める必要があります。
問題文には「ファイルの終わりに達したときfscanfはEOFを返す」とあります。したがって、fscanfがEOFを返した場合は、繰返しの条件に使われているlogEOFにもEOFを設定します。空欄aは「logEOF = EOF」です。logEOFを0のままにすると、ファイルの終わりを知らせられず、繰返しを止められません。
次に、空欄bを考えます。プログラムはstrcmp(cardID, lastID)で、読み込んだカードIDと直前に記録したカードIDを比べています。strcmpは、2つの文字列が同じなら0を返します。同じIDならカードIDと名前を印字せず、違うIDなら新しいカードの情報を印字します。
問題文には「同一カードIDが続くとき、先頭の行だけに印字する」とあります。そこで、新しいカードの情報を印字した後、現在のカードIDを直前のIDとして記録しておく必要があります。strcpy(s1, s2)はs2の文字列をs1に複写する関数なので、現在のcardIDをlastIDに複写します。空欄bは「strcpy(lastID, cardID)」です。
最後に、空欄cを考えます。問題文では、ドア番号の10の位がレベルを表します。たとえば、ドア番号11はレベル1、21はレベル2、31はレベル3です。door[0]はドア番号の最初の文字なので、door[0]から文字の「0」を引くと、レベルを表す数値になります。
プログラムでは、位置をずらすたびに「%20s」で20桁分の空白を印字します。レベル1の位置は基準の位置なので、追加する空白は0個です。レベル2は20桁分を1回、レベル3は20桁分を2回追加します。つまり、追加する空白の回数は「レベル − 1」です。
この計算に当たるのが「door[0] - '0' - 1」です。したがって、空欄cはこの式になります。
誤りやすいポイント
- 空欄aでlogEOFを0にすると、ファイルの終わりを検出できません。繰返しを止めるため、EOFを設定します。
- 空欄bでは、複写元と複写先を逆にしないようにします。現在読み込んだcardIDを、直前のIDを保存するlastIDに複写します。
- レベル1の空白の回数は1回ではなく0回です。レベル1が基準の位置なので、レベルから1を引きます。
- 「%20s」は空白を20個印字します。レベル2やレベル3の位置にするため、空白の回数に20を掛けるのではなく、20桁分の空白を必要な回数だけ印字しています。
FAQ
Q: なぜ、直前のカードIDだけと比べればよいのですか?
A: Access.LogはカードIDで昇順に整列されています。そのため、同じカードIDのレコードは連続します。直前のカードIDと比べれば、同じカードの続きかどうかを判断できます。
A: Access.LogはカードIDで昇順に整列されています。そのため、同じカードIDのレコードは連続します。直前のカードIDと比べれば、同じカードの続きかどうかを判断できます。
Q: なぜ空欄bはcardIDにlastIDを複写するのではなく、lastIDにcardIDを複写するのですか?
A: cardIDは今回読み込んだID、lastIDは前回のIDを保存する場所です。今回のIDを次の比較に備えて保存するため、cardIDからlastIDへ複写します。
A: cardIDは今回読み込んだID、lastIDは前回のIDを保存する場所です。今回のIDを次の比較に備えて保存するため、cardIDからlastIDへ複写します。
Q: door[0]の「0」は、数字の0ですか?
A: いいえ。door[0]は文字としてのドア番号の先頭の1桁です。「'0'」も文字の0を表します。文字の「2」から文字の「0」を引くことで、レベルを表す数値の2を得ています。
A: いいえ。door[0]は文字としてのドア番号の先頭の1桁です。「'0'」も文字の0を表します。文字の「2」から文字の「0」を引くことで、レベルを表す数値の2を得ています。
関連キーワード: C言語、文字列比較、文字列コピー、ファイル終端処理、出力位置調整
設問2:
問題文を見る 次の記述中のに入れる正しい答えを、解答群の中から選べ。
プログラムを変更して、入退室順序の整合性を検査する処理を追加する。
各入退室者は、最初は外部(レベル0)にいて、最後は外部に出るものとする。正しく入退室していれば、制限エリアのレベルは0から始まり、ドアを通過する都度、レベルが1ずつ変化し、最後は0に戻るはずである。しかし、抽出した日付・時間帯よりも前(後)に入室(退室)している場合や、ICカードをかざさずに人の後についてドアを通過した場合などには、順序の不整合が起きる。
なお、カードリーダにICカードをかざしてドアが開錠した場合は、必ず入退室するものとする。
(1) 順序の不整合が起きた場合は、本来入退室の記録があるべき行のレベル1の印字位置に“***** Level x-->y”と印字する。これは、入退室者がこの前後にレベルxからyへ移動したはずであるが、その記録がないことを意味する。
(2) 図4に、不整合がある入力レコードの例を示す。図4のレコードを用いて変更後のプログラムを実行すると、印字結果は、図5のようになる。


プログラムを、次のように変更する。
(1) プログラムに、次の文を追加する。

(2) プログラムの末尾に、次の二つの関数を追加する。

d, eに関する解答群
ア:行③
イ:行④
ウ:行⑤
エ:行⑥
オ:行⑦
カ:行⑧
f, gに関する解答群
ア:afterLevel
イ:beforeLevel
ウ:doorLevel
エ:level
模範解答
d:オ、e:イ、f:エ、g:イ
解説
解答の導き方
この設問では、入退室レコードを順に確認し、記録されていない移動があれば、その移動を印字する処理を考えます。ポイントは、入退室者の現在のレベルと、今回のドアを通る直前にいるべきレベルを比べることです。
1. ドア番号から、通過するレベルを求めます
問題文には「ドア番号は、10の位がレベルを表す」とあります。たとえば、ドア番号31ならレベル3のドアです。
追加する関数では、次の文でドア番号の先頭の文字を数値にしています。
doorLevel = door[0] - '0';
ドア番号が31なら、先頭の文字'3'から文字の'0'を引くので、doorLevelは3になります。
2. 入室・退室それぞれの通過前後のレベルを求めます
入室の場合は、ドアのレベルより1つ低い場所から、ドアのレベルへ移動します。たとえば、ドア31から入室するなら、レベル2からレベル3への移動です。
退室の場合は逆に、ドアのレベルから1つ低いレベルへ移動します。
関数では、最初にbeforeLevelとafterLevelをどちらもdoorLevelにしています。その後、入室ならbeforeLevelだけをdoorLevel - 1にし、退室ならafterLevelだけをdoorLevel - 1にしています。
なお、可否が“R”ならドアは開錠されていません。実際には移動していないので、afterLevelをbeforeLevelと同じ値にします。
3. 現在のレベルと、今回の移動に必要なレベルを比べます
正しい順序で入退室していれば、現在のlevelは、今回の移動の通過前のレベルであるbeforeLevelと一致します。
たとえば、現在のレベルが1なのに、ドア31から入室しようとしている場合、ドアを通る直前にいるべき場所はレベル2です。つまり、レベル1からレベル2への移動記録が抜けています。このとき印字する内容は「***** Level 1-->2」です。
印字する移動元は、現在のレベルを表すlevelです。移動先は、今回のドアを通る直前にいるべきレベルを表すbeforeLevelです。したがって、fにはlevel、gにはbeforeLevelを指定します。
不整合の印字後は、今回の入退室を終えた後のレベルをlevelに記録します。次のレコードを確認するときは、この値を現在のレベルとして使います。
4. checkLevel()を呼び出す位置を決めます
checkLevel()は、1件の入退室レコードについて、現在のレベルと通過前のレベルを比べる処理です。そのため、各レコードを印字するputRecord()の中で、レコードごとに呼び出す必要があります。
行⑦の直後は、カードIDと名前を印字する条件分岐を抜けた位置です。この後も、同じputRecord()の中で今回のレコードの印字が続きます。ここにcheckLevel();を追加すれば、各レコードについてレベルの整合性を確認できます。したがって、dは行⑦の直後に当たるオです。
5. clearLevel()を呼び出す位置を決めます
clearLevel()は、前の入退室者が外部まで戻ったかを確認する処理です。カードIDが変わった場合は前の人の確認が必要です。また、最後のレコードの後には次のカードIDがないので、ファイルの終わりでも確認が必要です。
プログラムでは、行④で次のレコードを読み込みます。その後なら、カードIDが変わったか、またはファイルの終わりに達したかを判定できます。したがって、clearLevel();は行④の直後に追加します。eはイです。
誤りやすいポイント
-
ドアのレベルと、通過前のレベルを混同すること
ドア31のレベルは3ですが、レベル3へ入室するときの通過前のレベルは2です。入室ではbeforeLevelがドアのレベルより1小さくなります。 -
不整合の表示にafterLevelを使うこと
不整合の表示は、現在の場所から、今回のドアを通る直前にいるべき場所への移動を表します。そのため、表示する値はlevelからbeforeLevelです。 -
拒否された記録で、レベルを移動後の値に変えてしまうこと
拒否された場合はドアを通過していません。afterLevelをbeforeLevelと同じ値にし、実際の移動がなかったことを表します。 -
clearLevel()を次のレコードを読む前に呼び出すこと
次のレコードを読んでからでないと、カードIDが変わったか、ファイルの終わりかを判断できません。したがって、clearLevel();は行④の直後です。
FAQ
Q: カードIDが変わったとき、なぜレベルを0に戻すのですか?
A: levelは、現在確認している入退室者の場所を表すためです。人が変わったら、新しい人の開始位置である外部、つまりレベル0から確認します。
A: levelは、現在確認している入退室者の場所を表すためです。人が変わったら、新しい人の開始位置である外部、つまりレベル0から確認します。
Q: ファイルの終わりでもclearLevel()を実行するのはなぜですか?
A: 最後の人には、次のカードIDがありません。ファイルの終わりで最後の人の状態を確認し、レベルが0でなければ、外部まで戻る記録が抜けていると判断します。
A: 最後の人には、次のカードIDがありません。ファイルの終わりで最後の人の状態を確認し、レベルが0でなければ、外部まで戻る記録が抜けていると判断します。
Q: “R”の記録もレベルの確認対象ですか?
A: 対象です。ただし、拒否された記録では実際に移動していないので、afterLevelをbeforeLevelと同じにします。これにより、拒否された記録を移動として扱わずに済みます。
A: 対象です。ただし、拒否された記録では実際に移動していないので、afterLevelをbeforeLevelと同じにします。これにより、拒否された記録を移動として扱わずに済みます。
関連キーワード: 状態遷移、文字列比較、ファイル処理、条件分岐、C言語



