225 lines
7.3 KiB
JavaScript
225 lines
7.3 KiB
JavaScript
/* time.js – exports: {record, number, text} */
|
||
|
||
var time = this;
|
||
|
||
/* -------- host helpers -------------------------------------------------- */
|
||
|
||
var now = time.now; // seconds since Misty epoch (C stub)
|
||
var computer_zone = time.computer_zone; // integral hours, no DST
|
||
var computer_dst = time.computer_dst; // true ↔ DST in effect
|
||
|
||
delete time.now;
|
||
delete time.computer_zone;
|
||
delete time.computer_dst;
|
||
|
||
/* -------- units & static tables ----------------------------------------- */
|
||
|
||
time.second = 1;
|
||
time.minute = 60;
|
||
time.hour = 3_600;
|
||
time.day = 86_400;
|
||
time.week = 604_800;
|
||
|
||
time.weekdays = [
|
||
"Sunday", "Monday", "Tuesday",
|
||
"Wednesday", "Thursday", "Friday", "Saturday"
|
||
];
|
||
|
||
time.monthstr = [
|
||
"January", "February", "March", "April", "May", "June",
|
||
"July", "August", "September", "October", "November", "December"
|
||
];
|
||
|
||
time.epoch = 1970;
|
||
|
||
/* ratios (kept K&R-style) */
|
||
time.hour2minute = function() { return time.hour / time.minute; };
|
||
time.day2hour = function() { return time.day / time.hour; };
|
||
time.minute2second = function() { return time.minute / time.second; };
|
||
time.week2day = function() { return time.week / time.day; };
|
||
|
||
/* leap-year helpers */
|
||
time.yearsize = function yearsize(y)
|
||
{
|
||
if (y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0)) return 366;
|
||
return 365;
|
||
};
|
||
time.isleap = function(y) { return time.yearsize(y) === 366; };
|
||
|
||
/* timecode utility */
|
||
time.timecode = function(t, fps = 24)
|
||
{
|
||
var s = Math.trunc(t);
|
||
var frac = t - s;
|
||
return `${s}:${Math.trunc(frac * fps)}`;
|
||
};
|
||
|
||
/* per-month day counts (non-leap) */
|
||
time.monthdays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||
|
||
/* -------- core converters ----------------------------------------------- */
|
||
|
||
function record(num = now(),
|
||
zone = computer_zone(),
|
||
dst = computer_dst())
|
||
{
|
||
/* caller passed an existing record → return it verbatim */
|
||
if (typeof num === "object") return num;
|
||
|
||
/* -------------------------------------------------------------------- */
|
||
/* convert seconds-since-epoch → broken-down record */
|
||
|
||
var monthdays = time.monthdays.slice();
|
||
var rec = {
|
||
second : 0, minute : 0, hour : 0,
|
||
yday : 0, year : 0,
|
||
weekday: 0, month : 0, day : 0,
|
||
zone : zone, dst : !!dst,
|
||
ce : "AD"
|
||
};
|
||
|
||
/* include DST in the effective offset */
|
||
var offset = zone + (dst ? 1 : 0);
|
||
num += offset * time.hour;
|
||
|
||
/* split into day + seconds-of-day */
|
||
var hms = num % time.day;
|
||
var day = Math.floor(num / time.day);
|
||
if (hms < 0) { hms += time.day; day--; }
|
||
|
||
rec.second = hms % time.minute;
|
||
var tmp = Math.floor(hms / time.minute);
|
||
rec.minute = tmp % time.minute;
|
||
rec.hour = Math.floor(tmp / time.minute);
|
||
rec.weekday = (day + 4_503_599_627_370_496 + 2) % 7; /* 2 → 1970-01-01 was Thursday */
|
||
|
||
/* year & day-of-year */
|
||
var y = time.epoch;
|
||
if (day >= 0) {
|
||
for (y = time.epoch; day >= time.yearsize(y); y++)
|
||
day -= time.yearsize(y);
|
||
} else {
|
||
for (y = time.epoch; day < 0; y--)
|
||
day += time.yearsize(y - 1);
|
||
}
|
||
rec.year = y;
|
||
rec.ce = (y <= 0) ? "BC" : "AD";
|
||
rec.yday = day;
|
||
|
||
/* month & month-day */
|
||
if (time.yearsize(y) === 366) monthdays[1] = 29;
|
||
var m = 0;
|
||
for (; day >= monthdays[m]; m++) day -= monthdays[m];
|
||
rec.month = m;
|
||
rec.day = day + 1;
|
||
|
||
return rec;
|
||
}
|
||
|
||
function number(rec = now())
|
||
{
|
||
/* fall through for numeric input or implicit “now” */
|
||
if (typeof rec === "number") return rec;
|
||
|
||
/* -------------------------------------------------------------------- */
|
||
/* record → seconds-since-epoch */
|
||
|
||
var c = 0;
|
||
var year = rec.year || 0;
|
||
var hour = rec.hour || 0;
|
||
var minute = rec.minute || 0;
|
||
var second = rec.second || 0;
|
||
var zone = rec.zone || 0;
|
||
var dst = rec.dst ? 1 : 0;
|
||
var yday = rec.yday || 0;
|
||
|
||
if (year > time.epoch) {
|
||
for (var i = time.epoch; i < year; i++)
|
||
c += time.day * time.yearsize(i);
|
||
} else if (year < time.epoch) {
|
||
for (var i = time.epoch - 1; i > year; i--)
|
||
c += time.day * time.yearsize(i);
|
||
c += (time.yearsize(year) - yday - 1) * time.day;
|
||
c += (time.day2hour() - hour - 1) * time.hour;
|
||
c += (time.hour2minute() - minute - 1) * time.minute;
|
||
c += time.minute2second() - second;
|
||
c += (zone + dst) * time.hour;
|
||
return -c; /* BCE */
|
||
}
|
||
|
||
c = second;
|
||
c += minute * time.minute;
|
||
c += hour * time.hour;
|
||
c += yday * time.day;
|
||
c -= (zone + dst) * time.hour;
|
||
|
||
return c;
|
||
}
|
||
|
||
/* -------- text formatting ----------------------------------------------- */
|
||
|
||
var default_fmt = "vB mB d hh:nn:ss a z y c"; /* includes new DST token */
|
||
|
||
function text(num = now(),
|
||
fmt = default_fmt,
|
||
zone = computer_zone(),
|
||
dst = computer_dst())
|
||
{
|
||
var rec = (typeof num === "number") ? record(num, zone, dst) : num;
|
||
zone = rec.zone;
|
||
dst = rec.dst;
|
||
|
||
/* am/pm */
|
||
if (fmt.includes("a")) {
|
||
if (rec.hour >= 13) { rec.hour -= 12; fmt = fmt.replaceAll("a", "PM"); }
|
||
else if (rec.hour === 12) { fmt = fmt.replaceAll("a", "PM"); }
|
||
else if (rec.hour === 0) { rec.hour = 12; fmt = fmt.replaceAll("a", "AM"); }
|
||
else fmt = fmt.replaceAll("a", "AM");
|
||
}
|
||
|
||
/* BCE/CE */
|
||
var year = rec.year > 0 ? rec.year : rec.year - 1;
|
||
if (fmt.includes("c")) {
|
||
if (year < 0) { year = Math.abs(year); fmt = fmt.replaceAll("c", "BC"); }
|
||
else fmt = fmt.replaceAll("c", "AD");
|
||
}
|
||
|
||
/* substitutions */
|
||
var full_offset = zone + (dst ? 1 : 0);
|
||
fmt = fmt.replaceAll("yyyy", year.toString().padStart(4, "0"));
|
||
fmt = fmt.replaceAll("y", year);
|
||
fmt = fmt.replaceAll("eee", rec.yday + 1);
|
||
fmt = fmt.replaceAll("dd", rec.day.toString().padStart(2, "0"));
|
||
fmt = fmt.replaceAll("d", rec.day);
|
||
fmt = fmt.replaceAll("hh", rec.hour.toString().padStart(2, "0"));
|
||
fmt = fmt.replaceAll("h", rec.hour);
|
||
fmt = fmt.replaceAll("nn", rec.minute.toString().padStart(2, "0"));
|
||
fmt = fmt.replaceAll("n", rec.minute);
|
||
fmt = fmt.replaceAll("ss", rec.second.toFixed(2).padStart(2, "0"));
|
||
fmt = fmt.replaceAll("s", rec.second);
|
||
fmt = fmt.replaceAll("x", dst ? "DST" : ""); /* new */
|
||
fmt = fmt.replaceAll("z", (full_offset >= 0 ? "+" : "") + full_offset);
|
||
fmt = fmt.replaceAll(/mm[^bB]/g, rec.month + 1);
|
||
fmt = fmt.replaceAll(/m[^bB]/g, rec.month + 1);
|
||
fmt = fmt.replaceAll(/v[^bB]/g, rec.weekday);
|
||
fmt = fmt.replaceAll("mb", time.monthstr[rec.month].slice(0, 3));
|
||
fmt = fmt.replaceAll("mB", time.monthstr[rec.month]);
|
||
fmt = fmt.replaceAll("vB", time.weekdays[rec.weekday]);
|
||
fmt = fmt.replaceAll("vb", time.weekdays[rec.weekday].slice(0, 3));
|
||
|
||
return fmt;
|
||
}
|
||
|
||
/* -------- docs ---------------------------------------------------------- */
|
||
|
||
time[cell.DOC] = {
|
||
doc : "Time utilities; epoch = 0000-01-01 00:00:00 +0000.",
|
||
record : "time.record([num | rec], [zone], [dst]) → detailed record (adds .dst).",
|
||
number : "time.number([rec]) → numeric seconds since epoch.",
|
||
text : "time.text([val], [fmt], [zone], [dst]) → formatted string (token Z = DST)."
|
||
};
|
||
|
||
/* -------- public exports ------------------------------------------------ */
|
||
|
||
return { record, number, text };
|