import { LuaBuiltinFunction, LuaTable } from "../runtime.ts"; const ONE_DAY = 1000 * 60 * 60 * 24; const ONE_WEEK = ONE_DAY * 7; function weekNumber( d: Date, utc: boolean, weekStartDay: number, iso: boolean, ): number { const year = utc ? d.getUTCFullYear() : d.getFullYear(); const month = utc ? d.getUTCMonth() : d.getMonth(); const day = utc ? d.getUTCDate() : d.getDate(); const date = new Date(Date.UTC(year, month, day)); if (iso) { const target = new Date(date); target.setUTCDate(target.getUTCDate() + 3 - ((target.getUTCDay() + 6) % 7)); const yearStart = new Date(Date.UTC(target.getUTCFullYear(), 0, 4)); const weekStart = new Date(yearStart); weekStart.setUTCDate( yearStart.getUTCDate() - ((yearStart.getUTCDay() + 6) % 7), ); return 1 + Math.floor((target.getTime() - weekStart.getTime()) / ONE_WEEK); } const yearStart = new Date(Date.UTC(year, 0, 1)); const startDay = yearStart.getUTCDay(); const offset = (7 + (startDay - weekStartDay)) % 7; const firstWeekStart = new Date(yearStart); firstWeekStart.setUTCDate(yearStart.getUTCDate() + ((7 - offset) % 7)); if (date < firstWeekStart) return 0; return 1 + Math.floor((date.getTime() - firstWeekStart.getTime()) / ONE_WEEK); } function dayOfYear(d: Date, utc: boolean): number { const year = utc ? d.getUTCFullYear() : d.getFullYear(); const start = new Date(Date.UTC(year, 0, 0)); const current = utc ? new Date( Date.UTC( year, d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), ), ) : new Date( Date.UTC( year, d.getMonth(), d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), ), ); return Math.floor((current.getTime() - start.getTime()) / ONE_DAY); } function isoWeekYear(d: Date, utc: boolean): number { const year = utc ? d.getUTCFullYear() : d.getFullYear(); const month = utc ? d.getUTCMonth() : d.getMonth(); const day = utc ? d.getUTCDate() : d.getDate(); const target = new Date(Date.UTC(year, month, day)); target.setUTCDate(target.getUTCDate() + 3 - ((target.getUTCDay() + 6) % 7)); return target.getUTCFullYear(); } function isDST(d: Date): boolean { const jan = new Date(d.getFullYear(), 0, 1); return d.getTimezoneOffset() < jan.getTimezoneOffset(); } function yr(d: Date, utc: boolean): number { return utc ? d.getUTCFullYear() : d.getFullYear(); } function mo(d: Date, utc: boolean): number { return utc ? d.getUTCMonth() : d.getMonth(); } function da(d: Date, utc: boolean): number { return utc ? d.getUTCDate() : d.getDate(); } function hr(d: Date, utc: boolean): number { return utc ? d.getUTCHours() : d.getHours(); } function mi(d: Date, utc: boolean): number { return utc ? d.getUTCMinutes() : d.getMinutes(); } function sc(d: Date, utc: boolean): number { return utc ? d.getUTCSeconds() : d.getSeconds(); } function wd(d: Date, utc: boolean): number { return utc ? d.getUTCDay() : d.getDay(); } function pad2(n: number): string { return n.toString().padStart(2, "0"); } function pad3(n: number): string { return n.toString().padStart(3, "0"); } function dateTable(d: Date, utc: boolean): LuaTable { const tbl = new LuaTable({ year: yr(d, utc), month: mo(d, utc) + 1, day: da(d, utc), hour: hr(d, utc), min: mi(d, utc), sec: sc(d, utc), wday: wd(d, utc) + 1, yday: dayOfYear(d, utc), }); if (!utc) { void tbl.rawSet("isdst", isDST(d)); } return tbl; } // Build the specifier map for a given `Date` and `utc` flag. // Returns a record mapping single-char specifier to its output string. function buildSpecMap(d: Date, utc: boolean): Record string> { const h = () => hr(d, utc); const h12 = () => h() % 12 || 12; const dow = () => wd(d, utc); return { // Date Y: () => yr(d, utc).toString(), y: () => pad2(yr(d, utc) % 100), C: () => pad2(Math.floor(yr(d, utc) / 100)), m: () => pad2(mo(d, utc) + 1), d: () => pad2(da(d, utc)), e: () => da(d, utc).toString().padStart(2, " "), j: () => pad3(dayOfYear(d, utc)), // Time H: () => pad2(h()), I: () => pad2(h12()), M: () => pad2(mi(d, utc)), S: () => pad2(sc(d, utc)), p: () => (h() >= 12 ? "PM" : "AM"), // Weekday A: () => d.toLocaleString("en-US", { weekday: "long", ...(utc ? { timeZone: "UTC" } : {}), }), a: () => d.toLocaleString("en-US", { weekday: "short", ...(utc ? { timeZone: "UTC" } : {}), }), w: () => dow().toString(), u: () => (dow() === 0 ? 7 : dow()).toString(), // Month name b: () => d.toLocaleString("en-US", { month: "short", ...(utc ? { timeZone: "UTC" } : {}), }), h: () => d.toLocaleString("en-US", { month: "short", ...(utc ? { timeZone: "UTC" } : {}), }), B: () => d.toLocaleString("en-US", { month: "long", ...(utc ? { timeZone: "UTC" } : {}), }), // Week number U: () => pad2(weekNumber(d, utc, 0, false)), W: () => pad2(weekNumber(d, utc, 1, false)), V: () => pad2(weekNumber(d, utc, 1, true)), G: () => isoWeekYear(d, utc).toString(), g: () => pad2(isoWeekYear(d, utc) % 100), // Composite specifiers c: () => d.toLocaleString("en-US", { ...(utc ? { timeZone: "UTC" } : {}), }), x: () => d.toLocaleDateString("en-US", { ...(utc ? { timeZone: "UTC" } : {}), }), X: () => d.toLocaleTimeString("en-US", { ...(utc ? { timeZone: "UTC" } : {}), }), D: () => `${pad2(mo(d, utc) + 1)}/${pad2(da(d, utc))}/${pad2(yr(d, utc) % 100)}`, F: () => `${yr(d, utc).toString()}-${pad2(mo(d, utc) + 1)}-${pad2(da(d, utc))}`, R: () => `${pad2(h())}:${pad2(mi(d, utc))}`, T: () => `${pad2(h())}:${pad2(mi(d, utc))}:${pad2(sc(d, utc))}`, r: () => `${pad2(h12())}:${pad2(mi(d, utc))}:${pad2(sc(d, utc))} ${ h() >= 12 ? "PM" : "AM" }`, // Epoch s: () => Math.floor(d.getTime() / 1000).toString(), // Whitespace n: () => "\n", t: () => "\t", // Timezone Z: () => { if (utc) return "UTC"; const match = d.toTimeString().match(/\((.*)\)/); return match ? match[1] : ""; }, z: () => { if (utc) return "+0000"; const offset = -d.getTimezoneOffset(); const sign = offset >= 0 ? "+" : "-"; const abs = Math.abs(offset); return `${sign}${pad2(Math.floor(abs / 60))}${pad2(abs % 60)}`; }, // Literal "%": () => "%", }; } function luaFormatTime(fmt: string, d: Date, utc: boolean): string { const specs = buildSpecMap(d, utc); let out = ""; let i = 0; while (i < fmt.length) { if (fmt[i] !== "%") { out += fmt[i]; i++; continue; } i++; // skip '%' if (i >= fmt.length) { throw new Error("invalid conversion specifier '%'"); } const ch = fmt[i]; const fn = specs[ch]; if (!fn) { throw new Error(`invalid conversion specifier '%${ch}'`); } out += fn(); i++; } return out; } export const osApi = new LuaTable({ time: new LuaBuiltinFunction({ callback: (_sf, tbl?: LuaTable) => { if (tbl) { if (!tbl.has("year")) { throw new Error("time(): year is required"); } if (!tbl.has("month")) { throw new Error("time(): month is required"); } if (!tbl.has("day")) { throw new Error("time(): day is required"); } const year = tbl.get("year"); const month = tbl.get("month"); const day = tbl.get("day"); const hour = tbl.get("hour") ?? 12; const min = tbl.get("min") ?? 0; const sec = tbl.get("sec") ?? 0; const date = new Date(year, month - 1, day, hour, min, sec); return Math.floor(date.getTime() / 1000); } return Math.floor(Date.now() / 1000); }, description: "Returns the current Unix timestamp or one built from a local date table.", signatures: ["os.time(): integer", "os.time(dateTable): integer"], parameters: [ { name: "dateTable", type: "table", description: "Local date fields `year`, `month`, `day`, and optional `hour`, `min`, and `sec`.", optional: true, }, ], returns: [ { type: "integer", description: "Seconds since the Unix epoch." }, ], examples: [ { code: "local timestamp = os.time({year = 2020, month = 1, day = 1})", }, ], }), // Returns the difference, from time `t1` to time `t2` in seconds // In POSIX and some other systems, this value is exactly $t2-t1$. difftime: new LuaBuiltinFunction({ callback: (_sf, t2: number, t1: number): number => { return t2 - t1; }, description: "Returns the difference in seconds from timestamp `t1` to `t2`.", parameters: [ { name: "t2", type: "number" }, { name: "t1", type: "number" }, ], returns: [ { type: "number", description: "The value `t2 - t1` in seconds." }, ], }), // Returns a string or a table containing date and time, formatted // according to the given string format. // // If format starts with '!', the date is formatted in UTC. // // If format is "*t" (or "!*t"), returns a table with fields: // // - `year`, // - `month` (1-12), // - `day` (1-31), // - `hour` (0-23), // - `min` (0-59), // - `sec` (0-61), // - `wday` (1-7, Sunday is 1), // - `yday` (1-366), and // - `isdst` (boolean). // // Otherwise, format specifiers follow ISO C `strftime`. // // If format is absent, it defaults to `%c`. date: new LuaBuiltinFunction({ callback: (_sf, format?: string, timestamp?: number) => { let fmt = format ?? "%c"; let utc = false; if (fmt.startsWith("!")) { utc = true; fmt = fmt.slice(1); } const d = timestamp !== undefined && timestamp !== null ? new Date(timestamp * 1000) : new Date(); if (fmt === "*t") { return dateTable(d, utc); } return luaFormatTime(fmt, d, utc); }, description: "Formats a timestamp as a date string or date table, optionally in UTC.", parameters: [ { name: "format", type: "string", description: "`strftime`-style format, `*t` for a table, and optional leading `!` for UTC.", optional: true, }, { name: "timestamp", type: "number", description: "Unix timestamp; defaults to the current time.", optional: true, }, ], returns: [ { type: "string|table", description: "Formatted date or date fields." }, ], examples: [ { code: 'print(os.date("%Y-%m-%d"))\nlocal utc = os.date("!*t")' }, ], }), // Returns an approximation of CPU time used by the program in seconds. clock: new LuaBuiltinFunction({ callback: (_sf): number => { return performance.now() / 1000.0; }, description: "Returns a high-resolution elapsed time value in seconds.", returns: [ { type: "number", description: "Browser performance timer in seconds.", }, ], }), });