PascalABC.NET

Модуль PT4TaskMakerNET: примеры разработки учебных заданий


Создание простейшей сводной группы

Вначале опишем действия по созданию наиболее простого варианта группы заданий — сводной группы, в которой не разрабатываются новые задания, а лишь производится перекомпоновка заданий из имеющихся групп.

Создадим группу заданий MakerDemo, в которую импортируем два первых задания из базовой группы Begin. Следуя правилам об именовании dll-файлов с группами заданий, дадим нашей библиотеке имя PT4MakerDemo.

Файл PT4MakerDemo.pas, содержащий сводную группу заданий, является кратким и имеет стандартную структуру:

library PT4MakerDemo;

uses PT4TaskMakerNET;

procedure InitTask(num: integer);
begin
  case num of
  1..2: UseTask('Begin', num);
  end;
end;

procedure inittaskgroup;
begin
  CreateGroup('MakerDemo', 'Примеры различных задач',
    'М. Э. Абрамян, 2013', 'qwqfsdf13dfttd', 2, InitTask);
end;

procedure activate(S: string);
begin
  ActivateNET(S);
end;

begin
end.

К библиотеке подключается модуль PT4TaskMakerNET, после чего в ней описывается основная процедура группы заданий InitTask, определяющая задание по его номеру. Поскольку мы не создавали своих заданий, в данной процедуре используется только стандартная процедура UseTask, позволяющая импортировать задания из имеющихся групп. В нашем случае импортируются задания с номерами 1 и 2 из группы Begin.

Затем описывается процедура инициализации данной группы заданий. Она должна иметь стандартное имя inittaskgroup (набранное строчными, т. е. маленькими буквами). В этой процедуре вызывается процедура CreateGroup, в которой задаются настройки создаваемой группы: имя ('MakerDemo'), описание ('Примеры различных задач'), сведения об авторе, строковый ключ, число заданий (2) и основная процедура группы (InitTask).

После процедуры inittaskgroup описывается вспомогательная процедура activate (ее имя также должно быть набрано строчными буквами), в которой необходимо вызвать процедуру ActivateNET, описанную в модуле PT4TaskMakerNET.

Тестирование созданной группы

