forked from amrali/TimeUtils
2409 lines
71 KiB
MQL5
2409 lines
71 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;
|
|
CTimeUtils::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("CTimeUtils::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 = CTimeUtils::StructToTimeFast(fast);
|
|
|
|
if(back != t)
|
|
{
|
|
ReportFailure("CTimeUtils::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;
|
|
CTimeUtils::TimeToStructFast(t, st);
|
|
|
|
const int year = CTimeUtils::Year(t);
|
|
const int month = CTimeUtils::Month(t);
|
|
const int day = CTimeUtils::Day(t);
|
|
const int hour = CTimeUtils::Hour(t);
|
|
const int minute = CTimeUtils::Minute(t);
|
|
const int second = CTimeUtils::Second(t);
|
|
const int dow = CTimeUtils::DayOfWeek(t);
|
|
const int doy = CTimeUtils::DayOfYear(t);
|
|
|
|
if(year != st.year)
|
|
{
|
|
ReportFailure("CTimeUtils::Year()",
|
|
t,
|
|
IntegerToString(st.year),
|
|
IntegerToString(year));
|
|
return false;
|
|
}
|
|
|
|
if(month != st.mon)
|
|
{
|
|
ReportFailure("CTimeUtils::Month()",
|
|
t,
|
|
IntegerToString(st.mon),
|
|
IntegerToString(month));
|
|
return false;
|
|
}
|
|
|
|
if(day != st.day)
|
|
{
|
|
ReportFailure("CTimeUtils::Day()",
|
|
t,
|
|
IntegerToString(st.day),
|
|
IntegerToString(day));
|
|
return false;
|
|
}
|
|
|
|
if(hour != st.hour)
|
|
{
|
|
ReportFailure("CTimeUtils::Hour()",
|
|
t,
|
|
IntegerToString(st.hour),
|
|
IntegerToString(hour));
|
|
return false;
|
|
}
|
|
|
|
if(minute != st.min)
|
|
{
|
|
ReportFailure("CTimeUtils::Minute()",
|
|
t,
|
|
IntegerToString(st.min),
|
|
IntegerToString(minute));
|
|
return false;
|
|
}
|
|
|
|
if(second != st.sec)
|
|
{
|
|
ReportFailure("CTimeUtils::Second()",
|
|
t,
|
|
IntegerToString(st.sec),
|
|
IntegerToString(second));
|
|
return false;
|
|
}
|
|
|
|
if(dow != st.day_of_week)
|
|
{
|
|
ReportFailure("CTimeUtils::DayOfWeek()",
|
|
t,
|
|
IntegerToString(st.day_of_week),
|
|
IntegerToString(dow));
|
|
return false;
|
|
}
|
|
|
|
if(doy != st.day_of_year)
|
|
{
|
|
ReportFailure("CTimeUtils::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 CTimeUtils::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;
|
|
CTimeUtils::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("CTimeUtils::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 CTimeUtils::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 CTimeUtils::CalendarDifference() |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateCalendarDifference(const datetime from,
|
|
const datetime to)
|
|
{
|
|
MqlDateTime diff;
|
|
CTimeUtils::CalendarDifference(from, to, diff);
|
|
|
|
//---------------------------------------------------------------
|
|
// Normalization
|
|
//---------------------------------------------------------------
|
|
|
|
if(!Assert(diff.year >= 0,
|
|
"CTimeUtils::CalendarDifference().year",
|
|
from))
|
|
return false;
|
|
|
|
if(!Assert(diff.mon >= 0 && diff.mon < 12,
|
|
"CTimeUtils::CalendarDifference().month normalized",
|
|
from))
|
|
return false;
|
|
|
|
if(!Assert(diff.day >= 0,
|
|
"CTimeUtils::CalendarDifference().day",
|
|
from))
|
|
return false;
|
|
|
|
if(!Assert(diff.hour >= 0 && diff.hour < 24,
|
|
"CTimeUtils::CalendarDifference().hour normalized",
|
|
from))
|
|
return false;
|
|
|
|
if(!Assert(diff.min >= 0 && diff.min < 60,
|
|
"CTimeUtils::CalendarDifference().minute normalized",
|
|
from))
|
|
return false;
|
|
|
|
if(!Assert(diff.sec >= 0 && diff.sec < 60,
|
|
"CTimeUtils::CalendarDifference().second normalized",
|
|
from))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Round-trip reconstruction using CTimeUtils::AddPeriod()
|
|
//---------------------------------------------------------------
|
|
|
|
datetime reconstructed = CTimeUtils::AddPeriod(from, diff);
|
|
|
|
if(reconstructed != to)
|
|
{
|
|
ReportFailure(
|
|
"CTimeUtils::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 = CTimeUtils::AddMonths(tmp, diff.year * 12 + diff.mon);
|
|
|
|
PrintFormat("After Months : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = CTimeUtils::AddDays(tmp, diff.day);
|
|
|
|
PrintFormat("After Days : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = CTimeUtils::AddHours(tmp, diff.hour);
|
|
|
|
PrintFormat("After Hours : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = CTimeUtils::AddMinutes(tmp, diff.min);
|
|
|
|
PrintFormat("After Minutes : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
tmp = CTimeUtils::AddSeconds(tmp, diff.sec);
|
|
|
|
PrintFormat("After Seconds : %s",
|
|
TimeToString(tmp,
|
|
TIME_DATE | TIME_SECONDS));
|
|
|
|
return false;
|
|
}
|
|
|
|
// //---------------------------------------------------------------
|
|
// // Inverse property: CTimeUtils::SubPeriod()
|
|
// //---------------------------------------------------------------
|
|
//
|
|
// datetime original = CTimeUtils::SubPeriod(to, diff);
|
|
//
|
|
// if(original != from)
|
|
// {
|
|
// ReportFailure(
|
|
// "CTimeUtils::SubPeriod() inverse",
|
|
// to,
|
|
// TimeToString(from, TIME_DATE | TIME_SECONDS),
|
|
// TimeToString(original, TIME_DATE | TIME_SECONDS));
|
|
//
|
|
// return false;
|
|
// }
|
|
|
|
//---------------------------------------------------------------
|
|
// Exact reconstruction
|
|
//---------------------------------------------------------------
|
|
|
|
MqlDateTime zero;
|
|
CTimeUtils::CalendarDifference(reconstructed, to, zero);
|
|
|
|
if(zero.year != 0 ||
|
|
zero.mon != 0 ||
|
|
zero.day != 0 ||
|
|
zero.hour != 0 ||
|
|
zero.min != 0 ||
|
|
zero.sec != 0)
|
|
{
|
|
ReportFailure(
|
|
"CTimeUtils::CalendarDifference() reconstruction",
|
|
from,
|
|
"0y 0m 0d 00:00:00",
|
|
CTimeUtils::CalendarDifferenceToString(reconstructed, to));
|
|
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::AddPeriod() / CTimeUtils::SubPeriod() are inverses
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(CTimeUtils::AddPeriod(from, diff),
|
|
to,
|
|
"CTimeUtils::AddPeriod()",
|
|
from))
|
|
return false;
|
|
|
|
// if(!AssertEqual(CTimeUtils::SubPeriod(to, diff),
|
|
// from,
|
|
// "CTimeUtils::SubPeriod()",
|
|
// to))
|
|
// return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate CTimeUtils::CalendarDifference() properties |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateCalendarDifferenceProperties(const datetime t)
|
|
{
|
|
//---------------------------------------------------------------
|
|
// Identity
|
|
//---------------------------------------------------------------
|
|
|
|
MqlDateTime zero;
|
|
CTimeUtils::CalendarDifference(t, t, zero);
|
|
|
|
if(zero.year != 0 ||
|
|
zero.mon != 0 ||
|
|
zero.day != 0 ||
|
|
zero.hour != 0 ||
|
|
zero.min != 0 ||
|
|
zero.sec != 0)
|
|
{
|
|
ReportFailure(
|
|
"CTimeUtils::CalendarDifference() identity",
|
|
t,
|
|
"0 seconds",
|
|
CTimeUtils::CalendarDifferenceToString(t, t));
|
|
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// Day offsets
|
|
//---------------------------------------------------------------
|
|
|
|
for(int i = 0; i < ArraySize(DAY_OFFSETS); i++)
|
|
{
|
|
datetime t2 = CTimeUtils::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 = CTimeUtils::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 = CTimeUtils::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 = CTimeUtils::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 = CTimeUtils::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 CTimeUtils::WeekOfMonth() and CTimeUtils::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,
|
|
CTimeUtils::WeekOfMonth(D'2024.05.01'),
|
|
"CTimeUtils::WeekOfMonth() May 1",
|
|
D'2024.05.01'))
|
|
return false;
|
|
|
|
if(!AssertEqual(1,
|
|
CTimeUtils::WeekOfMonth(D'2024.05.05'),
|
|
"CTimeUtils::WeekOfMonth() May 5",
|
|
D'2024.05.05'))
|
|
return false;
|
|
|
|
if(!AssertEqual(2,
|
|
CTimeUtils::WeekOfMonth(D'2024.05.06'),
|
|
"CTimeUtils::WeekOfMonth() May 6",
|
|
D'2024.05.06'))
|
|
return false;
|
|
|
|
if(!AssertEqual(5,
|
|
CTimeUtils::WeekOfMonth(D'2024.05.31'),
|
|
"CTimeUtils::WeekOfMonth() May 31",
|
|
D'2024.05.31'))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// August 2020 occupies six calendar rows (Monday-first)
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(1,
|
|
CTimeUtils::WeekOfMonth(D'2020.08.01'),
|
|
"CTimeUtils::WeekOfMonth() Aug 1",
|
|
D'2020.08.01'))
|
|
return false;
|
|
|
|
if(!AssertEqual(2,
|
|
CTimeUtils::WeekOfMonth(D'2020.08.03'),
|
|
"CTimeUtils::WeekOfMonth() Aug 3",
|
|
D'2020.08.03'))
|
|
return false;
|
|
|
|
if(!AssertEqual(5,
|
|
CTimeUtils::WeekOfMonth(D'2020.08.30'),
|
|
"CTimeUtils::WeekOfMonth() Aug 30",
|
|
D'2020.08.30'))
|
|
return false;
|
|
|
|
if(!AssertEqual(6,
|
|
CTimeUtils::WeekOfMonth(D'2020.08.31'),
|
|
"CTimeUtils::WeekOfMonth() Aug 31",
|
|
D'2020.08.31'))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WeekOfYear() (calendar week numbering)
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(1,
|
|
CTimeUtils::WeekOfYear(D'2023.01.01'),
|
|
"CTimeUtils::WeekOfYear() Jan 1",
|
|
D'2023.01.01'))
|
|
return false;
|
|
|
|
if(!AssertEqual(2,
|
|
CTimeUtils::WeekOfYear(D'2023.01.02'),
|
|
"CTimeUtils::WeekOfYear() Jan 2",
|
|
D'2023.01.02'))
|
|
return false;
|
|
|
|
if(!AssertEqual(53,
|
|
CTimeUtils::WeekOfYear(D'2023.12.31'),
|
|
"CTimeUtils::WeekOfYear() Dec 31",
|
|
D'2023.12.31'))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Consistency properties
|
|
//---------------------------------------------------------------
|
|
|
|
for(datetime t = D'1970.01.01';
|
|
t <= D'1972.12.31';
|
|
t += 86400)
|
|
{
|
|
if(!Assert(CTimeUtils::WeekOfMonth(t) >= 1 &&
|
|
CTimeUtils::WeekOfMonth(t) <= 6,
|
|
"CTimeUtils::WeekOfMonth() range",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::WeekOfYear(t) >= 1 &&
|
|
CTimeUtils::WeekOfYear(t) <= 54,
|
|
"CTimeUtils::WeekOfYear() range",
|
|
t))
|
|
return false;
|
|
|
|
if(CTimeUtils::StartOfWeek(t) != CTimeUtils::StartOfWeek(CTimeUtils::AddDays(t, 1)))
|
|
{
|
|
if(!Assert(CTimeUtils::WeekOfMonth(CTimeUtils::AddDays(t, 1)) ==
|
|
CTimeUtils::WeekOfMonth(t) + 1 ||
|
|
CTimeUtils::Month(CTimeUtils::AddDays(t, 1)) != CTimeUtils::Month(t),
|
|
"CTimeUtils::WeekOfMonth() increment",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::WeekOfYear(CTimeUtils::AddDays(t, 1)) ==
|
|
CTimeUtils::WeekOfYear(t) + 1 ||
|
|
CTimeUtils::Year(CTimeUtils::AddDays(t, 1)) != CTimeUtils::Year(t),
|
|
"CTimeUtils::WeekOfYear() increment",
|
|
t))
|
|
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(CTimeUtils::DateFrom(year, 1, 1, 0, 0, 0)))
|
|
return false;
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, 12, 31, 23, 59, 59)))
|
|
return false;
|
|
|
|
//----- Every month boundary
|
|
for(int month = 1; month <= 12; month++)
|
|
{
|
|
const int lastDay = CTimeUtils::DaysInMonth(year, month);
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, month, 1, 0, 0, 0)))
|
|
return false;
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, month, lastDay, 23, 59, 59)))
|
|
return false;
|
|
}
|
|
|
|
//----- February transition
|
|
if(CTimeUtils::IsLeapYear(year))
|
|
{
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, 2, 28, 23, 59, 59)))
|
|
return false;
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, 2, 29, 0, 0, 0)))
|
|
return false;
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, 2, 29, 12, 0, 0)))
|
|
return false;
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, 2, 29, 23, 59, 59)))
|
|
return false;
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, 3, 1, 0, 0, 0)))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if(!ValidateEdgeCase(CTimeUtils::DateFrom(year, 2, 28, 23, 59, 59)))
|
|
return false;
|
|
|
|
if(!ValidateEdgeCase(CTimeUtils::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;
|
|
|
|
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 |
|
|
//+------------------------------------------------------------------+
|
|
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;
|
|
}
|
|
|
|
|
|
#ifndef WEEK_OFFSET
|
|
#define WEEK_OFFSET \
|
|
((THURSDAY - (int)TIMEUTILS_FIRST_DAY_OF_WEEK) * 86400ULL)
|
|
#endif
|
|
|
|
bool ValidateStartEndProperties(const datetime t)
|
|
{
|
|
if(!Assert(CTimeUtils::StartOfMinute(t) <= t && t <= CTimeUtils::EndOfMinute(t), "Minute range", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfHour(t) <= t && t <= CTimeUtils::EndOfHour(t), "Hour range", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfDay(t) <= t && t <= CTimeUtils::EndOfDay(t), "Day range", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfWeek(t) <= t && t <= CTimeUtils::EndOfWeek(t), "Week range", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfMonth(t) <= t && t <= CTimeUtils::EndOfMonth(t), "Month range", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfQuarter(t) <= t && t <= CTimeUtils::EndOfQuarter(t), "Quarter range", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfYear(t) <= t && t <= CTimeUtils::EndOfYear(t), "Year range", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfMinute(t),
|
|
CTimeUtils::StartOfMinute(CTimeUtils::EndOfMinute(t)),
|
|
"CTimeUtils::StartOfMinute(CTimeUtils::EndOfMinute())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfHour(t),
|
|
CTimeUtils::StartOfHour(CTimeUtils::EndOfHour(t)),
|
|
"CTimeUtils::StartOfHour(CTimeUtils::EndOfHour())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfDay(t),
|
|
CTimeUtils::StartOfDay(CTimeUtils::EndOfDay(t)),
|
|
"CTimeUtils::StartOfDay(CTimeUtils::EndOfDay())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfWeek(t),
|
|
CTimeUtils::StartOfWeek(CTimeUtils::EndOfWeek(t)),
|
|
"CTimeUtils::StartOfWeek(CTimeUtils::EndOfWeek())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfWeek(t),
|
|
(datetime)(CTimeUtils::WeekIndex(t) * 604800ULL - WEEK_OFFSET),
|
|
"CTimeUtils::WeekIndex() <-> CTimeUtils::StartOfWeek()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfMonth(t),
|
|
CTimeUtils::StartOfMonth(CTimeUtils::EndOfMonth(t)),
|
|
"CTimeUtils::StartOfMonth(CTimeUtils::EndOfMonth())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfQuarter(t),
|
|
CTimeUtils::StartOfQuarter(CTimeUtils::EndOfQuarter(t)),
|
|
"CTimeUtils::StartOfQuarter(CTimeUtils::EndOfQuarter())",
|
|
t))
|
|
return false;
|
|
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::StartOfYear(t),
|
|
CTimeUtils::StartOfYear(CTimeUtils::EndOfYear(t)),
|
|
"CTimeUtils::StartOfYear(CTimeUtils::EndOfYear())",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateDecompositionProperties(const datetime t)
|
|
{
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::DateOnly(t) + CTimeUtils::TimeOnly(t),
|
|
"CTimeUtils::DateOnly() + CTimeUtils::TimeOnly()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::StartOfMinute(t) + CTimeUtils::Second(t),
|
|
"Minute decomposition",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::StartOfHour(t)
|
|
+ CTimeUtils::Minute(t) * 60
|
|
+ CTimeUtils::Second(t),
|
|
"Hour decomposition",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::StartOfDay(t)
|
|
+ CTimeUtils::Hour(t) * 3600
|
|
+ CTimeUtils::Minute(t) * 60
|
|
+ CTimeUtils::Second(t),
|
|
"Day decomposition",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateIdempotenceProperties(const datetime t)
|
|
{
|
|
if(!AssertEqual(CTimeUtils::StartOfMinute(CTimeUtils::StartOfMinute(t)), CTimeUtils::StartOfMinute(t), "CTimeUtils::StartOfMinute()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfHour(CTimeUtils::StartOfHour(t)), CTimeUtils::StartOfHour(t), "CTimeUtils::StartOfHour()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfDay(CTimeUtils::StartOfDay(t)), CTimeUtils::StartOfDay(t), "CTimeUtils::StartOfDay()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfWeek(CTimeUtils::StartOfWeek(t)), CTimeUtils::StartOfWeek(t), "CTimeUtils::StartOfWeek()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfMonth(CTimeUtils::StartOfMonth(t)), CTimeUtils::StartOfMonth(t), "CTimeUtils::StartOfMonth()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfQuarter(CTimeUtils::StartOfQuarter(t)), CTimeUtils::StartOfQuarter(t), "CTimeUtils::StartOfQuarter()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfYear(CTimeUtils::StartOfYear(t)), CTimeUtils::StartOfYear(t), "CTimeUtils::StartOfYear()", t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateOrderingProperties(const datetime t)
|
|
{
|
|
if(!Assert(CTimeUtils::StartOfMinute(t) >= CTimeUtils::StartOfHour(t), "Minute >= Hour", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfHour(t) >= CTimeUtils::StartOfDay(t), "Hour >= Day", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfDay(t) >= CTimeUtils::StartOfWeek(t), "Day >= Week", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfWeek(t) <= t && t <= CTimeUtils::EndOfWeek(t), "Week contains timestamp", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfMonth(t) >= CTimeUtils::StartOfQuarter(t), "Month >= Quarter", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfQuarter(t) >= CTimeUtils::StartOfYear(t), "Quarter >= Year", t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
bool ValidateIndexProperties(const datetime t)
|
|
{
|
|
if(!AssertEqual(CTimeUtils::WeekIndex(CTimeUtils::StartOfWeek(t)),
|
|
CTimeUtils::WeekIndex(t),
|
|
"WeekIndex",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::DayIndex(CTimeUtils::StartOfDay(t)),
|
|
CTimeUtils::DayIndex(t),
|
|
"DayIndex",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::HourIndex(CTimeUtils::StartOfHour(t)),
|
|
CTimeUtils::HourIndex(t),
|
|
"HourIndex",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::MinuteIndex(CTimeUtils::StartOfMinute(t)),
|
|
CTimeUtils::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(CTimeUtils::DayIndex(next) >= CTimeUtils::DayIndex(t),
|
|
"DayIndex monotonic",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::HourIndex(next) >= CTimeUtils::HourIndex(t),
|
|
"HourIndex monotonic",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::MinuteIndex(next) >= CTimeUtils::MinuteIndex(t),
|
|
"MinuteIndex monotonic",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateIntervalProperties(const datetime t)
|
|
{
|
|
if(!AssertEqual((datetime)59,
|
|
CTimeUtils::EndOfMinute(t)-CTimeUtils::StartOfMinute(t),
|
|
"Minute length",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)3599,
|
|
CTimeUtils::EndOfHour(t)-CTimeUtils::StartOfHour(t),
|
|
"Hour length",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)86399,
|
|
CTimeUtils::EndOfDay(t)-CTimeUtils::StartOfDay(t),
|
|
"Day length",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateLeapYearProperties(const datetime t)
|
|
{
|
|
const int y = CTimeUtils::Year(t);
|
|
|
|
if(!AssertEqual(365 + (CTimeUtils::IsLeapYear(y) ? 1 : 0),
|
|
CTimeUtils::DaysInYear(y),
|
|
"CTimeUtils::DaysInYear()",
|
|
t))
|
|
return false;
|
|
|
|
if(CTimeUtils::Month(t) == 12 && CTimeUtils::Day(t) == 31)
|
|
{
|
|
if(!AssertEqual(CTimeUtils::DaysInYear(y) - 1,
|
|
CTimeUtils::DayOfYear(t),
|
|
"CTimeUtils::DayOfYear(Dec31)",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateSuccessorProperties(const datetime t)
|
|
{
|
|
if(t >= MAX_TIME - 86400)
|
|
return true;
|
|
|
|
const datetime tomorrow = CTimeUtils::AddDays(t, 1);
|
|
|
|
if(!AssertEqual(1,
|
|
CTimeUtils::DifferenceInDays(t, tomorrow),
|
|
"CTimeUtils::AddDays()/CTimeUtils::DifferenceInDays()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::AddDays(tomorrow, -1),
|
|
"AddDays inverse",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate difference functions |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateDifferenceProperties(const datetime t)
|
|
{
|
|
// Identity
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInDays(t, t), "CTimeUtils::DifferenceInDays()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInWeeks(t, t), "CTimeUtils::DifferenceInWeeks()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInMonths(t, t), "CTimeUtils::DifferenceInMonths()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInQuarters(t, t), "CTimeUtils::DifferenceInQuarters()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInYears(t, t), "CTimeUtils::DifferenceInYears()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInBusinessDays(t, t), "CTimeUtils::DifferenceInBusinessDays()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInCalendarDays(t, t), "CTimeUtils::DifferenceInCalendarDays()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInCalendarMonths(t, t), "CTimeUtils::DifferenceInCalendarMonths()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(0, CTimeUtils::DifferenceInCalendarYears(t, t), "CTimeUtils::DifferenceInCalendarYears()", t))
|
|
return false;
|
|
|
|
//----------------------------------------------------------------
|
|
// Day offsets
|
|
//----------------------------------------------------------------
|
|
for(int i = 0; i < ArraySize(DAY_OFFSETS); i++)
|
|
{
|
|
const int n = DAY_OFFSETS[i];
|
|
const datetime t2 = CTimeUtils::AddDays(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInDays(t, t2),
|
|
"CTimeUtils::DifferenceInDays(CTimeUtils::AddDays())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(-n,
|
|
CTimeUtils::DifferenceInDays(t2, t),
|
|
"CTimeUtils::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 = CTimeUtils::AddWeeks(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInWeeks(t, t2),
|
|
"CTimeUtils::DifferenceInWeeks(CTimeUtils::AddWeeks())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(-n,
|
|
CTimeUtils::DifferenceInWeeks(t2, t),
|
|
"CTimeUtils::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 = CTimeUtils::AddMonths(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInMonths(t, t2),
|
|
"CTimeUtils::DifferenceInMonths(CTimeUtils::AddMonths())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(-n,
|
|
CTimeUtils::DifferenceInMonths(t2, t),
|
|
"CTimeUtils::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 = CTimeUtils::AddQuarters(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInQuarters(t, t2),
|
|
"CTimeUtils::DifferenceInQuarters(CTimeUtils::AddQuarters())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(-n,
|
|
CTimeUtils::DifferenceInQuarters(t2, t),
|
|
"CTimeUtils::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 = CTimeUtils::AddYears(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInYears(t, t2),
|
|
"CTimeUtils::DifferenceInYears(CTimeUtils::AddYears())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(-n,
|
|
CTimeUtils::DifferenceInYears(t2, t),
|
|
"CTimeUtils::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 = CTimeUtils::AddDays(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInDays(t, t2),
|
|
"CTimeUtils::AddDays() <-> CTimeUtils::DifferenceInDays()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::AddDays(t2, -n),
|
|
"CTimeUtils::AddDays() inverse",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
for(int i = 0; i < ArraySize(WEEK_OFFSETS); i++)
|
|
{
|
|
const int n = WEEK_OFFSETS[i];
|
|
datetime t2 = CTimeUtils::AddWeeks(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInWeeks(t, t2),
|
|
"CTimeUtils::AddWeeks() <-> CTimeUtils::DifferenceInWeeks()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::AddWeeks(t2, -n),
|
|
"CTimeUtils::AddWeeks() inverse",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
for(int i = 0; i < ArraySize(MONTH_OFFSETS); i++)
|
|
{
|
|
const int n = MONTH_OFFSETS[i];
|
|
datetime t2 = CTimeUtils::AddMonths(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInMonths(t, t2),
|
|
"CTimeUtils::AddMonths() <-> CTimeUtils::DifferenceInMonths()",
|
|
t))
|
|
return false;
|
|
|
|
// Inverse only when round-trip is lossless (avoid month-end clamping)
|
|
if(CTimeUtils::Day(t) == CTimeUtils::Day(t2))
|
|
{
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::AddMonths(t2, -n),
|
|
"CTimeUtils::AddMonths() inverse",
|
|
t))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < ArraySize(QUARTER_OFFSETS); i++)
|
|
{
|
|
const int n = QUARTER_OFFSETS[i];
|
|
datetime t2 = CTimeUtils::AddQuarters(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInQuarters(t, t2),
|
|
"CTimeUtils::AddQuarters() <-> CTimeUtils::DifferenceInQuarters()",
|
|
t))
|
|
return false;
|
|
|
|
if(CTimeUtils::Day(t) == CTimeUtils::Day(t2))
|
|
{
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::AddQuarters(t2, -n),
|
|
"CTimeUtils::AddQuarters() inverse",
|
|
t))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
for(int i = 0; i < ArraySize(YEAR_OFFSETS); i++)
|
|
{
|
|
const int n = YEAR_OFFSETS[i];
|
|
datetime t2 = CTimeUtils::AddYears(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInYears(t, t2),
|
|
"CTimeUtils::AddYears() <-> CTimeUtils::DifferenceInYears()",
|
|
t))
|
|
return false;
|
|
|
|
// Skip Feb-29 and end-of-month clamping cases
|
|
if(CTimeUtils::Month(t) == CTimeUtils::Month(t2) &&
|
|
CTimeUtils::Day(t) == CTimeUtils::Day(t2))
|
|
{
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::AddYears(t2, -n),
|
|
"CTimeUtils::AddYears() inverse",
|
|
t))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate week functions |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateWeekProperties(const datetime t)
|
|
{
|
|
if(!Assert(CTimeUtils::StartOfWeek(t) <= t &&
|
|
t <= CTimeUtils::EndOfWeek(t),
|
|
"Week range",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfWeek(CTimeUtils::StartOfWeek(t)),
|
|
CTimeUtils::StartOfWeek(t),
|
|
"CTimeUtils::StartOfWeek()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::WeekOfMonth(CTimeUtils::StartOfMonth(t)),
|
|
1,
|
|
"CTimeUtils::WeekOfMonth()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual((datetime)(604800 - 1),
|
|
CTimeUtils::EndOfWeek(t) - CTimeUtils::StartOfWeek(t),
|
|
"Week length",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate quarter functions |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateQuarterProperties(const datetime t)
|
|
{
|
|
int q = CTimeUtils::Quarter(t);
|
|
|
|
if(!Assert(q >= 1 && q <= 4,
|
|
"Quarter range",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::StartOfQuarter(t) <= t &&
|
|
t <= CTimeUtils::EndOfQuarter(t),
|
|
"Quarter boundaries",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Quarter(CTimeUtils::StartOfQuarter(t)),
|
|
q,
|
|
"CTimeUtils::Quarter(StartOfQuarter)",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Quarter(CTimeUtils::EndOfQuarter(t)),
|
|
q,
|
|
"CTimeUtils::Quarter(EndOfQuarter)",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::StartOfQuarter(CTimeUtils::StartOfQuarter(t)),
|
|
CTimeUtils::StartOfQuarter(t),
|
|
"CTimeUtils::StartOfQuarter()",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate business day functions |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateBusinessDayProperties(const datetime t)
|
|
{
|
|
if(!Assert(CTimeUtils::IsWeekend(t) != CTimeUtils::IsWeekday(t),
|
|
"Weekend/Weekday",
|
|
t))
|
|
return false;
|
|
|
|
int dow = CTimeUtils::DayOfWeek(t);
|
|
|
|
if(dow == SATURDAY || dow == SUNDAY)
|
|
{
|
|
if(!Assert(CTimeUtils::IsWeekend(t), "Weekend", t))
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if(!Assert(CTimeUtils::IsWeekday(t), "Weekday", t))
|
|
return false;
|
|
}
|
|
|
|
if(!AssertEqual(0,
|
|
CTimeUtils::DifferenceInBusinessDays(t, t),
|
|
"Business days",
|
|
t))
|
|
return false;
|
|
|
|
//----------------------------------------------------------------
|
|
// Day offsets
|
|
//----------------------------------------------------------------
|
|
for(int i = 0; i < ArraySize(DAY_OFFSETS); i++)
|
|
{
|
|
const int n = DAY_OFFSETS[i];
|
|
const datetime t2 = CTimeUtils::AddBusinessDays(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(CTimeUtils::IsWeekend(t))
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInBusinessDays(t, t2),
|
|
"CTimeUtils::DifferenceInBusinessDays(CTimeUtils::AddBusinessDays())",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(-n,
|
|
CTimeUtils::DifferenceInBusinessDays(t2, t),
|
|
"CTimeUtils::DifferenceInBusinessDays() 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 = CTimeUtils::AddBusinessDays(t, n);
|
|
|
|
if(t2 < MIN_TIME || t2 > MAX_TIME)
|
|
continue;
|
|
|
|
if(CTimeUtils::IsWeekend(t))
|
|
continue;
|
|
|
|
if(!AssertEqual(n,
|
|
CTimeUtils::DifferenceInBusinessDays(t, t2),
|
|
"CTimeUtils::AddBusinessDays() <-> CTimeUtils::DifferenceInBusinessDays()",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(t,
|
|
CTimeUtils::AddBusinessDays(t2, -n),
|
|
"CTimeUtils::AddBusinessDays() inverse",
|
|
t))
|
|
return false;
|
|
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Validate WithXxx() functions |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateWithProperties(const datetime t)
|
|
{
|
|
datetime res;
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithSecond()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int s = 0; s < 60; s++)
|
|
{
|
|
res = CTimeUtils::WithSecond(t, s);
|
|
|
|
if(!AssertEqual(CTimeUtils::Second(res), s, "CTimeUtils::WithSecond()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Minute(res), CTimeUtils::Minute(t), "CTimeUtils::WithSecond().Minute", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Hour(res), CTimeUtils::Hour(t), "CTimeUtils::WithSecond().Hour", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithMinute()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int m = 0; m < 60; m++)
|
|
{
|
|
res = CTimeUtils::WithMinute(t, m);
|
|
|
|
if(!AssertEqual(CTimeUtils::Minute(res), m, "CTimeUtils::WithMinute()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Hour(res), CTimeUtils::Hour(t), "CTimeUtils::WithMinute().Hour", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Second(res), CTimeUtils::Second(t), "CTimeUtils::WithMinute().Second", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithHour()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int h = 0; h < 24; h++)
|
|
{
|
|
res = CTimeUtils::WithHour(t, h);
|
|
|
|
if(!AssertEqual(CTimeUtils::Hour(res), h, "CTimeUtils::WithHour()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Minute(res), CTimeUtils::Minute(t), "CTimeUtils::WithHour().Minute", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Second(res), CTimeUtils::Second(t), "CTimeUtils::WithHour().Second", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithDay()
|
|
//---------------------------------------------------------------
|
|
|
|
const int dim = CTimeUtils::DaysInMonth(CTimeUtils::Year(t), CTimeUtils::Month(t));
|
|
|
|
for(int d = 1; d <= dim; d++)
|
|
{
|
|
res = CTimeUtils::WithDay(t, d);
|
|
|
|
if(!AssertEqual(CTimeUtils::Day(res), d, "CTimeUtils::WithDay()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Month(res), CTimeUtils::Month(t), "CTimeUtils::WithDay().Month", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Year(res), CTimeUtils::Year(t), "CTimeUtils::WithDay().Year", t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithMonth()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int m = 1; m <= 12; m++)
|
|
{
|
|
res = CTimeUtils::WithMonth(t, m);
|
|
|
|
if(!AssertEqual(CTimeUtils::Month(res), m, "CTimeUtils::WithMonth()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Year(res), CTimeUtils::Year(t), "CTimeUtils::WithMonth().Year", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::Day(res) <= CTimeUtils::DaysInMonth(CTimeUtils::Year(res), CTimeUtils::Month(res)),
|
|
"CTimeUtils::WithMonth().Clamp",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithYear()
|
|
//---------------------------------------------------------------
|
|
|
|
for(int y = 1970; y <= 3000; y += 50)
|
|
{
|
|
res = CTimeUtils::WithYear(t, y);
|
|
|
|
if(!AssertEqual(CTimeUtils::Year(res), y, "CTimeUtils::WithYear()", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Month(res), CTimeUtils::Month(t), "CTimeUtils::WithYear().Month", t))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::Day(res) <= CTimeUtils::DaysInMonth(y, CTimeUtils::Month(res)),
|
|
"CTimeUtils::WithYear().Clamp",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithDayOfWeek()
|
|
//---------------------------------------------------------------
|
|
|
|
const datetime sow = CTimeUtils::StartOfWeek(t);
|
|
const datetime eow = CTimeUtils::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 = CTimeUtils::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 * 86400)
|
|
continue;
|
|
|
|
res = CTimeUtils::WithDayOfWeek(t, (ENUM_DAY_OF_WEEK)w);
|
|
|
|
if(!AssertEqual(CTimeUtils::DayOfWeek(res),
|
|
w,
|
|
"CTimeUtils::WithDayOfWeek()",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(res >= sow,
|
|
"CTimeUtils::WithDayOfWeek() >= StartOfWeek",
|
|
t))
|
|
return false;
|
|
|
|
if(!Assert(res <= eow,
|
|
"CTimeUtils::WithDayOfWeek() <= EndOfWeek",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::TimeOnly(res),
|
|
CTimeUtils::TimeOnly(t),
|
|
"CTimeUtils::WithDayOfWeek().Time",
|
|
t))
|
|
return false;
|
|
}
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithTime()
|
|
//---------------------------------------------------------------
|
|
|
|
res = CTimeUtils::WithTime(t, 3, 14, 15);
|
|
|
|
if(!AssertEqual(CTimeUtils::Hour(res), 3, "CTimeUtils::WithTime().Hour", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Minute(res), 14, "CTimeUtils::WithTime().Minute", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Second(res), 15, "CTimeUtils::WithTime().Second", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::DateOnly(res),
|
|
CTimeUtils::DateOnly(t),
|
|
"CTimeUtils::WithTime().Date",
|
|
t))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// CTimeUtils::WithDate()
|
|
//---------------------------------------------------------------
|
|
|
|
res = CTimeUtils::WithDate(t, 2024, 6, 15);
|
|
|
|
if(!AssertEqual(CTimeUtils::Year(res), 2024, "CTimeUtils::WithDate().Year", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Month(res), 6, "CTimeUtils::WithDate().Month", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::Day(res), 15, "CTimeUtils::WithDate().Day", t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::TimeOnly(res),
|
|
CTimeUtils::TimeOnly(t),
|
|
"CTimeUtils::WithDate().Time",
|
|
t))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Replacement property
|
|
//---------------------------------------------------------------
|
|
|
|
const int safeDay = MathMin(15, CTimeUtils::DaysInMonth(CTimeUtils::Year(t), CTimeUtils::Month(t)));
|
|
const int safeMonth = (CTimeUtils::Month(t) == 6 ? 7 : 6);
|
|
const int safeYear = (CTimeUtils::Year(t) == 2024 ? 2025 : 2024);
|
|
|
|
// CTimeUtils::WithSecond()
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::WithSecond(CTimeUtils::WithSecond(t, 11), 37),
|
|
CTimeUtils::WithSecond(t, 37),
|
|
"CTimeUtils::WithSecond() replacement",
|
|
t))
|
|
return false;
|
|
|
|
// CTimeUtils::WithMinute()
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::WithMinute(CTimeUtils::WithMinute(t, 11), 37),
|
|
CTimeUtils::WithMinute(t, 37),
|
|
"CTimeUtils::WithMinute() replacement",
|
|
t))
|
|
return false;
|
|
|
|
// CTimeUtils::WithHour()
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::WithHour(CTimeUtils::WithHour(t, 5), 17),
|
|
CTimeUtils::WithHour(t, 17),
|
|
"CTimeUtils::WithHour() replacement",
|
|
t))
|
|
return false;
|
|
|
|
// CTimeUtils::WithTime()
|
|
|
|
if(!AssertEqual(
|
|
CTimeUtils::WithTime(
|
|
CTimeUtils::WithTime(t, 1, 2, 3),
|
|
20, 21, 22),
|
|
CTimeUtils::WithTime(t, 20, 21, 22),
|
|
"CTimeUtils::WithTime() replacement",
|
|
t))
|
|
return false;
|
|
|
|
//---------------------------------------------------------------
|
|
// Idempotence
|
|
//---------------------------------------------------------------
|
|
|
|
if(!AssertEqual(CTimeUtils::WithSecond(t, CTimeUtils::Second(t)),
|
|
t,
|
|
"CTimeUtils::WithSecond() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::WithMinute(t, CTimeUtils::Minute(t)),
|
|
t,
|
|
"CTimeUtils::WithMinute() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::WithHour(t, CTimeUtils::Hour(t)),
|
|
t,
|
|
"CTimeUtils::WithHour() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::WithDay(t, CTimeUtils::Day(t)),
|
|
t,
|
|
"CTimeUtils::WithDay() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::WithMonth(t, CTimeUtils::Month(t)),
|
|
t,
|
|
"CTimeUtils::WithMonth() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::WithYear(t, CTimeUtils::Year(t)),
|
|
t,
|
|
"CTimeUtils::WithYear() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
if(!AssertEqual(CTimeUtils::WithDayOfWeek(t, (ENUM_DAY_OF_WEEK)CTimeUtils::DayOfWeek(t)),
|
|
t,
|
|
"CTimeUtils::WithDayOfWeek() idempotent",
|
|
t))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Verify CTimeUtils::IsInSession() |
|
|
//+------------------------------------------------------------------+
|
|
bool ValidateIsInSessionEdges()
|
|
{
|
|
//----------------------------------------------------
|
|
// 08:00 -> 17:00
|
|
//----------------------------------------------------
|
|
|
|
if(!Assert(CTimeUtils::IsInSession(CTimeUtils::DateFromString("2025.01.01 08:00"),
|
|
8,0,17,0),
|
|
"CTimeUtils::IsInSession() start inclusive",
|
|
D'2025.01.01'))
|
|
return false;
|
|
|
|
if(!Assert(!CTimeUtils::IsInSession(CTimeUtils::DateFromString("2025.01.01 17:00"),
|
|
8,0,17,0),
|
|
"CTimeUtils::IsInSession() end exclusive",
|
|
D'2025.01.01'))
|
|
return false;
|
|
|
|
//----------------------------------------------------
|
|
// Overnight
|
|
//----------------------------------------------------
|
|
|
|
if(!Assert(CTimeUtils::IsInSession(CTimeUtils::DateFromString("2025.01.01 23:30"),
|
|
22,0,6,0),
|
|
"CTimeUtils::IsInSession() overnight evening",
|
|
D'2025.01.01'))
|
|
return false;
|
|
|
|
if(!Assert(CTimeUtils::IsInSession(CTimeUtils::DateFromString("2025.01.02 05:59"),
|
|
22,0,6,0),
|
|
"CTimeUtils::IsInSession() overnight morning",
|
|
D'2025.01.02'))
|
|
return false;
|
|
|
|
if(!Assert(!CTimeUtils::IsInSession(CTimeUtils::DateFromString("2025.01.01 12:00"),
|
|
22,0,6,0),
|
|
"CTimeUtils::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 =
|
|
CTimeUtils::IsInSession(now,
|
|
sh, sm,
|
|
eh, em);
|
|
|
|
if(!AssertEqual(
|
|
expected,
|
|
CTimeUtils::IsInSession(sh, sm, eh, em),
|
|
"CTimeUtils::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,
|
|
CTimeUtils::IsInSession(s1, s2),
|
|
"CTimeUtils::IsInSession(string) overload",
|
|
now))
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool ValidateIsInSession(const datetime t)
|
|
{
|
|
const int current = CTimeUtils::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 =
|
|
CTimeUtils::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,
|
|
"CTimeUtils::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 : 9203 ms
|
|
============================================================
|
|
|
|
*/
|