TimeUtils/CalendarValidation/Validation_Suite.mq5

1844 lines
50 KiB
MQL5
Raw Permalink Normal View History

2026-07-15 02:45:49 +03:00
#include "..\TimeUtils.mqh"
#define RANDOM_TESTS 60000000
#define FIRST_YEAR 1970
#define LAST_YEAR 3000
const datetime MIN_TIME = D'1970.01.01 00:00:00';
const datetime MAX_TIME = D'3000.12.31 23:59:59';
struct TestStatistics
{
ulong Timestamps; // Number of datetime values tested
ulong Core; // ValidateCore() calls
ulong Property; // ValidateProperties() calls
ulong Assertions; // Total assertions evaluated
ulong EdgeCases; // Boundary/special-case timestamps
ulong Random; // Random timestamps generated
ulong Failed; // Failed assertions
ulong StartTick;
};
TestStatistics Stats = {};
//+------------------------------------------------------------------+
//| Test offsets |
//+------------------------------------------------------------------+
const int DAY_OFFSETS[] =
{
-5000,-2000,-1000,-366,-365,-100,-31,-30,-7,-2,-1,
1,2,7,30,31,100,365,366,1000,2000,5000
};
const int WEEK_OFFSETS[] =
{
-520,-260,-104,-52,-26,-12,-4,-1,
1,4,12,26,52,104,260,520
};
const int MONTH_OFFSETS[] =
{
-240,-120,-60,-24,-12,-6,-3,-1,
1,3,6,12,24,60,120,240
};
const int QUARTER_OFFSETS[] =
{
-40,-20,-8,-4,-1,
1,4,8,20,40
};
const int YEAR_OFFSETS[] =
{
-200,-100,-50,-20,-10,-5,-1,
1,5,10,20,50,100,200
};
//+------------------------------------------------------------------+
//| Assertion: boolean |
//+------------------------------------------------------------------+
bool Assert(const bool condition,
const string test,
const datetime t,
const string expected = "",
const string actual = "")
{
Stats.Assertions++;
if(condition)
return true;
ReportFailure(test, t, expected, actual);
return false;
}
//+------------------------------------------------------------------+
//| Assertion: integer |
//+------------------------------------------------------------------+
bool AssertEqual(const int expected,
const int actual,
const string test,
const datetime t)
{
Stats.Assertions++;
if(expected == actual)
return true;
ReportFailure(test,
t,
IntegerToString(expected),
IntegerToString(actual));
return false;
}
//+------------------------------------------------------------------+
//| Assertion: datetime |
//+------------------------------------------------------------------+
bool AssertEqual(const datetime expected,
const datetime actual,
const string test,
const datetime t)
{
Stats.Assertions++;
if(expected == actual)
return true;
ReportFailure(test,
expected,
TimeToString(expected,TIME_DATE|TIME_SECONDS),
TimeToString(actual,TIME_DATE|TIME_SECONDS));
return false;
}
void PrintProgress(const ulong current,const ulong total)
{
static uint lastPercent=0;
uint percent=(uint)((100ULL*current)/total);
if(percent>=lastPercent+10)
{
lastPercent=percent;
Print(percent,"% completed...");
}
}
void ReportFailure(const string function,
const datetime t,
const string expected,
const string actual)
{
Stats.Failed++;
Print("");
Print("==================================");
Print("Validation FAILED");
Print("----------------------------------");
Print("Function : ",function);
Print("Timestamp: ",TimeToString(t,TIME_DATE|TIME_SECONDS));
Print("Epoch : ",(long)t);
Print("Expected : ",expected);
Print("Actual : ",actual);
Print("==================================");
}
struct SplitMix64
{
ulong x;
void Seed(const ulong seed)
{
x = seed;
}
ulong Next()
{
ulong z = (x += 0x9E3779B97F4A7C15);
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9;
z = (z ^ (z >> 27)) * 0x94D049BB133111EB;
return z ^ (z >> 31);
}
SplitMix64()
{
Seed(GetTickCount64());
}
};
//+------------------------------------------------------------------+
//| Validate the core conversion algorithms |
//+------------------------------------------------------------------+
bool ValidateTimestamp(const datetime t)
{
Stats.Timestamps++;
// Reference conversion
MqlDateTime ref;
if(!TimeToStruct(t, ref))
{
ReportFailure("TimeToStruct()",
t,
"Success",
"Failure");
return false;
}
// Fast conversion
MqlDateTime fast;
TimeToStructFast(t, fast);
// Compare every field
if(ref.year != fast.year
|| ref.mon != fast.mon
|| ref.day != fast.day
|| ref.hour != fast.hour
|| ref.min != fast.min
|| ref.sec != fast.sec
|| ref.day_of_week != fast.day_of_week
|| ref.day_of_year != fast.day_of_year)
{
Stats.Failed++;
Print("");
Print("========================================");
Print("TimeToStructFast() mismatch");
Print("----------------------------------------");
Print("Timestamp : ", TimeToString(t, TIME_DATE|TIME_SECONDS));
Print("Epoch : ", (long)t);
Print("");
Print("Reference:");
MqlDateTime temp[1];
temp[1] = ref;
ArrayPrint(temp);
Print("");
Print("Fast:");
temp[1] = fast;
ArrayPrint(temp);
Print("========================================");
return false;
}
// Round-trip verification
const datetime back = StructToTimeFast(fast);
if(back != t)
{
ReportFailure("StructToTimeFast()",
t,
TimeToString(t, TIME_DATE | TIME_SECONDS),
TimeToString(back, TIME_DATE | TIME_SECONDS));
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate extractor functions |
//+------------------------------------------------------------------+
bool ValidateExtractors(const datetime t)
{
MqlDateTime st;
TimeToStructFast(t, st);
const int year = Year(t);
const int month = Month(t);
const int day = Day(t);
const int hour = Hour(t);
const int minute = Minute(t);
const int second = Second(t);
const int dow = DayOfWeek(t);
const int doy = DayOfYear(t);
if(year != st.year)
{
ReportFailure("Year()",
t,
IntegerToString(st.year),
IntegerToString(year));
return false;
}
if(month != st.mon)
{
ReportFailure("Month()",
t,
IntegerToString(st.mon),
IntegerToString(month));
return false;
}
if(day != st.day)
{
ReportFailure("Day()",
t,
IntegerToString(st.day),
IntegerToString(day));
return false;
}
if(hour != st.hour)
{
ReportFailure("Hour()",
t,
IntegerToString(st.hour),
IntegerToString(hour));
return false;
}
if(minute != st.min)
{
ReportFailure("Minute()",
t,
IntegerToString(st.min),
IntegerToString(minute));
return false;
}
if(second != st.sec)
{
ReportFailure("Second()",
t,
IntegerToString(st.sec),
IntegerToString(second));
return false;
}
if(dow != st.day_of_week)
{
ReportFailure("DayOfWeek()",
t,
IntegerToString(st.day_of_week),
IntegerToString(dow));
return false;
}
if(doy != st.day_of_year)
{
ReportFailure("DayOfYear()",
t,
IntegerToString(st.day_of_year),
IntegerToString(doy));
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate one edge case and its neighborhood |
//+------------------------------------------------------------------+
bool ValidateEdgeCase(const datetime t)
{
for(int offset = -10; offset <= 10; offset++)
{
datetime tt = t + offset;
if(tt < MIN_TIME || tt > MAX_TIME)
continue;
Stats.EdgeCases++;
if(!ValidateTimestamp(tt))
return false;
if(!ValidateExtractors(tt))
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Assert CalendarDifference() result |
//+------------------------------------------------------------------+
bool AssertCalendarDifference(const datetime from,
const datetime to,
const int years,
const int months,
const int days,
const int hours,
const int minutes,
const int seconds)
{
MqlDateTime diff;
CalendarDifference(from, to, diff);
if(diff.year != years ||
diff.mon != months ||
diff.day != days ||
diff.hour != hours ||
diff.min != minutes ||
diff.sec != seconds)
{
string expected = StringFormat("%dy %dm %dd %02d:%02d:%02d",
years, months, days,
hours, minutes, seconds);
string actual = StringFormat("%dy %dm %dd %02d:%02d:%02d",
diff.year, diff.mon, diff.day,
diff.hour, diff.min, diff.sec);
ReportFailure("CalendarDifference()",
from,
expected,
actual);
Print("From : ", TimeToString(from, TIME_DATE | TIME_SECONDS));
Print("To : ", TimeToString(to, TIME_DATE | TIME_SECONDS));
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate CalendarDifference() known edge cases |
//+------------------------------------------------------------------+
bool ValidateCalendarDifferenceKnownCases()
{
// Zero interval
if(!AssertCalendarDifference(
D'1970.01.01 00:00:00',
D'1970.01.01 00:00:00',
0,0,0,0,0,0))
return false;
// One second
if(!AssertCalendarDifference(
D'1970.01.01 00:00:00',
D'1970.01.01 00:00:01',
0,0,0,0,0,1))
return false;
// One minute
if(!AssertCalendarDifference(
D'1970.01.01 00:00:00',
D'1970.01.01 00:01:00',
0,0,0,0,1,0))
return false;
// One hour
if(!AssertCalendarDifference(
D'1970.01.01 00:00:00',
D'1970.01.01 01:00:00',
0,0,0,1,0,0))
return false;
// One day
if(!AssertCalendarDifference(
D'1970.01.01 00:00:00',
D'1970.01.02 00:00:00',
0,0,1,0,0,0))
return false;
// Leap day
if(!AssertCalendarDifference(
D'2000.02.28',
D'2000.02.29',
0,0,1,0,0,0))
return false;
// Leap day -> March
if(!AssertCalendarDifference(
D'2000.02.29',
D'2000.03.01',
0,0,1,0,0,0))
return false;
// Non-leap century
if(!AssertCalendarDifference(
D'2100.02.28',
D'2100.03.01',
0,0,1,0,0,0))
return false;
// Month boundary
if(!AssertCalendarDifference(
D'2024.01.31',
D'2024.02.29',
0,1,0,0,0,0))
return false;
// February 29 edge case
if(!AssertCalendarDifference(
D'2024.02.29',
D'2025.02.28',
1,0,0,0,0,0))
return false;
// Month + day
if(!AssertCalendarDifference(
D'2024.01.31',
D'2024.03.01',
0,1,1,0,0,0))
return false;
// Year boundary
if(!AssertCalendarDifference(
D'2024.12.31 23:59:59',
D'2025.01.01 00:00:00',
0,0,0,0,0,1))
return false;
return true;
}
//+------------------------------------------------------------------+
//| Verify CalendarDifference() |
//+------------------------------------------------------------------+
bool ValidateCalendarDifference(const datetime from,
const datetime to)
{
MqlDateTime diff;
CalendarDifference(from, to, diff);
//---------------------------------------------------------------
// Normalization
//---------------------------------------------------------------
if(!Assert(diff.year >= 0,
"CalendarDifference().year",
from))
return false;
if(!Assert(diff.mon >= 0 && diff.mon < 12,
"CalendarDifference().month normalized",
from))
return false;
if(!Assert(diff.day >= 0,
"CalendarDifference().day",
from))
return false;
if(!Assert(diff.hour >= 0 && diff.hour < 24,
"CalendarDifference().hour normalized",
from))
return false;
if(!Assert(diff.min >= 0 && diff.min < 60,
"CalendarDifference().minute normalized",
from))
return false;
if(!Assert(diff.sec >= 0 && diff.sec < 60,
"CalendarDifference().second normalized",
from))
return false;
//---------------------------------------------------------------
// Reconstruction
//---------------------------------------------------------------
const int totalMonths = diff.year * 12 + diff.mon;
datetime reconstructed = from;
reconstructed = AddMonths(reconstructed, totalMonths);
reconstructed = AddDays(reconstructed, diff.day);
reconstructed = AddHours(reconstructed, diff.hour);
reconstructed = AddMinutes(reconstructed, diff.min);
reconstructed = AddSeconds(reconstructed, diff.sec);
if(reconstructed != to)
{
ReportFailure(
"CalendarDifference() reconstruction",
from,
TimeToString(to, TIME_DATE | TIME_SECONDS),
TimeToString(reconstructed, TIME_DATE | TIME_SECONDS));
PrintFormat("From : %s",
TimeToString(from, TIME_DATE | TIME_SECONDS));
PrintFormat("To : %s",
TimeToString(to, TIME_DATE | TIME_SECONDS));
PrintFormat("Difference : %d years, %d months, %d days, %02d:%02d:%02d",
diff.year,
diff.mon,
diff.day,
diff.hour,
diff.min,
diff.sec);
PrintFormat("After AddYears : %s",
TimeToString(AddYears(from, diff.year),
TIME_DATE | TIME_SECONDS));
datetime tmp = AddYears(from, diff.year);
tmp = AddMonths(tmp, diff.mon);
PrintFormat("After AddMonths : %s",
TimeToString(tmp,
TIME_DATE | TIME_SECONDS));
tmp = AddDays(tmp, diff.day);
PrintFormat("After AddDays : %s",
TimeToString(tmp,
TIME_DATE | TIME_SECONDS));
tmp = AddHours(tmp, diff.hour);
tmp = AddMinutes(tmp, diff.min);
tmp = AddSeconds(tmp, diff.sec);
PrintFormat("Final : %s",
TimeToString(tmp,
TIME_DATE | TIME_SECONDS));
return false;
}
//---------------------------------------------------------------
// Exact reconstruction
//---------------------------------------------------------------
MqlDateTime zero;
CalendarDifference(reconstructed, to, zero);
if(zero.year != 0 ||
zero.mon != 0 ||
zero.day != 0 ||
zero.hour != 0 ||
zero.min != 0 ||
zero.sec != 0)
{
ReportFailure(
"CalendarDifference() reconstruction",
from,
"0y 0m 0d 00:00:00",
CalendarDifferenceToString(reconstructed, to));
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate CalendarDifference() properties |
//+------------------------------------------------------------------+
bool ValidateCalendarDifferenceProperties(const datetime t)
{
//---------------------------------------------------------------
// Identity
//---------------------------------------------------------------
MqlDateTime zero;
CalendarDifference(t, t, zero);
if(zero.year != 0 ||
zero.mon != 0 ||
zero.day != 0 ||
zero.hour != 0 ||
zero.min != 0 ||
zero.sec != 0)
{
ReportFailure(
"CalendarDifference() identity",
t,
"0 seconds",
CalendarDifferenceToString(t, t));
return false;
}
//---------------------------------------------------------------
// Day offsets
//---------------------------------------------------------------
for(int i = 0; i < ArraySize(DAY_OFFSETS); i++)
{
datetime t2 = AddDays(t, DAY_OFFSETS[i]);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!ValidateCalendarDifference(MathMin(t, t2),
MathMax(t, t2)))
return false;
}
//---------------------------------------------------------------
// Week offsets
//---------------------------------------------------------------
for(int i = 0; i < ArraySize(WEEK_OFFSETS); i++)
{
datetime t2 = AddWeeks(t, WEEK_OFFSETS[i]);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!ValidateCalendarDifference(MathMin(t, t2),
MathMax(t, t2)))
return false;
}
//---------------------------------------------------------------
// Month offsets
//---------------------------------------------------------------
for(int i = 0; i < ArraySize(MONTH_OFFSETS); i++)
{
datetime t2 = AddMonths(t, MONTH_OFFSETS[i]);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!ValidateCalendarDifference(MathMin(t, t2),
MathMax(t, t2)))
return false;
}
//---------------------------------------------------------------
// Quarter offsets
//---------------------------------------------------------------
for(int i = 0; i < ArraySize(QUARTER_OFFSETS); i++)
{
datetime t2 = AddQuarters(t, QUARTER_OFFSETS[i]);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!ValidateCalendarDifference(MathMin(t, t2),
MathMax(t, t2)))
return false;
}
//---------------------------------------------------------------
// Year offsets
//---------------------------------------------------------------
for(int i = 0; i < ArraySize(YEAR_OFFSETS); i++)
{
datetime t2 = AddYears(t, YEAR_OFFSETS[i]);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!ValidateCalendarDifference(MathMin(t, t2),
MathMax(t, t2)))
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Assert CalendarDifferenceToString() |
//+------------------------------------------------------------------+
bool AssertCalendarDifferenceString(const datetime from,
const datetime to,
const string expected)
{
string actual = CalendarDifferenceToString(from, to);
if(actual != expected)
{
ReportFailure(
"CalendarDifferenceToString()",
from,
expected,
actual);
Print("From : ", TimeToString(from, TIME_DATE | TIME_SECONDS));
Print("To : ", TimeToString(to, TIME_DATE | TIME_SECONDS));
return false;
}
return true;
}
//+------------------------------------------------------------------+
//| Validate CalendarDifferenceToString() |
//+------------------------------------------------------------------+
bool ValidateCalendarDifferenceStringKnownCases()
{
if(!AssertCalendarDifferenceString(
D'1970.01.01',
D'1970.01.01',
"0 seconds"))
return false;
if(!AssertCalendarDifferenceString(
D'1970.01.01',
D'1970.01.01 00:00:01',
"1 second"))
return false;
if(!AssertCalendarDifferenceString(
D'1970.01.01',
D'1970.01.01 00:00:02',
"2 seconds"))
return false;
if(!AssertCalendarDifferenceString(
D'1970.01.01',
D'1970.01.01 00:01:00',
"1 minute"))
return false;
if(!AssertCalendarDifferenceString(
D'1970.01.01',
D'1970.01.01 01:00:00',
"1 hour"))
return false;
if(!AssertCalendarDifferenceString(
D'1970.01.01',
D'1970.01.02',
"1 day"))
return false;
if(!AssertCalendarDifferenceString(
D'2024.01.31',
D'2024.02.29',
"1 month"))
return false;
if(!AssertCalendarDifferenceString(
D'2024.02.29',
D'2025.02.28',
"1 year"))
return false;
if(!AssertCalendarDifferenceString(
D'2024.01.31',
D'2024.03.01',
"1 month 1 day"))
return false;
if(!AssertCalendarDifferenceString(
D'2024.12.31 23:59:59',
D'2025.01.01 00:00:00',
"1 second"))
return false;
return true;
}
// Each phase has a distinct purpose:
//
// Phase Purpose
// Calendar boundaries Test every transition (month-end, leap day, year-end, century)
// Every day Validate every calendar date exactly once
// Every hour Validate time decomposition and midnight rollovers
// Random stress Detect unexpected regressions over millions of arbitrary timestamps
/*
TestCalendarBoundaries()
ValidateLeapYears()
ValidateSpecialDates()
ValidateCalendarDifferenceKnownCases()
...
*/
//+------------------------------------------------------------------+
//| Test all leap years and special calendar days |
//+------------------------------------------------------------------+
bool TestCalendarBoundaries()
{
Print("Testing calendar boundaries...");
for(int year = FIRST_YEAR; year <= LAST_YEAR; year++)
{
//----- New Year's transition
if(!ValidateEdgeCase(DateFrom(year, 1, 1, 0, 0, 0)))
return false;
if(!ValidateEdgeCase(DateFrom(year, 12, 31, 23, 59, 59)))
return false;
//----- Every month boundary
for(int month = 1; month <= 12; month++)
{
const int lastDay = DaysInMonth(year, month);
if(!ValidateEdgeCase(DateFrom(year, month, 1, 0, 0, 0)))
return false;
if(!ValidateEdgeCase(DateFrom(year, month, lastDay, 23, 59, 59)))
return false;
}
//----- February transition
if(IsLeapYear(year))
{
if(!ValidateEdgeCase(DateFrom(year, 2, 28, 23, 59, 59)))
return false;
if(!ValidateEdgeCase(DateFrom(year, 2, 29, 0, 0, 0)))
return false;
if(!ValidateEdgeCase(DateFrom(year, 2, 29, 12, 0, 0)))
return false;
if(!ValidateEdgeCase(DateFrom(year, 2, 29, 23, 59, 59)))
return false;
if(!ValidateEdgeCase(DateFrom(year, 3, 1, 0, 0, 0)))
return false;
}
else
{
if(!ValidateEdgeCase(DateFrom(year, 2, 28, 23, 59, 59)))
return false;
if(!ValidateEdgeCase(DateFrom(year, 3, 1, 0, 0, 0)))
return false;
}
}
if(!ValidateCalendarDifferenceKnownCases())
return false;
Print("Calendar boundary tests passed.");
return true;
}
//+------------------------------------------------------------------+
//| Validate every day in the supported range |
//| 376,565 timestamps |
//+------------------------------------------------------------------+
bool TestEveryDay()
{
Print("Testing every calendar day...");
ulong tested = 0;
for(datetime t = MIN_TIME; t <= MAX_TIME; t += 86400)
{
if(!ValidateTimestamp(t))
return false;
if(!ValidateExtractors(t))
return false;
tested++;
if((tested % 50000) == 0)
Print(" ", tested, " days checked...");
}
Print("Every day passed (", tested, " days).");
return true;
}
//+------------------------------------------------------------------+
//| Validate every hour in the supported range |
//| 376,565 × 24 ≈ 9,037,560 timestamps |
//+------------------------------------------------------------------+
bool TestEveryHour()
{
Print("Testing every hour...");
ulong tested = 0;
for(datetime t = MIN_TIME; t <= MAX_TIME; t += 3600)
{
if(!ValidateTimestamp(t))
return false;
tested++;
if((tested % 1000000) == 0)
Print(" ", tested, " hours checked...");
}
Print("Every hour passed (", tested, " hours).");
return true;
}
//+------------------------------------------------------------------+
//| Random stress test |
//+------------------------------------------------------------------+
bool TestRandom()
{
Print("Random stress test...");
SplitMix64 rng;
const ulong range = (ulong)(MAX_TIME - MIN_TIME) + 1;
for(ulong i = 1; i <= RANDOM_TESTS; i++)
{
datetime t = MIN_TIME + (datetime)(rng.Next() % range);
Stats.Random++;
// Core validation
if(!ValidateCore(t))
return false;
// Property validation every 100th sample
if((i % 100) == 0)
{
if(!ValidateProperties(t))
return false;
}
PrintProgress(i, RANDOM_TESTS);
}
Print("Random stress test passed.");
return true;
}
bool ValidateStartEndProperties(const datetime t)
{
if(!Assert(StartOfMinute(t) <= t && t <= EndOfMinute(t), "Minute range", t))
return false;
if(!Assert(StartOfHour(t) <= t && t <= EndOfHour(t), "Hour range", t))
return false;
if(!Assert(StartOfDay(t) <= t && t <= EndOfDay(t), "Day range", t))
return false;
if(!Assert(StartOfWeek(t) <= t && t <= EndOfWeek(t), "Week range", t))
return false;
if(!Assert(StartOfMonth(t) <= t && t <= EndOfMonth(t), "Month range", t))
return false;
if(!Assert(StartOfQuarter(t) <= t && t <= EndOfQuarter(t), "Quarter range", t))
return false;
if(!Assert(StartOfYear(t) <= t && t <= EndOfYear(t), "Year range", t))
return false;
if(!AssertEqual(
StartOfMinute(t),
StartOfMinute(EndOfMinute(t)),
"StartOfMinute(EndOfMinute())",
t))
return false;
if(!AssertEqual(
StartOfHour(t),
StartOfHour(EndOfHour(t)),
"StartOfHour(EndOfHour())",
t))
return false;
if(!AssertEqual(
StartOfDay(t),
StartOfDay(EndOfDay(t)),
"StartOfDay(EndOfDay())",
t))
return false;
if(!AssertEqual(
StartOfWeek(t),
StartOfWeek(EndOfWeek(t)),
"StartOfWeek(EndOfWeek())",
t))
return false;
if(!AssertEqual(
StartOfMonth(t),
StartOfMonth(EndOfMonth(t)),
"StartOfMonth(EndOfMonth())",
t))
return false;
if(!AssertEqual(
StartOfQuarter(t),
StartOfQuarter(EndOfQuarter(t)),
"StartOfQuarter(EndOfQuarter())",
t))
return false;
if(!AssertEqual(
StartOfYear(t),
StartOfYear(EndOfYear(t)),
"StartOfYear(EndOfYear())",
t))
return false;
return true;
}
bool ValidateDecompositionProperties(const datetime t)
{
if(!AssertEqual(t,
DateOnly(t) + TimeOnly(t),
"DateOnly() + TimeOnly()",
t))
return false;
if(!AssertEqual(t,
StartOfMinute(t) + Second(t),
"Minute decomposition",
t))
return false;
if(!AssertEqual(t,
StartOfHour(t)
+ Minute(t) * 60
+ Second(t),
"Hour decomposition",
t))
return false;
if(!AssertEqual(t,
StartOfDay(t)
+ Hour(t) * 3600
+ Minute(t) * 60
+ Second(t),
"Day decomposition",
t))
return false;
return true;
}
bool ValidateIdempotenceProperties(const datetime t)
{
if(!AssertEqual(StartOfMinute(StartOfMinute(t)), StartOfMinute(t), "StartOfMinute()", t))
return false;
if(!AssertEqual(StartOfHour(StartOfHour(t)), StartOfHour(t), "StartOfHour()", t))
return false;
if(!AssertEqual(StartOfDay(StartOfDay(t)), StartOfDay(t), "StartOfDay()", t))
return false;
if(!AssertEqual(StartOfWeek(StartOfWeek(t)), StartOfWeek(t), "StartOfWeek()", t))
return false;
if(!AssertEqual(StartOfMonth(StartOfMonth(t)), StartOfMonth(t), "StartOfMonth()", t))
return false;
if(!AssertEqual(StartOfQuarter(StartOfQuarter(t)), StartOfQuarter(t), "StartOfQuarter()", t))
return false;
if(!AssertEqual(StartOfYear(StartOfYear(t)), StartOfYear(t), "StartOfYear()", t))
return false;
return true;
}
bool ValidateOrderingProperties(const datetime t)
{
if(!Assert(StartOfMinute(t) >= StartOfHour(t), "Minute >= Hour", t))
return false;
if(!Assert(StartOfHour(t) >= StartOfDay(t), "Hour >= Day", t))
return false;
if(!Assert(StartOfDay(t) >= StartOfWeek(t), "Day >= Week", t))
return false;
if(!Assert(StartOfWeek(t) <= t && t <= EndOfWeek(t), "Week contains timestamp", t))
return false;
if(!Assert(StartOfMonth(t) >= StartOfQuarter(t), "Month >= Quarter", t))
return false;
if(!Assert(StartOfQuarter(t) >= StartOfYear(t), "Quarter >= Year", t))
return false;
return true;
}
bool ValidateIndexProperties(const datetime t)
{
if(!AssertEqual(DayIndex(StartOfDay(t)),
DayIndex(t),
"DayIndex",
t))
return false;
if(!AssertEqual(HourIndex(StartOfHour(t)),
HourIndex(t),
"HourIndex",
t))
return false;
if(!AssertEqual(MinuteIndex(StartOfMinute(t)),
MinuteIndex(t),
"MinuteIndex",
t))
return false;
return true;
}
//+------------------------------------------------------------------+
//| Validate monotonicity |
//+------------------------------------------------------------------+
bool ValidateMonotonicityProperties(const datetime t)
{
const datetime next = t + 1;
if(next > MAX_TIME)
return true;
if(!Assert(DayIndex(next) >= DayIndex(t),
"DayIndex monotonic",
t))
return false;
if(!Assert(HourIndex(next) >= HourIndex(t),
"HourIndex monotonic",
t))
return false;
if(!Assert(MinuteIndex(next) >= MinuteIndex(t),
"MinuteIndex monotonic",
t))
return false;
return true;
}
bool ValidateIntervalProperties(const datetime t)
{
if(!AssertEqual((datetime)59,
EndOfMinute(t)-StartOfMinute(t),
"Minute length",
t))
return false;
if(!AssertEqual((datetime)3599,
EndOfHour(t)-StartOfHour(t),
"Hour length",
t))
return false;
if(!AssertEqual((datetime)86399,
EndOfDay(t)-StartOfDay(t),
"Day length",
t))
return false;
return true;
}
bool ValidateLeapYearProperties(const datetime t)
{
const int y = Year(t);
if(!AssertEqual(365 + (IsLeapYear(y) ? 1 : 0),
DaysInYear(y),
"DaysInYear()",
t))
return false;
if(Month(t) == 12 && Day(t) == 31)
{
if(!AssertEqual(DaysInYear(y) - 1,
DayOfYear(t),
"DayOfYear(Dec31)",
t))
return false;
}
return true;
}
bool ValidateSuccessorProperties(const datetime t)
{
if(t >= MAX_TIME - 86400)
return true;
const datetime tomorrow = AddDays(t, 1);
if(!AssertEqual(1,
DifferenceInDays(t, tomorrow),
"AddDays()/DifferenceInDays()",
t))
return false;
if(!AssertEqual(t,
AddDays(tomorrow, -1),
"AddDays inverse",
t))
return false;
return true;
}
//+------------------------------------------------------------------+
//| Validate difference functions |
//+------------------------------------------------------------------+
bool ValidateDifferenceProperties(const datetime t)
{
// Identity
if(!AssertEqual(0, DifferenceInDays(t, t), "DifferenceInDays()", t))
return false;
if(!AssertEqual(0, DifferenceInWeeks(t, t), "DifferenceInWeeks()", t))
return false;
if(!AssertEqual(0, DifferenceInMonths(t, t), "DifferenceInMonths()", t))
return false;
if(!AssertEqual(0, DifferenceInQuarters(t, t), "DifferenceInQuarters()", t))
return false;
if(!AssertEqual(0, DifferenceInYears(t, t), "DifferenceInYears()", t))
return false;
if(!AssertEqual(0, DifferenceInBusinessDays(t, t), "DifferenceInBusinessDays()", t))
return false;
if(!AssertEqual(0, DifferenceInCalendarDays(t, t), "DifferenceInCalendarDays()", t))
return false;
if(!AssertEqual(0, DifferenceInCalendarMonths(t, t), "DifferenceInCalendarMonths()", t))
return false;
if(!AssertEqual(0, DifferenceInCalendarYears(t, t), "DifferenceInCalendarYears()", t))
return false;
//----------------------------------------------------------------
// Day offsets
//----------------------------------------------------------------
for(int i = 0; i < ArraySize(DAY_OFFSETS); i++)
{
const int n = DAY_OFFSETS[i];
const datetime t2 = AddDays(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInDays(t, t2),
"DifferenceInDays(AddDays())",
t))
return false;
if(!AssertEqual(-n,
DifferenceInDays(t2, t),
"DifferenceInDays() symmetry",
t))
return false;
}
//----------------------------------------------------------------
// Week offsets
//----------------------------------------------------------------
for(int i = 0; i < ArraySize(WEEK_OFFSETS); i++)
{
const int n = WEEK_OFFSETS[i];
const datetime t2 = AddWeeks(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInWeeks(t, t2),
"DifferenceInWeeks(AddWeeks())",
t))
return false;
if(!AssertEqual(-n,
DifferenceInWeeks(t2, t),
"DifferenceInWeeks() symmetry",
t))
return false;
}
//----------------------------------------------------------------
// Month offsets
//----------------------------------------------------------------
for(int i = 0; i < ArraySize(MONTH_OFFSETS); i++)
{
const int n = MONTH_OFFSETS[i];
const datetime t2 = AddMonths(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInMonths(t, t2),
"DifferenceInMonths(AddMonths())",
t))
return false;
if(!AssertEqual(-n,
DifferenceInMonths(t2, t),
"DifferenceInMonths() symmetry",
t))
return false;
}
//----------------------------------------------------------------
// Quarter offsets
//----------------------------------------------------------------
for(int i = 0; i < ArraySize(QUARTER_OFFSETS); i++)
{
const int n = QUARTER_OFFSETS[i];
const datetime t2 = AddQuarters(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInQuarters(t, t2),
"DifferenceInQuarters(AddQuarters())",
t))
return false;
if(!AssertEqual(-n,
DifferenceInQuarters(t2, t),
"DifferenceInQuarters() symmetry",
t))
return false;
}
//----------------------------------------------------------------
// Year offsets
//----------------------------------------------------------------
for(int i = 0; i < ArraySize(YEAR_OFFSETS); i++)
{
const int n = YEAR_OFFSETS[i];
const datetime t2 = AddYears(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInYears(t, t2),
"DifferenceInYears(AddYears())",
t))
return false;
if(!AssertEqual(-n,
DifferenceInYears(t2, t),
"DifferenceInYears() symmetry",
t))
return false;
}
//---------------------------------------------------------------
// Round-trip: AddXXX() <-> DifferenceInXXX()
//---------------------------------------------------------------
for(int i = 0; i < ArraySize(DAY_OFFSETS); i++)
{
const int n = DAY_OFFSETS[i];
datetime t2 = AddDays(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInDays(t, t2),
"AddDays() <-> DifferenceInDays()",
t))
return false;
if(!AssertEqual(t,
AddDays(t2, -n),
"AddDays() inverse",
t))
return false;
}
for(int i = 0; i < ArraySize(WEEK_OFFSETS); i++)
{
const int n = WEEK_OFFSETS[i];
datetime t2 = AddWeeks(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInWeeks(t, t2),
"AddWeeks() <-> DifferenceInWeeks()",
t))
return false;
if(!AssertEqual(t,
AddWeeks(t2, -n),
"AddWeeks() inverse",
t))
return false;
}
for(int i = 0; i < ArraySize(MONTH_OFFSETS); i++)
{
const int n = MONTH_OFFSETS[i];
datetime t2 = AddMonths(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInMonths(t, t2),
"AddMonths() <-> DifferenceInMonths()",
t))
return false;
// Inverse only when round-trip is lossless (avoid month-end clamping)
if(Day(t) == Day(t2))
{
if(!AssertEqual(t,
AddMonths(t2, -n),
"AddMonths() inverse",
t))
return false;
}
}
for(int i = 0; i < ArraySize(QUARTER_OFFSETS); i++)
{
const int n = QUARTER_OFFSETS[i];
datetime t2 = AddQuarters(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInQuarters(t, t2),
"AddQuarters() <-> DifferenceInQuarters()",
t))
return false;
if(Day(t) == Day(t2))
{
if(!AssertEqual(t,
AddQuarters(t2, -n),
"AddQuarters() inverse",
t))
return false;
}
}
for(int i = 0; i < ArraySize(YEAR_OFFSETS); i++)
{
const int n = YEAR_OFFSETS[i];
datetime t2 = AddYears(t, n);
if(t2 < MIN_TIME || t2 > MAX_TIME)
continue;
if(!AssertEqual(n,
DifferenceInYears(t, t2),
"AddYears() <-> DifferenceInYears()",
t))
return false;
// Skip Feb-29 and end-of-month clamping cases
if(Month(t) == Month(t2) &&
Day(t) == Day(t2))
{
if(!AssertEqual(t,
AddYears(t2, -n),
"AddYears() inverse",
t))
return false;
}
}
return true;
}
//+------------------------------------------------------------------+
//| Validate week functions |
//+------------------------------------------------------------------+
bool ValidateWeekProperties(const datetime t)
{
if(!Assert(StartOfWeek(t) <= t &&
t <= EndOfWeek(t),
"Week range",
t))
return false;
if(!AssertEqual(StartOfWeek(StartOfWeek(t)),
StartOfWeek(t),
"StartOfWeek()",
t))
return false;
if(!AssertEqual(WeekOfMonth(StartOfMonth(t)),
1,
"WeekOfMonth()",
t))
return false;
if(!AssertEqual((datetime)(604800 - 1),
EndOfWeek(t) - StartOfWeek(t),
"Week length",
t))
return false;
return true;
}
//+------------------------------------------------------------------+
//| Validate quarter functions |
//+------------------------------------------------------------------+
bool ValidateQuarterProperties(const datetime t)
{
int q = Quarter(t);
if(!Assert(q >= 1 && q <= 4,
"Quarter range",
t))
return false;
if(!Assert(StartOfQuarter(t) <= t &&
t <= EndOfQuarter(t),
"Quarter boundaries",
t))
return false;
if(!AssertEqual(Quarter(StartOfQuarter(t)),
q,
"Quarter(StartOfQuarter)",
t))
return false;
if(!AssertEqual(Quarter(EndOfQuarter(t)),
q,
"Quarter(EndOfQuarter)",
t))
return false;
if(!AssertEqual(StartOfQuarter(StartOfQuarter(t)),
StartOfQuarter(t),
"StartOfQuarter()",
t))
return false;
return true;
}
//+------------------------------------------------------------------+
//| Validate business day functions |
//+------------------------------------------------------------------+
bool ValidateBusinessDayProperties(const datetime t)
{
if(!Assert(IsWeekend(t) != IsWeekday(t),
"Weekend/Weekday",
t))
return false;
int dow = DayOfWeek(t);
if(dow == SATURDAY || dow == SUNDAY)
{
if(!Assert(IsWeekend(t), "Weekend", t))
return false;
}
else
{
if(!Assert(IsWeekday(t), "Weekday", t))
return false;
}
if(!AssertEqual(0,
DifferenceInBusinessDays(t, t),
"Business days",
t))
return false;
return true;
}
// Introduce Test Suites
bool ValidateCore(const datetime t)
{
Stats.Core++;
return
ValidateTimestamp(t) &&
ValidateExtractors(t);
}
bool ValidateProperties(const datetime t)
{
Stats.Property++;
return
ValidateStartEndProperties(t) &&
ValidateDecompositionProperties(t) &&
ValidateIdempotenceProperties(t) &&
ValidateLeapYearProperties(t) &&
ValidateOrderingProperties(t) &&
ValidateDifferenceProperties(t) &&
ValidateIndexProperties(t) &&
ValidateIntervalProperties(t) &&
ValidateMonotonicityProperties(t) &&
ValidateWeekProperties(t) &&
ValidateQuarterProperties(t) &&
ValidateBusinessDayProperties(t) &&
ValidateCalendarDifferenceProperties(t);
}
//+------------------------------------------------------------------+
//| Script entry point |
//+------------------------------------------------------------------+
void OnStart()
{
Stats.StartTick = GetTickCount64();
Print("");
Print("============================================================");
Print(" TimeUtils Validation Suite");
Print("============================================================");
Print("");
//===============================================================
// Calendar boundary tests
//===============================================================
if(!TestCalendarBoundaries())
return;
//===============================================================
// Exhaustive daily validation
//===============================================================
if(!TestEveryDay())
return;
//===============================================================
// Exhaustive hourly validation
//===============================================================
if(!TestEveryHour())
return;
//===============================================================
// Random stress test
//===============================================================
if(!TestRandom())
return;
//===============================================================
// Summary
//===============================================================
ulong elapsed = GetTickCount64() - Stats.StartTick;
Print("");
Print("============================================================");
Print(" ALL TESTS PASSED");
Print("============================================================");
Print("Core validations : ", Stats.Core);
Print("Property validations : ", Stats.Property);
Print("Edge cases tested : ", Stats.EdgeCases);
Print("Random timestamps : ", Stats.Random);
Print("Failures : ", Stats.Failed);
Print("Elapsed : ", elapsed, " ms");
Print("============================================================");
}
/*
validate.mq5
Configuration
Statistics
Assertions
Reporting
Random Generator
Core Validation
ValidateTimestamp
ValidateExtractors
Property Validation
Calendar
Arithmetic
Specialized
Dispatcher
Test Suites
Calendar Boundaries
Every Day
Every Hour
Random Stress
Benchmark
OnStart()
*/
/*
============================================================
TimeUtils Validation Suite
============================================================
Testing calendar boundaries...
Calendar boundary tests passed.
Testing every calendar day...
50000 days checked...
100000 days checked...
150000 days checked...
200000 days checked...
250000 days checked...
300000 days checked...
350000 days checked...
Every day passed (376565 days).
Testing every hour...
1000000 hours checked...
2000000 hours checked...
3000000 hours checked...
4000000 hours checked...
5000000 hours checked...
6000000 hours checked...
7000000 hours checked...
8000000 hours checked...
9000000 hours checked...
Every hour passed (9037560 hours).
Random stress test...
10% completed...
20% completed...
30% completed...
40% completed...
50% completed...
60% completed...
70% completed...
80% completed...
90% completed...
100% completed...
Random stress test passed.
============================================================
ALL TESTS PASSED
============================================================
Core validations : 60000000
Property validations : 600000
Edge cases tested : 621938
Random timestamps : 60000000
Failures : 0
Elapsed : 6484 ms
============================================================
*/