Skip to content

Instantly share code, notes, and snippets.

@Blockzez
Created June 15, 2020 21:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Blockzez/cb48d8071a6684e37dd4a9a2c01a9c8c to your computer and use it in GitHub Desktop.
Save Blockzez/cb48d8071a6684e37dd4a9a2c01a9c8c to your computer and use it in GitHub Desktop.
A DateTime module for Roblox Lua
--[[
Version 2.0.0a
This is intended for Roblox ModuleScripts.
It works on vanilla Lua, but there are far superior implementations you should use instead.
BSD 2-Clause Licence
Copyright ©, 2020 - Blockzez (devforum.roblox.com/u/Blockzez and github.com/Blockzez)
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]--
-- International (not International Core) Module Support, just place the path of the module here (don't run require).
local intl;
local divmod = function(left, right) return math.floor(left / right), math.floor(left % right) end;
local dt = { };
local dt_proxy = { };
--[=[ DateTime & TimeSpan formatter ]=]--
--[=[
For DateTime, see CLDR https://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
Only the following pattern are valid:
y, M, d, a, h, H, J, j, m, s, Z, X, x, S
B and b aren't supported because day period data aren't supported.
Z are supported but z, v and V aren't because it's impossible to determine timezone without location
Attempting to input everything else will throw an error. If you want the literal character use "'"
If you're looking for fully fledged feature, see International and International Core.
For TimeSpan:
d - Day
h - Hour
m - Minute
s - Second
S (or f) - Millisecond
' - Literal ('' for literal ')
]=]--
local month_names = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
local quarter_names = { 'Q1', 'Q2', 'Q3', 'Q4' };
local day_names = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
local full_month_names = { "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" };
local full_day_names = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday" };
local full_quarter_names = { "1st quarter", "2nd quarter", "3rd quarter", "4th quarter" };
local function offset_to_string(offset, sep, include_sign)
local o_h, o_m = divmod(offset, 60);
if o_h == 0 and o_m == 0 and sep:match('^iso') then
return 'Z';
end;
return (o_h >= 0 and "+%02d%s%02d" or "%02d%s%02d"):format(o_h, sep:match('^iso') and sep:sub(4) or sep, o_m);
end;
local formats = { };
local function zero_pad(value, n)
return ('%0' .. n .. 'd'):format(value);
end;
formats.d =
setmetatable({
y = function(self, data, utc, n)
local yr = (utc and self.UtcYear or self.Year);
if n == 2 then
yr = yr % 100;
end;
return zero_pad(yr, n);
end;
Q = function(self, data, utc, n)
local q = math.ceil((utc and self.UtcMonth or self.Month) / 3);
if n < 3 then
return zero_pad(q, n);
end;
return (n == 4 and data.WideQuarterNames or data.QuarterNames)[q];
end;
M = function(self, data, utc, n)
local mo = (utc and self.UtcMonth or self.Month);
if n < 3 then
return zero_pad(mo, n);
end;
return (n == 4 and data.WideMonthNames or data.MonthNames)[mo];
end;
d = function(self, data, utc, n)
local mo = (utc and self.UtcDay or self.Day);
return zero_pad(mo, n);
end;
E = function(self, data, utc, n)
return (n == 4 and data.WideDayNames or data.DayNames)[utc and self.UtcDayOfWeek or self.DayOfWeek];
end;
e = function(self, data, utc, n)
local e = (utc and self.UtcDayOfWeek or self.DayOfWeek);
if n < 3 then
return zero_pad(e, n);
end;
return (n == 4 and data.WideDayNames or data.DayNames)[e];
end;
},
{
__index = function(self, index)
error("Format inputted was not in a correct format", 3);
end;
}
);
formats.dt =
setmetatable({
a = function(self, data, utc, n)
local hr = (utc and self.UtcHour or self.Hour);
return zero_pad(hr < 12 and data.AMName or data.PMName, n);
end;
h = function(self, data, utc, n)
return zero_pad((((utc and self.UtcHour or self.Hour) - 1) % 12) + 1, n);
end;
H = function(self, data, utc, n)
return zero_pad(utc and self.UtcHour or self.Hour, n);
end;
K = function(self, data, utc, n)
return zero_pad(((utc and self.UtcHour or self.Hour) % 12), n);
end;
k = function(self, data, utc, n)
return zero_pad((((utc and self.UtcHour or self.Hour) - 1) % 24) + 1, n);
end;
m = function(self, data, utc, n)
return zero_pad(utc and self.UtcMinute or self.Minute, n);
end;
s = function(self, data, utc, n)
return zero_pad(utc and self.UtcSecond or self.Second, n);
end;
S = function(self, data, utc, n)
return zero_pad(utc and self.UtcMillisecond or self.Millisecond, n);
end;
f = function(self, data, utc, n)
return zero_pad(utc and self.UtcMillisecond or self.Millisecond, n);
end;
Z = function(self, data, utc, n)
local offset = self.TimezoneOffset;
return (n == 4 and 'GMT' or '') .. offset_to_string(offset, n < 3 and '' or n == 4 and ':' or 'iso:', true);
end;
O = function(self, data, utc, n)
local offset = self.TimezoneOffset;
return 'GMT' .. (n == 1 and offset_to_string(offset, ':', true) or offset_to_string(offset, ':', true):gsub('^([+-])0', '%1'):gsub(':00$', ''));
end;
X = function(self, data, utc, n)
local offset = self.TimezoneOffset;
return (n == 1 and offset_to_string(offset, 'iso' .. ((n == 3 or n == 5) and ':' or ''), true):gsub('00$', ''));
end;
x = function(self, data, utc, n)
local offset = self.TimezoneOffset;
return (n == 1 and offset_to_string(offset, ((n == 3 or n == 5) and ':' or ''), true):gsub('00$', ''));
end;
},
{
__index = formats.d;
}
);
formats.ts =
setmetatable({
h = function(self, data, n)
return zero_pad(data.ModHours and (self.Hours % 24) or self.Hours, n);
end;
m = function(self, data, n)
return zero_pad(data.ModMinutes and (self.Minutes % 24) or self.Minutes, n);
end;
s = function(self, data, n)
return zero_pad(data.ModSeconds and (self.Seconds % 24) or self.Seconds, n);
end;
f = function(self, data, n)
return zero_pad(data.ModMilliseconds and (self.Milliseconds % 24) or self.Milliseconds, n);
end;
S = function(self, data, n)
return zero_pad(data.ModMilliseconds and (self.Milliseconds % 24) or self.Milliseconds, n);
end;
},
{
__index = function(self, index)
error("Format inputted was not in a correct format", 3);
end;
}
);
local max_char =
{
G = 5,
Q = 5, q = 5,
M = 5, L = 5,
w = 2, W = 1,
d = 2, D = 3, F = 1,
E = 6, e = 6, c = { true, false, true, true, true, true },
a = 1,
h = 2, H = 2, K = 2, k = 2,
m = 2,
s = 2,
z = 4, Z = 5, O = { true, false, false, true }, v = { true, false, false, true },
V = 4, x = 5, X = 5
};
local pattern_char_order = "GyYuUQqMLlwWdDFgEecabBChHKkjJmsSAzZOvVXx";
local ts_pattern_char_order = "dhmsfS";
local function format(t, data, utc, self, ptn)
if data == nil then
data =
utc == nil and {
ModHours = true;
ModMinutes = true;
ModSeconds = true;
ModMilliseconds = true;
} or {
QuarterNames = quarter_names;
WideQuarterNames = full_quarter_names;
MonthNames = month_names;
WideMonthNames = full_month_names;
DayNames = day_names;
WideDayNames = full_day_names;
AMName = 'AM';
PMName = 'PM';
};
elseif type(data) ~= "table" then
error("Data inputted must be a table", 2);
else
data =
utc == nil and {
ModHours = data.ModHours ~= false;
ModMinutes = data.ModMinutes ~= false;
ModSeconds = data.ModSeconds ~= false;
ModMilliseconds = data.ModMilliseconds ~= false;
} or {
QuarterNames = data.QuarterNames or quarter_names;
WideQuarterNames = data.WideQuarterNames or full_quarter_names;
MonthNames = data.MonthNames or month_names;
WideMonthNames = data.WideMonthNames or full_month_names;
DayNames = data.DayNames or day_names;
WideDayNames = data.WideDayNames or full_day_names;
AMName = data.AMName or 'AM';
PMName = data.PMName or 'PM';
};
end;
local ret = '';
local i0 = 0;
while i0 do
local i1 = ptn:find(utc == nil and ('[' .. ts_pattern_char_order .. '%%]') or ('[' .. pattern_char_order .. "']"), i0);
if i1 then
if ptn:sub(i0, i1 - 1) ~= '' then
ret = ret .. ptn:sub(i0, i1 - 1);
end;
local chr = ptn:sub(i1, i1);
if chr == "'" then
i0 = ptn:find("'", i1 + 1);
if not i0 then
error("Format inputted was not in a correct format", 2);
end;
if i0 == i1 + 1 then
ret = ret .. "'";
else
ret = ret .. ptn:sub(i1 + 1, i0 - 1);
end;
i0 = i0 + 1;
else
local i2 = ((utc == nil and chr == '%') and ptn:find('[' .. ts_pattern_char_order .. ']') or ptn:find('[^' .. chr .. ']+', i1 + 1)) or (#ptn + 1);
if type(max_char[chr]) == "table" then
if not max_char[chr][i2 - i1] then
error("Format inputted was not in a correct format", 2);
end;
elseif not i2 then
error("Format inputted was not in a correct format", 2);
else
if (i2 - i1) > (max_char[chr] or math.huge) then
error("Format inputted was not in a correct format", 2);
end;
end;
ret = ret .. utc == nil and t[chr](self, data, i2 - i1) or t[chr](self, data, utc, i2 - i1);
i0 = i2;
end;
else
if ptn:sub(i0) ~= '' then
ret = ret .. ptn:sub(i0);
end;
i0 = nil;
end;
end;
return ret;
end;
--[=[ Private functions ]=]--
local min_val = -8640000000000000;
local max_val = 8640000000000000;
local month_in_days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
local months_to_days = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
local function is_leap(y)
return (y % 4 == 0) and ((y % 100 ~= 0) or (y % 400 == 0));
end;
local function from_date(y, m, d, md)
local mdr = (months_to_days[m] + ((m > 2 and is_leap(y)) and 1 or 0)) + (d - 1);
if md then
return mdr;
end;
y = y - 1;
return (y * 365) + math.floor(y / 4) - math.floor(y / 100) + math.floor(y / 400) + mdr;
end;
local days_in_year = 365;
local days_in_4_years = from_date(5, 1, 1); -- Leap, 1'461
local days_in_100_years = from_date(101, 1, 1); -- Not leap, 36'524
local days_in_400_years = from_date(401, 1, 1); -- Leap, 146'097
local days_in_1970_years = from_date(1970, 1, 1);
local function to_date(n)
local y400, y100, y4, y;
y400, n = divmod(n, days_in_400_years);
y100, n = divmod(n, days_in_100_years);
y4, n = divmod(n, days_in_4_years);
y, n = divmod(n, days_in_year);
local t = (y400 * 400) + (y100 * 100) + (y4 * 4) + y;
if (y == 4) or (y100 == 4) then
-- The extra day
return t, 12, 31, 366;
end;
-- No months have 32 days
local m = bit32.rshift(n, 5) + 1;
if n >= months_to_days[m + 1] + (m > 1 and is_leap(t + 1) and 1 or 0) then
m = m + 1;
end;
return t + 1, m, (n - (months_to_days[m] + (m > 1 and is_leap(t + 1) and 1 or 0))) + 1, n + 1;
end;
--[=[ Methods ]=]--
local d_methods = setmetatable(
{ },
{
__newindex = function(self, index, func)
rawset(self, index, function(self, ...)
if not (dt_proxy[self] and dt_proxy[self].type ~= 0) then
error("Expected ':' not '.' calling member function " .. index, 2);
end;
return func(self, ...);
end);
end;
}
);
local check;
if typeof(intl) == "Instance" then
check = require(intl:WaitForChild("argChecker"));
intl = require(intl);
function d_methods:ToLocaleDateString(locale, options)
return intl.ToLocaleString(self, locale, check.check_options(options, 'dt/date'));
end;
d_methods.ToLocaleString = d_methods.ToLocaleDateString;
else
intl = nil;
end;
local dt_methods = setmetatable(
{ },
{
__index = d_methods;
__newindex = function(self, index, func)
rawset(self, index, function(self, ...)
if not (dt_proxy[self] and dt_proxy[self].type == 1) then
error("Expected ':' not '.' calling member function " .. index, 2);
end;
return func(self, ...);
end);
end;
}
);
function dt_methods:ToISOString(...)
if select('#', ...) > 0 then
if (...) ~= 'T' and (...) ~= ' ' then
if type((...)) ~= "string" and type((...)) ~= "number" then
error("The separator must be 'T' or ' ', not " .. typeof((...)), 2);
else
error("The separator must be 'T' or ' ', not " .. (...), 2);
end;
end
end;
return ("%04d-%02d-%02d%s%02d:%02d:%02d.%03d%s"):format(self.Year, self.Month, self.Day, (...) or ' ',
self.Hour, self.Minute, self.Second, self.Millisecond, offset_to_string(self.TimezoneOffset, ':', true));
end;
function dt_methods:ToISOUTCString(...)
if select('#', ...) > 0 then
if (...) ~= 'T' and (...) ~= ' ' then
if type((...)) ~= "string" and type((...)) ~= "number" then
error("The separator must be 'T' or ' ', not " .. typeof((...)), 2);
else
error("The separator must be 'T' or ' ', not " .. (...), 2);
end;
end
end;
return ("%04d-%02d-%02d%s%02d:%02d:%02d.%03dZ"):format(self.UtcYear, self.UtcMonth, self.UtcDay, (...) or ' ',
self.UtcHour, self.UtcMinute, self.UtcSecond, self.UtcMillisecond);
end;
function dt_methods:ToRFCString()
return ("%s, %02d %s %04d, %02d:%02d:%02d.%03d %s"):format(day_names[self.DayOfWeek], self.Day,
month_names[self.Month], self.Year, self.Hour, self.Minute, self.Second, self.Millisecond, offset_to_string(self.TimezoneOffset, '', true));
end;
function dt_methods:ToRFCUTCString()
return ("%s, %02d %s %04d, %02d:%02d:%02d.%03d +0000"):format(day_names[self.UtcDayOfWeek], self.UtcDay,
month_names[self.UtcMonth], self.UtcYear, self.UtcHour, self.UtcMinute, self.UtcSecond, self.UtcMillisecond);
end;
function dt_methods:ToCString()
return ("%s %s %2d %02d:%02d:%02d %04d"):format(day_names[self.DayOfWeek], month_names[self.Month],
self.Day, self.Hour, self.Minute, self.Second, self.Year);
end;
function dt_methods:Format(ptn, data)
return format(formats.dt, data, false, self, ptn);
end;
function dt_methods:UtcFormat(ptn, data)
return format(formats.dt, data, true, self, ptn);
end;
function dt_methods:ToOsDate()
return os.date('!*t', self.Epoch);
end;
local function comp_d(safe, left, right)
if (not dt_proxy[left]) or (not dt_proxy[right]) then
if safe then
return nil;
end;
error("attempt to compare " .. (typeof(left) .. ' and ' .. typeof(right)):gsub(' and ' .. typeof(left) .. '$', ''), 2);
end;
local left_tick = dt_proxy[left].ticks;
local right_tick = dt_proxy[right].ticks;
if dt_proxy[left].type ~= dt_proxy[right].type then
if dt_proxy[left].type == 2 then
left_tick = left_tick * 86400;
else
right_tick = right_tick * 86400;
end;
if dt_proxy[left].type == 0 or dt_proxy[right].type == 0 then
if safe then
return nil;
end;
error("attempt to compare userdata", 2);
end;
end;
return ((left_tick == right_tick) and 0) or ((left_tick < right_tick) and -1 or 1);
end;
local function comp_ts(safe, left, right)
if (not dt_proxy[left]) or (not dt_proxy[right]) then
if safe then
return nil;
end;
error("attempt to compare " .. (typeof(left) .. ' and ' .. typeof(right)):gsub(' and ' .. typeof(left) .. '$', ''), 2);
end;
if dt_proxy[left].type ~= 0 or dt_proxy[right].type ~= 0 then
if safe then
return nil;
end;
error("attempt to compare userdata", 2);
end;
local left_tick = dt_proxy[left].ticks;
local right_tick = dt_proxy[right].ticks;
return ((left_tick == right_tick) and 0) or ((left_tick < right_tick) and -1 or 1);
end;
function dt_methods:Compare(...)
if select('#', ...) == 0 then
error("missing argument #2", 2);
end;
return comp_d(false, self, (...));
end;
function dt_methods:AddDays(...)
if select('#', ...) == 0 then
error("bad argument #2 (number expected, got no value)");
elseif type((...)) ~= "number" then
error("bad arugment #2 (number expected, got " .. typeof((...)) .. ')');
end;
return self + ((...) * 86400);
end;
function dt_methods:AddMonths(...)
if select('#', ...) == 0 then
error("bad argument #2 (number expected, got no value)");
elseif type((...)) ~= "number" then
error("bad arugment #2 (number expected, got " .. typeof((...)) .. ')');
end;
return dt.new(self.Year, self.Month + (...), self.Day, self.Hour, self.Minute, self.Second, self.Millisecond);
end;
function dt_methods:AddYears(...)
if select('#', ...) == 0 then
error("bad argument #2 (number expected, got no value)");
elseif type((...)) ~= "number" then
error("bad arugment #2 (number expected, got " .. typeof((...)) .. ')');
end;
return self:AddMonths((...) * 12);
end;
if intl then
d_methods.ToLocaleString = intl.ToLocaleString;
function d_methods:ToLocaleTimeString(locale, options)
return intl.ToLocaleString(self, locale, check.check_options(options, 'dt/time'));
end;
end;
local ts_methods = setmetatable(
{ },
{
__newindex = function(self, index, func)
rawset(self, index, function(self, ...)
if not (dt_proxy[self] and dt_proxy[self].type == 0) then
error("Expected ':' not '.' calling member function " .. index, 2);
end;
return func(self, ...);
end);
end;
}
);
function ts_methods:Abs()
return rawnew(math.abs(self.TotalMilliseconds), 0);
end;
function ts_methods:Compare(...)
if select('#', ...) == 0 then
error("missing argument #2", 2);
end;
return comp_ts(false, self, (...));
end;
--[=[ Metamethods ]=]--
local dt_access_value =
{
Year = 1, DayOfYear = 2, Month = 3, DayOfWeek = 4, Day = 5, TimeOfDay = 6,
Hour = 7, Minute = 8, Second = 9, Millisecond = 10,
};
local ts_access_value =
{
Days = 1, Hours = 2, Minutes = 3, Seconds = 4, Milliseconds = 5,
TotalDays = 6, TotalHours = 7, TotalMinutes = 8, TotalSeconds = 9, TotalMilliseconds = 10,
TotalIntHours = 11, TotalIntMinutes = 12, TotalIntSeconds = 13,
};
local function index_dt(self, index)
if dt_methods[index] then
return dt_methods[index];
elseif index == "Epoch" then
return dt_proxy[self].ticks - days_in_1970_years;
elseif index == "Ticks" then
return dt_proxy[self].ticks;
elseif index == "TimezoneOffset" then
return dt_proxy[self].access.offset;
elseif dt_access_value[index] then
return dt_proxy[self].access.lo[dt_access_value[index]];
elseif type(index) == "string" and index:sub(1, 3) == "Utc" then
local prop = index:sub(4);
if dt_access_value[prop] then
return dt_proxy[self].access.utc[dt_access_value[prop]];
end;
end;
if type(index) ~= "string" and type(index) ~= "number" then
error("invalid argument #2 (string expected, got " .. typeof(index) .. ')', 2);
else
error(tostring(index) .. " is not a valid member of DateTime", 2);
end;
end;
local function index_ts(self, index)
if ts_methods[index] then
return ts_methods[index];
elseif ts_access_value[index] then
return dt_proxy[self].access[ts_access_value[index]];
end;
if type(index) ~= "string" and type(index) ~= "number" then
error("invalid argument #2 (string expected, got " .. typeof(index) .. ')', 2);
else
error(tostring(index) .. " is not a valid member of TimeSpan", 2);
end;
end;
local function newindex(self, index, value)
error(index .. " cannot be assigned to", 2);
end;
local function add(left, right)
local date, span;
if type(left) == "number" or (dt_proxy[left] and dt_proxy[left].type == 0) then
span = left;
date = right;
elseif type(right) == "number" or (dt_proxy[right] and dt_proxy[right].type == 0) then
span = right;
date = left;
end;
if (not span) or (not dt_proxy[date]) then
error("attempt to perform arithmetic (add) on " .. (typeof(left) .. ' and ' .. typeof(right)):gsub(' and ' .. typeof(left) .. '$', ''), 2);
end;
return rawnew(dt_proxy[date].ticks + (dt_proxy[span] and math.floor(dt_proxy[span].ticks / (dt_proxy[date].type == 2 and 86400 or 1)) or span), dt_proxy[date].type);
end;
local function sub(left, right)
local comb = (type(left) == "number" and 'ts' or (dt_proxy[left] and (dt_proxy[left].type == 0 and 'ts' or (dt_proxy[left].type == 1 and 'dt' or 'd')) or 'zz'))
.. '-' ..
(type(right) == "number" and 'ts' or (dt_proxy[right] and (dt_proxy[right].type == 0 and 'ts' or (dt_proxy[right].type == 1 and 'dt' or 'd')) or 'zz'));
if comb ~= 'dt-dt' and comb ~= 'dt-ts' and comb ~= 'ts-ts' and comb ~= 'd-d' and comb ~= 'd-ts' then
error("attempt to perform arithmetic (sub) on " .. (typeof(left) .. ' and ' .. typeof(right)):gsub(' and ' .. typeof(left) .. '$', ''), 2);
end;
return rawnew(dt_proxy[left].ticks - (dt_proxy[right] and math.floor(dt_proxy[right].ticks / (comb == 'd-ts' and 86400 or 1)) or right), (comb == 'dt-ts' or comb == 'd-ts') and 1 or 0);
end;
local function mul_ts(left, right)
if (not ((dt_proxy[left] and dt_proxy[left].type == 0) or type(left) == "number")) or (not ((dt_proxy[right] and dt_proxy[right].type == 0) or type(right) == "number")) then
error("attempt to perform arithmetic (mul) on " .. (typeof(left) .. ' and ' .. typeof(right)):gsub(' and ' .. typeof(left) .. '$', ''), 2);
end;
return rawnew((dt_proxy[left] and dt_proxy[left].ticks or left) * (dt_proxy[right] and dt_proxy[right].ticks or right), 0);
end;
local function div_ts(left, right)
if (not ((dt_proxy[left] and dt_proxy[left].type == 0) or type(left) == "number")) or (not ((dt_proxy[right] and dt_proxy[right].type == 0) or type(right) == "number")) then
error("attempt to perform arithmetic (div) on " .. (typeof(left) .. ' and ' .. typeof(right)):gsub(' and ' .. typeof(left) .. '$', ''), 2);
end;
return rawnew(math.floor((dt_proxy[left] and dt_proxy[left].ticks or left) / (dt_proxy[right] and dt_proxy[right].ticks or right)), 0);
end;
local function mod_ts(left, right)
if (not ((dt_proxy[left] and dt_proxy[left].type == 0) or type(left) == "number")) or (not ((dt_proxy[right] and dt_proxy[right].type == 0) or type(right) == "number")) then
error("attempt to perform arithmetic (mod) on " .. (typeof(left) .. ' and ' .. typeof(right)):gsub(' and ' .. typeof(left) .. '$', ''), 2);
end;
return rawnew((dt_proxy[left] and dt_proxy[left].ticks or left) % (dt_proxy[right] and dt_proxy[right].ticks or right), 0);
end;
local function unm_ts(self)
return rawnew(self.TotalMilliseconds * -1, 0);
end;
local function eq_d(left, right)
return comp_d(true, left, right) == 0;
end;
local function lt_d(left, right)
return comp_d(false, left, right) < 0;
end;
local function le_d(left, right)
return comp_d(false, left, right) <= 0;
end;
local function eq_ts(left, right)
return comp_ts(true, left, right) == 0;
end;
local function lt_ts(left, right)
return comp_ts(false, left, right) < 0;
end;
local function le_ts(left, right)
return comp_ts(false, left, right) <= 0;
end;
--[=[ Private function continuted ]=]--
local function parse_date(self)
-- Check ISO 8601
local y, M, d, h, m, s, f, o = self:match("(%d%d%d%d%d*)%-(%d%d)%-(%d%d)[T%s](%d%d):(%d%d):(%d%d).?(%d?%d?%d?)([%+%-Z]%d?%d?:?%d?%d?)");
local match = y and (#f == 3 or #f == 0) and (o:match("[%+%-]%d%d:?%d%d") or o == 'Z');
local o_h, o_m;
if match and o ~= '' then
o_h, o_m = o:match("([%+%-]%d%d):?(%d%d)");
if o_h:sub(1, 1) == '-' then
o_m = '-' .. o_m;
end;
end;
if not match then
-- Check RFC 2822
local E;
E, d, M, y, h, m, s, o = self:match("([A-Z][a-z][a-z]),?%s(%d%d)%s([A-Z][a-z][a-z]),?%s(%d%d):(%d%d):(%d%d)%s([%+%-]%d%d%d%d)");
M = table.find(month_names, M);
match = table.find(day_names, E) and M;
if match then
o_h, o_m = o:match("([%+%-]%d%d)(%d%d)");
if o_h:sub(1, 1) == '-' then
o_m = '-' .. o_m;
end;
end;
end;
if match then
return tonumber(y), tonumber(M) or 1, tonumber(d) or 1,
(tonumber(h) or 0) - (o_h or 0), (tonumber(m) or 0) - (o_m or 0),
tonumber(s) or 0, tonumber(f) or 0;
end;
local token = { };
for t in self:gsub("(%D+)", " %1 "):gmatch("[^%p%s]+") do
table.insert(token, (t:sub(0, 2) ~= '00' and tonumber(t)) or t);
end;
if #token < 2 or #token > 7 or #token == 4 then
return nil;
end;
local pm;
local yp, mp, dp, hp, np, sp, fp;
for i, v in ipairs(token) do
if type(v) == "string" then
v = v:sub(1, 1):upper() .. v:sub(2):lower();
if table.find(day_names, v) or table.find(full_day_names, v) then
continue;
elseif (table.find(month_names, v) or table.find(full_month_names, v)) and not mp then
mp = i;
elseif v == "Am" or v == "Pm" then
pm = v == "Pm";
elseif tonumber(v) then
if yp then
return nil;
end;
-- 4 digit year
yp = i;
else
return nil;
end;
else
if v % 1 ~= 0 or v < 0 then
return nil;
elseif v >= 1000 and v < 100000 then
if yp then
return nil;
end;
yp = i;
end;
end;
end;
local precded_by_time = (yp == #token or yp == #token - 2);
if #token > 3 and precded_by_time then
if mp then
if mp < 3 then
dp = (mp == 1) and 2 or ((yp == 1) and 3 or 1);
else
dp = (mp == #token - 2) and #token - 1 or ((yp == #token) and #token - 2 or #token);
end;
else
mp, dp = #token - 1, (yp == #token) and #token - 2 or #token;
end;
local tp = (mp < 3) and #token or #token - 2;
if #token == 5 then
np, hp = tp - 1, tp - 2;
elseif #token == 6 then
sp, np, hp = tp - 1, tp - 2, tp - 3;
else
fp, sp, np, hp = tp - 1, tp - 2, tp - 3, tp - 4;
end;
elseif mp then
dp = (mp == 1) and 2 or ((yp == 1) and 3 or 1);
else
mp, dp = 2, (yp == 1) and 3 or 1;
end;
local month = token[mp or 2];
local hour = token[hp or 4];
if pm ~= nil then
if hour > 12 or hour < 0 then
return nil;
end;
hour = (hour % 12) + (pm and 12 or 0);
end;
return tonumber(token[yp or 1]), table.find(month_names, month) or table.find(full_month_names, month) or month or 1, token[dp or 3], hour, token[np or 5], token[sp or 6], token[fp or 7];
end;
local function getoffset()
-- I'm sure by 10'000 AD they'll figure it out
return math.floor((os.time(os.date('*t')) - os.time(os.date('!*t'))) / 60);
end;
function rawnew(ticks, ttype, access)
if not access then
if ttype == 0 then
local f, s, m, h, d;
local d = math.floor(ticks);
d, f = divmod(d, 1000);
d, s = divmod(d, 60);
d, m = divmod(d, 60);
d, h = divmod(d, 24);
access =
{
d, h, m, s, f,
ticks / 86400000, ticks / 3600000, ticks / 60000, ticks / 1000, ticks,
math.floor(ticks / 3600000), math.floor(ticks / 60000), math.floor(ticks / 1000)
};
elseif ttype == 1 then
local f, s, m, h, M, D, y;
local d = math.floor(ticks);
local time_of_day = d % 86400000;
d, f = divmod(d, 1000);
d, s = divmod(d, 60);
d, m = divmod(d, 60);
d, h = divmod(d, 24);
local E = (d % 7) + 1;
y, M, d, D = to_date(d, 2);
local utc_access = { y, D, M, E, d, time_of_day, h, m, s, f };
local offset = getoffset();
d = math.floor(ticks) + (offset * 60000);
time_of_day = d % 86400000;
d, f = divmod(d, 1000);
d, s = divmod(d, 60);
d, m = divmod(d, 60);
d, h = divmod(d, 24);
E = (d % 7) + 1;
y, M, d, D = to_date(d, 2);
access = { lo = { y, D, M, E, d, time_of_day, h, m, s, f }, utf = utc_access, offset = offset };
end;
end;
-- Return nil if it's out of the range
if ticks < (min_val - days_in_1970_years) or ticks > (max_val - days_in_1970_years) then
return nil;
end;
local proxy = newproxy(true);
dt_proxy[proxy] = { type = ttype, ticks = ticks, access = access };
local proxy_mt = getmetatable(proxy);
proxy_mt.__index = index_dt;
proxy_mt.__newindex = newindex;
proxy_mt.__add = add;
proxy_mt.__sub = sub;
if ttype == 0 then
proxy_mt.__mul = mul_ts;
proxy_mt.__div = div_ts;
proxy_mt.__mod = mod_ts;
proxy_mt.__unm = unm_ts;
end;
proxy_mt.__le = ttype == 0 and le_ts or le_d;
proxy_mt.__lt = ttype == 0 and lt_ts or lt_d;
proxy_mt.__eq = ttype == 0 and eq_ts or eq_d;
proxy_mt.__tostring = dt_methods.ToRFCString;
proxy_mt.__metatable = "The metatable is locked";
return proxy;
end;
--[=[ DateTime ]=]--
function dt.new(...)
local arglen = select('#', ...);
local args, ret = { ... }, { };
if arglen == 0 then
-- Void not nil
return dt.Now();
elseif arglen == 1 then
-- Void not nil
if type(args[1]) == 'string' then
return dt.new(parse_date(args[1]));
elseif type(args[1]) == 'number' then
if args[1] < min_val or args[1] > max_val then
return nil;
end;
return rawnew(args[1] + (days_in_1970_years * 86400000), 1);
elseif type(args[1]) == 'table' then
if type(args[1].Year or args[1].year) == 'table' then
return nil;
end;
return dt.new(
args[1].Year or args[1].year,
args[1].Month or args[1].month,
args[1].Day or args[1].day,
args[1].Hour or args[1].hour,
args[1].Minute or args[1].min,
args[1].Second or args[1].sec,
args[1].Millisecond
);
elseif dt_proxy[args[1]] then
if dt_proxy[args[1]].type == 1 then
-- re-creating the object
return rawnew(dt_proxy[args[1]].ticks, 1, dt_proxy[args[1]].access);
elseif dt_proxy[args[1]].type == 2 then
return dt.new(args[1].Year, args[1].Month, args[1].Day);
else
return dt.new(args[1].TotalMilliseconds);
end;
end;
end;
for index = 1, 7 do
local value = args[index];
if index <= arglen then
value = tonumber(value);
if not value then
return nil;
end;
ret[index] = math.floor(value);
else
if index == 1 then
return nil;
end;
ret[index] = (index < 4) and 1 or 0;
end;
end;
local y, M, d, h, m, s, f = unpack(ret);
local y_offset, M_offset = divmod(M - 1, 12);
-- Fix the date if any values go over or below the range.
y, M = y + y_offset, M_offset + 1;
local days = from_date(y, M, 1, false) + (d - 1);
local seconds = (h * 3600) + (m * 60) + s;
local ticks = (days * 86400000) + (seconds * 1000) + f;
local offset = getoffset();
local utc_access = { y, from_date(y, M, d, true), M, days % 7, d, seconds, h, m, s, f };
-- Convert to local time
local d = ticks + (offset * 60000);
local time_of_day = d % 86400;
local D;
d, f = divmod(d, 1000);
d, s = divmod(d, 60);
d, m = divmod(d, 60);
d, h = divmod(d, 24);
local E = (d % 7) + 1;
y, M, d, D = to_date(d, 2);
return rawnew(ticks, 1, { lo = { y, D, M, E, d, time_of_day, h, m, s, f }, utc = utc_access, offset = offset });
end;
-- https://devforum.roblox.com/t/is-there-any-way-to-get-the-exact-millisecond-of-now-in-utc-from-a-server/526492
-- Mix both os.time and tick for milliseconds, may not be consistent but it's better than nothing
-- It'll get the timezone offset automatically
function dt.Now()
return dt.new((os.time() * 1000) + math.floor((tick() * 1000) % 1000));
end;
--[=[ TimeSpan ]=]--
local ts = { };
function ts.new(...)
local d, h, m, s, f = 0, 0, 0, 0, 0;
local arglen = select('#', ...);
if arglen == 1 then
f = ...;
elseif arglen == 2 then
s, f = ...;
elseif arglen == 3 then
h, m, s = ...;
elseif arglen == 4 then
d, h, m, s = ...;
else
d, h, m, s, f = ...;
end;
d, h, m, s, f = tonumber(d), tonumber(h), tonumber(m), tonumber(s), tonumber(f);
if not (d and h and m and s and f) then
return nil;
end;
return rawnew((d * 86400000) + (h * 3600000) + (m * 60000) + (s * 1000) + f, 0);
end;
function ts.FromSeconds(v)
return ts.new(0, 0, v);
end;
function ts.FromMinutes(v)
return ts.new(0, v, 0);
end;
function ts.FromHours(v)
return ts.new(v, 0, 0);
end;
function ts.FromDays(v)
return ts.new(v, 0, 0, 0);
end;
return { DateTime = dt, TimeSpan = ts };
Copy link

ghost commented Apr 10, 2022

bery nice

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment