@struktoai/mirage-core
Version:
A Unified Virtual Filesystem For AI Agents
786 lines • 32.7 kB
JavaScript
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
import { gnuStrftime, winningPad } from "../../../builtin/utils/strftime.js";
import { LOCAL_ZONE, TZ_VAR, posixTransitions, posixZone, resolveTz, zoneFromEnv, } from "../../../../utils/timezone.js";
import { DateFormatColonError, UnknownDateFormatError } from "./errors.js";
import { DateKind } from "./types.js";
import { MONTHS } from "../../../builtin/utils/constants.js";
import { systemNow } from "../../../../utils/clock.js";
const DAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// git's test clock: when set, the moment relative and human dates count from;
// its leading integer is read, and anything else reads as 0.
const NOW_VAR = 'GIT_TEST_DATE_NOW';
const LEADING_INT = /^[ \t\n\v\f\r]*([+-]?\d+)/;
// The style names --date takes whole, each also with -local after it (git
// 2.50.1 and 2.47.3 refuse anything longer or shorter, ISO in capitals
// included). `format` instead takes its template after a colon.
const STYLE_NAMES = new Map([
['default', DateKind.NORMAL],
['relative', DateKind.RELATIVE],
['short', DateKind.SHORT],
['iso', DateKind.ISO8601],
['iso8601', DateKind.ISO8601],
['iso-strict', DateKind.ISO8601_STRICT],
['iso8601-strict', DateKind.ISO8601_STRICT],
['rfc', DateKind.RFC2822],
['rfc2822', DateKind.RFC2822],
['human', DateKind.HUMAN],
['raw', DateKind.RAW],
['unix', DateKind.UNIX],
]);
const FORMAT_WORD = 'format';
const AUTO_PREFIX = 'auto:';
const LOCAL_ALIAS = 'local';
const LOCAL_SUFFIX = '-local';
// One strftime directive: flags, width, an E or O modifier, conversion.
const DIRECTIVE = /%([-_0^#]*)(\d*)([EO]?)([\s\S]?)/g;
// The conversions the C library knows; git leaves any other one as typed
// (git 2.47.3 on glibc prints %q, %N, %+ and %:z literally).
const LIBC_CONVERSIONS = new Set('aAbBcCdDeFgGhHIjklmMnpPrRsStTuUVwWxXyYzZ%');
// The numbers the C library prints, with the digits each fills by default; a
// width pads past them, and `-` drops them (git 2.47.3: `%I` is `04`, `%1I`
// `04`, `%-I` `4` and `%-3I` ` 4`).
const PADDED_NUMBERS = {
C: 2,
d: 2,
e: 2,
g: 2,
G: 1,
H: 2,
I: 2,
j: 3,
k: 2,
l: 2,
m: 2,
M: 2,
S: 2,
s: 1,
u: 1,
U: 2,
V: 2,
w: 1,
W: 2,
y: 2,
Y: 1,
};
// The numbers it fills with spaces unless `0` says zeros; every other number
// fills with zeros unless `_` or `-` says spaces.
const SPACE_FILLED_NUMBERS = new Set('ekls');
// The zone a date shown at its own offset hands strftime: offset zero, named
// as glibc names it.
const GMT = posixZone('GMT0');
// The spans tzdata 2026c marks a zone's lower offset as daylight saving time,
// each with the standard offset in force across it: start, end (null for
// open) and offset, in seconds. Only tzdata's main format says so; its
// rearguard format, which some systems and ICU ship, marks the higher offset
// instead, so the spans are spelled out to read every zone the same way on
// every host.
const NEGATIVE_DST = {
'Africa/Casablanca': [[1540692000, 1789866000, 3600]],
'Africa/El_Aaiun': [[1540692000, 1789866000, 3600]],
'Africa/Windhoek': [[764200800, 1508796000, 7200]],
Eire: [[57722400, null, 3600]],
'Europe/Dublin': [[57722400, null, 3600]],
};
// The longest step the search for where a tzdata zone's reading changes takes,
// and how far it looks, in seconds. No tzdata 2026c zone holds a reading for
// less than 166 hours after 1970, so a day's step never passes one. A POSIX TZ
// string is read off its rules instead, and the host's zone, which nothing
// bounds, an hour at a time.
const STANDARD_SCAN_STEP = 86400;
const HOST_SCAN_STEP = 3600;
const STANDARD_SCAN_LIMIT = 800 * 86400;
// The UTC years either side of an instant a POSIX TZ string's daylight saving
// windows are laid out over, past STANDARD_SCAN_LIMIT.
const POSIX_SCAN_YEARS = 3;
// How far before an instant a zone with no daylight names is read, and as far
// after, for whether its offset stands above both sides as daylight saving
// time does: every 30 days, out past the longest summer, the US's 1974 one
// included.
const SEASON_READINGS = Array.from({ length: 12 }, (_, i) => (i + 1) * 30 * 86400);
// The host's zone per name Intl gives it, read through one formatter; null
// for a name Intl does not know, which leaves the host's offsets to Date.
const HOST_ZONES = new Map();
// One Intl formatter per zone name, read for the zone's long name, which
// says whether ICU counts an instant as daylight saving time; null for a name
// Intl does not know.
const LONG_NAMES = new Map();
// The name a POSIX TZ string gives its standard time: its first one.
const POSIX_STANDARD = /^(?:<([^>]*)>|([A-Za-z]+))/;
// The daylight saving stretches found so far, per zone, sorted by start, so a
// later date in one costs a lookup.
const STRETCHES = new WeakMap();
/**
* The default style, carrying the clock an invocation renders by.
*
* @param env the command environment, read for `GIT_TEST_DATE_NOW` and `TZ`
*/
export function dateClock(env) {
let now = Math.floor(systemNow());
const test = env?.[NOW_VAR];
if (test !== undefined) {
const found = LEADING_INT.exec(test);
now = found ? parseInt(found[1] ?? '0', 10) : 0;
}
const zoneName = env?.[TZ_VAR]?.replace(/^:/, '') ?? new Intl.DateTimeFormat().resolvedOptions().timeZone;
return {
kind: DateKind.NORMAL,
local: false,
strftime: '',
now,
zone: zoneFromEnv(env),
zoneName,
};
}
/**
* Read a `--date` value.
*
* `auto:<anything>` is the default style, since it names the style for a
* terminal and mirage's output is never one. `local` alone is `default-local`.
* A value starting `format` takes `-local` and then must have its colon; any
* other must be a style name, with or without `-local`.
*
* @param value the value as typed
* @param clock the invocation's clock, from `dateClock`
*/
export function parseDateMode(value, clock) {
if (value.startsWith(AUTO_PREFIX))
return { ...clock, kind: DateKind.NORMAL, local: false, strftime: '' };
if (value === LOCAL_ALIAS)
return { ...clock, kind: DateKind.NORMAL, local: true, strftime: '' };
if (value.startsWith(FORMAT_WORD)) {
let rest = value.slice(FORMAT_WORD.length);
const local = rest.startsWith(LOCAL_SUFFIX);
if (local)
rest = rest.slice(LOCAL_SUFFIX.length);
if (!rest.startsWith(':'))
throw new DateFormatColonError(value);
return { ...clock, kind: DateKind.STRFTIME, local, strftime: rest.slice(1) };
}
const local = value.endsWith(LOCAL_SUFFIX);
const kind = STYLE_NAMES.get(local ? value.slice(0, -LOCAL_SUFFIX.length) : value);
if (kind === undefined)
throw new UnknownDateFormatError(value);
return { ...clock, kind, local, strftime: '' };
}
const pad = (n, width = 2) => String(n).padStart(width, '0');
/**
* An offset as git prints one, `%+05d` of `±HHMM`.
*
* @param offsetMinutes minutes east of UTC
*/
export function zoneText(offsetMinutes) {
const sign = offsetMinutes < 0 ? '-' : '+';
const total = Math.abs(offsetMinutes);
return `${sign}${pad(Math.floor(total / 60))}${pad(total % 60)}`;
}
function fixedWall(timestamp, offsetMinutes) {
const shifted = new Date((timestamp + offsetMinutes * 60) * 1000);
return {
year: shifted.getUTCFullYear(),
month: shifted.getUTCMonth() + 1,
day: shifted.getUTCDate(),
hour: shifted.getUTCHours(),
minute: shifted.getUTCMinutes(),
second: shifted.getUTCSeconds(),
weekday: shifted.getUTCDay(),
};
}
/** A moment on the session's clock, its `TZ` or the host's zone, and its offset. */
function localWall(timestamp, mode) {
const parts = (mode.zone ?? LOCAL_ZONE).parts(new Date(timestamp * 1000));
return [
{
year: parts.year,
month: parts.month + 1,
day: parts.day,
hour: parts.hour,
minute: parts.minute,
second: parts.second,
weekday: parts.weekday,
},
Math.round(parts.offsetSec / 60),
];
}
/** `1 day` or `2 days`, git's Q_ in the untranslated locale. */
export function plural(count, unit) {
return count === 1 ? `${String(count)} ${unit}` : `${String(count)} ${unit}s`;
}
/**
* How long before `now` a moment was, in git's words.
*
* Seconds count exactly; every larger unit is the one below it rounded half up
* (minutes from seconds, hours from minutes, days from hours, then weeks,
* months and years from days), and the first unit whose count stays under its
* limit is the one shown: seconds under 90, minutes under 90, hours under 36,
* days under 14, weeks while under 70 days and months while under 365. Up to
* five years the months left over follow the years. Measured against git
* 2.50.1 and 2.47.3 at every second of the first two days and of every
* transition up to 40 years: 90 s is `2 minutes ago`, 89.5 minutes `2 hours
* ago`.
*
* @param timestamp the moment, in epoch seconds
* @param now the moment counted from
*/
export function relativeDate(timestamp, now) {
const elapsed = now - timestamp;
if (elapsed < 0)
return 'in the future';
if (elapsed < 90)
return `${plural(elapsed, 'second')} ago`;
const minutes = Math.floor((elapsed + 30) / 60);
if (minutes < 90)
return `${plural(minutes, 'minute')} ago`;
const hours = Math.floor((minutes + 30) / 60);
if (hours < 36)
return `${plural(hours, 'hour')} ago`;
const days = Math.floor((hours + 12) / 24);
if (days < 14)
return `${plural(days, 'day')} ago`;
if (days < 70)
return `${plural(Math.floor((days + 3) / 7), 'week')} ago`;
if (days < 365)
return `${plural(Math.floor((days + 15) / 30), 'month')} ago`;
if (days >= 5 * 365)
return `${plural(Math.floor((days + 183) / 365), 'year')} ago`;
const total = Math.floor((days * 24 + 365) / 730);
const years = Math.floor(total / 12);
const months = total % 12;
if (!months)
return `${plural(years, 'year')} ago`;
return `${plural(years, 'year')}, ${plural(months, 'month')} ago`;
}
/**
* The wall clock a style shows and the offset it is at: the date's own, or
* with `-local` the session's.
*
* @param timestamp seconds since the epoch
* @param offsetMinutes the date's own offset, minutes east of UTC
* @param mode carries `local` and the session's zone
*/
function shown(timestamp, offsetMinutes, mode) {
return mode.local
? localWall(timestamp, mode)
: [fixedWall(timestamp, offsetMinutes), offsetMinutes];
}
/**
* The human style: only what the reader cannot tell from now.
*
* git 2.50.1 and 2.47.3, measured over 3.6 million dates against six clocks in
* six zones: a date on today's day is relative, one in another year is
* `Jan 5 2026`, one to four days back in this month is the weekday and time,
* followed by the date's offset when it is not the session's own, and any
* other is `Mon Jan 5 09:05`. The date's day is read at its own offset (the
* session's with `-local`, which never adds an offset) and today's in the
* session's zone.
*
* @param timestamp seconds since the epoch
* @param offsetMinutes the date's own offset, minutes east of UTC
* @param mode carries the clock and the zone
*/
function human(timestamp, offsetMinutes, mode) {
const [today, todayOffset] = localWall(mode.now, mode);
const [wall, offset] = shown(timestamp, offsetMinutes, mode);
if (wall.year === today.year && wall.month === today.month && wall.day === today.day)
return relativeDate(timestamp, mode.now);
const month = MONTHS[wall.month - 1] ?? '';
if (wall.year !== today.year)
return `${month} ${String(wall.day)} ${String(wall.year)}`;
const clock = `${pad(wall.hour)}:${pad(wall.minute)}`;
const weekday = DAYS[wall.weekday] ?? '';
if (wall.month === today.month && today.day - wall.day > 0 && today.day - wall.day < 5) {
const zone = mode.local || offset === todayOffset ? '' : ` ${zoneText(offset)}`;
return `${weekday} ${clock}${zone}`;
}
return `${weekday} ${month} ${String(wall.day)} ${clock}`;
}
/**
* A `format:` template, as git hands it to the C library.
*
* git answers a bare `%s` (the instant), `%z` (the shown offset) and `%Z`
* (empty, or the session zone's name with `-local`) itself, and `%%` stays a
* percent sign. Everything else goes to strftime over the shown wall clock, at
* offset zero named `GMT` unless `-local`, which is why glibc's `%#Z` reads
* `gmt`; a conversion the C library lacks, and a `%` that ends the template,
* stay as typed.
*
* @param timestamp seconds since the epoch
* @param offsetMinutes the date's own offset, minutes east of UTC
* @param mode carries the template, `local` and the zone
*/
function formatted(timestamp, offsetMinutes, mode) {
const zone = mode.local ? (mode.zone ?? LOCAL_ZONE) : GMT;
const moment = new Date((mode.local ? timestamp : timestamp + offsetMinutes * 60) * 1000);
const reading = zone.parts(moment);
const offset = mode.local ? Math.round(reading.offsetSec / 60) : offsetMinutes;
return mode.strftime.replace(DIRECTIVE, (whole, flags, width, modifier, conversion) => {
const bare = !flags && !width && !modifier;
const size = width ? parseInt(width, 10) : 0;
if (bare && conversion === 's')
return bareEpoch(timestamp, reading.year, offsetMinutes, mode);
if (bare && conversion === 'z')
return zoneText(offset);
if (bare && conversion === 'Z')
return mode.local ? reading.abbrev : '';
if (bare && conversion === '%')
return '%';
if (!LIBC_CONVERSIONS.has(conversion))
return whole;
if (conversion === 'z')
return libcZone(reading.offsetSec, flags, size);
const text = conversion === 's'
? String(wallAsStandard(timestamp, moment, mode))
: conversionText(moment, conversion, zone);
return paddedConversion(text, conversion, flags, size);
});
}
/**
* What git prints for a bare `%s`: the instant while the shown wall clock is in
* the years 1970 to 2099, and outside them -1 as an unsigned 64-bit number,
* less the date's own offset unless `-local` (git 2.47.3 and 2.50.1:
* `4102444800 +0530`, 2100-01-01 05:30 there, prints `18446744073709531815`,
* and `253402300799 -1200` `43199`).
*
* @param timestamp seconds since the epoch
* @param year the year of the shown wall clock
* @param offsetMinutes the date's own offset, minutes east of UTC
* @param mode carries `local`
*/
function bareEpoch(timestamp, year, offsetMinutes, mode) {
if (year >= 1970 && year <= 2099)
return String(timestamp);
const offset = mode.local ? 0n : BigInt(offsetMinutes * 60);
return BigInt.asUintN(64, -1n - offset).toString();
}
/**
* One conversion over the shown wall clock, unpadded: a number's digits alone,
* and `%F` with its year unsigned past 9999 where GNU date signs it (git
* 2.47.3: `10000-01-01`).
*
* @param moment the shown wall clock
* @param conversion the conversion letter
* @param zone the zone the moment is read in
*/
function conversionText(moment, conversion, zone) {
if (conversion in PADDED_NUMBERS)
return gnuStrftime(moment, `%-${conversion}`, zone);
if (conversion === 'F')
return gnuStrftime(moment, '%Y-%m-%d', zone);
if (conversion === 'n')
return '\n';
if (conversion === 't')
return '\t';
if (conversion === '%')
return '%';
return gnuStrftime(moment, `%${conversion}`, zone);
}
/**
* A conversion's text cased and padded as the C library does, git 2.47.3
* measured over every conversion under 22 flag sets and 6 widths: the last of
* `_`, `-` and `0` typed decides the padding (`%0_12s` is spaces, `%_012s`
* zeros). `#` upcases a day or month name and lowers `%p` and `%Z`; otherwise
* `^` upcases anything but `%P`. A width pads text and the composites with
* spaces unless `0` is the padding, even under `-`.
*
* @param text the conversion's text, a number's digits unpadded
* @param conversion the conversion letter
* @param flags the directive's flags
* @param width its width, 0 for none
*/
function paddedConversion(text, conversion, flags, width) {
let cased = text;
if (flags.includes('#') && 'aAbBh'.includes(conversion))
cased = text.toUpperCase();
else if (flags.includes('#') && 'pZ'.includes(conversion))
cased = text.toLowerCase();
else if (flags.includes('^') && conversion !== 'P')
cased = text.toUpperCase();
const pad = winningPad(flags);
const digits = PADDED_NUMBERS[conversion];
if (digits !== undefined) {
const spaced = pad === '_' || pad === '-' || (pad === null && SPACE_FILLED_NUMBERS.has(conversion));
return cased.padStart(Math.max(pad === '-' ? 0 : digits, width), spaced ? ' ' : '0');
}
return cased.padStart(width, pad === '0' ? '0' : ' ');
}
/**
* The formatter that reads a zone's long name, built once per name; null for a
* name Intl does not know.
*
* @param name the zone's name
*/
function longNames(name) {
let format = LONG_NAMES.get(name);
if (format === undefined) {
try {
format = new Intl.DateTimeFormat('en-US', { timeZone: name, timeZoneName: 'long' });
}
catch (err) {
if (!(err instanceof RangeError))
throw err;
format = null;
}
LONG_NAMES.set(name, format);
}
return format;
}
/**
* Whether ICU counts an instant as daylight saving time in a named zone, read
* off the zone's long name, or null for a zone ICU names only by its offset
* (`GMT+02:00`) or does not know.
*
* @param name the zone's tzdata name
* @param instant seconds since the epoch
*/
function daylightName(name, instant) {
const format = longNames(name);
if (format === null)
return null;
const long = format.formatToParts(new Date(instant * 1000)).find((part) => part.type === 'timeZoneName')
?.value ?? '';
if (/^GMT([+-]|$)/.test(long))
return null;
return /Daylight|Summer/.test(long);
}
/**
* The session zone's offset at an instant, and whether tzdata calls it
* standard time there. Intl has no daylight saving flag, so a named zone is
* read through its long name, a POSIX TZ string through the name it shows
* (its first is its standard time's), and any other zone through its offsets
* up to a year either side: daylight saving time stands above both.
* Mirrors Python's _zone_reading, which reads tzinfo's dst().
*
* @param instant seconds since the epoch
* @param mode carries the session's zone and its name
*/
function zoneReading(instant, mode) {
const zone = sessionZone(mode);
const offset = offsetAt(zone, instant);
const name = zone === LOCAL_ZONE ? '' : (mode.zoneName ?? '');
for (const [start, end, standard] of NEGATIVE_DST[name] ?? []) {
if (start <= instant && (end === null || instant < end))
return [offset, offset === standard];
}
const daylight = name === '' ? null : daylightName(name, instant);
if (daylight !== null)
return [offset, !daylight];
const posix = mode.zone !== null && longNames(name) === null ? POSIX_STANDARD.exec(name) : null;
if (posix !== null) {
return [offset, zone.parts(new Date(instant * 1000)).abbrev === (posix[1] ?? posix[2])];
}
const lowest = (sign) => Math.min(...SEASON_READINGS.map((shift) => offsetAt(zone, instant + sign * shift)));
return [offset, !(offset > lowest(-1) && offset > lowest(1))];
}
/**
* A zone's offset at an instant, seconds east of UTC; the host's straight from
* Date, which reads it without building a formatter.
*
* @param zone the zone
* @param instant seconds since the epoch
*/
function offsetAt(zone, instant) {
const at = new Date(instant * 1000);
return zone === LOCAL_ZONE ? -at.getTimezoneOffset() * 60 : zone.parts(at).offsetSec;
}
/**
* The host's zone as Intl names it, built once per name; null for a name Intl
* does not know. Mirrors Python's _host_zone.
*
* @param name the name Intl gives the host's zone
*/
function hostZone(name) {
let zone = HOST_ZONES.get(name);
if (zone === undefined) {
zone = name !== '' && longNames(name) !== null ? resolveTz(name) : null;
HOST_ZONES.set(name, zone);
}
return zone;
}
/**
* The zone a session reads its dates in: its TZ's, else the host's when Intl
* can name it, else LOCAL_ZONE, whose offsets only Date knows. Mirrors Python's
* _session_zone.
*
* @param mode carries the session's zone and, without TZ, the host's name
*/
function sessionZone(mode) {
return mode.zone ?? hostZone(mode.zoneName ?? '') ?? LOCAL_ZONE;
}
/**
* Where the zone's reading at an instant changes: the first instant of the
* stretch holding it (`direction` -1) or the first after it (1), found by
* doubling steps up to STANDARD_SCAN_STEP and then halving the last one down to
* the second; null when the reading holds past STANDARD_SCAN_LIMIT. Mirrors
* Python's _reading_edge.
*
* @param instant seconds since the epoch
* @param direction -1 to look back, 1 to look ahead
* @param reading the zone's reading at `instant`
* @param mode carries the session's zone
*/
function readingEdge(instant, direction, reading, mode) {
const same = (at) => {
const [offset, standard] = zoneReading(at, mode);
return offset === reading[0] && standard === reading[1];
};
const longest = sessionZone(mode) === LOCAL_ZONE ? HOST_SCAN_STEP : STANDARD_SCAN_STEP;
let held = instant;
let step = 3600;
while (Math.abs(held - instant) < STANDARD_SCAN_LIMIT) {
let probe = held + direction * step;
if (!same(probe)) {
while (Math.abs(probe - held) > 1) {
const middle = Math.floor((held + probe) / 2);
if (same(middle))
held = middle;
else
probe = middle;
}
return direction < 0 ? held : probe;
}
held = probe;
step = Math.min(2 * step, longest);
}
return null;
}
/**
* The daylight saving stretch holding an instant in a zone given as a POSIX TZ
* string, read off its rules: each UTC year's window from the instant its
* start rule lays out to the one its end rule does, wrapping the year when the
* end comes first, merged across years. Null for a zone of any other kind.
* Mirrors Python's _posix_stretch.
*
* @param instant seconds since the epoch, in daylight saving time
* @param offset the zone's offset then, seconds east of UTC
* @param mode carries the session's zone
*/
function posixStretch(instant, offset, mode) {
if (mode.zone === null)
return null;
const year = new Date(instant * 1000).getUTCFullYear();
const first = year - POSIX_SCAN_YEARS;
const last = year + POSIX_SCAN_YEARS;
const windows = [];
for (let each = first; each <= last; each++) {
const laid = posixTransitions(mode.zone, each);
if (laid === null)
return null;
const [start, end] = laid;
const opens = Date.UTC(each, 0, 1) / 1000;
const closes = Date.UTC(each + 1, 0, 1) / 1000;
const pieces = start <= end
? [[start, end]]
: [
[opens, end],
[start, closes],
];
for (const [begin, finish] of pieces) {
const from = Math.max(begin, opens);
const to = Math.min(finish, closes);
if (from >= to)
continue;
const previous = windows[windows.length - 1];
if (previous !== undefined && previous[1] >= from)
previous[1] = Math.max(previous[1], to);
else
windows.push([from, to]);
}
}
const low = Date.UTC(first, 0, 1) / 1000;
const high = Date.UTC(last + 1, 0, 1) / 1000;
for (const [start, end] of windows) {
if (start <= instant && instant < end) {
return {
start,
end,
offset,
before: start <= low ? null : [start - 1, zoneReading(start - 1, mode)[0]],
after: end >= high ? null : [end, zoneReading(end, mode)[0]],
};
}
}
return { start: instant, end: instant + 1, offset, before: null, after: null };
}
/**
* The standard time nearest an edge, walking on past any other daylight saving
* stretch next to it: the instant where it meets the edge's side and its
* offset, null when none lies within reach. Mirrors Python's _standard_beyond.
*
* @param edge the first instant outside a stretch on this side
* @param direction -1 to look back, 1 to look ahead
* @param mode carries the session's zone
*/
function standardBeyond(edge, direction, mode) {
let at = edge;
while (at !== null && Math.abs(at - edge) < STANDARD_SCAN_LIMIT) {
const reading = zoneReading(at, mode);
if (reading[1])
return [at, reading[0]];
const far = readingEdge(at, direction, reading, mode);
at = far === null ? null : direction < 0 ? far - 1 : far;
}
return null;
}
/**
* The daylight saving stretches found so far in the session's zone, the host's
* under LOCAL_ZONE. Mirrors Python's _stretches_of.
*
* @param mode carries the session's zone, null for the host's
*/
function stretchesOf(mode) {
const zone = sessionZone(mode);
let found = STRETCHES.get(zone);
if (found === undefined) {
found = [];
STRETCHES.set(zone, found);
}
return found;
}
/**
* The standard offset nearest an instant in the session's zone: its own where
* tzdata calls it standard time, else that of the standard time nearer to it,
* before or after. Measured on git 2.47.3 over 20 zones and 2,376 dates each,
* among them Europe/Dublin, whose standard time is IST and whose GMT winter is
* negative daylight saving time, Africa/Casablanca since 2018,
* Africa/Windhoek from 1994 to 2017, Australia/Lord_Howe's half hour and the
* southern hemisphere. A daylight saving stretch is searched once per zone and
* kept. Mirrors Python's _standard_offset.
*
* @param instant seconds since the epoch
* @param mode carries the session's zone
*/
function standardOffset(instant, mode) {
const stretches = stretchesOf(mode);
let low = 0;
let high = stretches.length;
while (low < high) {
const middle = (low + high) >> 1;
if ((stretches[middle]?.start ?? 0) <= instant)
low = middle + 1;
else
high = middle;
}
let stretch = stretches[low - 1];
if (stretch === undefined || instant >= stretch.end) {
const reading = zoneReading(instant, mode);
if (reading[1])
return reading[0];
let found = posixStretch(instant, reading[0], mode);
if (found === null) {
const start = readingEdge(instant, -1, reading, mode);
const end = readingEdge(instant, 1, reading, mode);
found = {
start: start ?? instant,
end: end ?? instant + 1,
offset: reading[0],
before: start === null ? null : standardBeyond(start - 1, -1, mode),
after: end === null ? null : standardBeyond(end, 1, mode),
};
}
stretch = found;
if (stretch.before !== null && stretch.after !== null)
stretches.splice(low, 0, stretch);
}
const { before, after } = stretch;
if (before !== null && (after === null || instant - before[0] <= after[0] - instant)) {
return before[1];
}
return after === null ? stretch.offset : after[1];
}
/**
* What a `%s` that carries a flag, a width or a modifier prints. With
* `-local` that is the instant; otherwise the date's own wall clock read back
* as standard time in the session's zone, so it moves by the difference (git
* 2.47.3, `TZ=UTC`: `999999999 -0930` prints `999965799`; in New York it
* counts EST all year, and in Dublin IST). Mirrors Python's _wall_as_standard.
*
* @param timestamp seconds since the epoch
* @param moment the shown wall clock, as a UTC reading unless `-local`
* @param mode carries `local` and the session's zone
*/
function wallAsStandard(timestamp, moment, mode) {
if (mode.local)
return timestamp;
const wall = Math.floor(moment.getTime() / 1000);
return wall - standardOffset(wall, mode);
}
/**
* What glibc prints for a `%z` that carries a flag, a width or a modifier, from
* the offset its wall clock carries: zero for a date shown at its own offset,
* the session's with `-local`.
*
* The width pads twice, in front of the sign and inside the `hhmm` number,
* which keeps four digits unless `-` drops them, and the last of `_`, `-` and
* `0` typed decides both (git 2.47.3: `%5z` is ` +00000`, `%05z`
* `0000+00000`, `%_z` `+ 0`, `%-z` `+0` and `%0_3z` ` + 0`).
*
* @param offsetSec seconds east of UTC
* @param flags the directive's flags
* @param width its width, 0 for none
*/
function libcZone(offsetSec, flags, width) {
const sign = offsetSec < 0 ? '-' : '+';
const total = Math.floor(Math.abs(offsetSec) / 60);
const number = String(Math.floor(total / 60) * 100 + (total % 60));
const pad = winningPad(flags);
const lead = (pad === '0' ? '0' : ' ').repeat(Math.max(0, width - 1));
if (pad === '-')
return `${lead}${sign}${number.padStart(width)}`;
return `${lead}${sign}${number.padStart(Math.max(4, width), pad === '_' ? ' ' : '0')}`;
}
/**
* A date as `--date` renders it.
*
* `default` is `Mon Jan 5 09:05:00 2026 +0000` (the day unpadded, no offset
* with `-local`), `iso` is `2026-01-05 09:05:00 +0000`, `iso-strict`
* `2026-01-05T09:05:00+00:00` with `Z` for a zero offset, `rfc`
* `Mon, 5 Jan 2026 09:05:00 +0000`, `short` the date alone, `raw` the instant
* and the shown offset, and `unix` and `relative` the instant whatever
* `-local` says.
*
* @param timestamp seconds since the epoch
* @param offsetMinutes the date's own offset, minutes east of UTC
* @param mode the style, with its clock and zone
*/
export function showDate(timestamp, offsetMinutes, mode) {
const kind = mode.kind;
if (kind === DateKind.UNIX)
return String(timestamp);
if (kind === DateKind.RELATIVE)
return relativeDate(timestamp, mode.now);
if (kind === DateKind.HUMAN)
return human(timestamp, offsetMinutes, mode);
if (kind === DateKind.STRFTIME)
return formatted(timestamp, offsetMinutes, mode);
const [wall, offset] = shown(timestamp, offsetMinutes, mode);
if (kind === DateKind.RAW)
return `${String(timestamp)} ${zoneText(offset)}`;
const day = `${pad(wall.year, 4)}-${pad(wall.month)}-${pad(wall.day)}`;
if (kind === DateKind.SHORT)
return day;
const clock = `${pad(wall.hour)}:${pad(wall.minute)}:${pad(wall.second)}`;
const weekday = DAYS[wall.weekday] ?? '';
const month = MONTHS[wall.month - 1] ?? '';
if (kind === DateKind.ISO8601)
return `${day} ${clock} ${zoneText(offset)}`;
if (kind === DateKind.ISO8601_STRICT) {
const zone = zoneText(offset);
return `${day}T${clock}${offset ? `${zone.slice(0, 3)}:${zone.slice(3)}` : 'Z'}`;
}
if (kind === DateKind.RFC2822)
return `${weekday}, ${String(wall.day)} ${month} ${String(wall.year)} ${clock} ${zoneText(offset)}`;
const text = `${weekday} ${month} ${String(wall.day)} ${clock} ${String(wall.year)}`;
return mode.local ? text : `${text} ${zoneText(offset)}`;
}
//# sourceMappingURL=dates.js.map