Skip to content
358 changes: 358 additions & 0 deletions library/lua/datetime.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
local _ENV = mkmodule('datetime')

local DAYS_PER_MONTH = 28
local DAYS_PER_YEAR = 336
local MONTHS_PER_YEAR = 12

DWARF_TICKS_PER_DAY = 1200
--local DWARF_TICKS_PER_MONTH = DWARF_TICKS_PER_DAY * DAYS_PER_MONTH
--local DWARF_TICKS_PER_YEAR = DWARF_TICKS_PER_MONTH * MONTHS_PER_YEAR

ADVENTURE_TICKS_PER_DAY = 172800
--local ADVENTURE_TICKS_PER_MONTH = ADVENTURE_TICKS_PER_DAY * DAYS_PER_MONTH
--local ADVENTURE_TICKS_PER_YEAR = ADVENTURE_TICKS_PER_MONTH * MONTHS_PER_YEAR

--local TICKS_PER_DAY = DWARF_TICKS_PER_DAY
--local TICKS_PER_MONTH = DWARF_TICKS_PER_MONTH
--local TICKS_PER_YEAR = TICKS_PER_MONTH * MONTHS_PER_YEAR

local CALENDAR_MONTHS = {
'Granite',
'Slate',
'Felsite',
'Hematite',
'Malachite',
'Galena',
'Limestone',
'Sandstone',
'Timber',
'Moonstone',
'Opal',
'Obsidian'
}

-- day -> month
local CALENDAR_FULLMOON_MAP = {
[25] = 1,
[23] = 2,
[21] = 3,
[19] = 4,
[17] = 5,
[15] = 6,
[13] = 7,
[10] = 8,
[8] = 9,
[6] = 10,
[4] = 11,
[2] = 12,
[28] = 12
}

-- Ordinal suffix rules found here: https://en.wikipedia.org/wiki/Ordinal_indicator
-- global for scripts to use
function getOrdinalSuffix(ordinal)
if (ordinal < 0) then
ordinal = math.abs(ordinal)
end

local rem = (ordinal < 100) and (ordinal % 10) or (ordinal % 100)
-- rem can be between 11 and 13 only when ordinal is > 100
if (ordinal >= 11 and ordinal <= 13) or (rem >= 11 and rem <= 13) then
return 'th'
end
-- modulo again to handle the case when ordinal is > 100
return ({'st', 'nd', 'rd'})[rem % 10] or 'th'
end

DwarfCalendar = defclass(DwarfCalendar)

DwarfCalendar.ATTRS{
year=0,
year_tick=0,
ticks_per_day=DWARF_TICKS_PER_DAY
}

function DwarfCalendar:init()
self:setTickRates(self.ticks_per_day)
self:normalize()
end

function DwarfCalendar:setTickRates(ticks_per_day)
-- game_mode.DWARF and .ADVENTURE values are < 10
-- too low for sane tick rates, so we can utilize em.
-- this might be useful if the caller wants to set by game mode
if (ticks_per_day == df.game_mode.DWARF) then
self.ticks_per_day = DWARF_TICKS_PER_DAY
elseif (ticks_per_day == df.game_mode.ADVENTURE)
self.ticks_per_day = ADVENTURE_TICKS_PER_DAY
else
-- should we throw an error if caller passed in <= 0 here?
-- if not, we'll divide by zero later
self.ticks_per_day = (ticks_per_day > 0) and ticks_per_day or DWARF_TICKS_PER_DAY
end
self.ticks_per_month = self.ticks_per_day * DAYS_PER_MONTH
self.ticks_per_year = self.ticks_per_month * MONTHS_PER_YEAR
end

function DwarfCalendar:addTicks(ticks)
self.year_tick = self.year_tick + ticks
self:normalize()
return self
end

function DwarfCalendar:addDays(days)
self.year_tick = self.year_tick + self.daysToTicks(days)
self:normalize()
return self
end

function DwarfCalendar:addMonths(months)
self.year_tick = self.year_tick + self.monthsToTicks(months)
self:normalize()
return self
end

