Files
plainleaf/client/space_lua/stdlib/os.ts
T

374 lines
9.4 KiB
TypeScript

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) {
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, () => 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((_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);
}),
// 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((_sf, t2: number, t1: number): number => {
return t2 - t1;
}),
// 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(
(_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);
},
),
// Returns an approximation of CPU time used by the program in seconds.
clock: new LuaBuiltinFunction((_sf): number => {
return performance.now() / 1000.0;
}),
});