-- A of date time object
import "Turbine";
--import "strict";
import "FrostyPlugins.Calendar.CalTools";
--------------------------------------------------------------------------
--- DateTime
--
-- A date + time object
-- Contains fields:
-- Year = the 4 digit year
-- Month = the month of the year (1 thru 12)
-- Day = the day of the month (1 thru 31)
-- Hour = the hour (0-23)
-- Minute = the minute (0-59)
-- utcOffset = the UTC timezone offset (-11 to +12, where 0 = UTC)
-- isDST = if Daylight Savings time is active or not
-- See: RFC-822/ISO 8601 standard timezone specification
DateTime = {
-- Ideally this object would simply be the current seconds since epoch UTC.
-- However, the epoch to local time routines are not available to LOTRO
-- so rather than implement them in LUA (non-trivial), instead we'll
-- capture the fields needed to allow sharing a date in a way that
-- supports localization to different timezones.
Year = 1970;
Month = 1;
Day = 1;
Hour = 0;
Minute = 0;
Second = 0;
DayOfWeek = 1;
DayOfYear = 1;
Offset = 0; -- +/- hh:mm - but expressed as minutes
isDST = false;
};
local expectedTypeMap = {
Year = "number";
Month = "number";
Day = "number";
Hour = "number";
Minute = "number";
Second = "number";
Offset = "number";
isDST = "boolean";
};
--Turbine.Shell.WriteLine("type: " .. type(DateTime.Year));
-- protect the metatable so it cannot be modified
-- DateTime.mt.__metatable = "not your business"
-- print(getmetatable(s1)) --> not your business
-- setmetatable(s1, {})
-- stdin:1: cannot change protected metatable
--------------------------------------------------------------------------
--- getmetatable - returns the metatable for the DateTime
-- Used for validating input arguments of type DateTime
-- @return a new object set to today
function DateTime:getmetatable()
return getmetatable(o, self); -- inherit operations from DateTime
end
--------------------------------------------------------------------------
--- new - Create a new datetime object - set to the epoch.
-- Some description, can be over several lines.
-- @return a new object set to today
function DateTime:new()
return DateTime:epoch();
end
--------------------------------------------------------------------------
--- new - Create a new datetime object - set to the epoch.
-- @return a new object set to the start of the epoch
function DateTime:epoch()
o = {}; -- create NEW object (NOT another reference to *same* object)
setmetatable(o, self); -- inherit operations from DateTime
self.__index = self;
return o;
end
--------------------------------------------------------------------------
--- now - Create a new datetime object - set to now.
-- @return a new object set to now
function DateTime:now()
--o = o or Turbine.Engine.GetDate();
--o = o or {}; -- create new object if one not provided
o = {}; -- create NEW object (NOT another reference to *same* object)
setmetatable(o, self); -- inherit operations from DateTime
self.__index = self;
-- initialize from new object.
-- - intentionally iterate over a DateTime to make sure
-- ONLY the EXACT fields from the DateTime are copied
-- and that none are missing
local now = Turbine.Engine.GetDate();
for k,v in pairs(DateTime) do
o[k] = now[k];
end
-- TODO - determine local utcOffset
o.Offset = 0; -- assume zulu time
return o;
end
--------------------------------------------------------------------------
--- clone - Returns a COPY/clone of the current existing datetime object
-- @return a new object copy of the current
function DateTime:clone()
o = DateTime:new(); -- create NEW object
-- copy values from other object to this object
for k,v in pairs(self) do
o[k] = v;
end
return o;
end
--------------------------------------------------------------------------
--- validate - Verify the DateTime is valid
-- @param level of caller in callstack to report on (default = 0)
-- @return 1 if is valid
function DateTime:validate(level)
level = level or 2; -- set default value to zero
-- verify data types
for k,v in pairs(expectedTypeMap) do
local actualType = type(self[k]);
local expectedType = expectedTypeMap[k];
if( expectedType ~= actualType ) then
error("DataObject." .. k .. " type expected to be " .. expectedType .. ", not " .. actualType, level );
end
end
-- verify values within range
if( self.Year < 1970 ) then
error( "Invalid Year [" .. self.Year .. "]", level);
end
if( (self.Month < 1) or (self.Month > 12) ) then
error( "Invalid Month [" .. self.Month .. "]", level);
end
if( (self.Day < 1) or (self.Day > 31) or
(self.Day > self:daysInMonth()) ) then
error( "Invalid Day [" .. self.Day .. "]", level);
end
if( (self.Hour < 0) or (self.Hour>=24) ) then
error( "Invalid Hour [" .. self.Hour .. "]", level);
end
if( (self.Minute < 0) or (self.Minute >= 60) ) then
error( "Invalid Minute [" .. self.Minute .. "]", level);
end
if( (self.Second < 0) or (self.Second >= 60) ) then
error( "Invalid Second [" .. self.Second .. "]", level);
end
if( (self.Offset < -11*60) or (self.Offset >= 12*60) ) then
error( "Invalid Offset [" .. self.Offset .. "]", level);
end
return 1;
end
--------------------------------------------------------------------------
--- parse - Create a DateObect from a string in format "yyyy-mm-dd hh:mm"
-- @param str the string to parse
-- @return a new object set to today
function DateTime:parse( str )
--Turbine.Shell.WriteLine( "DateTime:parse = [" .. str .. "]" );
-- validate input parameters
if( "string" ~= type(str) ) then
error("Parameter expected to be a string, not " .. type(str), 2 );
end
o = DateTime:new(); -- create NEW object
-- Parse the input string
local position = nil;
local positionEnd;
-- try to parse a time designated as zulu time (zero UTC offset)
position, positionEnd, o.Year, o.Month, o.Day, o.Hour, o.Minute, o.Second = string.find(
str, "(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)Z");
if( position ) then
--table.dump("o",o);
o.Offset = 0;
else
-- try to parse with utcOffset
local offsetS, offsetH, offsetM;
position, positionEnd, o.Year, o.Month, o.Day, o.Hour, o.Minute, o.Second, offsetS, offsetH, offsetM = string.find(
str, "(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)([+-])(%d%d):(%d%d)");
if( position ) then
--Turbine.Shell.WriteLine(
-- string.format("offsetS=[%s] offsetH=[%s] offsetM=[%s]",
-- offsetS, offsetH, offsetM) );
local sign = {
["\-"] = -1;
["\+"] = 1;
};
o.Offset = sign[offsetS] * (tonumber(offsetH) * 60 + tonumber(offsetM));
end
end
-- try to parse without utcOffset designation
if( not position ) then
position, positionEnd, o.Year, o.Month, o.Day, o.Hour, o.Minute, o.Second = string.find(
str, "(%d%d%d%d)-(%d%d)-(%d%d)T(%d%d):(%d%d):(%d%d)");
if( position ) then
o.Offset = 0;
end
end
--Turbine.Shell.WriteLine("position:" .. tostring(position) .. " positionEnd:" .. tostring(positionEnd) );
if( nil == position ) then
error(string.format("Parameter [%s] did not match expected format: %s",
str,
'yyyy-mm-ddThh:mm:ss[+-]hh:mm'),
2 );
end
-- Convert captured strings to numbers
for k,v in pairs(expectedTypeMap) do
local expectedType = expectedTypeMap[k];
if( "number" == expectedType ) then
o[k] = tonumber(o[k]);
elseif( "boolean" == expectedType ) then
stringtoboolean={ ["true"]=true, ["false"]=false };
o[k] = stringtoboolean[o[k]];
end
end
--o.isDST = ?; -- TODO, how to compute correct value for this?
-- validate!
o:validate(3);
--local status, err = pcall( DateTime.validate, self, 3);
--if( not status ) then
-- Turbine.Shell.WriteLine(string.format("validate[%s] - error = [%s]",self:tostring(),err));
-- return null
--end
--if( pcall( DateTime.validate, self, 3) ) then
-- Turbine.Shell.WriteLine("validate - INVALID DATE");
-- return nil;
--end
--function testValidate()
-- self:validate(3);
--end
--function myerrorhandler( err )
-- --print( "ERROR:", err )
-- Turbine.Shell.WriteLine("TRAPERROR: " .. err);
--end
--if( xpcall( testValidate, myerrorhandler) ) then
-- Turbine.Shell.WriteLine("validate - INVALID DATE");
-- return nil;
--end
return o;
end
--------------------------------------------------------------------------
--- set - apply a table of field values to the current datetime object
-- Can specify multiple fields such as "set({Hour=0,Minute=0})"
-- @param mods a table of modifications
-- @return the modified object
function DateTime:set( mods )
-- validate input parameters
if( "table" ~= type(mods) ) then
error("Parameter expected to be a table of modification, not " .. type(other), 2 );
end
-- iterate over mods provided and *copy* values to this object
for k,v in pairs(mods) do
--Turbine.Shell.WriteLine("set [" .. k .. "]=[" .. tostring(v) .. "]");
-- ensure keys of mods are valid
if( nil == DateTime[k] ) then
error( "Invalid key in modification [" .. k .. "]", 2);
end
if( "number" ~= type(mods[k]) ) then
error( "Modification value for key " .. k .. " expected number not " .. type(mods[k]), 2);
end
self[k] = v;
end
-- This is less than ideal, because it validates the changes *AFTER*
-- the object has already been changed.
-- But - since invalid changes are fatal... ignoring for now
-- validate!
self:validate(3);
-- return modified object, so that you can assign result to object
-- local d = DateTime:now():set({Hour=0,Minute=0});
return self;
end
--------------------------------------------------------------------------
--- add - adds the specified amount the current datetime object
-- Can specify multiple fields such as "set({Hour=30,Minute=90})"
-- Will carry over amounts into higher fields as needed
-- Will accept negative values
-- Adding months adds a varying amount depending on the days of
-- the month involved
-- Limitation: This simplistic algorithm does NOT adjust for leap-seconds
-- @param mods a table of fields to add
-- @return the modified object
function DateTime:add( mods )
-- validate input parameters
if( "table" ~= type(mods) ) then
error("Parameter expected to be a table of modification, not " .. type(other), 2 );
end
-- validate input mod table field
for k,v in pairs(mods) do
--Turbine.Shell.WriteLine("add [" .. k .. "]=[" .. tostring(v) .. "]");
-- ensure keys of mods are valid
if( nil == DateTime[k] ) then
error( "Invalid key in modification [" .. k .. "]", 2);
end
if( "number" ~= type(mods[k]) ) then
error( "Modification value for key " .. k .. " expected number not " .. type(mods[k]), 2);
end
end
-- Apply changes in this order to properly handle rollover
local orderToApply = {
"Offset"; -- Timezone offset
"Second";
"Minute";
"Hour";
"Year";
"Day";
"Month";
};
-- if other provided, then apply mods to this object
-- Note: accepts negative values as well
for i,f in ipairs(orderToApply) do
if( mods[f] ) then
local v = mods[f];
--Turbine.Shell.WriteLine(string.format(
-- "before [%s] add [%s]=[%s]",
-- self:tostring(),
-- f,
-- tostring(v)
-- ));
self[f] = self[f] + v; -- adjust specified field
--Turbine.Shell.WriteLine(string.format(
-- "after [%s]",
-- self:tostring()));
-- re-normalize field values due to rollover
-- adjust Hour from Minute as needed
local minAdjustment = math.floor( self.Second / 60 );
if( 0 ~= minAdjustment ) then
--Turbine.Shell.WriteLine("minAdjustment [" .. minAdjustment .. "]");
self.Minute = self.Minute + minAdjustment;
self.Second = self.Second % 60;
end
-- adjust Hour from Minute as needed
local hourAdjustment = math.floor( self.Minute / 60 );
if( 0 ~= hourAdjustment ) then
--Turbine.Shell.WriteLine("hourAdjustment [" .. hourAdjustment .. "]");
self.Hour = self.Hour + hourAdjustment;
self.Minute = self.Minute % 60;
end
-- adjust Day from Hour as needed
local dayAdjustment = math.floor( self.Hour / 24 );
if( 0 ~= dayAdjustment ) then
--Turbine.Shell.WriteLine("dayAdjustment [" .. dayAdjustment .. "]");
self.Day = self.Day + dayAdjustment;
self.Hour = self.Hour % 24;
end
-- adjusting years from months
local yearAdjustment = math.floor( (self.Month-1) / 12 ); -- adjust Day from Hour
--Turbine.Shell.WriteLine(string.format(
-- "self.Month [%d] yearAdjustment [%d]",
-- self.Month, yearAdjustment) );
if( 0 ~= yearAdjustment ) then
--Turbine.Shell.WriteLine("yearAdjustment [" .. yearAdjustment .. "]");
self.Year = self.Year + yearAdjustment;
self.Month = ((self.Month-1) % 12)+1;
end
-- adjusting Months from Days
-- - iterate over months as needed due to varying days of the month
local daysInMonth = self:daysInMonth(); -- days in current month
--Turbine.Shell.WriteLine("daysInMonth [" .. tostring(daysInMonth) .. "]");
while( self.Day > daysInMonth ) do
--Turbine.Shell.WriteLine( string.format("+ month=[%d] day=[%d]",self.Month,self.Day));
self.Day = self.Day - self:daysInMonth();
self.Month = self.Month + 1;
--Turbine.Shell.WriteLine(string.format(
-- "rollover to month=[%d] day=[%d]",
-- self.Month,self.Day));
if( self.Month > 12 ) then
self.Year = self.Year + 1;
--Turbine.Shell.WriteLine("rollover to year [" .. tostring(self.Year) .. "]");
self.Month = 1;
end
daysInMonth = self:daysInMonth(); -- days in current month
--Turbine.Shell.WriteLine("daysInMonth [" .. tostring(daysInMonth) .. "]");
end
while( self.Day < 1 ) do
--Turbine.Shell.WriteLine( string.format("- month=[%d] day=[%d]",self.Month,self.Day));
self.Month = self.Month - 1;
if( 0 == self.Month ) then
self.Year = self.Year - 1;
--Turbine.Shell.WriteLine("rollback to year [" .. tostring(self.Year) .. "]");
self.Month = 12;
end
--Turbine.Shell.WriteLine("rollback to month [" .. tostring(self.Month) .. "]");
--self.Month = ((self.Month-1-1) % 12)+1; -- complicated due to range 1 thru 12
local dayAdjustment = self:daysInMonth(); -- days in prior month
--Turbine.Shell.WriteLine("adding days [" .. tostring(dayAdjustment) .. "]");
self.Day = self.Day + dayAdjustment;
end
end
end
-- return modified object, so that you can assign result to object
-- local d = DateTime:now():set({Hour=0,Minute=0});
return self;
end
--------------------------------------------------------------------------
--- isLeapYear (Gregorian calendar)
-- determine if year is a leap year (assumes AD 4 is a leap year)
-- @return true if the specified year is a leap year, false otherwise
function DateTime:isLeapYear()
assert( type(self.Year) == "number",
"expected number for year, not " .. type(self.Year) );
local isLeapYear = (math.fmod(self.Year,4)==0 and math.fmod(self.Year,100) ~= 0) or math.fmod(self.Year,400)==0;
return isLeapYear;
end
--------------------------------------------------------------------------
--- daysInMonth - Returns the number of days in the specified month & year
-- (Gregorian calendar)
-- @eturn the number of days in the specified month & year
function DateTime:daysInMonth()
-- validate input parameters
assert( type(self.Month) == "number",
"Unexpected type for month: " .. type(self.Month));
assert( (self.Month >= 1) and (self.Month <= 12),
"Invalid Month [" .. self.Month .. "]" );
assert( type(self.Year) == "number",
"Unexpected type for Year: " .. type(self.Year));
-- Thirty days hath September, April, June, and November,
-- All the rest have thirty-one,
-- Except February, twenty-eight days clear,
-- And twenty-nine in each leap year.
local daysPerMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if ( (self.Month == 2) and self:isLeapYear()) then
return 29;
end
local days = daysPerMonth[ self.Month ];
--Turbine.Shell.WriteLine( "daysInMonth " ..
-- " Month=" .. tostring(self.Month) ..
-- " Year=" .. tostring(self.Year) ..
-- " retval=" .. tostring(days) );
return days;
end
--------------------------------------------------------------------------
--- dayOfWeek - Returns the day of the week as an integer
--
-- @eturn the days of the week as a number 1 (Sunday) to 7 (Saturday).
function DateTime:dayOfWeek()
self:validate(3);
local a = math.floor((14 - self.Month) / 12); -- anchor day
local y = self.Year - a;
local m = self.Month + 12*a - 2;
return math.fmod(math.floor(self.Day + y + math.floor(y/4) - math.floor(y/100) + math.floor(y/400) + (31*m)/12),7) + 1;
end
--------------------------------------------------------------------------
--- daysSince - Returns the number of days between the two dates
-- Limitation: ignores leap seconds
-- @return the number of days between the two dates
function DateTime:daysSinceEpoch()
local epoch = DateTime:epoch();
local days = 0;
-- iterate over years accumulating days per year
local year = epoch.Year;
while( year < self.Year ) do
if( CalTools:isLeapYear(year) ) then
days = days + 366;
else
days = days + 365;
end
year = year + 1;
end
-- iterate over months, accumulating days per month
local month = epoch.Month;
while( month < self.Month ) do
days = days + CalTools:daysInMonth(month, self.Year);
month = month+1;
end
-- add remaining days
days = days + (self.Day - epoch.Day);
return days;
end
--------------------------------------------------------------------------
--- daysSince - Returns the number of days between the two dates
-- @eturn the number of days between the two dates
function DateTime:DaysSince(start)
-- validate input parameters
if( getmetatable(start) ~= self.getmetatable() ) then
error("Parameter expected to be a DateTime not " .. type(other), 2 );
end
start:validate(3);
--if( not start:validate(3) ) then
-- error("Parameter expected to be a valid DateTime", 2 );
--end
local daysSinceEpochEnd = self:daysSinceEpoch();
local daysSinceEpochStart = start:daysSinceEpoch();
local days = daysSinceEpochEnd - daysSinceEpochStart;
return days;
end
--------------------------------------------------------------------------
--- MinutesSince - Returns the number of minutes between the two dates
-- Limitation: ignores leap seconds
-- @return the number of minutes between the two dates
function DateTime:MinutesSince(other)
local MINS_PER_HR = 60;
local MINS_PER_DAY = 1440;
local days = self:DaysSince(other);
local thisMinOfDay = self.Hour * 60 + self.Minute;
local otherMinOfDay = other.Hour * 60 + other.Minute;
local minutes = days * MINS_PER_DAY +
(thisMinOfDay - otherMinOfDay);
return minutes;
end
--------------------------------------------------------------------------
--- ymd - Returns the year-month-day
-- @return a string of corresponding year-month-day
function DateTime:ymd()
local str = string.format("%04d-%02d-%02d",
self.Year,
self.Month,
self.Day);
return str;
end
--------------------------------------------------------------------------
--- time - Returns the hour-minute
-- @return a string of corresponding hour-minute
function DateTime:time()
local str = string.format("%02d:%02d",
self.Hour,
self.Minute);
return str;
end
--------------------------------------------------------------------------
--- utcOffset - Returns the utcOffset
-- @return a string of corresponding utcOffset
function DateTime:utcOffset()
local str;
if( 0 == self.Offset ) then
str = "Z"; -- zulu time
else
local s = (self.Offset > 0) and "+" or "-"; -- ternary
local hh = math.floor( math.abs(self.Offset) / 60); -- number of hours
local mm = self.Offset % 60; -- mod (always positive)
str = string.format("%s%02d:%02d", s, hh, mm);
end
return str;
end
--------------------------------------------------------------------------
--- tostring - Returns a string representing the date object in ISO 8601
-- format, including the UTC offset
-- @return a string of form 'yyyy-mm-ddThh:dd:ss[+-]hh:mm
function DateTime:tostring()
--local str = string.format("%s %s%s",
-- self:ymd(),
-- self:time(),
-- self:utcOffset() );
local str = self:strftime("%Y-%m-%dT%H:%M:%S%z");
return str;
end
--------------------------------------------------------------------------
--- format - Returns a string representing the date object in
-- the requested format
-- @param a format string of the output that can contain directives
-- as seen with strftime()
-- @return a string of form 'yyyy-mm-dd hh:dd:ss[+-]hh:mm
function DateTime:strftime(fmt)
if( "string" ~= type(fmt) ) then
error("Parameter expected to be a string not " .. type(fmt), 2 );
end
local output = fmt; -- make working copy of string
-- %a Abbreviated name of the day of the week
-- %A Full name of the day of the week
-- %b Abbreviated month name
-- %h Equivalent to %b
output = string.gsub(output, '%%h', '%%b');
-- %B Full month name
-- %c Preferred date and time representation for the current locale
-- %D Equivalent to %m/%d/%y
output = string.gsub(output, '%%D', '%%m/%%d/%%y');
-- %F Equivalent to %Y-%m-%d (ISO 8601 date format)
output = string.gsub(output, '%%F', '%%Y-%%m-%%d');
-- %R The time in 24-hour notation (%H:%M)
output = string.gsub(output, '%%R', '%%H:%%M');
-- %r The time in a.m. or p.m. notation
output = string.gsub(output, '%%r', '%%I:%%M:%%S %%p');
-- %T The time in 24-hour notation (%H:%M:%S)
-- %C Century number (year/100) as a 2-digit integer
output = string.gsub(output, '%%C', string.sub(tostring(self.Year),1,2));
-- %y The year without a century (range 00 to 99)
output = string.gsub(output, '%%y', string.sub(tostring(self.Year),3,4));
-- %Y The year including the century
output = string.gsub(output, '%%Y', self.Year);
-- %m The month as a decimal number (01 to 12)
output = string.gsub(output, '%%m', string.format("%02d",self.Month));
-- %d Day of the month as a decimal number (01 thru 31)
output = string.gsub(output, '%%d', string.format("%02d",self.Day));
-- %e like %d, but leading zero replaced by space
output = string.gsub(output, '%%e', string.format("%d",self.Day));
-- %H The hour using a 24-hour clock (00 to 23)
output = string.gsub(output, '%%H', string.format("%02d",self.Hour));
-- %k Like %H but single digits preceded by blank (0 to 23)
output = string.gsub(output, '%%k', string.format("%d",self.Hour));
-- %I The hour using a 12-hour clock (01 to 12)
output = string.gsub(output, '%%I', string.format("%02d",self.Hour % 12));
-- %l Like %I but single digits preceded by a blank (1 to 12);
output = string.gsub(output, '%%l', string.format("%d",self.Hour % 12));
-- %M The minute as a decimal number (range 00 to 59)
output = string.gsub(output, '%%M', string.format("%02d",self.Minute));
-- %S The second as a decimal number (range 00 to 60)
output = string.gsub(output, '%%S', string.format("%02d",self.Second));
-- %p Either "AM" or "PM" according to the given time value
output = string.gsub(output, '%%p', string.format("%s", (self.Hour>12) and "PM" or "AM" ));
-- %P Like %p but in lowercase: "am" or "pm"
output = string.gsub(output, '%%P', string.format("%s", (self.Hour>12) and "pm" or "am" ));
-- %G ISO 8601 week-based year
-- %g Like %G, but without century (e.g. 2 digit year)
-- %O Modifier: use alternative numeric symbols
-- %s The number of seconds since the Epoch (1970-01-01 00:00:00Z)
-- %j The day of the year (001 to 366)
output = string.gsub(output, '%%j', string.format("%03d",self.DayOfYear));
-- %w The day of the week, range 0 to 6, Sunday being 0
output = string.gsub(output, '%%w', string.format("%d",self:dayOfWeek()-1));
-- %u The day of the week as a decimal, range 1 to 7 (Monday=1)
--output = string.gsub(output, '%%u', string.format("%d",self:dayOfWeek()));
-- %W The week number of the current year (00 to 53) first Monday as first day of week 1
-- %U The week number of the current year (00 to 53) first Sunday as first day of week 1
-- %V The ISO 8601 week number
-- %x The preferred date representation for the current locale
-- %X The preferred time representation for the current locale
-- %z The +hhmm or -hhmm numeric timezone
output = string.gsub(output, '%%z', self:utcOffset());
-- %Z The timezone name or abbreviation
-- %+ The date and time in date(1) format
-- %n A newline character
output = string.gsub(output, '%%n', "\n");
-- %t A tab character
output = string.gsub(output, '%%t', "\t");
-- %% A literal '%' character
output = string.gsub(output, '%%%%', '%%');
-- use: string.match(), and string.gsub()
--function expand (s)
-- s = string.gsub(s, "$(%w+)", function (n)
-- return _G[n]
-- end)
-- return s
--end
--name = "Lua"; status = "great"
--print(expand("$name is $status, isn't it?"))
-- --> Lua is great, isn't it?
--local output = fmt; -- start with input format string
--output = string.format("Hi %s buddy","there");
--Turbine.Shell.WriteLine("output: " .. output);
--s = string.sub(output,5,8);
--Turbine.Shell.WriteLine("s: " .. s);
----s = string.sub(output,5,8);
--s = string.gsub("I love tacos!", "tacos", "Roblox")
--Turbine.Shell.WriteLine("s: " .. s);
--Turbine.Shell.WriteLine( string.format("output [%s]",output));
local unimplementedFormat = string.match(output,'%%.');
if( unimplementedFormat ) then
assert(false,string.format("not implemented yet [%s]",unimplementedFormat));
end
return output;
end
-- commented out so we can use 'tostring()' on object to print address
--function DateTime:__xtostring()
-- local str = self:tostring();
-- return str;
--end
-- comparators
DateTime.__eq = function (a,b) -- equal
return( a:tostring() == b:tostring() );
end
DateTime.__lt = function (a,b) -- less-than
return( a:tostring() < b:tostring() );
end
DateTime.__le = function (a,b) -- less-than-or-equal
return( a:tostring() <= b:tostring() );
end
--------------------------------------------------------------------------
--------------------------------------------------------------------------
-- Unit Tests
function DateTimeUnitTest()
Turbine.Shell.WriteLine("\n\n>>> DateTimeUnitTest: START ...");
Turbine.Shell.WriteLine("LUA version: " .. _VERSION);
-- Verify new()
local d1 = DateTime:new();
assert(1970 == d1.Year, "wrong year " .. tostring(d1.Year) );
assert(1 == d1.Month, "wrong month");
assert(1 == d1.Day, "wrong day");
assert(0 == d1.Hour, "wrong hour");
assert(0 == d1.Minute, "wrong minute");
assert(0 == d1.Offset, "wrong offset:" .. tostring(d1.Offset) );
-- Verify tostring()
Turbine.Shell.WriteLine("d1: " .. d1:tostring());
local expected = "1970-01-01T00:00:00Z";
local actual = d1:tostring();
assert(expected == actual, string.format(
"wrong datestring!\n exp[%s]\n act[%s]",
expected,
actual));
-- Verify today()
local now = Turbine.Engine.GetDate();
local d2 = DateTime:now();
Turbine.Shell.WriteLine("d2: " .. d2:tostring());
assert(now.Year == d2.Year, "wrong year");
assert(now.Month == d2.Month, "wrong month");
assert(now.Day == d2.Day, "wrong day");
assert(now.Hour == d2.Hour, "wrong hour");
assert(now.Minute == d2.Minute, "wrong minute");
assert(0 == d2.Offset, "wrong offset [" ..
tostring(0) .. "] != [" .. tostring(d2.Offset) .. "]");
-- Verify getmetatable() - proves all object have same metatable - so are of same type
--local mt = tostring(getmetatable(DateTime)); -- will be nil
local d1mt = tostring(d1:getmetatable());
local d2mt = tostring(d2:getmetatable());
--Turbine.Shell.WriteLine("mt: " .. mt);
Turbine.Shell.WriteLine("d1mt: " .. d1mt);
Turbine.Shell.WriteLine("d2mt: " .. d2mt);
--assert( mt == d1mt );
assert( d1mt == d2mt, "metatables do not match" );
-- an error handler to print trapped error messages
function myerrorhandler( err )
--print( "ERROR:", err )
Turbine.Shell.WriteLine("TRAPERROR: " .. err);
end
-- verify clone()
Turbine.Shell.WriteLine("test: clone");
local d3 = d2:clone();
Turbine.Shell.WriteLine("d3: " .. d3:tostring());
for k,v in pairs(DateTime) do
assert(d2[k] == d3[k], "wrong value for [" .. k .. "]" );
end
local d3addr = tostring(d3);
local d2addr = tostring(d2);
Turbine.Shell.WriteLine("d3addr: " .. d3addr);
Turbine.Shell.WriteLine("d2addr: " .. d2addr);
assert(d3addr ~= d2addr, "should be copies, not same object" );
-- verify daysInMonth()
Turbine.Shell.WriteLine("test: daysInMonth");
local expectedDaysPerMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
for i,v in ipairs(expectedDaysPerMonth) do
local d = DateTime:new():set({Month=i});
local days = d:daysInMonth();
Turbine.Shell.WriteLine(string.format("daysInMonth[%d]: %s => [%s]",
i,
d:ymd(),
tostring(days)
) );
assert( v == days, "mismatch\n [" .. v .. "] != [" .. days .. "]");
end
local d = DateTime:new():set({Year=2020, Month=2}); -- leap year
local days = d:daysInMonth();
assert( 29 == days, "mismatch\n [" .. 29 .. "] != [" .. days .. "]");
local badDayInMonthTests = {
--function() local d = DateTime:now(); d:daysInMonth(); end, -- comment to varify test catches error
function() local d = DateTime:now(); d.Month = "barf"; d:daysInMonth(); end, -- invalid month
function() local d = DateTime:now(); d.Month = 0; d:daysInMonth(); end, -- invalid month
function() local d = DateTime:now(); d.Month = 13; d:daysInMonth(); end, -- invalid month
function() local d = DateTime:now(); d.Year = "barf"; d:daysInMonth(); end, -- invalid year
};
for i,f in ipairs(badDayInMonthTests) do
Turbine.Shell.WriteLine("badDayInMonthTests[" .. i .. "]");
assert( not xpcall(f,myerrorhandler ), "badValidateTests[" .. i .. "] should have failed");
end
-- verify validate()
Turbine.Shell.WriteLine("test: validate");
Turbine.Shell.WriteLine("d1: " .. d1:tostring());
d1:validate(1);
Turbine.Shell.WriteLine("d2: " .. d2:tostring());
d2:validate();
local badValidateTests = {
--function() d5.Year = 1980; d5:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Year = -1; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Year = 1969; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Month = -1; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Month = 0; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Month = 13; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Day = -1; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Day = 0; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Day = 32; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Hour = -1; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Hour = 25; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Minute = -1; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Minute = 61; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Second = -1; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Second = 61; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Offset = -11*60-1; d:validate(); end, -- invalid argument
function() local d = DateTime:now(); d.Offset = 12*60+1; d:validate(); end, -- invalid argument
};
for i,f in ipairs(badValidateTests) do
Turbine.Shell.WriteLine("badValidateTests[" .. i .. "]");
--assert( not pcall(f), "badValidateTests[" .. i .. "] should have failed");
assert( not xpcall(f,myerrorhandler ), "badValidateTests[" .. i .. "] should have failed");
end
-- verify utcOffset()
Turbine.Shell.WriteLine("test: utcOffset");
assert(0 == d3.Offset, "wrong offset:" .. tostring(d3.Offset) );
assert("Z" == d3:utcOffset(), "wrong offset:" .. tostring(d3:utcOffset()) );
-- verify set()
Turbine.Shell.WriteLine("test: set");
local d3str = d3:tostring(); -- save for later compare
local d4 = d3:clone();
d4:set({Hour=0,Minute=0});
Turbine.Shell.WriteLine("d4: " .. d4:tostring());
for k,v in pairs(DateTime) do
if( ("Hour" == k) or ("Minute" == k) ) then
assert(0 == d4[k], "wrong value for [" .. k .. "]" );
else
assert(d3[k] == d4[k], "wrong value for [" .. k .. "]");
end
end
assert(d3:tostring() == d3str, "original object should not have changed" );
Turbine.Shell.WriteLine("d3: " .. d3:tostring());
--d4:set({Hour=1,Minute=68}); -- success!
local d5 = d3:clone():set({Hour=0,Minute=0,Second=0});
assert(0 == d5.Hour, "wrong Hour:" .. tostring(d5.Hour) );
assert(0 == d5.Minute, "wrong Minute:" .. tostring(d5.Minute) );
assert(0 == d5.Minute, "wrong Second:" .. tostring(d5.Second) );
Turbine.Shell.WriteLine("d5: " .. d5:tostring());
local d6 = DateTime:now():set({Offset=-5*60});
for k,v in pairs(DateTime) do
if( "Offset" == k ) then
assert(-5*60 == d6[k], "wrong value for [" .. k .. "]" );
else
-- d2 was set to now() above
assert(d2[k] == d6[k], "wrong value for [" .. k .. "]");
end
end
Turbine.Shell.WriteLine("d6: " .. d6:tostring());
local badSetTests = {
--function() local d = DateTime:now(); end, -- uncomment to test if detecting no error
function() local d = DateTime:now():set({bad="stuff"}); end,
function() local d = DateTime:now():set({Year = "x"}); end,
function() local d = DateTime:now():set({Year = -1}); end,
function() local d = DateTime:now():set({Year = 1969}); end,
function() local d = DateTime:now():set({Month = "x"}); end,
function() local d = DateTime:now():set({Month = -1}); end,
function() local d = DateTime:now():set({Month = 0}); end,
function() local d = DateTime:now():set({Month = 13}); end,
function() local d = DateTime:now():set({Day = "x"}); end,
function() local d = DateTime:now():set({Day = -1}); end,
function() local d = DateTime:now():set({Day = 0}); end,
function() local d = DateTime:now():set({Day = 32}); end,
function() local d = DateTime:now():set({Hour = "x"}); end,
function() local d = DateTime:now():set({Hour = -1}); end,
function() local d = DateTime:now():set({Hour = 25}); end,
function() local d = DateTime:now():set({Minute = "x"}); end,
function() local d = DateTime:now():set({Minute = -1}); end,
function() local d = DateTime:now():set({Minute = 61}); end,
function() local d = DateTime:now():set({Second = "x"}); end,
function() local d = DateTime:now():set({Second = -1}); end,
function() local d = DateTime:now():set({Second = 61}); end,
function() local d = DateTime:now():set({Offset = "Z"}); end,
function() local d = DateTime:now():set({Offset = -11*60-1}); end,
function() local d = DateTime:now():set({Offset = 12*60+1}); end,
};
for i,f in ipairs(badSetTests) do
Turbine.Shell.WriteLine("badSetTests[" .. i .. "]");
assert( not xpcall(f,myerrorhandler ), "badSetTests[" .. i .. "] should have failed");
end
-- verify parse()
Turbine.Shell.WriteLine("test: parse");
local d6 = DateTime:parse("1981-02-03T13:45:23Z");
Turbine.Shell.WriteLine("d6: " .. d6:tostring());
assert(1981 == d6.Year, "wrong year");
assert(2 == d6.Month, "wrong month");
assert(3 == d6.Day, "wrong day");
assert(13 == d6.Hour, "wrong hour");
assert(45 == d6.Minute, "wrong minute");
assert(23 == d6.Second, "wrong second");
assert(0 == d6.Offset, "wrong offset");
-- input dates
local validDateStrings = {
"1981-02-03T13:45:10Z",
"2020-02-29T12:00:20Z", -- a leap year
"2019-01-30T13:01:30-05:00", -- EST/EDT
}
for i,v in ipairs(validDateStrings) do
Turbine.Shell.WriteLine("validDateStrings[" .. i .. "] = [" .. v .. "]");
local d = DateTime:parse(v);
local s = d:tostring();
assert( v == d:tostring(), "mismatch\n [" .. v .. "] != [" .. s .. "]");
end
-- assumes zulu time
local s = "2022-02-28T12:01:02";
local d7 = DateTime:parse(s);
Turbine.Shell.WriteLine("d7: " .. d7:tostring());
assert( s .. "Z" == d7:tostring(), "mismatch\n [" .. s .. "] != [" .. d7:tostring() .. "]");
-- negative tests
local invalidDateStrings = {
--"2022-02-28 12:00Z", -- is ok, uncomment to verify can trap no error
nil,
3,
{bad=value},
"barf", -- not valid date
"1971-03-02 02:03:1", -- invalid second
"1971-03-02 02:6:01", -- invalid minute
"1971-03-02 2:06:01", -- invalid hour
"1971-03-2 02:06:01", -- invalid day
"1971-3-02 02:06:01", -- invalid month
"71-03-02 02:06:01", -- invalid year
"1971-03-02 02:03:60", -- invalid second
"1971-03-02 02:60:01", -- invalid minute
"1971-03-02 02:60:01X", -- invalid zone
"1971-03-02 24:00:01", -- invalid hour
"1971-03-32 02:06:01", -- invalid day
"1971-04-31 02:06:01", -- invalid day
"1971-13-02 02:06:01", -- invalid month
"1968-13-32 25:68:01", -- before 1970
"2022-02-29 12:00:01", -- NOT a leap year
}
function testbadparse(s)
local d = DateTime:parse(s);
end
for i,v in ipairs(invalidDateStrings) do
s = invalidDateStrings[i];
Turbine.Shell.WriteLine("invalidDateStrings[" .. i .. "] = [" .. tostring(s) .. "]");
assert( not pcall(testbadparse,s), "invalidDateStrings[" .. i .. "] should have failed");
-- can't pass arguments to function via xpcall until LUA 5.2
--assert( not xpcall(f,myerrorhandler ), "badSetTests[" .. i .. "] should have failed");
end
-- verify dayOfWeek() 1=Sun => 7=Sat
Turbine.Shell.WriteLine("test: dayOfWeek");
local validDayOfWeekTests = {
-- input expected
{"1970-01-01T00:00:00Z"; 5}; -- Thu
{"2021-12-05T00:00:00Z"; 1}; -- Sun
{"2021-12-06T00:00:00Z"; 2}; -- Mon
{"2021-12-07T00:00:00Z"; 3}; -- Tue
{"2021-12-08T00:00:00Z"; 4}; -- Wed
{"2021-12-09T00:00:00Z"; 5}; -- Thu
{"2021-12-10T00:00:00Z"; 6}; -- Fri
{"2021-12-11T00:00:00Z"; 7}; -- Sat
{"2022-02-26T22:04:00Z"; 7}; -- Saturday
{"2016-02-29T00:00:00Z"; 2}; -- Mon : leap year
{"2022-02-28T00:00:00Z"; 2}; -- Mon
}
for idx,t in ipairs(validDayOfWeekTests) do
local input = t[1];
local expected = t[2];
local d = DateTime:parse(input);
local actual = d:dayOfWeek();
Turbine.Shell.WriteLine( string.format(
"idx [%d] input[%s] exp[%s] act[%s]", idx, input, expected, actual ) );
assert( expected == actual, "wrong value [" .. actual .. "]" );
end
-- verify add()
Turbine.Shell.WriteLine("test: add");
local validAddTests = {
-- input mod expected
{"1981-02-03T13:45:00Z"; {Minute=5}; "1981-02-03T13:50:00Z"};
{"1981-02-03T13:45:00Z"; {Minute=-5}; "1981-02-03T13:40:00Z"};
{"1981-02-03T13:45:00Z"; {Minute=30}; "1981-02-03T14:15:00Z"}; -- rollover +hours
{"1981-02-03T13:15:00Z"; {Minute=-30}; "1981-02-03T12:45:00Z"}; -- rollover -hours
{"1981-02-01T00:02:00Z"; {Minute=-3}; "1981-01-31T23:59:00Z"}; -- rollover -day
{"1981-02-03T13:15:00Z"; {Hour=1}; "1981-02-03T14:15:00Z"};
{"1981-02-03T13:15:00Z"; {Hour=-1}; "1981-02-03T12:15:00Z"};
{"1981-02-03T23:15:00Z"; {Hour=1}; "1981-02-04T00:15:00Z"}; -- rollover +days
{"1981-02-03T01:15:00Z"; {Hour=-2}; "1981-02-02T23:15:00Z"}; -- rollover -days
{"2022-02-03T10:45:00Z"; {Day=1}; "2022-02-04T10:45:00Z"};
{"2022-02-03T11:45:00Z"; {Day=-2}; "2022-02-01T11:45:00Z"};
{"2022-02-03T12:45:00Z"; {Day=35}; "2022-03-10T12:45:00Z"}; -- rollover +month !!
{"2022-02-03T13:45:00Z"; {Day=-5}; "2022-01-29T13:45:00Z"}; -- rollover -month
{"2020-02-28T14:45:00Z"; {Day=1}; "2020-02-29T14:45:00Z"}; -- leap year
{"2020-02-29T15:45:00Z"; {Day=2}; "2020-03-02T15:45:00Z"}; -- leap year rollover +month
{"2020-03-02T16:45:00Z"; {Day=-3}; "2020-02-28T16:45:00Z"}; -- leap year rollover -month
{"1981-02-03T10:45:00Z"; {Month=1}; "1981-03-03T10:45:00Z"};
{"1981-02-03T11:45:00Z"; {Month=-1}; "1981-01-03T11:45:00Z"};
{"2022-11-01T13:45:00Z"; {Month=1}; "2022-12-01T13:45:00Z"};
{"2022-12-02T13:45:00Z"; {Month=1}; "2023-01-02T13:45:00Z"}; -- rollover +year
{"1981-02-03T13:45:00Z"; {Month=13}; "1982-03-03T13:45:00Z"}; -- rollover +year
{"1981-02-03T13:35:00Z"; {Month=-13}; "1980-01-03T13:35:00Z"}; -- rollover -year
{"1981-01-03T13:05:00Z"; {Month=-1}; "1980-12-03T13:05:00Z"}; -- rollover -year
{"1981-02-03T13:45:00Z"; {Year=2}; "1983-02-03T13:45:00Z"};
{"1981-02-03T13:45:00Z"; {Year=-2}; "1979-02-03T13:45:00Z"};
-- Compound updates
{"2000-02-03T13:45:00Z"; {Minute=1,Hour=1,Day=1,Month=1,Year=1}; "2001-03-04T14:46:00Z"};
{"1981-02-03T23:15:00Z"; {Minute=1*60}; "1981-02-04T00:15:00Z"}; -- rollover +days
{"1981-02-03T13:45:00Z"; {Day=-35}; "1980-12-30T13:45:00Z"}; -- wrap -year
{"1981-11-30T13:45:00Z"; {Day=40}; "1982-01-09T13:45:00Z"}; -- wrap +year
{"1981-02-03T13:45:00Z"; {Day=367}; "1982-02-05T13:45:00Z"}; -- wrap +year
{"1981-02-03T13:45:00Z"; {Day=-367}; "1980-02-02T13:45:00Z"}; -- wrap -year
}
for idx,t in ipairs(validAddTests) do
local input = t[1];
local mod = t[2];
local exp = t[3];
--Turbine.Shell.WriteLine( string.format(
-- "idx [%d] input[%s] exp[%s]", idx, input, exp) );
local d = DateTime:parse(input);
d:add(mod);
Turbine.Shell.WriteLine( string.format(
"idx [%d] input[%s] exp[%s] act[%s]", idx, input, exp, d:tostring() ) );
assert( exp == d:tostring(), "wrong value [" .. d:tostring() .. "]" );
end
-- verify daysSinceEpoch()
Turbine.Shell.WriteLine("test: daysSinceEpoch");
local daysSinceEpochTests = {
-- input expected
{"1970-01-01T00:00:00Z"; 0};
{"1970-01-02T00:00:00Z"; 1};
{"1971-01-01T00:00:00Z"; 365};
{"1980-01-01T00:00:00Z"; 3652};
{"2000-01-01T00:00:00Z"; 10957};
{"2022-01-01T00:00:00Z"; 18993};
{"2022-02-25T00:00:00Z"; 19048};
{"2022-02-25T20:00:00Z"; 19048};
}
for idx,t in ipairs(daysSinceEpochTests) do
local input = t[1];
local expected = t[2];
local d = DateTime:parse(input);
local actual = d:daysSinceEpoch();
Turbine.Shell.WriteLine( string.format(
"idx [%d] input[%s] exp[%d] act[%d]", idx, d:tostring(), expected, actual ) );
assert( expected == actual, "wrong value [" .. actual .. "]" );
end
-- verify DaysSince()
Turbine.Shell.WriteLine("test: daysSince");
local daysSinceTests = {
-- start end expected
{"1970-01-01T00:00:00Z"; "1970-01-01T00:00:00Z", 0};
{"1970-01-01T00:00:00Z"; "1970-01-02T00:00:00Z", 1};
{"1970-01-01T00:00:00Z"; "1970-03-01T00:00:00Z", 59};
{"1970-01-01T00:00:00Z"; "1971-01-01T00:00:00Z", 365};
{"1980-01-01T00:00:00Z"; "2000-01-01T00:00:00Z", 7305};
{"2022-02-01T00:00:00Z"; "2022-02-01T00:00:00Z", 0};
{"2022-02-01T00:00:00Z"; "2022-02-22T00:00:00Z", 21};
{"2022-02-01T00:00:00Z"; "2022-02-25T20:08:00Z", 24};
{"2021-06-25T00:00:00Z"; "2015-05-15T00:00:00Z", -2233};
{"2021-02-23T00:00:00Z"; "2021-02-23T23:59:00Z", 0};
}
for idx,t in ipairs(daysSinceTests) do
local startDate = t[1];
local endDate = t[2];
local expected = t[3];
local startDateTime = DateTime:parse(startDate);
local endDateTime = DateTime:parse(endDate);
local actual = endDateTime:DaysSince(startDateTime);
Turbine.Shell.WriteLine( string.format(
"idx [%d] start[%s] end[%s] exp[%d] act[%d]",
idx, startDate, endDate, expected, actual ) );
assert( expected == actual, "wrong value [" .. actual .. "]" );
end
-- verify comparators
Turbine.Shell.WriteLine("test: comparators");
local validComparatorTests = {
-- A B op{lt,lte,gt,gte,eq};
-- vary A
{"1981-02-03T13:45:00Z"; "1981-02-03T13:50:00Z"; "<"};
{"1981-02-03T13:55:00Z"; "1981-02-03T13:50:00Z"; ">"};
{"1981-02-03T13:50:00Z"; "1981-02-03T13:50:00Z"; "=="};
-- vary B
{"1981-02-03T13:50:00Z"; "1981-02-03T13:55:00Z"; "<"};
{"1981-02-03T13:50:00Z"; "1981-02-03T13:45:00Z"; ">"};
{"1981-02-03T13:50:00Z"; "1981-02-03T13:50:00Z"; "=="};
-- TODO: UTC conversion
--{"1981-02-03 13:45Z"; "1981-02-03 6:50+5:00"; "<"};
--{"1981-02-03 13:55Z"; "1981-02-03 6:50+5:00"; ">"};
--{"1981-02-03 13:50Z"; "1981-02-03 6:50+5:00"; "=="};
--{"1981-02-03 13:50Z"; "1981-02-03 6:55+5:00"; "<"};
--{"1981-02-03 13:50Z"; "1981-02-03 6:45+5:00"; ">"};
--{"1981-02-03 13:50Z"; "1981-02-03 6:50+5:00"; "=="};
}
for idx,t in ipairs(validComparatorTests) do
local a = t[1];
local b = t[2];
local op = t[3];
Turbine.Shell.WriteLine( string.format(
" idx [%d] a[%s] b[%s] op[%s]", idx, a, b, op) );
local dtA = DateTime:parse(a);
local dtB = DateTime:parse(b);
if( "op" == "<" ) then
assert( dtA < dtB, "failed <" );
assert( not( dtA > dtB), "failed >" );
assert( not( dtA <= dtB), "failed <=" );
assert( not( dtA >= dtB), "failed >=" );
assert( not( dtA == dtB), "failed ==" );
elseif( "op" == ">" ) then
assert( not(dtA < dtB), "failed <" );
assert( ( dtA > dtB), "failed >" );
assert( not( dtA <= dtB), "failed <=" );
assert( not( dtA >= dtB), "failed >=" );
assert( not( dtA == dtB), "failed ==" );
elseif( "op" == "==" ) then
assert( not(dtA < dtB), "failed <" );
assert( not( dtA > dtB), "failed >" );
assert( ( dtA <= dtB), "failed <=" );
assert( ( dtA >= dtB), "failed >=" );
assert( ( dtA == dtB), "failed ==" );
end
end
-- verify MinutesSince
Turbine.Shell.WriteLine("test: MinutesSince");
local validMinutesSinceTests = {
-- A B expected;
{'2022-03-06T13:50:00Z'; '2022-03-06T13:45:00Z'; 5},
{'2022-03-06T13:45:00Z'; '2022-03-06T13:50:00Z'; -5},
{'2022-03-06T14:45:00Z'; '2022-03-06T13:50:00Z'; 55},
{'2022-03-06T12:50:00Z'; '2022-03-06T13:45:00Z'; -55},
{'2022-03-07T14:45:00Z'; '2022-03-06T13:50:00Z'; 1495},
{'2022-03-05T12:50:00Z'; '2022-03-06T13:45:00Z'; -1495},
{'2022-03-01T15:00:00Z'; '2022-02-27T16:45:00Z'; 2775},
{'2022-02-27T15:00:00Z'; '2022-03-01T16:45:00Z'; -2985},
{'2020-03-01T15:00:00Z'; '2020-02-27T16:45:00Z'; 4215}, -- leap year
{'2020-02-27T15:00:00Z'; '2020-03-01T16:45:00Z'; -4425}, -- leap year
};
for idx,t in ipairs(validMinutesSinceTests) do
local thisDateTime = DateTime:parse(t[1]);
local otherDateTime = DateTime:parse(t[2]);
local expected = t[3];
local actual = thisDateTime:MinutesSince(otherDateTime);
Turbine.Shell.WriteLine( string.format(
"idx [%d] this[%s] other[%s] exp[%d] act[%d]",
idx,
thisDateTime:tostring(),
otherDateTime:tostring(),
expected,
actual ) );
assert( expected == actual, "wrong value [" .. actual .. "]" );
end
-- verify format
Turbine.Shell.WriteLine("test: strftime");
local validFormatTests = {
-- date fmt expected;
{'2022-03-06T13:50:00Z'; '%Y-%m-%d %H:%M'; '2022-03-06 13:50'},
{'2022-03-06T08:50:00Z'; '%Y-%m-%e %k:%M'; '2022-03-6 8:50'},
{'2022-03-06T08:50:00Z'; '%m/%d/%y'; '03/06/22'},
{'2022-03-06T08:50:00Z'; '%D'; '03/06/22'},
{'2022-03-06T08:50:00Z'; '%F'; '2022-03-06'},
{'2022-03-06T13:50:10Z'; '%H:%M:%S'; '13:50:10'},
{'2022-03-06T13:50:10Z'; '%k:%M:%S'; '13:50:10'},
{'2022-03-06T03:50:10Z'; '%k:%M:%S'; '3:50:10'},
{'2022-03-06T03:50:10Z'; '%I:%M:%S %p'; '03:50:10 AM'},
{'2022-03-06T13:50:10Z'; '%I:%M:%S %p'; '01:50:10 PM'},
{'2022-03-06T03:50:10Z'; '%l:%M:%S %P'; '3:50:10 am'},
{'2022-03-06T13:50:10Z'; '%l:%M:%S %P'; '1:50:10 pm'},
{'2022-03-06T13:50:00Z'; '%R'; '13:50'},
{'2022-03-06T13:50:00Z'; '%C'; '20'}, -- century
{'2022-03-04T13:50:00Z'; '%w'; '5'}, -- day of week
{'2022-03-06T13:50:00Z'; '%w'; '0'}, -- day of week (Sunday=0)
--{'2022-03-06 08:50Z'; '%Y-%m-%e %k:%M %%'; '2022-03-6 8:50 %%'},
};
for idx,t in ipairs(validFormatTests) do
local dt = DateTime:parse(t[1]);
local actual = dt:strftime(t[2]);
local expected = t[3];
Turbine.Shell.WriteLine( string.format(
"idx [%d] this[%s] fmt[%s] exp[%s] act[%s]",
idx,
t[1],
t[2],
expected,
actual ) );
assert( expected == actual, "wrong value [" .. actual .. "]" );
end
-- zone
--assert(false,"ABORT");
-- if you get this far - it passed!
Turbine.Shell.WriteLine("\n>>> DateTimeUnitTest: -- PASS!\n.\n\n");
assert(false, "ABORT");
end
-- Uncomment below to unit test
--DateTimeUnitTest();