Escritura y lectura de archivos en modo simplificado

Entre las funciones de archivo de MQL5 destinadas a la escritura y lectura de datos existe una división en dos grupos desiguales. El primero de ellos incluye dos funciones, FileSave y FileLoad, que permiten escribir o leer datos en modo binario con una sola llamada a una función. Por un lado, este planteamiento tiene una ventaja innegable, la sencillez, pero por otro tiene algunas limitaciones (más adelante hablaremos de ellas). En el segundo gran grupo, todas las funciones de archivo se utilizan de forma diferente: es necesario llamar a varias de ellas de forma secuencial para realizar una operación de lectura o escritura completa desde el punto de vista lógico. Esto parece más complejo, pero proporciona flexibilidad y control sobre el proceso. Las funciones del segundo grupo operan con enteros especiales, es decir, descriptores de archivo, que deben obtenerse utilizando la función FileOpen (véase la sección siguiente).

Vamos a ver la descripción formal de estas dos funciones y, a continuación, consideraremos su ejemplo (FileSaveLoad.mq5).

bool FileSave(const string filename, const void &data[], const int flag = 0)

La función escribe todos los elementos del array data pasado en un archivo binario llamado filename. El parámetro filename puede contener no sólo el nombre del archivo, sino también los nombres de carpetas de varios niveles de anidamiento: la función creará las carpetas especificadas si aún no existen. Si el archivo existe, se sobrescribirá (a menos que esté ocupado por otro programa).

Como parámetro data, se puede pasar un array de cualquier tipo integrado, excepto cadenas. También puede ser un array de estructuras simples que contenga campos de tipos integrados con la excepción de cadenas, arrays dinámicos y punteros. Tampoco se admiten clases.

El parámetro flag puede, si es necesario, contener la constante predefinida FILE_COMMON, que significa crear y escribir un archivo en el directorio de datos común de todos los terminales (Common/Files/). Si no se especifica la bandera (que corresponde al valor predeterminado de 0), el archivo se escribe en el directorio de datos habitual (si el programa MQL se ejecuta en el terminal) o en el directorio del agente de pruebas (si sucede en el probador). En los dos últimos casos, se utiliza la «sandbox» MQL5/Files/ dentro del directorio, tal y como se describe al principio del capítulo.

La función devuelve una indicación de éxito de la operación (true) o de error (false).

long FileLoad(const string filename, void &data[], const int flag = 0)

La función lee todo el contenido de un archivo binario filename en el array data especificado. El nombre del archivo puede incluir una jerarquía de carpetas dentro de la «sandbox» MQL5/Files o Common/Files.

El array data debe ser de cualquier tipo integrado excepto string, o de un tipo de estructura simple (véase más arriba).

El parámetro flag controla la selección del directorio donde se busca y abre el archivo: por defecto (con un valor de 0) es la «sandbox» estándar, pero si se establece el valor FILE_COMMON, entonces es la «sandbox» compartida por todos los terminales.

La función devuelve el número de elementos leídos, o -1 en caso de error.

Tenga en cuenta que los datos del archivo se leen en bloques de un elemento de array. Si el tamaño del archivo no es múltiplo del tamaño del elemento, los datos restantes se omiten (no se leen). Por ejemplo, si el tamaño del archivo es de 10 bytes, al leerlo en un array de tipo double (sizeof(double)=8) sólo se cargarán realmente 8 bytes, es decir, 1 elemento (y la función devolverá 1). Los 2 bytes restantes al final del archivo serán ignorados.

En el script FileSaveLoad.mq5 definimos dos estructuras para las pruebas.

struct Pair
{
   short xy;
};
  
struct Simple
{
   double d;
   int i;
   datetime t;
   color c;
   uchar a[10]; // fixed size array allowed
   bool b;
   Pair p;      // compound fields (nested simple structures) are also allowed
   
   // strings and dynamic arrays will cause a compilation error when used
   // FileSave/FileLoad: structures or classes containing objects are not allowed
   // string s;
   // uchar a[];
   
   // pointers are also not supported
   // void *ptr;
};

La estructura Simple contiene campos de la mayoría de los tipos permitidos, así como un campo compuesto con el tipo de estructura Pair. En la función OnStart rellenamos un pequeño array del tipo Simple.

