Управление позицией внутри файла

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

В некоторых случаях требуется изменить положение указателя без операций ввода-вывода. В частности, когда нужно дописать данные в конец файла, мы его открываем в "смешанном" режиме FILE_READ | FILE_WRITE, а потом должны каким-то образом очутиться в конце файла (иначе мы начнем перезаписывать данные с начала). Можно было бы вызывать функции чтения, пока есть что читать (тем самым сдвигая указатель), но это не эффективно. Лучше применить специальную функцию FileSeek. А получить фактическое значение указателя (позицию в файле) позволяет функция FileTell.

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

bool FileSeek(int handle, long offset, ENUM_FILE_POSITION origin)

Функция перемещает файловый указатель на количество offset байтов, используя в качестве точки отсчета origin — одно из предопределенных положений, описанных в перечислении ENUM_FILE_POSITION. Смещение offset может быть как положительным (движение к концу файла и далее за его пределы), так и отрицательным (движение к началу). Перечисление ENUM_FILE_POSITION имеет следующие элементы:

  • SEEK_SET — начало файла
  • SEEK_CUR — текущая позиция
  • SEEK_END — конец файла

Если вычисление новой позиции относительно точки привязки дало отрицательное значение (то есть запрашивается смещение "левее" начала файла), то файловый указатель будет установлен на начало файла.

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

При успешном завершении функция возвращает true, а в случае ошибки — false.

ulong FileTell(int handle)

Для файла, открытого с дескриптором handle, функция возвращает текущее положение внутреннего указателя (смещение относительно начала файла). В случае ошибки будет получено значение ULONG_MAX ((ulong)-1). Код ошибки доступен в переменной _LastError или через функцию GetLastError.

bool FileIsEnding(int handle)

Функция возвращает признак того, находится ли указатель в конце файла handle. Если это так, результат равен true.

bool FileIsLineEnding(int handle)

Для текстового файла с дескриптором handle функция возвращает признак того, находится ли файловый указатель в конце строки (сразу после символов перевода строки '\n' или '\r\n'). Иными словами, возвращенное значение true означает, что текущая позиция находится на начале следующей строки (или в конце файла). Для бинарных файлов результат всегда равен false.

Тестовый скрипт для рассмотренных функций называется FileCursor.mq5. Работа в нем ведется с тремя файлами: двумя бинарными и одним текстовым.

const string fileraw = "MQL5Book/cursor.raw";
const string filetxt = "MQL5Book/cursor.csv";
const string file100 = "MQL5Book/k100.raw";

Для упрощения вывода в журнал текущей позиции и признаков конца файла (End-Of-File, EOF) и конца строки (End-Of-Line, EOL) создана вспомогательная функция FileState.

string FileState(int handle)
{
   return StringFormat("P:%I64d, F:%s, L:%s",
      FileTell(handle),
      (string)FileIsEnding(handle),
      (string)FileIsLineEnding(handle));
}

Сценарий проверки функций на бинарном файле включает следующие шаги.

Мы создаем новый или открываем существующий файл fileraw ("MQL5Book/cursor.raw") в режиме на чтение и запись. Сразу после открытия и далее после каждой операции выводим текущее состояние файла с помощью вызова FileState.

