Modulo:Stardate
Module:Stardate converts between real-world Gregorian dates and Star Trek TNG-era stardates, and vice-versa.
The conversion follows the most widely accepted TNG-era convention:
- Stardate 41000.0 = 1 January 2364
- 1000 stardate units span exactly one full year
- Each stardate unit therefore equals 1/1000 of a year
- Leap years are accounted for when mapping days within a given year
This module is intended to be called via a wrapper template using {{{{#invoke:}}}}, but it also exposes internal Lua functions for use by other modules.
Usage
Via wrapper template (recommended)
Use template {{Stardate}} containing:
{{#invoke:Stardate | {{{1}}} | date={{{date|}}} | stardate={{{stardate|}}} | format={{{format|full}}} }}
Then call it from any page:
| Wikitext | Output |
|---|---|
41202.2 |
41202.2
|
41000x |
41000x
|
2364-02-26 |
2364-02-26
|
2364 |
2364
|
Direct #invoke (without a wrapper template)
{{#invoke:Stardate | toStardate | date=2364-03-15 }}
{{#invoke:Stardate | toStardate | date=2364-03-15 | format=year }}
{{#invoke:Stardate | toRealDate | stardate=41153.7 }}
{{#invoke:Stardate | toRealDate | stardate=41153.7 | format=year }}
From another Lua module
local stardate = require('Module:Stardate')
-- date → stardate
local sd = stardate._toStardate(2364, 3, 15, 'full') -- returns "41202.2"
-- stardate → date
local dt = stardate._toRealDate(41153.7, 'full') -- returns "2364-02-26"
Entry points
toStardate
Converts a real date to a TNG-era stardate.
| Parameter | Required | Description |
|---|---|---|
date |
Yes | The date to convert, in yyyy-mm-dd format (e.g. 2364-03-15).
|
format |
No | Controls the output format. See the table below. Defaults to full.
|
toRealDate
Converts a TNG-era stardate to a real date.
| Parameter | Required | Description |
|---|---|---|
stardate |
Yes | The stardate to convert, as a decimal number (e.g. 41153.7). Must not be negative.
|
format |
No | Controls the output format. See the table below. Defaults to full.
|
format parameter
Shared by both entry points. Controls how the result is rendered.
| Value | toStardate output |
toRealDate output
|
|---|---|---|
full (default) |
Stardate with one decimal place (e.g. 41202.2) |
Full date as yyyy-mm-dd (e.g. 2364-03-15)
|
year |
Millennium prefix with a trailing x (e.g. 41000x) |
Year only (e.g. 2364)
|
Internal API
The following functions are exposed on the module table for direct Lua-to-Lua use. They bypass frame parsing and validation — callers are responsible for passing correct types.
_toStardate(y, m, d, format)
| Parameter | Type | Description |
|---|---|---|
y |
number | Year. |
m |
number | Month (1–12). |
d |
number | Day of month (1–31). |
format |
string | "full" or "year".
|
Returns a formatted stardate string.
_toRealDate(sd, format)
| Parameter | Type | Description |
|---|---|---|
sd |
number | Stardate value (e.g. 41153.7).
|
format |
string | "full" or "year".
|
Returns a formatted date string.
Error handling
Both public entry points validate their input. On failure they return an HTML <span class="error"> element containing a human-readable message. Validation covers:
- Missing required parameters (
dateorstardate). - Malformed
datestrings (must matchyyyy-mm-ddexactly). - Out-of-range month or day values, including correct per-month maximums and leap-year handling (e.g. Feb 29 is accepted only in leap years).
- Non-numeric or negative
stardatevalues. - Unrecognised
formatvalues.
Conversion convention
The module uses the epoch and scaling rules most commonly cited in Star Trek reference material:
| Rule | Value |
|---|---|
Stardate 41000.0 = 1 January 2364
| |
| Year length | 1000 stardate units per year |
| Intra-year mapping | Fractional position = (day-of-year − 1) ÷ (days in that year) |
| Leap years | Handled per the Gregorian calendar; the day-count of each year is used when mapping fractions |
Because each year maps to exactly 1000 stardate units regardless of whether it has 365 or 366 days, individual stardate units are slightly longer in leap years than in common years. Round-trip conversion (date → stardate → date) is exact for every valid Gregorian date.
--- Converts between real-world Gregorian dates and Star Trek TNG-era stardates.
--
-- Convention used (widely accepted TNG baseline):
-- Stardate 41000.0 = 1 January 2364
-- 1000 stardate units span one full year
-- Each stardate unit therefore equals 1/1000 of a year
--
-- The module also exposes internal functions (_toStardate, _toRealDate)
-- so that other Lua modules can require() it and call those directly
-- without needing a frame object.
--
-- @module Stardate
-- @author Luca Mauri
-- @license GPLv2
-- @Keyword wikitrek
local p = {}
--- Constants
-- Epoch: Stardate 41000.0 = 1 January 2364 (day 1 of the year)
local EPOCH_YEAR = 2364
local EPOCH_STARDATE = 41000.0
--- Private helpers
--- Return true when y is a leap year.
-- @private
-- @param y Year to test (number).
-- @return boolean True if y is a leap year.
local function isLeapYear(y)
return (y % 4 == 0 and y % 100 ~= 0) or (y % 400 == 0)
end
--- Total days in a given year (365 or 366).
-- @private
-- @param y Year (number).
-- @return number 365 for a common year, 366 for a leap year.
local function daysInYear(y)
return isLeapYear(y) and 366 or 365
end
--- Days in each month for a common year [1] and a leap year [2].
-- @private
-- @type table
local DAYS_IN_MONTH = {
{ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }, -- [1] common
{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } -- [2] leap
}
--- Return the day-of-year (1-based) for a given date.
-- @private
-- @param y Year (number).
-- @param m Month, 1-12 (number).
-- @param d Day of month, 1-31 (number).
-- @return number Day of year (1 = Jan 1).
local function dayOfYear(y, m, d)
local idx = isLeapYear(y) and 2 or 1
local total = 0
for i = 1, m - 1 do
total = total + DAYS_IN_MONTH[idx][i]
end
return total + d
end
--- Given a year and a 1-based day-of-year, return the month and day.
-- @private
-- @param y Year (number).
-- @param doy Day of year, 1-based (number).
-- @return number Month (1-12).
-- @return number Day of month (1-31).
local function monthDayFromDOY(y, doy)
local idx = isLeapYear(y) and 2 or 1
local remaining = doy
for m = 1, 12 do
if remaining <= DAYS_IN_MONTH[idx][m] then
return m, remaining
end
remaining = remaining - DAYS_IN_MONTH[idx][m]
end
-- Safety fallback (should never be reached with valid input)
return 12, 31
end
--- Zero-pad a number to a given width.
-- @private
-- @param n Number to pad (number).
-- @param width Minimum character width (number).
-- @return string Zero-padded string representation.
local function zeroPad(n, width)
local s = tostring(math.floor(n))
while #s < width do
s = "0" .. s
end
return s
end
--- Trim whitespace from a string; return nil if the result is empty.
-- @private
-- @param s Input value (any type).
-- @return string|nil Trimmed string, or nil if s is not a string or is blank.
local function trimOrNil(s)
if type(s) ~= "string" then return nil end
s = s:match("^%s*(.-)%s*$")
return s ~= "" and s or nil
end
-- ─── Core conversion logic ───────────────────────────────────────────────────
--- Convert a Gregorian date to a TNG stardate.
-- Each completed year from the epoch contributes exactly 1000 units;
-- the position within the current year is expressed as a 0-1 fraction
-- of that year's actual day-count (correctly handling leap years).
-- @private
-- @param y Year (number).
-- @param m Month, 1-12 (number).
-- @param d Day of month, 1-31 (number).
-- @return number Stardate value (e.g. 41153.0).
local function dateToStardate(y, m, d)
local units = (y - EPOCH_YEAR) * 1000
local fracInYear = (dayOfYear(y, m, d) - 1) / daysInYear(y)
return EPOCH_STARDATE + units + fracInYear * 1000
end
--- Convert a TNG stardate to a Gregorian date.
-- Walks whole-year blocks of 1000 units to find the target year, then
-- maps the remaining fractional units back to a day-of-year using rounding
-- (not truncation) so that the last day of every year survives the round-trip.
-- @private
-- @param sd Stardate value (number), e.g. 41153.7.
-- @return number Year.
-- @return number Month (1-12).
-- @return number Day of month (1-31).
local function stardateToDate(sd)
local y = EPOCH_YEAR
local remaining = sd - EPOCH_STARDATE -- total stardate units from epoch
-- Walk forward or backward by whole years
if remaining >= 0 then
while remaining >= 1000 do
remaining = remaining - 1000
y = y + 1
end
else
while remaining < 0 do
y = y - 1
remaining = remaining + 1000
end
end
-- remaining is now in [0, 1000) — the fractional units within year y.
-- Map to a 1-based day-of-year using rounding to avoid off-by-one
-- on dates that sit at or very near a day boundary.
local fracInYear = remaining / 1000
local doy = math.floor(fracInYear * daysInYear(y) + 0.5) + 1
-- Clamp to valid range for the year
if doy < 1 then doy = 1 end
if doy > daysInYear(y) then doy = daysInYear(y) end
local m, d = monthDayFromDOY(y, doy)
return y, m, d
end
-- ─── Input validation ────────────────────────────────────────────────────────
--- Validate and parse a date string in yyyy-mm-dd format.
-- @private
-- @param dateStr Date string to parse (string).
-- @return number|nil Year on success, or nil on failure.
-- @return number|string Month on success, or an error message string on failure.
-- @return number|nil Day on success, or nil on failure.
local function parseDate(dateStr)
if not dateStr then
return nil, "Missing 'date' parameter."
end
local y, m, d = dateStr:match("^(%d+)-(%d+)-(%d+)$")
if not y then
return nil, "Invalid date format '" .. dateStr ..
"'. Expected yyyy-mm-dd (e.g. 2364-03-15)."
end
y, m, d = tonumber(y), tonumber(m), tonumber(d)
if m < 1 or m > 12 then
return nil, "Month must be between 1 and 12 (got " .. m .. ")."
end
local idx = isLeapYear(y) and 2 or 1
local maxDay = DAYS_IN_MONTH[idx][m]
if d < 1 or d > maxDay then
return nil, "Day must be between 1 and " .. maxDay ..
" for month " .. m .. " of " .. y .. " (got " .. d .. ")."
end
return y, m, d
end
--- Validate and parse a stardate string.
-- @private
-- @param sdStr Stardate string to parse (string).
-- @return number|nil Stardate as a number on success, or nil on failure.
-- @return string|nil Error message on failure, or nil on success.
local function parseStardate(sdStr)
if not sdStr then
return nil, "Missing 'stardate' parameter."
end
local sd = tonumber(sdStr)
if not sd then
return nil, "Invalid stardate '" .. sdStr ..
"'. Must be a number (e.g. 41153.7)."
end
if sd < 0 then
return nil, "Stardate cannot be negative (got " .. sdStr .. ")."
end
return sd
end
--- Validate the format parameter.
-- @private
-- @param fmtStr Format string to validate (string or nil).
-- @return string|nil Validated format ("full" or "year"), or nil on failure.
-- @return string|nil Error message on failure, or nil on success.
local function parseFormat(fmtStr)
local fmt = fmtStr or "full"
if fmt ~= "full" and fmt ~= "year" then
return nil, "Invalid format '" .. fmt ..
"'. Allowed values: 'full' (default) or 'year'."
end
return fmt
end
-- ─── Internal API (callable from other Lua modules via require) ──────────────
--- Convert a date to a formatted stardate string.
-- @param y Year (number).
-- @param m Month, 1-12 (number).
-- @param d Day of month, 1-31 (number).
-- @param format Output format: "full" for one-decimal stardate, "year" for
-- millennium prefix only (string).
-- @return string Formatted stardate (e.g. "41202.2" or "41000x").
function p._toStardate(y, m, d, format)
local sd = dateToStardate(y, m, d)
if format == "year" then
-- Return the millennium prefix with a trailing 'x' (e.g. "41000x")
local prefix = math.floor(sd / 1000) * 1000
return tostring(prefix) .. "x"
else
return string.format("%.1f", sd)
end
end
--- Convert a stardate to a formatted real date string.
-- @param sd Stardate value (number), e.g. 41153.7.
-- @param format Output format: "full" for yyyy-mm-dd, "year" for year only
-- (string).
-- @return string Formatted date (e.g. "2364-02-26" or "2364").
function p._toRealDate(sd, format)
local y, m, d = stardateToDate(sd)
if format == "year" then
return tostring(y)
else
return zeroPad(y, 4) .. "-" .. zeroPad(m, 2) .. "-" .. zeroPad(d, 2)
end
end
-- ─── Public entry points (called via {{#invoke:}}) ──────────────────────────
-- Arguments are read from the *parent* frame (i.e. the template that contains
-- the #invoke call), which is the standard Scribunto pattern when the module
-- is meant to be wrapped in a template.
--- Convert a real date to a TNG-era stardate.
-- Reads named parameters from the calling template's frame.
-- @param frame Scribunto frame object (provided automatically by MediaWiki).
-- @return string The computed stardate, or an HTML error span on invalid input.
-- @usage
-- From a wrapper template:
-- <nowiki>{{#invoke:Stardate | toStardate | date=2364-03-15 }}</nowiki>
-- <nowiki>{{#invoke:Stardate | toStardate | date=2364-03-15 | format=year }}</nowiki>
function p.toStardate(frame)
local args = frame:getParent().args
local date = trimOrNil(args["date"])
local fmt = trimOrNil(args["format"])
local format, fmtErr = parseFormat(fmt)
if not format then
return '<span class="error">Stardate error: ' .. fmtErr .. '</span>'
end
local y, m, d = parseDate(date)
if not y then
-- When parseDate fails, y is nil and m holds the error string
return '<span class="error">Stardate error: ' .. m .. '</span>'
end
return p._toStardate(y, m, d, format)
end
--- Convert a TNG-era stardate to a real date.
-- Reads named parameters from the calling template's frame.
-- @param frame Scribunto frame object (provided automatically by MediaWiki).
-- @return string The computed date, or an HTML error span on invalid input.
-- @usage
-- From a wrapper template:
-- <nowiki>{{#invoke:Stardate | toRealDate | stardate=41153.7 }}</nowiki>
-- <nowiki>{{#invoke:Stardate | toRealDate | stardate=41153.7 | format=year }}</nowiki>
function p.toRealDate(frame)
local args = frame:getParent().args
local sdStr = trimOrNil(args["stardate"])
local fmt = trimOrNil(args["format"])
local format, fmtErr = parseFormat(fmt)
if not format then
return '<span class="error">Stardate error: ' .. fmtErr .. '</span>'
end
local sd, sdErr = parseStardate(sdStr)
if not sd then
return '<span class="error">Stardate error: ' .. sdErr .. '</span>'
end
return p._toRealDate(sd, format)
end
return p