void OnStart()
{
   Simple write[] =
   {
      {+1.0, -1D'2021.01.01', clrBlue, {'a'}, true, {100016000}},
      {-1.0, -2D'2021.01.01', clrRed,  {'b'}, true, {100016000}},
   };
   ...

Seleccionaremos el archivo para escribir los datos junto con la subcarpeta MQL5Book para que nuestros experimentos no se mezclen con sus archivos de trabajo:

   const string filename = "MQL5Book/rawdata";

Escribamos un array en un archivo, leámoslo en otro array y comparémoslos.

   PRT(FileSave(filenamewrite/*, FILE_COMMON*/)); // true
   
   Simple read[];
   PRT(FileLoad(filenameread/*, FILE_COMMON*/)); // 2
   
   PRT(ArrayCompare(writeread)); // 0

FileLoad ha devuelto 2, es decir, se han leído 2 elementos (2 estructuras). Si el resultado de la comparación es 0, significa que los datos coinciden. Puede abrir la carpeta en su administrador de archivos favorito MQL5/Files/MQL5Book y asegurarse de que está el archivo 'rawdata' (no se recomienda ver su contenido usando un editor de texto; sugerimos usar un visor que admita modo binario).

Más adelante en el script convertimos el array de estructuras leída en bytes y los enviamos al registro en forma de códigos hexadecimales. Se trata de una especie de volcado de memoria que permite entender qué son los archivos binarios.

   uchar bytes[];
   for(int i = 0i < ArraySize(read); ++i)
   {
      uchar temp[];
      PRT(StructToCharArray(read[i], temp));
      ArrayCopy(bytestempArraySize(bytes));
   }
   ByteArrayPrint(bytes);

Resultado:

 [00] 00 | 00 | 00 | 00 | 00 | 00 | F0 | 3F | FF | FF | FF | FF | 00 | 66 | EE | 5F | 
 [16] 00 | 00 | 00 | 00 | 00 | 00 | FF | 00 | 61 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 
 [32] 00 | 00 | 01 | E8 | 03 | 80 | 3E | 00 | 00 | 00 | 00 | 00 | 00 | F0 | BF | FE | 
 [48] FF | FF | FF | 00 | 66 | EE | 5F | 00 | 00 | 00 | 00 | FF | 00 | 00 | 00 | 62 | 
 [64] 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 01 | E8 | 03 | 80 | 3E | 

Debido a que la función integrada ArrayPrint no puede imprimir en formato hexadecimal, hemos tenido que desarrollar nuestra propia función ByteArrayPrint (aquí no daremos su código fuente, vea el archivo adjunto).

A continuación, recordemos que FileLoad es capaz de cargar datos en un array de cualquier tipo, por lo que leeremos el mismo archivo usándolo directamente en un array de bytes.

   uchar bytes2[];
   PRT(FileLoad(filenamebytes2/*, FILE_COMMON*/)); // 78,  39 * 2
   PRT(ArrayCompare(bytesbytes2)); // 0, equality

Una comparación satisfactoria de arrays de dos bytes muestra que FileLoad puede operar con datos brutos del archivo de forma arbitraria, en la que se le indique (no hay información en el archivo de que almacena un array de estructuras Simple).

Es importante señalar aquí que, dado que el tipo byte tiene un tamaño mínimo (1), es múltiplo de cualquier tamaño de archivo. Por lo tanto, cualquier archivo se lee siempre en un array de bytes sin resto. Aquí, la función FileLoad ha devuelto el número 78 (el número de elementos es igual al número de bytes). Este es el tamaño del archivo (dos estructuras de 39 bytes cada una).

Básicamente, la capacidad de FileLoad para interpretar datos de cualquier tipo requiere atención y comprobaciones por parte del programador. En concreto, más adelante en el script, leemos el mismo archivo en un array de estructuras MqlDateTime. Esto, por supuesto, es incorrecto, pero funciona sin errores.

   MqlDateTime mdt[];
   PRT(sizeof(MqlDateTime)); // 32
   PRT(FileLoad(filenamemdt)); // 2
 // attention: 14 bytes left unread
   ArrayPrint(mdt);

El resultado contiene un conjunto de números sin sentido:

        [year]      [mon] [day]     [hour]    [min]    [sec] [day_of_week] [day_of_year]
[0]          0 1072693248    -1 1609459200        0 16711680            97             0
[1] -402587648    4096003     0  -20975616 16777215  6286950     -16777216    1644167168

Dado que el tamaño de MqlDateTime es 32, sólo caben dos estructuras de este tipo en un archivo de 78 bytes, y 14 bytes más resultan superfluos. La presencia de un resto indica que hay un problema. Pero incluso si no lo hay, ello no garantiza el sentido de la operación realizada, ya que dos tamaños diferentes pueden, por pura casualidad, encajar un número entero (pero diferente) de veces a lo largo del archivo. Además, dos estructuras de significado diferente pueden tener el mismo tamaño, pero esto no significa que deban escribirse y leerse de una a otra.

No es sorprendente que el registro del array de estructuras MqlDateTime muestre valores extraños, ya que se trataba, de hecho, de un tipo de datos completamente diferente.

Para que la lectura sea algo más cuidadosa, el script implementa un análogo de la función FileLoad, MyFileLoad. Analizaremos esta función en detalle, así como su par MyFileSave, en las secciones siguientes, cuando descubramos nuevas funciones de archivo y las utilicemos para modelar la estructura interna FileSave/FileLoad. Mientras tanto, sólo hay que tener en cuenta que en nuestra versión podemos comprobar la presencia de un resto no leído en el archivo y mostrar una advertencia.

Para terminar, veamos un par de errores potenciales más demostrados en el script.

   /*
  // compilation error, string type not supported here
   string texts[];
   FileSave("any", texts); // parameter conversion not allowed
   */
   
   double data[];
   PRT(FileLoad("any"data)); // -1
   PRT(_LastError); // 5004, ERR_CANNOT_OPEN_FILE

El primero se produce en tiempo de compilación (por eso se comenta el bloque de código) porque no se permiten los arrays de cadenas.

El segundo es leer un archivo inexistente, razón por la cual FileLoad devuelve -1. Se puede obtener fácilmente un código de error explicativo utilizando GetLastError (o _LastError).