forked from amrali/TimeUtils
2508 lines
70 KiB
MQL5
2508 lines
70 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Validation_Suite.mq5 |
|
|
//| Copyright © 2026, Amr Ali |
|
|
//| https://www.mql5.com/en/users/amrali |
|
|
//+------------------------------------------------------------------+
|
|
#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,
|
|
t,
|
|
StringFormat("%s (%I64d)",
|
|
TimeToString(expected, TIME_DATE | TIME_SECONDS),
|
|
(long)expected),
|
|
StringFormat("%s (%I64d)",
|
|
TimeToString(actual, TIME_DATE | TIME_SECONDS),
|
|
(long)actual));
|
|
|
|
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());
|
|
}
|
|
|
|
// [min, max]
|
|
long Next(const long min, const long max)
|
|
{
|
|
return min + (long)(Next() % (max - min + 1));
|
|
}
|
|
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
|
|
//---------------------------------------------------------------
|
|
// Round-trip reconstruction using AddPeriod()
|
|
//---------------------------------------------------------------
|
|
|
|
datetime reconstructed = AddPeriod(from, diff);
|
|
|
|
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);
|
|
|
|
datetime tmp = from;
|
|
|
|
tmp = AddMonths(tmp, diff.year * 12 + diff.mon);
|
|
|
|
PrintFormat("After Months : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = AddDays(tmp, diff.day);
|
|
|
|
PrintFormat("After Days : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = AddHours(tmp, diff.hour);
|
|
|
|
PrintFormat("After Hours : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = AddMinutes(tmp, diff.min);
|
|
|
|
PrintFormat("After Minutes : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = AddSeconds(tmp, diff.sec);
|
|
|
|
PrintFormat("After Seconds : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
return false;
|
|
}
|
|
|
|
// //---------------------------------------------------------------
|
|
// // Inverse property: SubPeriod()
|
|
// //---------------------------------------------------------------
|
|
//
|
|
// datetime original = SubPeriod(to, diff);
|
|
//
|
|
// if(original != from)
|
|
// {
|
|
// ReportFailure(
|
|
// "SubPeriod() inverse",
|
|
// to,
|
|
// TimeToString(from, TIME_DATE | TIME_SECONDS),
|
|
// TimeToString(original, 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;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// AddPeriod() / SubPeriod() are inverses
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(AddPeriod(from, diff),
|
|
to,
|
|
"AddPeriod()",
|
|
from))
|
|
return false;
|
|
|
|
// if(!AssertEqual(SubPeriod(to, diff),
|
|
// from,
|
|
// "SubPeriod()",
|
|
// 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;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Verify WeekOfMonth() and WeekOfYear() calendar layout |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateWeekCalendarLayout()
|
|
{
|
|
//---------------------------------------------------------------
|
|
// May 2024 (Monday-first)
|
|
//
|
|
// 1 2 3 4 5
|
|
// 6 7 8 9 10 11 12
|
|
// 13 14 15 16 17 18 19
|
|
// 20 21 22 23 24 25 26
|
|
// 27 28 29 30 31
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(1,
|
|
WeekOfMonth(D'2024.05.01'),
|
|
"WeekOfMonth() May 1",
|
|
D'2024.05.01'))
|
|
return false;
|
|
|
|
if(!AssertEqual(1,
|
|
WeekOfMonth(D'2024.05.05'),
|
|
"WeekOfMonth() May 5",
|
|
D'2024.05.05'))
|
|
return false;
|
|
|
|
if(!AssertEqual(2,
|
|
WeekOfMonth(D'2024.05.06'),
|
|
"WeekOfMonth() May 6",
|
|
D'2024.05.06'))
|
|
return false;
|
|
|
|
if(!AssertEqual(5,
|
|
WeekOfMonth(D'2024.05.31'),
|
|
"WeekOfMonth() May 31",
|
|
D'2024.05.31'))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// August 2020 occupies six calendar rows (Monday-first)
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(1,
|
|
WeekOfMonth(D'2020.08.01'),
|
|
"WeekOfMonth() Aug 1",
|
|
D'2020.08.01'))
|
|
return false;
|
|
|
|
if(!AssertEqual(2,
|
|
WeekOfMonth(D'2020.08.03'),
|
|
"WeekOfMonth() Aug 3",
|
|
D'2020.08.03'))
|
|
return false;
|
|
|
|
if(!AssertEqual(5,
|
|
WeekOfMonth(D'2020.08.30'),
|
|
"WeekOfMonth() Aug 30",
|
|
D'2020.08.30'))
|
|
return false;
|
|
|
|
if(!AssertEqual(6,
|
|
WeekOfMonth(D'2020.08.31'),
|
|
"WeekOfMonth() Aug 31",
|
|
D'2020.08.31'))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// WeekOfYear() (calendar week numbering)
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(1,
|
|
WeekOfYear(D'2023.01.01'),
|
|
"WeekOfYear() Jan 1",
|
|
D'2023.01.01'))
|
|
return false;
|
|
|
|
if(!AssertEqual(2,
|
|
WeekOfYear(D'2023.01.02'),
|
|
"WeekOfYear() Jan 2",
|
|
D'2023.01.02'))
|
|
return false;
|
|
|
|
if(!AssertEqual(53,
|
|
WeekOfYear(D'2023.12.31'),
|
|
"WeekOfYear() Dec 31",
|
|
D'2023.12.31'))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Consistency properties
|
|
//---------------------------------------------------------------
|
|
|
|
for(datetime t = D'1970.01.01';
|
|
t <= D'1972.12.31';
|
|
t += DAYSECS)
|
|
{
|
|
if(!Assert(WeekOfMonth(t) >= 1 &&
|
|
WeekOfMonth(t) <= 6,
|
|
"WeekOfMonth() range",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(WeekOfYear(t) >= 1 &&
|
|
WeekOfYear(t) <= 54,
|
|
"WeekOfYear() range",
|
|
t))
|
|
return false;
|
|
|
|
if(StartOfWeek(t) != StartOfWeek(AddDays(t, 1)))
|
|
{
|
|
if(!Assert(WeekOfMonth(AddDays(t, 1)) ==
|
|
WeekOfMonth(t) + 1 ||
|
|
Month(AddDays(t, 1)) != Month(t),
|
|
"WeekOfMonth() increment",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(WeekOfYear(AddDays(t, 1)) ==
|
|
WeekOfYear(t) + 1 ||
|
|
Year(AddDays(t, 1)) != Year(t),
|
|
"WeekOfYear() increment",
|
|
t))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Verify SessionOverlap() |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateSessionOverlap()
|
|
{
|
|
datetime overlapStart, overlapEnd;
|
|
|
|
//---------------------------------------------------------------
|
|
// Partial overlap
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(SessionOverlap(10, 20,
|
|
15, 25,
|
|
overlapStart,
|
|
overlapEnd),
|
|
"SessionOverlap() partial",
|
|
0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)15, overlapStart,
|
|
"SessionOverlap().Start", 0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)20, overlapEnd,
|
|
"SessionOverlap().End", 0))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// One interval contains the other
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(SessionOverlap(10, 30,
|
|
15, 20,
|
|
overlapStart,
|
|
overlapEnd),
|
|
"SessionOverlap() contains",
|
|
0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)15, overlapStart,
|
|
"SessionOverlap().ContainsStart", 0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)20, overlapEnd,
|
|
"SessionOverlap().ContainsEnd", 0))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Identical intervals
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(SessionOverlap(10, 20,
|
|
10, 20,
|
|
overlapStart,
|
|
overlapEnd),
|
|
"SessionOverlap() identical",
|
|
0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)10, overlapStart,
|
|
"SessionOverlap().IdenticalStart", 0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)20, overlapEnd,
|
|
"SessionOverlap().IdenticalEnd", 0))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Touching endpoints (half-open intervals)
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(!SessionOverlap(10, 20,
|
|
20, 30,
|
|
overlapStart,
|
|
overlapEnd),
|
|
"SessionOverlap() touching",
|
|
0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)0, overlapStart,
|
|
"SessionOverlap().TouchingStart", 0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)0, overlapEnd,
|
|
"SessionOverlap().TouchingEnd", 0))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Disjoint intervals
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(!SessionOverlap(10, 20,
|
|
30, 40,
|
|
overlapStart,
|
|
overlapEnd),
|
|
"SessionOverlap() disjoint",
|
|
0))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Empty interval
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(!SessionOverlap(10, 10,
|
|
5, 20,
|
|
overlapStart,
|
|
overlapEnd),
|
|
"SessionOverlap() empty",
|
|
0))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Invalid interval
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(!SessionOverlap(20, 10,
|
|
5, 15,
|
|
overlapStart,
|
|
overlapEnd),
|
|
"SessionOverlap() invalid",
|
|
0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)0, overlapStart,
|
|
"SessionOverlap().InvalidStart", 0))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)0, overlapEnd,
|
|
"SessionOverlap().InvalidEnd", 0))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Symmetry
|
|
//---------------------------------------------------------------
|
|
|
|
datetime s1, e1;
|
|
datetime s2, e2;
|
|
|
|
if(!AssertEqual(
|
|
SessionOverlap(10, 25, 15, 30, s1, e1),
|
|
SessionOverlap(15, 30, 10, 25, s2, e2),
|
|
"SessionOverlap() symmetry",
|
|
0))
|
|
return false;
|
|
|
|
if(!AssertEqual(s1, s2,
|
|
"SessionOverlap().SymmetryStart", 0))
|
|
return false;
|
|
|
|
if(!AssertEqual(e1, e2,
|
|
"SessionOverlap().SymmetryEnd", 0))
|
|
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
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| 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;
|
|
|
|
if(!ValidateIsInSessionEdges())
|
|
return false;
|
|
|
|
if(!ValidateIsInSessionOverloads())
|
|
return false;
|
|
|
|
if(!ValidateWeekCalendarLayout())
|
|
return false;
|
|
|
|
if(!ValidateSessionOverlap())
|
|
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 += DAYSECS)
|
|
{
|
|
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 |
|
|
//+------------------------------------------------------------------+
|
|
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(
|
|
StartOfWeek(t),
|
|
(datetime)(WeekIndex(t) * WEEKSECS - WEEK_OFFSET),
|
|
"WeekIndex() <-> StartOfWeek()",
|
|
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(WeekIndex(StartOfWeek(t)),
|
|
WeekIndex(t),
|
|
"WeekIndex",
|
|
t))
|
|
return false;
|
|
|
|
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 - (int)DAYSECS)
|
|
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;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate WithXxx() functions |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateWithProperties(const datetime t)
|
|
{
|
|
datetime res;
|
|
|
|
//---------------------------------------------------------------
|
|
// WithSecond()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int s = 0; s < 60; s++)
|
|
{
|
|
res = WithSecond(t, s);
|
|
|
|
if(!AssertEqual(Second(res), s, "WithSecond()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Minute(res), Minute(t), "WithSecond().Minute", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Hour(res), Hour(t), "WithSecond().Hour", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// WithMinute()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int m = 0; m < 60; m++)
|
|
{
|
|
res = WithMinute(t, m);
|
|
|
|
if(!AssertEqual(Minute(res), m, "WithMinute()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Hour(res), Hour(t), "WithMinute().Hour", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Second(res), Second(t), "WithMinute().Second", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// WithHour()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int h = 0; h < 24; h++)
|
|
{
|
|
res = WithHour(t, h);
|
|
|
|
if(!AssertEqual(Hour(res), h, "WithHour()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Minute(res), Minute(t), "WithHour().Minute", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Second(res), Second(t), "WithHour().Second", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// WithDay()
|
|
//---------------------------------------------------------------
|
|
|
|
const int dim = DaysInMonth(Year(t), Month(t));
|
|
|
|
for(int d = 1; d <= dim; d++)
|
|
{
|
|
res = WithDay(t, d);
|
|
|
|
if(!AssertEqual(Day(res), d, "WithDay()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Month(res), Month(t), "WithDay().Month", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Year(res), Year(t), "WithDay().Year", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// WithMonth()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int m = 1; m <= 12; m++)
|
|
{
|
|
res = WithMonth(t, m);
|
|
|
|
if(!AssertEqual(Month(res), m, "WithMonth()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Year(res), Year(t), "WithMonth().Year", t))
|
|
return false;
|
|
|
|
if(!Assert(Day(res) <= DaysInMonth(Year(res), Month(res)),
|
|
"WithMonth().Clamp",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// WithYear()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int y = 1970; y <= 3000; y += 50)
|
|
{
|
|
res = WithYear(t, y);
|
|
|
|
if(!AssertEqual(Year(res), y, "WithYear()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Month(res), Month(t), "WithYear().Month", t))
|
|
return false;
|
|
|
|
if(!Assert(Day(res) <= DaysInMonth(y, Month(res)),
|
|
"WithYear().Clamp",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// WithDayOfWeek()
|
|
//---------------------------------------------------------------
|
|
|
|
const datetime sow = StartOfWeek(t);
|
|
const datetime eow = EndOfWeek(t);
|
|
|
|
for(int w = SUNDAY; w <= SATURDAY; w++)
|
|
{
|
|
// Skip tests that would move before the Unix epoch
|
|
const int first = (int)TIMEUTILS_FIRST_DAY_OF_WEEK;
|
|
|
|
int current = DayOfWeek(t) - first;
|
|
if(current < 0)
|
|
current += 7;
|
|
|
|
int target = w - first;
|
|
if(target < 0)
|
|
target += 7;
|
|
|
|
int delta = target - current;
|
|
|
|
// Would move before 1970-01-01?
|
|
if(delta < 0 && t < -delta * (int)DAYSECS)
|
|
continue;
|
|
|
|
res = WithDayOfWeek(t, (ENUM_DAY_OF_WEEK)w);
|
|
|
|
if(!AssertEqual(DayOfWeek(res),
|
|
w,
|
|
"WithDayOfWeek()",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(res >= sow,
|
|
"WithDayOfWeek() >= StartOfWeek",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(res <= eow,
|
|
"WithDayOfWeek() <= EndOfWeek",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(TimeOnly(res),
|
|
TimeOnly(t),
|
|
"WithDayOfWeek().Time",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// WithTime()
|
|
//---------------------------------------------------------------
|
|
|
|
res = WithTime(t, 3, 14, 15);
|
|
|
|
if(!AssertEqual(Hour(res), 3, "WithTime().Hour", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Minute(res), 14, "WithTime().Minute", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Second(res), 15, "WithTime().Second", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(DateOnly(res),
|
|
DateOnly(t),
|
|
"WithTime().Date",
|
|
t))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// WithDate()
|
|
//---------------------------------------------------------------
|
|
|
|
res = WithDate(t, 2024, 6, 15);
|
|
|
|
if(!AssertEqual(Year(res), 2024, "WithDate().Year", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Month(res), 6, "WithDate().Month", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(Day(res), 15, "WithDate().Day", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(TimeOnly(res),
|
|
TimeOnly(t),
|
|
"WithDate().Time",
|
|
t))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Replacement property
|
|
//---------------------------------------------------------------
|
|
|
|
const int safeDay = MathMin(15, DaysInMonth(Year(t), Month(t)));
|
|
const int safeMonth = (Month(t) == 6 ? 7 : 6);
|
|
const int safeYear = (Year(t) == 2024 ? 2025 : 2024);
|
|
|
|
// WithSecond()
|
|
|
|
if(!AssertEqual(
|
|
WithSecond(WithSecond(t, 11), 37),
|
|
WithSecond(t, 37),
|
|
"WithSecond() replacement",
|
|
t))
|
|
return false;
|
|
|
|
// WithMinute()
|
|
|
|
if(!AssertEqual(
|
|
WithMinute(WithMinute(t, 11), 37),
|
|
WithMinute(t, 37),
|
|
"WithMinute() replacement",
|
|
t))
|
|
return false;
|
|
|
|
// WithHour()
|
|
|
|
if(!AssertEqual(
|
|
WithHour(WithHour(t, 5), 17),
|
|
WithHour(t, 17),
|
|
"WithHour() replacement",
|
|
t))
|
|
return false;
|
|
|
|
// WithTime()
|
|
|
|
if(!AssertEqual(
|
|
WithTime(
|
|
WithTime(t, 1, 2, 3),
|
|
20, 21, 22),
|
|
WithTime(t, 20, 21, 22),
|
|
"WithTime() replacement",
|
|
t))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Idempotence
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(WithSecond(t, Second(t)),
|
|
t,
|
|
"WithSecond() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(WithMinute(t, Minute(t)),
|
|
t,
|
|
"WithMinute() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(WithHour(t, Hour(t)),
|
|
t,
|
|
"WithHour() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(WithDay(t, Day(t)),
|
|
t,
|
|
"WithDay() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(WithMonth(t, Month(t)),
|
|
t,
|
|
"WithMonth() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(WithYear(t, Year(t)),
|
|
t,
|
|
"WithYear() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(WithDayOfWeek(t, (ENUM_DAY_OF_WEEK)DayOfWeek(t)),
|
|
t,
|
|
"WithDayOfWeek() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Verify IsInSession() |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateIsInSessionEdges()
|
|
{
|
|
//----------------------------------------------------
|
|
// 08:00 -> 17:00
|
|
//----------------------------------------------------
|
|
|
|
if(!Assert(IsInSession(DateFromString("2025.01.01 08:00"),
|
|
8,0,17,0),
|
|
"IsInSession() start inclusive",
|
|
D'2025.01.01'))
|
|
return false;
|
|
|
|
if(!Assert(!IsInSession(DateFromString("2025.01.01 17:00"),
|
|
8,0,17,0),
|
|
"IsInSession() end exclusive",
|
|
D'2025.01.01'))
|
|
return false;
|
|
|
|
//----------------------------------------------------
|
|
// Overnight
|
|
//----------------------------------------------------
|
|
|
|
if(!Assert(IsInSession(DateFromString("2025.01.01 23:30"),
|
|
22,0,6,0),
|
|
"IsInSession() overnight evening",
|
|
D'2025.01.01'))
|
|
return false;
|
|
|
|
if(!Assert(IsInSession(DateFromString("2025.01.02 05:59"),
|
|
22,0,6,0),
|
|
"IsInSession() overnight morning",
|
|
D'2025.01.02'))
|
|
return false;
|
|
|
|
if(!Assert(!IsInSession(DateFromString("2025.01.01 12:00"),
|
|
22,0,6,0),
|
|
"IsInSession() outside overnight",
|
|
D'2025.01.01'))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateIsInSessionOverloads()
|
|
{
|
|
datetime now = TimeTradeServer();
|
|
|
|
for(int sh=0; sh<24; sh++)
|
|
{
|
|
for(int sm=0; sm<60; sm+=5)
|
|
{
|
|
for(int eh=0; eh<24; eh++)
|
|
{
|
|
for(int em=0; em<60; em+=5)
|
|
{
|
|
bool expected =
|
|
IsInSession(now,
|
|
sh, sm,
|
|
eh, em);
|
|
|
|
if(!AssertEqual(
|
|
expected,
|
|
IsInSession(sh, sm, eh, em),
|
|
"IsInSession() overload",
|
|
now))
|
|
return false;
|
|
|
|
string s1 =
|
|
IntegerToString(sh, 2, '0') + ":" +
|
|
IntegerToString(sm, 2, '0');
|
|
|
|
string s2 =
|
|
IntegerToString(eh, 2, '0') + ":" +
|
|
IntegerToString(em, 2, '0');
|
|
|
|
if(!AssertEqual(
|
|
expected,
|
|
IsInSession(s1, s2),
|
|
"IsInSession(string) overload",
|
|
now))
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateIsInSession(const datetime t)
|
|
{
|
|
const int current = SecondsSinceMidnight(t);
|
|
|
|
SplitMix64 rng;
|
|
|
|
for(int i = 0; i < 64; i++)
|
|
{
|
|
int sh = (int)rng.Next(0, 23);
|
|
int sm = (int)rng.Next(0, 59);
|
|
|
|
int eh = (int)rng.Next(0, 23);
|
|
int em = (int)rng.Next(0, 59);
|
|
|
|
const bool result =
|
|
IsInSession(t,
|
|
sh, sm,
|
|
eh, em);
|
|
|
|
//----------------------------------------------------
|
|
// Expected result (reference implementation)
|
|
//----------------------------------------------------
|
|
|
|
const int start = sh * 3600 + sm * 60;
|
|
const int end = eh * 3600 + em * 60;
|
|
|
|
bool expected;
|
|
|
|
if(start <= end)
|
|
expected = (current >= start && current < end);
|
|
else
|
|
expected = (current >= start || current < end);
|
|
|
|
if(!AssertEqual(expected,
|
|
result,
|
|
"IsInSession(datetime)",
|
|
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) &&
|
|
ValidateWithProperties(t) &&
|
|
ValidateIsInSession(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("============================================================");
|
|
}
|
|
|
|
/*
|
|
|
|
============================================================
|
|
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 : 37437 ms
|
|
============================================================
|
|
|
|
*/
|