Module:Time

From Star of Providence Wiki
Jump to navigation Jump to search

Details

The Time module offers a Lua class for parsing times in the format used for audio and video links. It's intended to be used by other Lua modules instead of templates.

It has no dependencies and exports a Time "class" you can use directly. It provides annotations documenting the various functions that are understood by Lua Language Server.


--- Module:Time
--- by User:LtDk, licensed CC-BY-SA 4.0
--- Simple Lua class for parsing times.

local Time = {}
local TimeKeys = {
    hours = true,
    minutes = true,
    seconds = true,
}

--- HH:MM:SS time, allowing both MM:SS and SS as well.
--- Automatically converts overflowed minutes and seconds into larger units.
--- @class Time
--- @field hours   number
--- @field minutes number
--- @field seconds number
local TimeProto = {}
local TimeMeta = {}

--- Checks if a value is a Time
--- @param value any
--- @return boolean
function Time.isTime(value)
    return type(value) == 'table' and rawequal(getmetatable(value), TimeMeta)
end

function TimeMeta.__eq(lhs, rhs)
    if not Time.isTime(rhs) then
        return false
    end
    return lhs.hours == rhs.hours and lhs.minutes == rhs.minutes and lhs.seconds == rhs.seconds
end

function TimeMeta.__lt(lhs, rhs)
    if not Time.isTime(rhs) then
        error('cannot compare Time with non-Time')
    end
    local hours = lhs.hours - rhs.hours
    if hours ~= 0 then
        return hours < 0
    end
    local minutes = lhs.minutes - rhs.minutes
    if minutes ~= 0 then
        return minutes < 0
    end
    local seconds = lhs.seconds - rhs.seconds
    return seconds < 0
end

function TimeMeta.__le(lhs, rhs)
    if not Time.isTime(rhs) then
        error('cannot compare Time with non-Time')
    end
    local hours = lhs.hours - rhs.hours
    if hours ~= 0 then
        return hours < 0
    end
    local minutes = lhs.minutes - rhs.minutes
    if minutes ~= 0 then
        return minutes < 0
    end
    local seconds = lhs.seconds - rhs.seconds
    return seconds <= 0
end

--- Times automatically convert excess seconds and minutes into the appropriate units.
function TimeMeta:__newindex(k, v) --
    if not TimeKeys[k] then
        error(string.format('invalid key %s', k))
    end
    if v ~= nil and (type(v) ~= 'number' or math.floor(v) ~= v or v < 0) then
        error(string.format('%s must be whole number, was %s', k, v))
    end

    if k == 'seconds' and v >= 60 then
        local hours = rawget(self, 'hours') or 0
        local minutes = rawget(self, 'minutes') or 0
        local seconds = v or 0
        minutes = minutes + math.floor(seconds / 60)
        seconds = seconds % 60
        hours = hours + math.floor(minutes / 60)
        minutes = minutes % 60
        rawset(self, '__hours',   hours)
        rawset(self, '__minutes', minutes)
        rawset(self, '__seconds', seconds)
    elseif k == 'minutes' and v >= 60 then
        local hours = rawget(self, 'hours') or 0
        local minutes = v or 0
        hours = hours + math.floor(minutes / 60)
        minutes = minutes % 60
        rawset(self, '__hours',   hours)
        rawset(self, '__minutes', minutes)
    else
        rawset(self, '__hours', v or 0)
    end
end

--- We rename the actual keys to ensure newindex always fires.
function TimeMeta:__index(k)
    if not TimeKeys[k] then
        return TimeProto[k]
    end
    return rawget(self, '__' .. k) or 0
end

--- Times are formatted as HH:MM:SS, MM:SS, or SS based upon the largest nonzero unit.
function TimeMeta:__tostring()
    if self.hours ~= 0 then
        return string.format('%02d:%02d:%02d', self.hours, self.minutes, self.seconds)
    elseif self.minutes ~= 0 then
        return string.format('%02d:%02d', self.minutes, self.seconds)
    else
        return tostring(self.seconds)
    end
end

--- Parses a time from a string. Accepts HH:MM:SS, MM:SS, and SS.
--- @param time string
--- @return Time
function Time.parse(time)
    time = tostring(time)

    local one = nil
    local one_n = nil
    local two = nil
    local two_n = nil
    local three = nil
    local three_n = nil

    local i, j = string.find(time, ':', 1, true)
    if i == nil then
        one = time
    else
        one = string.sub(time, 1, i - 1)
    end
    one_n = tonumber(one, 10)
    if one_n == nil then
        error(string.format('%q was not an integer in %q', one, time))
    end

    if j ~= nil then
        local k, l = string.find(time, ':', j + 1, true)
        if k == nil then
            two = string.sub(time, j + 1)
        else
            two = string.sub(time, j + 1, k - 1)
            three = string.sub(time, l + 1)
        end
        two_n = tonumber(two, 10)
        if two_n == nil then
            error(string.format('%q was not an integer in %q', two, time))
        end

        if three ~= nil then
            three_n = tonumber(three, 10)
            if three_n == nil then
                error(string.format('%q was not an integer in %q', three, time))
            end
        end
    end

    return Time.new(one_n, two_n, three_n)
end

--- Creates a new Time from parts.
--- If three values are provided, they're interpreted as hours, minutes, and seconds.
--- If two values are provided, they're interpreted as minutes and seconds.
--- If one value is provided, it's interpreted as seconds.
--- @param hours   number?
--- @param minutes number?
--- @param seconds number?
--- @overload fun(hours: number, minutes: number, seconds: number): Time
--- @overload fun(minutes: number, seconds: number): Time
--- @overload fun(seconds: number): Time
--- @overload fun(): Time
--- @return Time
function Time.new(hours, minutes, seconds)
    if hours == nil then
        seconds = 0
        minutes = 0
        hours = 0
    elseif minutes == nil then
        seconds = hours
        minutes = 0
        hours = 0
    elseif seconds == nil then
        seconds = minutes
        minutes = hours
        hours = 0
    end

    if type(hours) ~= 'number' and (math.floor(hours) ~= hours or hours < 0) then
        error(string.format('hours must be whole number, was %q', hours))
    end
    if type(minutes) ~= 'number' and (math.floor(minutes) ~= minutes or minutes < 0) then
        error(string.format('minutes must be whole number, was %q', minutes))
    end
    if type(seconds) ~= 'number' and (math.floor(seconds) ~= seconds or seconds < 0) then
        error(string.format('seconds must be whole number, was %q', seconds))
    end

    minutes = minutes + math.floor(seconds / 60)
    seconds = seconds % 60
    hours = hours + math.floor(minutes / 60)
    minutes = minutes % 60

    return setmetatable(
        { hours = hours, minutes = minutes, seconds = seconds },
        TimeMeta
    )
end

return Time