void OnStart()
{
   int handle;
   Print("\n * Phase I. Binary file");
   handle = PRTF(FileOpen(filerawFILE_BIN | FILE_WRITE | FILE_READ));
   Print(FileState(handle));
   ...

Перемещаем указатель в конец файла, что позволит при каждом запуске скрипта дописывать данные в этот файл (а не перезаписывать с начала). Самый очевидный способ сослаться на конец файла: нулевой offset относительно origin=SEEK_END.

   PRTF(FileSeek(handle0SEEK_END));
   Print(FileState(handle));

Если файл уже не пустой (не новый), мы можем считывать существующие данные в его произвольной позиции (относительной или абсолютной). В частности, если параметр origin функции FileSeek равен SEEK_CUR, значит при отрицательном смещении offset текущая позиция сдвинется на соответствующее число байтов назад (влево), а при положительном — вперед (направо).

В данном примере мы пробуем отступить назад на размер одного значения типа int. Чуть позже мы увидим, что в этом месте должно находиться поле day_of_year (последнее поле) структуры MqlDateTime, потому что мы её записываем в файл в последующих инструкциях, и эти данные доступны из файла при следующем запуске. Прочитанное значение выводится в журнал для сверки с тем, что было ранее сохранено.

   if(PRTF(FileSeek(handle, -1 * sizeof(int), SEEK_CUR)))
   {
      Print(FileState(handle));
      PRTF(FileReadInteger(handle));
   }

В новом пустом файле вызов FileSeek закончится ошибкой 4003 (INVALID_PARAMETER), и блок инструкции if не выполнится.

Далее происходит пополнение файла данными. Сначала текущее локальное время компьютера (8 байтов datetime) пишется с помощью FileWriteLong.

   datetime now = TimeLocal();
   PRTF(FileWriteLong(handlenow));
   Print(FileState(handle));

Затем мы пытаемся отступить назад с текущего места на 4 байта (-4) и прочитать long.

   PRTF(FileSeek(handle, -4SEEK_CUR));
   long x = PRTF(FileReadLong(handle));
   Print(FileState(handle));

Эта попытка закончится ошибкой 5015 (FILE_READERROR), потому что мы были в конце файла и после смещения на 4 байта влево не можем прочитать 8 байтов справа (размер long). Однако, как мы увидим из журнала, в результате этой неудачной попытки указатель все же сместится снова в конец файла.

Если отступить назад на 8 байтов (-8), последующее чтение значения long окажется успешным, и оба времени — исходное и полученное из файла — должны совпасть.

   PRTF(FileSeek(handle, -8SEEK_CUR));
   Print(FileState(handle));
   x = PRTF(FileReadLong(handle));
   PRTF((now == x));

Наконец, запишем в файл структуру MqlDateTime, заполненную тем же временем. Позиция в файле увеличится на 32 (размер структуры в байтах).

   MqlDateTime mdt;
   TimeToStruct(nowmdt);
   StructPrint(mdt); // выводим дату/время в журнал наглядно
   PRTF(FileWriteStruct(handlemdt)); // 32 = sizeof(MqlDateTime)
   Print(FileState(handle));
   FileClose(handle);

После первого запуска скрипта для сценария с файлом fileraw ("MQL5Book/cursor.raw") получим примерно следующее (время будет отличаться):

первый запуск
 * Phase I. Binary file
FileOpen(fileraw,FILE_BIN|FILE_WRITE|FILE_READ)=1 / ok
P:0, F:true, L:false
FileSeek(handle,0,SEEK_END)=true / ok
P:0, F:true, L:false
FileSeek(handle,-1*sizeof(int),SEEK_CUR)=false / INVALID_PARAMETER(4003)
FileWriteLong(handle,now)=8 / ok
P:8, F:true, L:false
FileSeek(handle,-4,SEEK_CUR)=true / ok
FileReadLong(handle)=0 / FILE_READERROR(5015)
P:8, F:true, L:false
FileSeek(handle,-8,SEEK_CUR)=true / ok
P:0, F:false, L:false
FileReadLong(handle)=1629683392 / ok
(now==x)=true / ok
  2021     8    23      1    49    52             1           234
FileWriteStruct(handle,mdt)=32 / ok
P:40, F:true, L:false

Согласно статусу, размер файла равен сначала нулю, поскольку позиция "P:0" после смещения на конец файла ("F:true"). После каждой записи (с помощью FileWriteLong и FileWriteStruct) позиция P увеличивается на размер записанных данных.

После второго запуска скрипта можно заметить в логе кое-какие изменения:

второй запуск
 * Phase I. Binary file
FileOpen(fileraw,FILE_BIN|FILE_WRITE|FILE_READ)=1 / ok
P:0, F:false, L:false
FileSeek(handle,0,SEEK_END)=true / ok
P:40, F:true, L:false
FileSeek(handle,-1*sizeof(int),SEEK_CUR)=true / ok
P:36, F:false, L:false
FileReadInteger(handle)=234 / ok
FileWriteLong(handle,now)=8 / ok
P:48, F:true, L:false
FileSeek(handle,-4,SEEK_CUR)=true / ok
FileReadLong(handle)=0 / FILE_READERROR(5015)
P:48, F:true, L:false
FileSeek(handle,-8,SEEK_CUR)=true / ok
P:40, F:false, L:false
FileReadLong(handle)=1629683397 / ok
(now==x)=true / ok
  2021     8    23      1    49    57             1           234
FileWriteStruct(handle,mdt)=32 / ok
P:80, F:true, L:false

Во-первых, размер файла после открытия равен 40 (судим по позиции "P:40" после смещения в конец файла). С каждым запуском скрипта файл будет увеличиваться на 40 байтов.

Во-вторых, поскольку файл не пустой, удается перемещаться в нем и читать "старые" данные. В частности, после отступа на -1*sizeof(int) от текущей позиции (которая по совместительству является концом файла), мы успешно читаем значение 234 — последнее поле структуры MqlDateTime (это номер дня в году — он будет у вас, скорее всего, другим).

Вторым тестовым сценарием является работа с текстовым csv-файлом filetxt ("MQL5Book/cursor.csv"). Его мы также открываем в "смешанном" режиме чтения и записи, однако не перемещаем указатель в конец файла. Из-за этого каждый запуск скрипта будет перезаписывать данные, начиная с начала файла. Чтобы легко заметить различия, числа в первой колонке CSV генерируются случайным образом. Во второй колонке всегда подставляются один и те же строки из шаблона в функции StringFormat.

   Print(" * Phase II. Text file");
   srand(GetTickCount());
   // создаем новый или открываем существующий файл для записи/перезаписи
   // с самого начала и последующего чтения; внутри CSV-данные (Unicode)
   handle = PRTF(FileOpen(filetxtFILE_CSV | FILE_WRITE | FILE_READ, ','));
   // три ряда с данными (пара "число,строка" в каждом), разделенные '\n'
   // заметьте, что последний элемент не заканчивается переводом строки '\n'
   // это не обязательно, но допустимо
   string content = StringFormat(
      "%02d,abc\n%02d,def\n%02d,ghi",
      rand() % 100rand() % 100rand() % 100);
   // '\n' будет заменен на '\r\n' автоматически, благодаря FileWriteString
   PRTF(FileWriteString(handlecontent));

Вот пример генерируемых данных:

34,abc
20,def
02,ghi

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

   PRTF(FileSeek(handle0SEEK_SET));
   Print(FileState(handle));
   // подсчитаем строки в файле с помощью признака FileIsLineEnding
   int lineCount = 0;
   while(!FileIsEnding(handle))
   {
      PRTF(FileReadString(handle));
      Print(FileState(handle));
      // FileIsLineEnding также равен true, когда FileIsEnding равен true,
      // даже если завершающий символ '\n' отсутствует
      if(FileIsLineEnding(handle)) lineCount++;
   }
   FileClose(handle);
   PRTF(lineCount);

Ниже представлены фрагменты журнала для файла filetxt после первого и второго запуска скрипта. Сначала первый:

первый запуск
 * Phase II. Text file
FileOpen(filetxt,FILE_CSV|FILE_WRITE|FILE_READ,',')=1 / ok
FileWriteString(handle,content)=44 / ok
FileSeek(handle,0,SEEK_SET)=true / ok
P:0, F:false, L:false
FileReadString(handle)=08 / ok
P:8, F:false, L:false
FileReadString(handle)=abc / ok
P:18, F:false, L:true
FileReadString(handle)=37 / ok
P:24, F:false, L:false
FileReadString(handle)=def / ok
P:34, F:false, L:true
FileReadString(handle)=96 / ok
P:40, F:false, L:false
FileReadString(handle)=ghi / ok
P:46, F:true, L:true
lineCount=3 / ok

А вот второй:

второй запуск
 * Phase II. Text file
FileOpen(filetxt,FILE_CSV|FILE_WRITE|FILE_READ,',')=1 / ok
FileWriteString(handle,content)=44 / ok
FileSeek(handle,0,SEEK_SET)=true / ok
P:0, F:false, L:false
FileReadString(handle)=34 / ok
P:8, F:false, L:false
FileReadString(handle)=abc / ok
P:18, F:false, L:true
FileReadString(handle)=20 / ok
P:24, F:false, L:false
FileReadString(handle)=def / ok
P:34, F:false, L:true
FileReadString(handle)=02 / ok
P:40, F:false, L:false
FileReadString(handle)=ghi / ok
P:46, F:true, L:true
lineCount=3 / ok

Нетрудно заметить, что файл не меняется в размерах, но по одним и тем же смещениям записываются различные числа. Поскольку данный CSV-файл имеет две колонки, после каждого второго прочитанного значения мы видим взведенный признак EOL ("L:true").

Количество обнаруженных строк равно 3, несмотря на то, что в файле есть только 2 символа перевода строк: последняя (третья) строка оканчивается вместе с файлом.

Наконец, последний тестовый сценарий использует файл file100 ("MQL5Book/k100.raw") для перемещения в нем указателя за конец файла (на отметку 1000000 байтов) и тем самым увеличивает его размер (резервирует место на диске для потенциальных будущих операций записи).

   Print(" * Phase III. Allocate large file");
   handle = PRTF(FileOpen(file100FILE_BIN | FILE_WRITE));
   PRTF(FileSeek(handle1000000SEEK_END));
   // чтобы размер изменился, нужно хоть что-то записать
   PRTF(FileWriteInteger(handle0xFF1));
   PRTF(FileTell(handle));
   FileClose(handle);

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

 * Phase III. Allocate large file
FileOpen(file100,FILE_BIN|FILE_WRITE)=1 / ok
FileSeek(handle,1000000,SEEK_END)=true / ok
FileWriteInteger(handle,0xFF,1)=1 / ok
FileTell(handle)=1000001 / ok