Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
This commit is contained in:
commit
aa5e262f3a
880 changed files with 111042 additions and 0 deletions
65
packages/scheduling/src/nominal.ts
Normal file
65
packages/scheduling/src/nominal.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { InvalidNominalLocalError } from "./errors.js";
|
||||
import type { NominalLocal, WallClock } from "./timezone.js";
|
||||
|
||||
/** Format a wall clock as a nominal local date-time string, `YYYY-MM-DDTHH:mm`. */
|
||||
export function formatNominal(wall: WallClock): NominalLocal {
|
||||
const y = String(wall.year).padStart(4, "0");
|
||||
const mo = String(wall.month).padStart(2, "0");
|
||||
const d = String(wall.day).padStart(2, "0");
|
||||
const h = String(wall.hour).padStart(2, "0");
|
||||
const mi = String(wall.minute).padStart(2, "0");
|
||||
return `${y}-${mo}-${d}T${h}:${mi}`;
|
||||
}
|
||||
|
||||
const NOMINAL_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2}))?$/;
|
||||
|
||||
/**
|
||||
* Parse a nominal local date-time string back into a wall clock. Rejects
|
||||
* malformed strings and impossible calendar values with
|
||||
* `InvalidNominalLocalError`. Note the string may denote a wall time that does
|
||||
* not exist in a zone (gap) or exists twice (fold); existence is a
|
||||
* materialization concern, not a parse concern.
|
||||
*/
|
||||
export function parseNominal(nominal: NominalLocal): WallClock {
|
||||
const match = NOMINAL_RE.exec(nominal);
|
||||
if (!match) {
|
||||
throw new InvalidNominalLocalError(`malformed nominal local date-time: "${nominal}"`);
|
||||
}
|
||||
const wall: WallClock = {
|
||||
year: Number(match[1]),
|
||||
month: Number(match[2]),
|
||||
day: Number(match[3]),
|
||||
hour: Number(match[4]),
|
||||
minute: Number(match[5]),
|
||||
second: match[6] === undefined ? 0 : Number(match[6]),
|
||||
};
|
||||
if (wall.month < 1 || wall.month > 12) {
|
||||
throw new InvalidNominalLocalError(`month out of range in "${nominal}"`);
|
||||
}
|
||||
if (wall.day < 1 || wall.day > 31) {
|
||||
throw new InvalidNominalLocalError(`day out of range in "${nominal}"`);
|
||||
}
|
||||
if (wall.hour > 23 || wall.minute > 59 || wall.second > 59) {
|
||||
throw new InvalidNominalLocalError(`time of day out of range in "${nominal}"`);
|
||||
}
|
||||
return wall;
|
||||
}
|
||||
|
||||
/** Total ordering over wall clocks; safe because fields are validated. */
|
||||
export function compareWall(a: WallClock, b: WallClock): number {
|
||||
if (a.year !== b.year) return a.year < b.year ? -1 : 1;
|
||||
if (a.month !== b.month) return a.month < b.month ? -1 : 1;
|
||||
if (a.day !== b.day) return a.day < b.day ? -1 : 1;
|
||||
if (a.hour !== b.hour) return a.hour < b.hour ? -1 : 1;
|
||||
if (a.minute !== b.minute) return a.minute < b.minute ? -1 : 1;
|
||||
return a.second < b.second ? -1 : a.second > b.second ? 1 : 0;
|
||||
}
|
||||
|
||||
export function wallEquals(a: WallClock, b: WallClock): boolean {
|
||||
return compareWall(a, b) === 0;
|
||||
}
|
||||
|
||||
/** Milliseconds between two wall readings when interpreted as UTC. */
|
||||
export function wallAsEpochMs(wall: WallClock): number {
|
||||
return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue