merging MOODLE_19_QUESTIONS with HEAD
[moodle.git] / question / type / match / questiontype.php
CommitLineData
516cf3eb 1<?php // $Id$
2
3/////////////
4/// MATCH ///
5/////////////
6
7/// QUESTION TYPE CLASS //////////////////
1976496e 8/**
9 * @package questionbank
10 * @subpackage questiontypes
7375c542 11 */
af3830ee 12class question_match_qtype extends default_questiontype {
516cf3eb 13
14 function name() {
15 return 'match';
16 }
17
18 function get_question_options(&$question) {
32a189d6 19 $question->options = get_record('question_match', 'question', $question->id);
f07d1d31 20 $question->options->subquestions = get_records('question_match_sub', 'question', $question->id, 'id ASC');
516cf3eb 21 return true;
22 }
23
24 function save_question_options($question) {
a58ffe3f 25 $result = new stdClass;
069a820a 26
32a189d6 27 if (!$oldsubquestions = get_records("question_match_sub", "question", $question->id, "id ASC")) {
516cf3eb 28 $oldsubquestions = array();
29 }
30
516cf3eb 31 // $subquestions will be an array with subquestion ids
32 $subquestions = array();
33
34 // Insert all the new question+answer pairs
35 foreach ($question->subquestions as $key => $questiontext) {
36 $answertext = $question->subanswers[$key];
37 if (!empty($questiontext) or !empty($answertext)) {
38 if ($subquestion = array_shift($oldsubquestions)) { // Existing answer, so reuse it
39 $subquestion->questiontext = $questiontext;
40 $subquestion->answertext = $answertext;
32a189d6 41 if (!update_record("question_match_sub", $subquestion)) {
7518b645 42 $result->error = "Could not insert match subquestion! (id=$subquestion->id)";
516cf3eb 43 return $result;
44 }
45 } else {
a58ffe3f 46 $subquestion = new stdClass;
516cf3eb 47 // Determine a unique random code
48 $subquestion->code = rand(1,999999999);
18bd0d68 49 while (record_exists('question_match_sub', 'code', $subquestion->code, 'question', $question->id)) {
516cf3eb 50 $subquestion->code = rand();
51 }
52 $subquestion->question = $question->id;
53 $subquestion->questiontext = $questiontext;
54 $subquestion->answertext = $answertext;
32a189d6 55 if (!$subquestion->id = insert_record("question_match_sub", $subquestion)) {
7518b645 56 $result->error = "Could not insert match subquestion!";
516cf3eb 57 return $result;
58 }
59 }
60 $subquestions[] = $subquestion->id;
61 }
a58ffe3f 62 if (!empty($questiontext) && empty($answertext)) {
63 $result->notice = get_string('nomatchinganswer', 'quiz', $questiontext);
64 }
516cf3eb 65 }
66
67 // delete old subquestions records
68 if (!empty($oldsubquestions)) {
69 foreach($oldsubquestions as $os) {
32a189d6 70 delete_records('question_match_sub', 'id', $os->id);
516cf3eb 71 }
72 }
73
32a189d6 74 if ($options = get_record("question_match", "question", $question->id)) {
516cf3eb 75 $options->subquestions = implode(",",$subquestions);
76 $options->shuffleanswers = $question->shuffleanswers;
32a189d6 77 if (!update_record("question_match", $options)) {
7518b645 78 $result->error = "Could not update match options! (id=$options->id)";
516cf3eb 79 return $result;
80 }
81 } else {
82 unset($options);
83 $options->question = $question->id;
84 $options->subquestions = implode(",",$subquestions);
85 $options->shuffleanswers = $question->shuffleanswers;
32a189d6 86 if (!insert_record("question_match", $options)) {
7518b645 87 $result->error = "Could not insert match options!";
516cf3eb 88 return $result;
89 }
90 }
a58ffe3f 91
92 if (!empty($result->notice)) {
93 return $result;
94 }
95
96 if (count($subquestions) < 3) {
97 $result->notice = get_string('notenoughanswers', 'quiz', 3);
98 return $result;
99 }
100
516cf3eb 101 return true;
102 }
103
104 /**
105 * Deletes question from the question-type specific tables
106 *
107 * @return boolean Success/Failure
108 * @param integer $question->id
109 */
90c3f310 110 function delete_question($questionid) {
111 delete_records("question_match", "question", $questionid);
112 delete_records("question_match_sub", "question", $questionid);
516cf3eb 113 return true;
114 }
115
116 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
f07d1d31 117 if (!$state->options->subquestions = get_records('question_match_sub', 'question', $question->id, 'id ASC')) {
00c30069 118 notify('Error: Missing subquestions!');
119 return false;
516cf3eb 120 }
121
122 foreach ($state->options->subquestions as $key => $subquestion) {
123 // This seems rather over complicated, but it is useful for the
124 // randomsamatch questiontype, which can then inherit the print
125 // and grading functions. This way it is possible to define multiple
126 // answers per question, each with different marks and feedback.
127 $answer = new stdClass();
128 $answer->id = $subquestion->code;
129 $answer->answer = $subquestion->answertext;
130 $answer->fraction = 1.0;
131 $state->options->subquestions[$key]->options
00c30069 132 ->answers[$subquestion->code] = clone($answer);
516cf3eb 133
134 $state->responses[$key] = '';
135 }
136
137 // Shuffle the answers if required
138 if ($cmoptions->shuffleanswers and $question->options->shuffleanswers) {
139 $state->options->subquestions = swapshuffle_assoc($state->options->subquestions);
140 }
141
142 return true;
143 }
144
145 function restore_session_and_responses(&$question, &$state) {
146 // The serialized format for matching questions is a comma separated
147 // list of question answer pairs (e.g. 1-1,2-3,3-2), where the ids of
32a189d6 148 // both refer to the id in the table question_match_sub.
516cf3eb 149 $responses = explode(',', $state->responses['']);
150 $responses = array_map(create_function('$val',
151 'return explode("-", $val);'), $responses);
152
f07d1d31 153 if (!$questions = get_records('question_match_sub', 'question', $question->id, 'id ASC')) {
516cf3eb 154 notify('Error: Missing subquestions!');
155 return false;
156 }
157
158 // Restore the previous responses and place the questions into the state options
159 $state->responses = array();
160 $state->options->subquestions = array();
161 foreach ($responses as $response) {
162 $state->responses[$response[0]] = $response[1];
163 $state->options->subquestions[$response[0]] = $questions[$response[0]];
164 }
165
166 foreach ($state->options->subquestions as $key => $subquestion) {
167 // This seems rather over complicated, but it is useful for the
168 // randomsamatch questiontype, which can then inherit the print
169 // and grading functions. This way it is possible to define multiple
170 // answers per question, each with different marks and feedback.
171 $answer = new stdClass();
172 $answer->id = $subquestion->code;
173 $answer->answer = $subquestion->answertext;
174 $answer->fraction = 1.0;
175 $state->options->subquestions[$key]->options
176 ->answers[$subquestion->code] = clone($answer);
177 }
178
179 return true;
180 }
181
182 function save_session_and_responses(&$question, &$state) {
87ee4968 183 $subquestions = &$state->options->subquestions;
184
185 // Prepare an array to help when disambiguating equal answers.
186 $answertexts = array();
187 foreach ($subquestions as $subquestion) {
188 $ans = reset($subquestion->options->answers);
189 $answertexts[$ans->id] = $ans->answer;
190 }
069a820a 191
516cf3eb 192 // Serialize responses
193 $responses = array();
87ee4968 194 foreach ($subquestions as $key => $subquestion) {
7d6af8ca 195 $response = 0;
0c24ee0f 196 if ($subquestion->questiontext) {
87ee4968 197 if ($state->responses[$key]) {
198 $response = $state->responses[$key];
199 if (!array_key_exists($response, $subquestion->options->answers)) {
200 // If studen's answer did not match by id, but there may be
201 // two answers with the same text, but different ids,
202 // so we need to try matching the answer text.
203 $expected_answer = reset($subquestion->options->answers);
204 if ($answertexts[$response] == $expected_answer->answer) {
205 $response = $expected_answer->id;
206 $state->responses[$key] = $response;
207 }
208 }
87ee4968 209 }
0c24ee0f 210 }
7d6af8ca 211 $responses[] = $key.'-'.$response;
516cf3eb 212 }
213 $responses = implode(',', $responses);
214
215 // Set the legacy answer field
0c24ee0f 216 if (!set_field('question_states', 'answer', $responses, 'id', $state->id)) {
516cf3eb 217 return false;
218 }
219 return true;
220 }
221
222 function get_correct_responses(&$question, &$state) {
223 $responses = array();
224 foreach ($state->options->subquestions as $sub) {
225 foreach ($sub->options->answers as $answer) {
a58ffe3f 226 if (1 == $answer->fraction && $sub->questiontext) {
516cf3eb 227 $responses[$sub->id] = $answer->id;
228 }
229 }
230 }
231 return empty($responses) ? null : $responses;
232 }
233
234 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
37a12367 235 global $CFG;
516cf3eb 236 $subquestions = $state->options->subquestions;
237 $correctanswers = $this->get_correct_responses($question, $state);
238 $nameprefix = $question->name_prefix;
239 $answers = array();
0b4ce29d 240 $allanswers = array();
87ee4968 241 $answerids = array();
516cf3eb 242 $responses = &$state->responses;
243
069a820a 244 // Prepare a list of answers, removing duplicates.
516cf3eb 245 foreach ($subquestions as $subquestion) {
246 foreach ($subquestion->options->answers as $ans) {
0b4ce29d 247 $allanswers[$ans->id] = $ans->answer;
87ee4968 248 if (!in_array($ans->answer, $answers)) {
249 $answers[$ans->id] = $ans->answer;
250 $answerids[$ans->answer] = $ans->id;
87ee4968 251 }
516cf3eb 252 }
253 }
069a820a 254
0b4ce29d 255 // Fix up the ids of any responses that point the the eliminated duplicates.
256 foreach ($responses as $subquestionid => $ignored) {
257 if ($responses[$subquestionid]) {
258 $responses[$subquestionid] = $answerids[$allanswers[$responses[$subquestionid]]];
259 }
260 }
261 foreach ($correctanswers as $subquestionid => $ignored) {
262 $correctanswers[$subquestionid] = $answerids[$allanswers[$correctanswers[$subquestionid]]];
263 }
516cf3eb 264
265 // Shuffle the answers
266 $answers = draw_rand_array($answers, count($answers));
267
37a12367 268 // Print formulation
1b8a7434 269 $questiontext = $this->format_text($question->questiontext,
270 $question->questiontextformat, $cmoptions);
37a12367 271 $image = get_question_image($question, $cmoptions->course);
516cf3eb 272
069a820a 273 // Print the input controls
516cf3eb 274 foreach ($subquestions as $key => $subquestion) {
a58ffe3f 275 if ($subquestion->questiontext) {
069a820a 276 // Subquestion text:
a58ffe3f 277 $a = new stdClass;
1b8a7434 278 $a->text = $this->format_text($subquestion->questiontext,
279 $question->questiontextformat, $cmoptions);
2b087056 280
069a820a 281 // Drop-down list:
a58ffe3f 282 $menuname = $nameprefix.$subquestion->id;
283 $response = isset($state->responses[$subquestion->id])
284 ? $state->responses[$subquestion->id] : '0';
2b087056 285
286 $a->class = ' ';
287 $a->feedbackimg = ' ';
288
069a820a 289 if ($options->readonly and $options->correct_responses) {
290 if (isset($correctanswers[$subquestion->id])
2b087056 291 and ($correctanswers[$subquestion->id] == $response)) {
069a820a 292 $correctresponse = 1;
293 } else {
294 $correctresponse = 0;
295 }
2b087056 296
1b16ecd1 297 if ($options->feedback && $response) {
069a820a 298 $a->class = question_get_feedback_class($correctresponse);
299 $a->feedbackimg = question_get_feedback_image($correctresponse);
300 }
2b087056 301 }
302
303 $a->control = choose_from_menu($answers, $menuname, $response, 'choose',
304 '', 0, true, $options->readonly);
069a820a 305
a58ffe3f 306 // Neither the editing interface or the database allow to provide
307 // fedback for this question type.
308 // However (as was pointed out in bug bug 3294) the randomsamatch
309 // type which reuses this method can have feedback defined for
310 // the wrapped shortanswer questions.
311 //if ($options->feedback
312 // && !empty($subquestion->options->answers[$responses[$key]]->feedback)) {
313 // print_comment($subquestion->options->answers[$responses[$key]]->feedback);
314 //}
2b087056 315
a58ffe3f 316 $anss[] = $a;
516cf3eb 317 }
516cf3eb 318 }
aaae75b0 319 include("$CFG->dirroot/question/type/match/display.html");
516cf3eb 320 }
321
322 function grade_responses(&$question, &$state, $cmoptions) {
323 $subquestions = &$state->options->subquestions;
324 $responses = &$state->responses;
325
87ee4968 326 // Prepare an array to help when disambiguating equal answers.
327 $answertexts = array();
328 foreach ($subquestions as $subquestion) {
329 $ans = reset($subquestion->options->answers);
330 $answertexts[$ans->id] = $ans->answer;
331 }
069a820a 332
87ee4968 333 // Add up the grades from each subquestion.
516cf3eb 334 $sumgrade = 0;
a58ffe3f 335 $totalgrade = 0;
516cf3eb 336 foreach ($subquestions as $key => $sub) {
a58ffe3f 337 if ($sub->questiontext) {
338 $totalgrade += 1;
87ee4968 339 $response = $responses[$key];
340 if ($response && !array_key_exists($response, $sub->options->answers)) {
341 // If studen's answer did not match by id, but there may be
342 // two answers with the same text, but different ids,
343 // so we need to try matching the answer text.
344 $expected_answer = reset($sub->options->answers);
345 if ($answertexts[$response] == $expected_answer->answer) {
346 $response = $expected_answer->id;
347 }
348 }
349 if (array_key_exists($response, $sub->options->answers)) {
350 $sumgrade += $sub->options->answers[$response]->fraction;
a58ffe3f 351 }
516cf3eb 352 }
353 }
354
a58ffe3f 355 $state->raw_grade = $sumgrade/$totalgrade;
516cf3eb 356 if (empty($state->raw_grade)) {
357 $state->raw_grade = 0;
358 }
359
360 // Make sure we don't assign negative or too high marks
361 $state->raw_grade = min(max((float) $state->raw_grade,
362 0.0), 1.0) * $question->maxgrade;
363 $state->penalty = $question->penalty * $question->maxgrade;
364
f30bbcaf 365 // mark the state as graded
366 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
367
516cf3eb 368 return true;
369 }
370
b25486fc 371 function compare_responses($question, $state, $teststate) {
2c89cfb5 372 foreach ($state->responses as $i=>$sr) {
373 if (empty($teststate->responses[$i])) {
374 if (!empty($state->responses[$i])) {
375 return false;
376 }
377 } else if ($state->responses[$i] != $teststate->responses[$i]) {
b25486fc 378 return false;
379 }
380 }
381 return true;
382 }
383
516cf3eb 384 // ULPGC ecastro for stats report
385 function get_all_responses($question, $state) {
974383f9 386 $answers = array();
516cf3eb 387 if (is_array($question->options->subquestions)) {
645d7610 388 foreach ($question->options->subquestions as $aid => $answer) {
389 if ($answer->questiontext) {
390 $r = new stdClass;
391 $r->answer = $answer->questiontext . ": " . $answer->answertext;
392 $r->credit = 1;
393 $answers[$aid] = $r;
394 }
516cf3eb 395 }
516cf3eb 396 }
a58ffe3f 397 $result = new stdClass;
516cf3eb 398 $result->id = $question->id;
399 $result->responses = $answers;
400 return $result;
401 }
402
403 // ULPGC ecastro
404 function get_actual_response($question, $state) {
01bd54e0 405 $subquestions = &$state->options->subquestions;
406 $responses = &$state->responses;
407 $results=array();
408 foreach ($subquestions as $key => $sub) {
409 foreach ($responses as $ind => $code) {
410 if (isset($sub->options->answers[$code])) {
645d7610 411 $results[$ind] = $subquestions[$ind]->questiontext . ": " . $sub->options->answers[$code]->answer;
01bd54e0 412 }
413 }
414 }
415 return $results;
416 }
0a5b58af 417
418 function response_summary($question, $state, $length=80) {
419 // This should almost certainly be overridden
755bddf1 420 return substr(implode(', ', $this->get_actual_response($question, $state)), 0, $length);
0a5b58af 421 }
069a820a 422
c5d94c41 423/// BACKUP FUNCTIONS ////////////////////////////
424
425 /*
426 * Backup the data in the question
427 *
428 * This is used in question/backuplib.php
429 */
430 function backup($bf,$preferences,$question,$level=6) {
431
432 $status = true;
433
f07d1d31 434 $matchs = get_records('question_match_sub', 'question', $question, 'id ASC');
c5d94c41 435 //If there are matchs
436 if ($matchs) {
437 $status = fwrite ($bf,start_tag("MATCHS",6,true));
438 //Iterate over each match
439 foreach ($matchs as $match) {
440 $status = fwrite ($bf,start_tag("MATCH",7,true));
441 //Print match contents
442 fwrite ($bf,full_tag("ID",8,false,$match->id));
443 fwrite ($bf,full_tag("CODE",8,false,$match->code));
444 fwrite ($bf,full_tag("QUESTIONTEXT",8,false,$match->questiontext));
445 fwrite ($bf,full_tag("ANSWERTEXT",8,false,$match->answertext));
446 $status = fwrite ($bf,end_tag("MATCH",7,true));
447 }
448 $status = fwrite ($bf,end_tag("MATCHS",6,true));
449 }
450 return $status;
451 }
516cf3eb 452
315559d3 453/// RESTORE FUNCTIONS /////////////////
454
455 /*
456 * Restores the data in the question
457 *
458 * This is used in question/restorelib.php
459 */
460 function restore($old_question_id,$new_question_id,$info,$restore) {
461
462 $status = true;
463
464 //Get the matchs array
465 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
466
467 //We have to build the subquestions field (a list of match_sub id)
468 $subquestions_field = "";
469 $in_first = true;
470
471 //Iterate over matchs
472 for($i = 0; $i < sizeof($matchs); $i++) {
473 $mat_info = $matchs[$i];
474
475 //We'll need this later!!
476 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
477
478 //Now, build the question_match_SUB record structure
a58ffe3f 479 $match_sub = new stdClass;
315559d3 480 $match_sub->question = $new_question_id;
1f4d6e9a 481 $match_sub->code = isset($mat_info['#']['CODE']['0']['#'])?backup_todb($mat_info['#']['CODE']['0']['#']):'';
315559d3 482 if (!$match_sub->code) {
483 $match_sub->code = $oldid;
484 }
485 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
486 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
487
488 //The structure is equal to the db, so insert the question_match_sub
489 $newid = insert_record ("question_match_sub",$match_sub);
490
491 //Do some output
492 if (($i+1) % 50 == 0) {
493 if (!defined('RESTORE_SILENTLY')) {
494 echo ".";
495 if (($i+1) % 1000 == 0) {
496 echo "<br />";
497 }
498 }
499 backup_flush(300);
500 }
501
502 if ($newid) {
503 //We have the newid, update backup_ids
504 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
505 $newid);
506 //We have a new match_sub, append it to subquestions_field
507 if ($in_first) {
508 $subquestions_field .= $newid;
509 $in_first = false;
510 } else {
511 $subquestions_field .= ",".$newid;
512 }
513 } else {
514 $status = false;
515 }
516 }
517
518 //We have created every match_sub, now create the match
87ee4968 519 $match = new stdClass;
315559d3 520 $match->question = $new_question_id;
521 $match->subquestions = $subquestions_field;
522
523 //The structure is equal to the db, so insert the question_match_sub
524 $newid = insert_record ("question_match",$match);
525
526 if (!$newid) {
527 $status = false;
528 }
529
530 return $status;
531 }
532
533 function restore_map($old_question_id,$new_question_id,$info,$restore) {
534
535 $status = true;
536
537 //Get the matchs array
538 $matchs = $info['#']['MATCHS']['0']['#']['MATCH'];
539
540 //We have to build the subquestions field (a list of match_sub id)
541 $subquestions_field = "";
542 $in_first = true;
543
544 //Iterate over matchs
545 for($i = 0; $i < sizeof($matchs); $i++) {
546 $mat_info = $matchs[$i];
547
548 //We'll need this later!!
549 $oldid = backup_todb($mat_info['#']['ID']['0']['#']);
550
551 //Now, build the question_match_SUB record structure
552 $match_sub->question = $new_question_id;
553 $match_sub->questiontext = backup_todb($mat_info['#']['QUESTIONTEXT']['0']['#']);
554 $match_sub->answertext = backup_todb($mat_info['#']['ANSWERTEXT']['0']['#']);
555
556 //If we are in this method is because the question exists in DB, so its
557 //match_sub must exist too.
558 //Now, we are going to look for that match_sub in DB and to create the
559 //mappings in backup_ids to use them later where restoring states (user level).
560
561 //Get the match_sub from DB (by question, questiontext and answertext)
562 $db_match_sub = get_record ("question_match_sub","question",$new_question_id,
563 "questiontext",$match_sub->questiontext,
564 "answertext",$match_sub->answertext);
565 //Do some output
566 if (($i+1) % 50 == 0) {
567 if (!defined('RESTORE_SILENTLY')) {
568 echo ".";
569 if (($i+1) % 1000 == 0) {
570 echo "<br />";
571 }
572 }
573 backup_flush(300);
574 }
575
576 //We have the database match_sub, so update backup_ids
577 if ($db_match_sub) {
578 //We have the newid, update backup_ids
579 backup_putid($restore->backup_unique_code,"question_match_sub",$oldid,
580 $db_match_sub->id);
581 } else {
582 $status = false;
583 }
584 }
585
586 return $status;
587 }
588
589 function restore_recode_answer($state, $restore) {
590
591 //The answer is a comma separated list of hypen separated math_subs (for question and answer)
592 $answer_field = "";
593 $in_first = true;
594 $tok = strtok($state->answer,",");
595 while ($tok) {
596 //Extract the match_sub for the question and the answer
597 $exploded = explode("-",$tok);
598 $match_question_id = $exploded[0];
599 $match_answer_code = $exploded[1];
600 //Get the match_sub from backup_ids (for the question)
601 if (!$match_que = backup_getid($restore->backup_unique_code,"question_match_sub",$match_question_id)) {
602 echo 'Could not recode question_match_sub '.$match_question_id.'<br />';
603 }
604 if ($in_first) {
605 $answer_field .= $match_que->new_id."-".$match_answer_code;
606 $in_first = false;
607 } else {
608 $answer_field .= ",".$match_que->new_id."-".$match_answer_code;
609 }
610 //check for next
611 $tok = strtok(",");
612 }
613 return $answer_field;
614 }
615
c85607f0 616 /**
617 * Decode links in question type specific tables.
618 * @return bool success or failure.
619 */
620 function decode_content_links_caller($questionids, $restore, &$i) {
e3b2eb60 621 $status = true;
622
c85607f0 623 // Decode links in the question_match_sub table.
624 if ($subquestions = get_records_list('question_match_sub', 'question',
625 implode(',', $questionids), '', 'id, questiontext')) {
626
627 foreach ($subquestions as $subquestion) {
628 $questiontext = restore_decode_content_links_worker($subquestion->questiontext, $restore);
629 if ($questiontext != $subquestion->questiontext) {
630 $subquestion->questiontext = addslashes($questiontext);
631 if (!update_record('question_match_sub', $subquestion)) {
632 $status = false;
633 }
634 }
635
636 // Do some output.
637 if (++$i % 5 == 0 && !defined('RESTORE_SILENTLY')) {
638 echo ".";
639 if ($i % 100 == 0) {
640 echo "<br />";
641 }
642 backup_flush(300);
643 }
644 }
645 }
e3b2eb60 646
647 return $status;
c85607f0 648 }
516cf3eb 649}
650//// END OF CLASS ////
651
652//////////////////////////////////////////////////////////////////////////
653//// INITIATION - Without this line the question type is not in use... ///
654//////////////////////////////////////////////////////////////////////////
a2156789 655question_register_questiontype(new question_match_qtype());
516cf3eb 656?>