Для успешной компиляции программы с созданной группой необходимо, чтобы ей был доступен модуль PT4TaskMakerNET. Этот модуль входит в число стандартных модулей библиотеки системы PascalABC.NET и размещается в подкаталоге LIB системного каталога PascalABC.NET, поэтому копировать его в рабочий каталог не требуется. Однако даже при успешной компиляции программы просмотреть задания группы не удастся, так как созданную библиотеку (dll-файл) нельзя запускать на выполнение (при успешной компиляции будет выведено сообщение «Невозможно запустить динамическую библиотеку».

Для тестирования полученной библиотеки необходимо создать вспомогательную программу, являющуюся заготовкой для выполнения заданий из созданной группы. Так как после успешной компиляции библиотеки в рабочем каталоге уже содержится файл PT4MakerDemo.dll, для создания программы-заготовки можно использовать программный модуль PT4Load. Вызвав его окно на экран (для этого достаточно использовать клавиатурную комбинацию [Shift]+[Ctrl]+[L]) и удалив, при необходимости, имя ранее введенного задания, мы должны увидеть в списке доступных групп заданий созданную нами группу MakerDemo. Если имя группы MakerDemo не отображается, значит, задачник не смог успешно загрузить эту группу из библиотеки PT4MakerDemo.dll. В этом случае необходимо проверить имя созданной библиотеки (в частности, наличие в нем префикса PT4) и наличие в файле библиотеки процедур inittaskgroup и activate, определенных по описанным выше правилам.

Если имя группы появилось в списке, то надо ввести в поле «Задание» имя «MakerDemo1» и нажать клавишу [Enter] (или кнопку «Загрузка»); в результате будет создан файл MakerDemo1.pas, который сразу загрузится в редактор среды PascalABC.NET. Приведем содержимое этого файла:

uses PT4;

begin
  Task('MakerDemo1');

end.

Поскольку мы собираемся просматривать задания группы в демо-режиме, добавим в конец строки с именем задания символ «?»:

  Task('MakerDemo1?');

После компиляции и запуска полученной программы на экране отобразится окно задачника с указанным заданием данной группы:

По умолчанию окно задачника отображается в режиме с динамической компоновкой, который появился в версии 4.11 и является более наглядным, чем режим с фиксированной компоновкой. Однако при разработке заданий желательно применять режим с фиксированной компоновкой, поскольку он позволит выявить недостатки форматирования (в частности, вертикального выравнивания данных), присущие только этому режиму. Для переключения между режимами отображения данных достаточно нажать клавишу [F4]. После выполнения этого действия окно задачника изменится следующим образом:

В окне задачника можно просматривать все имеющиеся задания данной группы (нажимая клавиши [Enter] и [Backspace], а также генерировать различные варианты исходных данных и связанных с ними контрольных (т. е. «правильных») результатов. При закрытии окна программа немедленно завершит работу, и мы вернемся в редактор среды PascalABC.NET. Заметим, что при последующих запусках программы будет автоматически выбираться тот режим окна задачника, в котором оно находилось в момент его предшествующего закрытия.

Примечание. После добавления в группу нового задания было бы желательно, чтобы при звпуске тестирующей программы на экране сразу отображались данные, связанные с последним добавленным заданием. Чтобы не приходилось каждый раз изменять номер задания в процедуре Task, можно удалить этот номер, указав символ «?» сразу после имени группы: Task('MakerDemo?'). В этом случае при запуске программы на экране будет отображаться последнее задание данной группы.

Добавление описания группы и ее подгрупп

По тексту, расположенному выше названия задания MakerDemo1 (см. приведенные выше рисунки), мы видим, что импортированные из группы Begin задания входят в подгруппу с заголовком «Ввод и вывод данных, оператор присваивания». В сводной группе MakerDemo мы можем добавить комментарий (преамбулу) как к самой группе, так и к любой имеющейся в ней подгруппе. Кроме того, мы можем импортировать преамбулу любой имеющейся группы или подгруппы. Для иллюстрации этих возможностей добавим в процедуру inittaskgroup новые операторы (их надо указать после вызова процедуры CreateGroup):

CommentText('Данная группа демонстрирует различные возможности');
CommentText('\Iконструктора учебных заданий\i \MPT4TaskMaker\m.');

Subgroup('Ввод и вывод данных, оператор присваивания');
CommentText('В этой подгруппе содержатся задания, импортированные');
CommentText('из группы Begin.\PПриводимый ниже абзац преамбулы');
CommentText('также импортирован из данной группы.\P');
UseComment('Begin');

Два первых вызова процедуры CommentText определяют текст преамбулы для группы MakerDemo. Обратите внимание на управляющие последовательности: пара последовательностей \I и \i выделяет курсивный фрагмент, а пара \M и \m выделяет фрагмент, в которым используется моноширинный шрифт. Последующий вызов процедуры Subgroup устанавливает режим определения преамбулы для подгруппы с указанным именем. В тексте этой преамбулы, который, как и текст преамбулы группы, определяется с помощью процедуры CommentText, используется управляющая последовательность \P, обеспечивающая переход к новому абзацу.

Наконец, последняя процедура (UseComment) импортирует преамбулу группы Begin в преамбулу нашей подгруппы «Ввод и вывод данных, оператор присваивания». Имеется также вариант процедуры UseComment, позволяющий импортировать преамбулу подгруппы; в этом варианте следует указать два параметра: имя группы и заголовок требуемой подгруппы, входящей в эту группу. Импортировать преамбулы подгрупп можно только для тех групп заданий, в которых имеется разделение на подгруппы (обычно это группы, содержащие большое количество заданий). В группе Begin деления на подгруппы нет, поэтому из нее можно импортировать только преамбулу самой группы.

Для того чтобы ознакомиться с результатом сделанных изменений, следует сгенерировать html-страницу с текстом группы MakerDemo. Для этого достаточно внести небольшое изменение в тестирующую программу, а именно, следует заменить символ «?» в параметре процедуры Task на «#»: Task('MakerDemo?'). Теперь при запуске данной программы на экране вместо окна задачника появится html-браузер с описанием созданной группы:

Обратите внимание на последний абзац в описании подгруппы («Все входные и выходные данные в заданиях этой группы являются вещественными числами»), который был импортирован из группы Begin.

Примечание. Если указать в параметре процедуры Task символ «#», не удаляя номер задания (например, Task('MakerDemo2#')), то в html-описание будет включено только задание с указанным номером. При этом будут также выведены комментарии ко всей группе и к той подгруппе, к которой относится выбранное задание. Для включения в html-страницу нескольких заданий (или групп заданий) достаточно для каждого из них вызвать процедуру Task с параметром, оканчивающимся символом «#».

Добавление нового задания

Добавим к нашей группе новое задание. Фактически это задание будет дублировать задание Begin3, однако вместо импортирования этого задания мы разработаем его самостоятельно. Все действия по созданию нового задания удобно реализовать во вспомогательной процедуре, которую можно назвать MakerDemo3 (таким образом, название процедуры будет соответствовать имени создаваемого задания, хотя это и не является обязательным):

procedure MakerDemo3;
var
  a, b: real;
begin
  CreateTask('Ввод и вывод данных, оператор присваивания');
  TaskText('Даны стороны прямоугольника~{a} и~{b}.', 0, 2);
  TaskText('Найти его площадь {S}~=~{a}\*{b} и периметр {P}~=~2\*({a}\;+\;{b}).',
    0, 4);
  a := RandomN(1, 99) / 10;
  b := RandomN(1, 99) / 10;
  DataR('a = ', a, xLeft, 3, 4);
  DataR('b = ', b, xRight, 3, 4);
  ResultR('S = ', a * b, 0, 2, 4);
  ResultR('P = ', 2 * (a + b), 0, 4, 4);
  SetTestCount(3);
end;

Описание процедуры MakerDemo3 (как и описания всех других процедур, обеспечивающих формирование новых заданий) следует разместить перед описанием процедуры InitTask.

Процедура MakerDemo3 включает все основные действия, используемые при формировании нового задания:

  • инициализацию нового задания (процедура CreateTask; мы указали в этой процедуре, что данное задание должно входить в подгруппу «Ввод и вывод данных, оператор присваивания», т. е. в ту же подгруппу, что и два предыдущих задания);
  • определение его формулировки (процедуры TaskText; обратите внимание на используемые в этих процедурах управляющие последовательности);
  • определение исходных (процедуры DataR) и результирующих данных (процедуры ResultR); при этом исходные данные генерируются с помощью датчика случайных чисел (процедура RandomN);
  • указание количества успешных тестовых запусков программы учащегося, достаточных для регистрации задания как выполненного (процедура SetTestCount; для нашего простого задания достаточно трех проведенных подряд успешных тестовых запусков).

Необходимо также включить вызов созданной процедуры в основную процедуру группы MakerDemo, связав его с номером 3:

procedure InitTask(num: integer);
begin
  case num of
  1..2: UseTask('Begin', num);
  3: MakerDemo3;
  end;
end;

Наконец, следует откорректировать число заданий в вызове процедуры CreateGroup, изменив его на 3.

Запустив тестирующую программу, мы увидим в html-описании группы MakerDemo формулировки трех заданий, а выполнив обратную замену в этой программе символа «#» на «?» (в результате вызов процедуры Task опять примет вид Task('MakerDemo?')) и повторно запустив программу на выполнение, мы увидим окно задачника с загруженным заданием MakerDemo3. Заметим, что при последующих запусках проекта мы будем получать в окне задачника различные исходные данные; это связано с тем, что при генерации исходных данных используется датчик случайных чисел.

Добавление заданий на обработку двумерных массивов и символьных строк

Добавим к группе MakerDemo еще два задания: первое из них дублирует задание Matrix7 (подгруппа «Двумерные массивы (матрицы): вывод элементов»), а второе не имеет полного аналога в группе String, однако может быть отнесено к ее первой подгруппе: «Символы и строки: основные операции». Реализуем эти задания в процедурах MakerDemo4 и MakerDemo5:

procedure MakerDemo4;
var
  m, n, i, j, k: integer;
  a: array [1..5, 1..8] of real;
begin
  CreateTask('Двумерные массивы (матрицы): вывод элементов');
  TaskText('Дана матрица размера~{M}\;\x\;{N} и целое число~{K} (1~\l~{K}~\l~{M}).',
    0, 2);
  TaskText('Вывести элементы {K}-й строки данной матрицы.', 0, 4);
  m := RandomN(2, 5);
  n := RandomN(4, 8);
  k := 1;
  if m = 5 then k := 0;
  DataN('M = ', m, 3, 1, 1);
  DataN('N = ', n, 10, 1, 1);
  for i := 1 to m do
    for j := 1 to n do
    begin
      a[i, j] := RandomR(-9.99, 9.99);
      DataR(a[i,j], Center(j, n, 5, 1), i + k, 5);
    end;
  k := RandomN(1, m);
  DataN('K = ', k, 68, 5, 1);
  for j := 1 to n do
    ResultR(a[k, j], Center(j, n, 5, 1), 3, 5);
  SetTestCount(5);
end;

procedure MakerDemo5;
var
  s: string;
begin
  CreateTask('Символы и строки: основные операции');
  TaskText('Дана непустая строка~{S}.', 0, 2);
  TaskText('Вывести ее первый и последний символ.', 0, 4);
  s := WordSample(RandomN(0, WordCount-1));
  if CurrentTest = 3 then
    while s[1] = s[Length(s)] do
      s := WordSample(RandomN(0, WordCount-1));
  DataS('S = ', s, 0, 3);
  ResultC('Первый символ: ', s[1], xLeft, 3);
  ResultC('Последний символ: ', s[Length(s)], xRight, 3);
  SetTestCount(4);
end;

Обратите внимание на использование вспомогательной функции Center для центрирования строк матрицы в области исходных и результирующих данных: каждый элемент матрицы занимает 5 экранных позиций, а между элементами размещается по одному пробелу. В процедуре MakerDemo4 не вызывается процедура SetTestCount; в этом случае число успешных тестов, необходимых для регистрации задания как выполненного, по умолчанию полагается равным 5.

При выводе элементов исходной матрицы и результирующей матричной строки дополнительные комментарии указывать не требуется, поэтому используется вариант процедур DataR и ResultR, в котором комментарий отсутствует (этот вариант процедур групп Data и Result добавлен в версию 4.11 конструктора учебных заданий).

В процедуре MakerDemo5 для получения исходных символьных строк используются функции WordCount и WordSample. С помощью этих функций можно получать различные варианты русских слов. Заметим, что в конструкторе PT4TaskMaker имеются также функции EnWordCount и EnWordSample, с помощью которых можно получать варианты английских слов.

В процедуре MakerDemo5 использована еще одна возможность, появившаяся в версии 4.11 конструктора: функция CurrentTest, возвращающая порядковый номер текущего тестового запуска. Использование этой функции позволяет связать какой-либо особый вариант теста с некоторым номером тестового испытания, и тем самым гарантировать, что программа с решением задачи обязательно будет проверена на этом особом варианте теста. В нашем случае строка S выбирается из набора слов-образцов, среди которых имеется сравнительно большое число слов, начинающихся и оканчивающихся одной и той же буквой. Для более надежного тестирования решения желательно гарантировать, что в наборе тестов будет хотя бы один тест, в котором начальный и конечный символ исходной строки различаются. Разумеется, можно было бы всегда выбирать подобные строки, используя соответствующий цикл while. Однако при наличии функции CurrentTest в этом нет необходимости: достаточно выполнять подобный цикл для единственного теста, например, с номером 3, как это сделано в приведенной реализации задания. В дальнейшем мы рассмотрим более содержательный пример использования функции CurrentTest.

Осталось изменить количество заданий в вызове процедуры CreateGroup на 5 и включить вызовы новых процедур в основную процедуру группы InitTask:

procedure InitTask(num: integer);
begin
  case num of
  1..2: UseTask('Begin', num);
  3: MakerDemo3;
  4: MakerDemo4;
  5: MakerDemo5;
  end;
end;

Приведем вид окна задачника для новых заданий:

Добавление заданий на обработку файлов

Добавим к группе MakerDemo еще два задания: первое из них дублирует задание File63 (подгруппа «Символьные и строковые файлы»), а второе — задание Text16 (подгруппа «Текстовые файлы: основные операции»). Реализуем эти задания в процедурах MakerDemo6 и MakerDemo7:

function FileName(Len: integer): string;
const
  c = '0123456789abcdefghijklmnopqrstuvwxyz';
var
  i: integer;
begin
  result := '';
  for i := 1 to Len do
    result := result + c[RandomN(1, Length(c))];
end;

procedure MakerDemo6;
var
  k, i, j, jmax: integer;
  s1, s2, s3: string;
  fs1: file of ShortString;
  fs2: file of ShortString;
  fc3: file of char;
  s: ShortString;
  c: char;
begin
  CreateTask('Символьные и строковые файлы');
  TaskText(
  'Дано целое число~{K} (>\,0) и строковый файл.'#13 +
  'Создать два новых файла: строковый, содержащий первые {K}~символов'#13 +
  'каждой строки исходного файла, и символьный, содержащий {K}-й символ'#13 +
  'каждой строки (если длина строки меньше~{K}, то в строковый файл'#13 +
  'записывается вся строка, а в символьный файл записывается пробел).'
  );
  s1 := '1' + FileName(5) + '.tst';
  s2 := '2' + FileName(5) + '.tst';
  s3 := '3' + FileName(5) + '.tst';
  Assign(fs1, s1);
  Rewrite(fs1);
  Assign(fs2, s2);
  Rewrite(fs2);
  Assign(fc3, s3);
  Rewrite(fc3);
  k := RandomN(2, 11);
  jmax := 0;
  for i := 1 to RandomN(10, 20) do
  begin
    j := RandomN(2, 16);
    if jmax < j then
      jmax := j;
    s := FileName(j);
    write(fs1, s);
    if j >= k then
      c := s[k]
    else
      c := ' ';
    write(fc3, c);
    s := copy(s, 1, k);
    write(fs2,s);
  end;
  Close(fs1);
  Close(fs2);
  Close(fc3);
  DataN('K = ', k, 0, 1, 1);
  DataS('Имя исходного файла: ', s1, 3, 2);
  DataS('Имя результирующего строкового файла:  ', s2, 3, 4);
  DataS('Имя результирующего символьного файла: ', s3, 3, 5);
  DataComment('Содержимое исходного файла:', xRight, 2);
  DataFileS(s1, 3, jmax + 3);
  ResultComment('Содержимое результирующего строкового файла:',
    0, 2);
  ResultComment('Содержимое результирующего символьного файла:',
    0, 4);
  ResultFileS(s2, 3, k + 3);
  ResultFileC(s3, 5, 4);
end;

procedure MakerDemo7;
var
  p: integer;
  s, s1, s2, s0: string;
  t1, t2: text;
begin
  CreateTask('Текстовые файлы: основные операции');
  TaskText('Дан текстовый файл.', 0, 2);
  TaskText('Удалить из него все пустые строки.', 0, 4);
  s1 := FileName(6) + '.tst';
  s2 := '#' + FileName(6) + '.tst';
  s := TextSample(RandomN(0, TextCount-1));
  Assign(t2, s2);
  Rewrite(t2);
  Assign(t1, s1);
  Rewrite(t1);
  writeln(t2, s);
  Close(t2);
  s0 := #13#10#13#10;
  p := Pos(s0, s);
  while p <> 0 do
  begin
    Delete(s, p, 2);
    p := Pos(s0, s);
  end;
  writeln(t1, s);
  Close(t1);
  ResultFileT(s1, 1, 5);
  Rename(t2, s1);
  DataFileT(s1, 2, 5);
  DataS('Имя файла: ', s1, 0, 1);
  SetTestCount(3);
end;

При реализации этих заданий используется вспомогательная функция FileName(Len), позволяющая создать случайное имя файла длины Len (без расширения). Имя файла при этом будет содержать только цифры и строчные (маленькие) латинские буквы.

Имена файлов, полученные с помощью функции FileName, дополняются расширением .tst (заметим, что в базовых группах File, Text и Param это расширение используется в именах всех исходных и результирующих файлов).

Функция FileName используется также для генерации элементов строкового файла в процедуре MakerDemo6.

Для того чтобы предотвратить возможность случайного совпадения имен файлов, в процедуре MakerDemo6 к созданным именам добавляются префиксы: 1 для первого файла, 2 для второго, 3 для третьего. В процедуре MakerDemo7 имя временного файла дополняется префиксом #, что также гарантирует его отличие от имени основного файла задания.

В процедуре MakerDemo6 использован новый вариант процедуры TaskText, появившийся в версии 4.11 задачника. В этом варианте процедура TaskText принимает один строковый параметр, который определяет всю формулировку задания, причем в качестве разделителей строк, входящих в формулировку, можно использовать символы #13, #10 или их комбинацию #13#10 (в указанном порядке). Новый вариант процедуры TaskText позволяет более наглядно отобразить формулировку задания и не требует указания дополнительных параметров.

При реализации задания на обработку текстовых файлов для генерации содержимого файла используются функции TextCount и TextSample. Строка, возвращаемая функцией TextSample, представляет собой текст, содержащий маркеры конца строки — символы #13#10. Указанные символы разделяют соседние строки текста (в конце текста маркер конца строки не указывается). Благодаря наличию маркеров конца строки полученный текст можно записать в текстовый файл с помощью единственной процедуры writeln, которая, кроме записи текста, обеспечивает добавление маркера конца строки в конец файла.

После разработки новых заданий необходимо изменить количество заданий в вызове процедуры CreateGroup на 7 и включить вызовы новых процедур в основную процедуру группы InitTask:

procedure InitTask(num: integer);
begin
  case num of
  1..2: UseTask('Begin', num);
  3: MakerDemo3;
  4: MakerDemo4;
  5: MakerDemo5;
  6: MakerDemo6;
  7: MakerDemo7;
  end;
end;

Приведем вид окна задачника для новых заданий:

Добавление заданий на обработку динамических структур данных

Наконец, добавим в нашу группу задание, посвященное обработке динамических структур данных, причем представим его в двух вариантах: традиционном, основанном на использовании записей типа TNode и связанных с ними указателей типа PNode, и «объектном», характерном для .NET-языков (C#, Visual Basic .NET, PascalABC.NET), а также языков Python и Java. Следует подчеркнуть, что при разработке как традиционного, так и объектного варианта заданий на динамические структуры надо использовать типы TNode и PNode и связанные с ними процедуры конструктора учебных заданий. В то же время, при выполнении объектного варианта задания на соответствующем языке требуется использовать объекты типа Node (которые при разработке задания не применяются).

Задание, которое мы реализуем, дублирует задание Dynamic30, посвященное преобразованию односвязного списка в двусвязный (подгруппа «Динамические структуры данных: двусвязный список»). Оформим два варианта этого задания в виде процедур MakerDemo8 и MakerDemo8Obj:

var WrongNode: TNode;

procedure MakerDemo8Data;
var
  i, n: integer;
  p, p1, p2: PNode;
begin
  if RandomN(1, 4) = 1 then
    n := 1
  else
    n := RandomN(2, 9);
  case CurrentTest of
  2: n := 1;
  4: n := RandomN(2, 9);
  end;
  new(p1);
  p1^.Data := RandomN(10, 99);
  p1^.Prev := nil;
  p2 := p1;
  for i := 2 to n do
  begin
    new(p);
    p^.Data := RandomN(10, 99);
    p^.Prev := p2;
    p2^.Next := p;
    p2 := p;
  end;
  p2^.Next := nil;
  SetPointer(1, p1);
  SetPointer(2, p2);
  ResultP('Последний элемент: ', 2, 0, 2);
  ResultList(1, 0, 3);
  ShowPointer(2);
  DataP(1, 0, 2);
  p := p1;
  for i := 1 to n do
  begin
    p^.Prev := @WrongNode;
    p := p^.Next;
  end;
  DataList(1, 0, 3);
  ShowPointer(1);
end;

procedure MakerDemo8;
begin
  CreateTask('Динамические структуры данных: двусвязный список');
  TaskText('Дан указатель~{P}_1 на начало непустой цепочки ' +
    'элементов-записей типа TNode,', 0, 1);
  TaskText('связанных между собой с помощью поля Next. Используя ' +
    'поле Prev записи TNode,', 0, 2);
  TaskText('преобразовать исходную (\Iодносвязную\i) цепочку ' +
    'в \Iдвусвязную\i, в которой каждый', 0, 3);
  TaskText('элемент связан не только с последующим элементом ' +
    '(с помощью поля Next),', 0, 4);
  TaskText('но и с предыдущим (с помощью поля Prev). Поле Prev ' +
    'первого элемента положить', 0, 5);
  TaskText('равным \N. Вывести указатель на последний элемент ' +
    'преобразованной цепочки.', 0, 0);
  MakerDemo8Data;
end;

procedure MakerDemo8Obj;
begin
  CreateTask('Динамические структуры данных: двусвязный список');
  TaskText(
  'Дана ссылка~{A}_1 на начало непустой цепочки элементов-объектов типа Node,'#13 +
  'связанных между собой с помощью своих свойств Next. Используя свойства Prev'#13 +
  'данных объектов, преобразовать исходную (\Iодносвязную\i) цепочку в \Iдвусвязную\i,'#13 +
  'в которой каждый элемент связан не только с последующим элементом (с помощью'#13 +
  'свойства Next), но и с предыдущим (с помощью свойства Prev). Свойство Prev'#13 +
  'первого элемента положить равным \O. Вывести ссылку~{A}_2 на последний'#13 +
  'элемент преобразованной цепочки.'
  );
  SetObjectStyle;
  MakerDemo8Data;
end;

Анализируя приведенные варианты процедур, легко заметить, что они отличаются лишь деталями формулировки задания. Алгоритмы генерации исходных и контрольных данных для традиционного и объектного вариантов совпадают, поэтому они выделены в отдельную вспомогательную процедуру MakerDemo8Data. В то же время представления динамических структур и связанных с ними указателей или объектов будут отличаться (см. рисунки, приведенные ниже). Необходимые корректировки в представлении динамических структур выполняются задачником автоматически, с учетом используемого языка программирования.

Однако для языка PascalABC.NET требуемую настройку необходимо выполнить явно, так как в нем можно использовать оба варианта представления динамических структур: традиционный (как для обычного Паскаля в системах Delphi и Free Pascal Lazarus) и объектный (как в языках C#, Visual Basic .NET, Python и Java). Для того чтобы представление динамических данных при выполнении задания в среде PascalABC.NET соответствовало объектному варианту, следует в начале процедуры, реализующей задание (перед вызовом любых процедур, связанных с указателями и динамическими структурами), вызвать специальную процедуру без параметров SetObjectStyle. Для остальных языков данная процедура не выполняет никаких действий.

Обратите внимание на возможность использования в формулировке задания более 5 экранных строк. Строки, которые не умещаются в области формулировки задания, следует добавлять к заданию процедурой TaskText, указывая в качестве последнего параметра процедуры число 0 (см. процедуру MakerDemo8). Еще проще задавать «длинные» формулировки заданий с помощью нового варианта процедуры TaskText с единственным строковым параметром, содержащим все строки формулировки (см. процедуру MakerDemo9). При наличии подобных строк в окне задачника (если окно находится в режиме с фиксированной компоновкой) слева от области формулировки появятся кнопки, обеспечивающие прокрутку формулировки задания; кроме этих кнопок для прокрутки можно также использовать стандартные клавиши, в частности, клавиши со стрелками.

Для того чтобы имя нулевого указателя (или объекта) соответствовало используемому языку программирования, в формулировке задания применяются управляющие последовательности \N (имя нулевого указателя) и \O (имя нулевого объекта). Для языка PascalABC.NET обе эти последовательности генерируют текст nil.

Достаточно часто алгоритмы, разработанные учащимися для обработки динамических структур данных, дают неверные результаты в случае особых (хотя и допустимых) структур, например, состоящих только из одного элемента. Поэтому желательно предусмотреть появление подобных структур в тестовых наборах исходных данных. В наших заданиях исходный список, состоящий из одного элемента, будет предлагаться программе учащегося в среднем один раз при каждых четырех тестовых испытаниях. Кроме того, благодаря использованию функции CurrentTest, появившейся в версии 4.11 конструктора, вариант списка с единственным элементом будет предложен программе учащегося для обработки в тесте номер 2, а вариант списка с более чем одним элементом — в тесте номер 4. Таким образом, можно гарантировать, что при прохождении набора из 5 тестовых испытаний программе будут предложены как «стандартные», так и «особые» наборы исходных данных.

При формировании односвязной структуры неиспользуемые поля Prev для каждого элемента структуры следует положить равными адресу «фиктивного» элемента (в нашем случае — переменной WrongNode), не связанного с данной структурой. Заметим, что для всех элементов, кроме первого, значения поля Prev можно было бы положить равными nil, однако это не подходит для первого элемента: если поле Prev первого элемента будет равно nil, то слева от него будет выведен «лишний» (в данной ситуации) текст nil<.

Характерной особенностью разработки заданий на динамические структуры является обратный порядок создания этих структур: вначале создаются контрольные структуры (которые сразу передаются в задачник), а затем они преобразуются в соответствующие исходные структуры, которые должны не только передаваться в задачник, но и оставаться в памяти, чтобы в дальнейшем их можно было использовать в программе учащегося, выполняющей это задание.

Если в группу включаются задания на динамические структуры, то необходимо анализировать текущий язык программирования, используемый задачником. Это обусловлено двумя причинами:

  • имеются языки, для которых отсутствует возможность выполнять задания на обработку динамических структур (например, Visual Basic и 1C);
  • в языках платформы .NET, а также Python и Java, необходимо использовать «объектный» стиль формулировок вместо стиля, основанного на указателях и применяемого для языков Pascal и C++.

Кроме того, следует определиться с выбором стиля для языка PascalABC.NET, поскольку в нем можно использовать как стиль указателей, так и стиль объектов. Можно, например, включить в группу заданий для языка PascalABC.NET оба варианта каждого задания.

Отмеченные обстоятельства приводят к тому, что для разных языков программирования создаваемая группа может содержать разное число заданий и, кроме того, для этих заданий будут использоваться разные инициализирующие процедуры.

С учетом этих замечаний изменим основную процедуру группы InitTask следующим образом:

procedure InitTask(num: integer);
begin
  case num of
  1..2: UseTask('Begin', num);
  3: MakerDemo3;
  4: MakerDemo4;
  5: MakerDemo5;
  6: MakerDemo6;
  7: MakerDemo7;
  8: if CurrentLanguage and lgWithPointers <> 0 then
       MakerDemo8
     else
       MakerDemo8Obj;
  9: MakerDemo8Obj;
  end;
end;

В этой процедуре используется функция CurrentLanguage, позволяющая определить текущий язык программирования, используемый задачником. Если текущий язык относится к категории языков, поддерживающих указатели (в том числе PascalABC.NET), то в качестве задания номер 8 вызывается процедура MakerDemo8, в которой задание формулируется в терминах указателей. В противном случае вызывается вариант задания, использующий объектную терминологию. При использовании языка PascalABC.NET число заданий в группе будет равно 9; при этом дополнительное задание номер 9 будет представлять собой «объектный» вариант задания номер 8.

Функцию CurrentLanguage потребуется использовать и в начале процедуры inittaskgroup для того, чтобы правильно определить количество заданий в группе для разных языков программирования. Приведем фрагмент, на который надо заменить вызов процедуры CreateGroup и предшествующее ему ключевое слово begin (обратите внимание на то, что теперь в качестве предпоследнего параметра процедуры CreateGroup используется переменная n):

var
  n: integer;
begin
  n := 7;
  if CurrentLanguage = lgPascalABCNET then
    n := 9
  else
  if CurrentLanguage and (lgWithPointers or lgWithObjects) <> 0 then
    n := 8;
  CreateGroup('MakerDemo', 'Примеры различных задач',
    'М. Э. Абрамян, 2013', 'qwqfsdf13dfttd', n, InitTask);

Приведенный набор условий будет правильно определять количество заданий и в случае, если состав языков, поддерживаемых задачником, будет расширен. Это обеспечивается тем, что в условиях используются не константы для конкретных языков (за исключением константы lgPascalABCNET), а битовые маски lgWithPointers и lgWithObjects. Первая из этих масок включает все языки, для которых в задачнике можно использовать варианты заданий на динамические структуры, основанные на указателях, а вторая — все языки, позволяющие использовать варианты аналогичных заданий в объектной терминологии.

В среде PascalABC.NET можно протестировать оба варианта реализованного задания. Приведем вид окна задачника для этого задания (первый рисунок соответствует варианту задания, использующему указатели, второй — варианту, использующему объекты). Обратите внимание на кнопки, расположенные справа от формулировки задания и обеспечивающие ее прокрутку.

Завершая оформление модуля PT4MakerDemo, добавим комментарии к новым подгруппам заданий (указанные операторы следует поместить в конец процедуры inittaskgroup):

  Subgroup('Двумерные массивы (матрицы): вывод элементов');
  CommentText('Данное задание дублирует задание Matrix7.');

  Subgroup('Символьные и строковые файлы');
  CommentText('Данное задание дублирует задание File63.');
  CommentText('Оно демонстрирует особенности, связанные с двоичными');
  CommentText('\Iстроковыми\i файлами.');

  Subgroup('Текстовые файлы: основные операции');
  CommentText('Данное задание дублирует задание Text16.');

  Subgroup('Динамические структуры данных: двусвязный список');
  CommentText('Данное задание дублирует задание Dynamic30.');
  CommentText('\PЗадание реализовано в двух вариантах: основанном на использовании указателей');
  CommentText('(для языков Pascal и C++) и основанном на использовании объектов (для языков платформы .NET,');
  CommentText('а также Python и Java). Для языка Visual Basic это задание недоступно.');
  CommentText('В системе PascalABC.NET доступны оба варианта задания.');

Приведем заключительную часть html-страницы с описанием данной группы: