Дата и время

Значения типа datetime, предназначенные для хранения даты и/или времени, как правило, подвергаются нескольким видам конвертации:

  • в строки и обратно для отображения пользователю и чтения из внешних источников данных;
  • в специальные структуры MqlDateTime (см. ниже) для работы с отдельными компонентами даты и времени;
  • в количество секунд, прошедшее с 01.01.1970, что соответствует внутреннему представлению datetime, и эквивалентно целому типу long.

Для последнего пункта используйте приведение datetime к (long) или обратно long к (datetime), но учтите, что поддерживаемый диапазон дат — от 1-го января 1970 года (значение 0) до 31 декабря 3000 года (32535215999 секунд).

Для первых двух пунктов MQL5 API предоставляет нижеследующие функции.

string TimeToString(datetime value, int mode = TIME_DATE | TIME_MINUTES)

Функция TimeToString преобразует значение типа datetime в строку с компонентами даты и времени, в соответствии с параметром mode, в котором можно задать произвольное сочетание флагов:

  • TIME_DATE — дата в формате "YYYY.MM.DD";
  • TIME_MINUTES — время в формате "hh:mm", то есть с часами и минутами;
  • TIME_SECONDS — время в формате "hh:mm:ss", то есть с часами, минутами и секундами.

Для полного вывода даты и времени можно задать mode равный TIME_DATE | TIME_SECONDS (вариант TIME_DATE | TIME_MINUTES | TIME_SECONDS тоже сработает, но является избыточным). Это эквивалентно приведению значения типа datetime к (string).

Примеры использования сведены в файл ConversionTime.mq5.

#define PRT(APrint(#A"=", (A))
 
void OnStart()
{
   datetime time = D'2021.01.21 23:00:15';
   PRT((string)time);
   PRT(TimeToString(time));
   PRT(TimeToString(timeTIME_DATE | TIME_MINUTES | TIME_SECONDS));
   PRT(TimeToString(timeTIME_MINUTES | TIME_SECONDS));
   PRT(TimeToString(timeTIME_DATE | TIME_SECONDS));
   PRT(TimeToString(timeTIME_DATE));
   PRT(TimeToString(timeTIME_MINUTES));
   PRT(TimeToString(timeTIME_SECONDS));
}

Скрипт выведет в журнал:

(string)time=2021.01.21 23:00:15
TimeToString(time)=2021.01.21 23:00
TimeToString(time,TIME_DATE|TIME_MINUTES|TIME_SECONDS)=2021.01.21 23:00:15
TimeToString(time,TIME_MINUTES|TIME_SECONDS)=23:00:15
TimeToString(time,TIME_DATE|TIME_SECONDS)=2021.01.21 23:00:15
TimeToString(time,TIME_DATE)=2021.01.21
TimeToString(time,TIME_MINUTES)=23:00
TimeToString(time,TIME_SECONDS)=23:00:15

 

datetime StringToTime(string value)

Функция StringToTime преобразует строку, содержащую дату и/или время, в значение типа datetime. Строка может содержать только дату, только время или дату и время вместе.

Для дат распознаются следующие форматы:

  • "YYYY.MM.DD"
  • "YYYYMMDD"
  • "YYYY/MM/DD"
  • "YYYY-MM-DD"
  • "DD.MM.YYYY"
  • "DD/MM/YYYY"
  • "DD-MM-YYYY"

Для времени поддерживаются следующие форматы:

  • "hh:mm"
  • "hh:mm:ss"
  • "hhmmss"

Между датой и временем должен быть как минимум один пробел.

Если в строке присутствует только время, в результат будет подставлена текущая дата. Если в строке присутствует только дата, время будет равно 00:00:00.

Если поддерживаемый синтаксис в строке нарушен, результатом будет текущая дата.

Примеры использования функции приведены в скрипте ConversionTime.mq5.

void OnStart()
{
   string timeonly = "21:01";   // только время
   PRT(timeonly);
   PRT((datetime)timeonly);
   PRT(StringToTime(timeonly));
   
   string date = "2000-10-10";  // только дата
   PRT((datetime)date);
   PRT(StringToTime(date));
   PRT((long)(datetime)date);
   long seconds = 60;
   PRT((datetime)seconds); // 1 минута с начала 1970
   
   string ddmmyy = "15/01/2012 01:02:03"// дата и время, причем дата в
   PRT(StringToTime(ddmmyy));             // "прямом" порядке, все равно ok
   
   string wrong = "January 2-nd";
   PRT(StringToTime(wrong));
}

В журнале мы увидим примерно следующее (####.##.## — текущая дата запуска скрипта):

timeonly=21:01
(datetime)timeonly=####.##.## 21:01:00
StringToTime(timeonly)=####.##.## 21:01:00
(datetime)date=2000.10.10 00:00:00
StringToTime(date)=2000.10.10 00:00:00
(long)(datetime)date=971136000
(datetime)seconds=1970.01.01 00:01:00
StringToTime(ddmmyy)=2012.01.15 01:02:03
(datetime)wrong=####.##.## 00:00:00

Помимо StringToTime для преобразования строк в дату и время можно пользоваться оператором приведения к типу (datetime). Однако плюс функции в том, что при обнаружении некорректной исходной строки она устанавливает внутреннюю переменную с кодом ошибки _LastError (доступную также через функцию GetLastError). В зависимости от того, какая часть строки содержит неинтерпретируемые данные, код ошибки может быть ERR_WRONG_STRING_DATE (5031), ERR_WRONG_STRING_TIME (5032) или другой из списка относящихся к получению даты и времени из строки.

bool TimeToStruct(datetime value, MqlDateTime &struct)

Для анализа компонентов даты и времени по отдельности MQL5 API предоставляет функцию TimeToStruct, которая преобразует значение типа datetime в структуру MqlDateTime:

struct MqlDateTime

   int year;           // год
   int mon;            // месяц
   int day;            // день
   int hour;           // час
   int min;            // минуты
   int sec;            // секунды
   int day_of_week;    // день недели
   int day_of_year;    // номер дня в году (1-е января имеет номер 0)
};

Дни недели нумеруются на американский манер: 0 — воскресенье, 1 — понедельник, и так далее вплоть до 6 — суббота. Для их идентификации существует встроенное перечисление ENUM_DAY_OF_WEEK.

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

Проверим работу функции с помощью скрипта ConversionTimeStruct.mq5. Для этого создадим массив time типа datetime с тестовыми значениями, и в цикле вызовем TimeToStruct для каждого из них.

Результаты будем складывать в массив структур MqlDateTime mdt[]. Предварительно мы его проинициализируем нулями, но поскольку встроенная функция ArrayInitialize не умеет обрабатывать структуры, нам придется написать её перегрузку (в будущем мы познакомимся с более простым способом заполнить массив нулями: в разделе Обнуление объектов и массивов будет представлена функция ZeroMemory).

int ArrayInitialize(MqlDateTime &mdt[], MqlDateTime &init)
{
   const int n = ArraySize(mdt);
   for(int i = 0i < n; ++i)
   {
      mdt[i] = init;
   }
   return n;
}

После процесса мы выведем массив структур в журнал с помощью встроенной функции ArrayPrint — это самый простой способ красиво отформатировать данные (его можно использовать даже если структура одна: достаточно положить её в массив размером 1).

void OnStart()
{
   // заполним массив с тестами
   datetime time[] =
   {
      D'2021.01.28 23:00:15', // корректное значение datetime
      D'3000.12.31 23:59:59', // максимальная поддерживаемая дата и время
      LONG_MAX // неверная дата: вызовет ошибку ERR_INVALID_DATETIME (4010)
   };
   
   // вычислим размер массива во время компиляции
   const int n = sizeof(time) / sizeof(datetime);
   
   MqlDateTime null = {}; // образец с нулями
   MqlDateTime mdt[];
   
   // выделим память под массив структур с результатами
   ArrayResize(mdtn);
   
   // вызовем нашу перегрузку ArrayInitialize 
   ArrayInitialize(mdtnull);
   
   // запустим тесты
   for(int i = 0i < n; ++i)
   {
      PRT(time[i]); // выводим исходные данные
   
      if(!TimeToStruct(time[i], mdt[i])) // при ошибке выводим её код
      {
         Print("error: "_LastError);
         mdt[i].year = _LastError;
      }
   }
   
   // выведем в лог результаты
   ArrayPrint(mdt);
   ...
}