function DwarfCalendar:addYears(years)
self.year = self.year + years
return self
end

-- should this be named getYear()?
-- returns an integer pair: (year), (year tick count)
function DwarfCalendar:getYears()
return self.year, self.year_tick
end

function DwarfCalendar:setDayOfMonth(month, day)
self.year_tick = DwarfCalendar.monthsToTicks(month) + DwarfCalendar.daysToTicks(day)
end

-- returns an integer pair: (day of month starting from 1), (day tick count)
function DwarfCalendar:getDayOfMonth()
return self:ticksToDayOfMonth() + 1, self:getDayTicks()
end

-- returns a string in ordinal form (e.g. 1st, 12th, 22nd, 101st, 111th, 133rd)
function DwarfCalendar:getDayOfMonthWithSuffix()
local d = self:getDayOfMonth()
return tostring(d)..getOrdinalSuffix(d)
end

-- returns an integer pair: (current day of year, from 1), (day tick count)
function DwarfCalendar:getDayOfYear()
return self:ticksToDays() + 1, self:getDayTicks()
end

-- returns an integer pair: (current month of the year, from 1), (month tick count)
function DwarfCalendar:getMonth()
return self:ticksToMonths() + 1, self:getMonthTicks()
end

-- returns a string of the current month of the year
function DwarfCalendar:getNameOfMonth()
return CALENDAR_MONTHS[self:getMonth()] or error("bad index?")
end

-- returns days since beginning of a year, starting from zero
function DwarfCalendar:ticksToDays()
return self.year_tick // self.ticks_per_day
end

-- returns days since the beginning of a month, starting from zero
function DwarfCalendar:ticksToDayOfMonth()
return self:ticksToDays() % DAYS_PER_MONTH
end

-- returns months since the beginning of a year, starting from zero
function DwarfCalendar:ticksToMonths()
return self.year_tick // self.ticks_per_month
end

-- returns ticks since the beginning of a day
function DwarfCalendar:getDayTicks()
return self.year_tick % self.ticks_per_day
end

-- returns ticks since the beginning of a month
function DwarfCalendar:getMonthTicks()
return self.year_tick % self.ticks_per_month
end

function DwarfCalendar:daysToTicks(days)
return days * self.ticks_per_day
end

function DwarfCalendar:monthsToTicks(months)
return months * self.ticks_per_month
end

function DwarfCalendar:isFullMoon()
return (self:getMonth() == CALENDAR_FULLMOON_MAP[self:getDayOfMonth()])
and true or false
end

function DwarfCalendar:nextFullMoon()
local dateT = DateTime{ year = self.year, year_tick = self.year_tick }
if (dateT:isFullMoon()) then dateT:addDays(1) end

local cur_m = dateT:getMonth()
local fm = {
25,
23,
21,
19,
17,
15,
13,
10,
8,
6,
4,
2, 28
}

if (dateT:getDayOfMonth() < fm[cur_m])
dateT:setDayOfMonth(cur_m, fm[cur_m])
else
-- Next full moon is on the next month
-- or next year if addDays() rolled us over.
-- Obsidian is a possible exception since it has 2 full moons
-- this also handles the case when Obsidian day is between 2 and 28 exclusive
dateT:setDayOfMonth(cur_m, fm[cur_m+1])
end

return dateT
end

function DwarfCalendar:normalize()
if (self.year_tick > self.ticks_per_year) then
self.year = self.year + (self.year_tick // self.ticks_per_year)
self.year_tick = self.year_tick % self.ticks_per_year
elseif (self.year_tick < 0) then
-- going backwards in time, subtract year by at least one.
self.year = self.year - math.max(1, math.abs(self.year_tick) // self.ticks_per_year)
-- Lua's modulo operator applies floor division,
-- hence year_tick will always be positive after assignment
-- equivalent to: year_tick - (TICKS_PER_YEAR * (year_tick // TICKS_PER_YEAR))
self.year_tick = self.year_tick % self.ticks_per_year
end
end

function DwarfCalendar:__add(other)
if DEBUG then self:_debugOps(other) end
-- normalize() handles adjustments to year and year_tick
return DwarfCalendar{ year = (self.year + other.year), year_tick = (self.year_tick + other.year_tick) }
end

function DwarfCalendar:__sub(other)
if DEBUG then self:_debugOps(other) end
-- normalize() handles adjustments to year and year_tick
return DwarfCalendar{ year = (self.year - other.year) , year_tick = (self.year_tick - other.year_tick) }
end

function DwarfCalendar:_debugOps(other)
print('first: '..self.year,self.year_tick)
print('second: '..other.year,other.year_tick)
end


function DwarfCalendar.getMonthNames()
return CALENDAR_MONTHS
end


DateTime = defclass(DateTime, DwarfCalendar)


-- returns hours (24 hour format), minutes, seconds
function DateTime:getTime()
-- probably only useful in adv mode where a day is 144x longer
local h = self:getDayTicks() / (self.ticks_per_day / 24)
local m = (h * 60) % 60
local s = (m * 60) % 60
-- return as integers, rounded down
return h//1, m//1, s//1
end

-- TODO: maybe add setTime()

--function DateTime:daysTo(other)
-- local dateT = other - self
-- return dateT.year * DAYS_PER_YEAR + dateT:ticksToDays()
--end
-- maybe useful addition
--function DateTime:daysTo(other)
-- return (other - self):toDuration().getDays()
--end

-- Alternatively, instead of a Duration object,
-- we can simply synthesize a table with
-- key value pairs of years, months, days, ticks.
-- If table, it could optionally take an argument or variadic
-- where the caller provides the time unit specifiers
-- i.e. getDuration/toDuration('ymd') or toDuration('y', 'm', 'd'), etc
function DateTime:toDuration()
return Duration{ year = self.year, year_tick = self.year_tick }
end

function DateTime:__add(other)
if DEBUG then self:_debugOps(other) end
-- normalize() handles adjustments to year and year_tick
return DateTime{ year = (self.year + other.year), year_tick = (self.year_tick + other.year_tick) }
end

-- might make sense to return a Duration here
function DateTime:__sub(other)
if DEBUG then self:_debugOps(other) end
-- normalize() handles adjustments to year and year_tick
return DateTime{ year = (self.year - other.year) , year_tick = (self.year_tick - other.year_tick) }
end

function DateTime.now(game_mode)
game_mode = game_mode or df.global.gamemode
-- if game_mode is not given or not ADVENTURE then default to DWARF mode
local ticks = (game_mode == df.game_mode.ADVENTURE) and
(df.global.cur_year_tick_advmode) or (df.global.cur_year_tick)
return DateTime{ year = df.global.cur_year, year_tick = ticks }
end

Duration = defclass(Duration, DwarfCalendar)

-- returns ticks since year zero
function Duration:getTicks()
return self.year * self.ticks_per_year + self.year_tick
end

-- returns an integer pair: (days since year zero), (day tick count)
function Duration:getDays()
return self.year * DAYS_PER_YEAR + self:ticksToDays(), self:getDayTicks()
end

-- returns an integer pair: (months since year zero), (month tick count)
function Duration:getMonths()
return self.year * MONTHS_PER_YEAR + self:ticksToMonths(), self:getMonthTicks()
end

-- returns parts of an elapsed time:
-- years,
-- months, - since start of year
-- days, - since start of month
-- day tick count - since start of day
function Duration:getYearsMonthsDays()
return self.year, self:ticksToMonths(), self:ticksToDayOfMonth(), self:getDayTicks()
end

function Duration:__add(other)
if DEBUG then self:_debugOps(other) end
-- normalize() handles adjustments to year and year_tick
return Duration{ year = (self.year + other.year), year_tick = (self.year_tick + other.year_tick) }
end

function Duration:__sub(other)
if DEBUG then self:_debugOps(other) end
-- normalize() handles adjustments to year and year_tick
return Duration{ year = (self.year - other.year) , year_tick = (self.year_tick - other.year_tick) }
end


return _ENV