В результате получим следующие строки в журнале:

time[i]=2021.01.28 23:00:15
time[i]=3000.12.31 23:59:59
time[i]=wrong datetime
wrong datetime -> 4010
    [year] [mon] [day] [hour] [min] [sec] [day_of_week] [day_of_year]
[0]   2021     1    28     23     0    15             4            27
[1]   3000    12    31     23    59    59             3           364
[2]   4010     0     0      0     0     0             0             0

Можно убедиться, что все поля получили соответствующие значения. Для некорректных исходных дат мы сохраняем код ошибки в поле year (в данном случае, такая ошибка одна: 4010, ERR_INVALID_DATETIME).

Напомним, что для максимального значения даты в MQL5 заведена константа DATETIME_MAX, равная целочисленному значению 0x793406fff, что соответствует 23:59:59 31 декабря 3000.

Наиболее часто встречающаяся задача, которую решают при помощи функции TimeToStruct, — это получение значения конкретной компоненты даты/времени. Поэтому имеет смысл подготовить вспомогательный заголовочный файл (MQL5Book/DateTime.mqh) с готовым вариантом реализации. В файле имеется класс DateTime.

class DateTime
{
private:
   MqlDateTime mdtstruct;
   datetime origin;
   
   DateTime() : origin(0)
   {
      TimeToStruct(0mdtstruct);
   }
   
   void convert(const datetime &dt)
   {
      if(origin != dt)
      {
         origin = dt;
         TimeToStruct(dtmdtstruct);
      }
   }
   
public:
   static DateTime *assign(const datetime dt)
   {
      _DateTime.convert(dt);
      return &_DateTime;
   }
   ENUM_DAY_OF_WEEK timeDayOfWeek() const
   {
      return (ENUM_DAY_OF_WEEK)mdtstruct.day_of_week;
   }
   int timeDayOfYear() const
   {
      return mdtstruct.day_of_year;
   }
   int timeYear() const
   {
      return mdtstruct.year;
   }
   int timeMonth() const
   {
      return mdtstruct.mon;
   }
   int timeDay() const
   {
      return mdtstruct.day;
   }
   int timeHour() const
   {
      return mdtstruct.hour;
   }
   int timeMinute() const
   {
      return mdtstruct.min;
   }
   int timeSeconds() const
   {
      return mdtstruct.sec;
   }
   
   static DateTime _DateTime;
};
   
static DateTime DateTime::_DateTime;

К классу прилагается несколько макросов, упрощающих вызов его методов.

#define TimeDayOfWeek(TDateTime::assign(T).timeDayOfWeek()
#define TimeDayOfYear(TDateTime::assign(T).timeDayOfYear()
#define TimeYear(TDateTime::assign(T).timeYear()
#define TimeMonth(TDateTime::assign(T).timeMonth()
#define TimeDay(TDateTime::assign(T).timeDay()
#define TimeHour(TDateTime::assign(T).timeHour()
#define TimeMinute(TDateTime::assign(T).timeMinute()
#define TimeSeconds(TDateTime::assign(T).timeSeconds()
   
#define _TimeDayOfWeek DateTime::_DateTime.timeDayOfWeek
#define _TimeDayOfYear DateTime::_DateTime.timeDayOfYear
#define _TimeYear DateTime::_DateTime.timeYear
#define _TimeMonth DateTime::_DateTime.timeMonth
#define _TimeDay DateTime::_DateTime.timeDay
#define _TimeHour DateTime::_DateTime.timeHour
#define _TimeMinute DateTime::_DateTime.timeMinute
#define _TimeSeconds DateTime::_DateTime.timeSeconds

В классе есть поле mdtstruct типа структуры MqlDateTime. Именно оно используется во всех внутренних преобразованиях. Поля структуры читаются с помощью методов-"геттеров": для каждого поля выделен соответствующий метод.

Внутри класса определен единственный статический экземпляр _DateTime (одного объекта достаточно, потому что все MQL-программы однопоточные). Конструктор является закрытым, поэтому создать другие объекты DateTime не удастся.

С помощью макросов можно удобно выделить из datetime, например, год (TimeYear(T)), месяц (TimeMonth(T)), число (TimeDay(T)) или день недели (TimeDayOfWeek(T)).

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

   // используем класс DateTime из MQL5Book/DateTime.mqh:
   // сначала получим день недели для заданного значения datetime
   PRT(EnumToString(TimeDayOfWeek(time[0])));
   // затем прочитаем год, месяц и число для того же значения
   PRT(_TimeYear());
   PRT(_TimeMonth());
   PRT(_TimeDay());

В журнале должны появиться такие строки.

EnumToString(DateTime::_DateTime.assign(time[0]).__TimeDayOfWeek())=THURSDAY
DateTime::_DateTime.__TimeYear()=2021
DateTime::_DateTime.__TimeMonth()=1
DateTime::_DateTime.__TimeDay()=28

Встроенная функция EnumToString преобразует элемент любого перечисления в строку и будет описана в отдельном разделе.

datetime StructToTime(MqlDateTime &struct)

Функция StructToTime выполняет конвертацию из структуры MqlDateTime (см. выше описание функции TimeToStruct), содержащей компоненты даты и времени, в значение типа datetime. Поля day_of_week и day_of_year в процессе не участвуют.

Если состояние остальных полей некорректное (соответствует несуществующей или неподдерживаемой дате), функция может вернуть, в зависимости от серьезности проблемы, либо некоторое исправленное значение, либо значение WRONG_VALUE (-1 в представлении типа long). Поэтому следует проверять наличие ошибки по состоянию глобальной переменной _LastError. Успешная конвертация заканчивается с кодом 0. Перед конвертацией следует сбросить возможное ошибочное состояние в _LastError (сохранившееся как артефакт исполнения каких-то предыдущих инструкций) с помощью функции ResetLastError.

Проверка функции StructToTime также приведена в скрипте ConversionTimeStruct.mq5. Массив структур parts конвертируется в datetime в цикле.

   MqlDateTime parts[] =
   {
      {00000000},
      {1000000000},
      {202123000000},
      {202113, -500000},
      {20215010000000},
      {20211020153015500},
      {2021102015305500},
   };
   ArrayPrint(parts);
   Print("");
   
   // преобразуем все элементы в цикле
   for(int i = 0i < sizeof(parts) / sizeof(MqlDateTime); ++i)
   {
      ResetLastError();
      datetime result = StructToTime(parts[i]);
      Print("["i"] ", (long)result" "result" "_LastError);
   }

Для каждого элемента выводится получившееся значение и код ошибки.

       [year] [mon] [day] [hour] [min] [sec] [day_of_week] [day_of_year]
   [0]      0     0     0      0     0     0             0             0
   [1]    100     0     0      0     0     0             0             0
   [2]   2021     2    30      0     0     0             0             0
   [3]   2021    13    -5      0     0     0             0             0
   [4]   2021    50   100      0     0     0             0             0
   [5]   2021    10    20     15    30   155             0             0
   [6]   2021    10    20     15    30    55             0             0
   
   [0] -1 wrong datetime 4010
   [1] 946684800 2000.01.01 00:00:00 4010
   [2] 1614643200 2021.03.02 00:00:00 0
   [3] 1638316800 2021.12.01 00:00:00 4010
   [4] 1640908800 2021.12.31 00:00:00 4010
   [5] 1634743859 2021.10.20 15:30:59 4010
   [6] 1634743855 2021.10.20 15:30:55 0

Обратите внимание, что некоторые корректировки функция выполняет, не взводя флаг ошибки. Так, в элементе под номером 2 мы передавали в функцию 30 февраля 2021 года, которое было преобразовано во 2-е марта 2021, и _LastError = 0.