From ff9626c7c5651cb80e6e0f968345c802bcc1e018 Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Fri, 16 Jan 2026 20:19:07 -0500 Subject: [PATCH 1/2] Fixing this up --- data/timezones.json | 16 +++- src/commands/timezone/timezone.ts | 131 ++++++++++++++++++++++-------- 2 files changed, 112 insertions(+), 35 deletions(-) diff --git a/data/timezones.json b/data/timezones.json index d1a6c2de..182afb71 100644 --- a/data/timezones.json +++ b/data/timezones.json @@ -1,3 +1,15 @@ { - "366895110027214858": "EST" -} + "version": 1, + "updatedAt": "2026-01-17T01:18:58.565Z", + "zones": { + "global:366895110027214858": { + "version": 1, + "userId": "366895110027214858", + "guildId": null, + "timezone": "America/New_York", + "label": "ET", + "createdAt": "2026-01-17T01:18:58.565Z", + "updatedAt": "2026-01-17T01:18:58.565Z" + } + } +} \ No newline at end of file diff --git a/src/commands/timezone/timezone.ts b/src/commands/timezone/timezone.ts index a0463d19..6dcc2cc1 100644 --- a/src/commands/timezone/timezone.ts +++ b/src/commands/timezone/timezone.ts @@ -16,11 +16,11 @@ export const data = new SlashCommandBuilder() .addSubcommand((s) => s .setName("set") - .setDescription("Save your timezone (IANA like America/New_York or alias like EST)") + .setDescription("Save your timezone (IANA like America/New_York or short name like ET)") .addStringOption((o) => o .setName("zone") - .setDescription("IANA zone (America/New_York) or alias (EST, CST, PST)") + .setDescription('Examples: "US Eastern", "ET", "America/New_York"') .setRequired(true), ) .addBooleanOption((o) => @@ -87,7 +87,7 @@ export const data = new SlashCommandBuilder() .addStringOption((o) => o .setName("to") - .setDescription("Target zone (IANA or alias like PST)") + .setDescription('Target zone (examples: "US Pacific", "PT", "America/Los_Angeles")') .setRequired(true), ) .addStringOption((o) => @@ -108,7 +108,6 @@ export const data = new SlashCommandBuilder() export async function execute(interaction: ChatInputCommandInteraction): Promise { const sub = interaction.options.getSubcommand(true); - // Most timezone ops should be ephemeral to avoid channel noise. await interaction.deferReply({ ephemeral: true }); try { @@ -126,20 +125,54 @@ export async function execute(interaction: ChatInputCommandInteraction): Promise } /* -------------------------------------------------------------------------- */ -/* IMPLEMENTATION */ +/* Timezone input normalization */ /* -------------------------------------------------------------------------- */ -const ALIAS_TO_IANA: Record = { - est: { tz: "America/New_York", label: "EST" }, - edt: { tz: "America/New_York", label: "EDT" }, - cst: { tz: "America/Chicago", label: "CST" }, - cdt: { tz: "America/Chicago", label: "CDT" }, - mst: { tz: "America/Denver", label: "MST" }, - mdt: { tz: "America/Denver", label: "MDT" }, - pst: { tz: "America/Los_Angeles", label: "PST" }, - pdt: { tz: "America/Los_Angeles", label: "PDT" }, - gmt: { tz: "Etc/UTC", label: "GMT" }, +/** + * Small curated set of friendly names -> IANA zones. + * This avoids exposing the full IANA list while covering common needs. + */ +const COMMON_TIMEZONES: Array<{ name: string; tz: string; label?: string }> = [ + { name: "US Eastern", tz: "America/New_York", label: "ET" }, + { name: "US Central", tz: "America/Chicago", label: "CT" }, + { name: "US Mountain", tz: "America/Denver", label: "MT" }, + { name: "US Pacific", tz: "America/Los_Angeles", label: "PT" }, + + { name: "UTC", tz: "Etc/UTC", label: "UTC" }, + + { name: "UK", tz: "Europe/London" }, + { name: "Central Europe", tz: "Europe/Berlin" }, + + { name: "India", tz: "Asia/Kolkata" }, + { name: "Japan", tz: "Asia/Tokyo" }, + { name: "Australia East", tz: "Australia/Sydney" }, +]; + +/** + * Common abbreviations people actually type. + * Note: abbreviations are ambiguous globally, but this is a pragmatic bot UX choice. + */ +const ALIAS_TO_IANA: Record = { + // US / common + et: { tz: "America/New_York", label: "ET" }, + est: { tz: "America/New_York", label: "ET" }, + edt: { tz: "America/New_York", label: "ET" }, + + ct: { tz: "America/Chicago", label: "CT" }, + cst: { tz: "America/Chicago", label: "CT" }, + cdt: { tz: "America/Chicago", label: "CT" }, + + mt: { tz: "America/Denver", label: "MT" }, + mst: { tz: "America/Denver", label: "MT" }, + mdt: { tz: "America/Denver", label: "MT" }, + + pt: { tz: "America/Los_Angeles", label: "PT" }, + pst: { tz: "America/Los_Angeles", label: "PT" }, + pdt: { tz: "America/Los_Angeles", label: "PT" }, + + // UTC-ish utc: { tz: "Etc/UTC", label: "UTC" }, + gmt: { tz: "Etc/UTC", label: "UTC" }, }; function isValidIanaZone(tz: string): boolean { @@ -151,14 +184,32 @@ function isValidIanaZone(tz: string): boolean { } } +function normalizeKey(s: string): string { + return s.trim().toLowerCase().replace(/\s+/g, " "); +} + function normalizeZoneInput(raw: string): { tz: string; label?: string } | null { const v = raw.trim(); if (!v) return null; - const key = v.toLowerCase(); - const hit = ALIAS_TO_IANA[key]; - if (hit) return { tz: hit.tz, label: hit.label }; + const key = normalizeKey(v); + + // Friendly names: "us eastern", "central europe", etc. + for (const z of COMMON_TIMEZONES) { + if (normalizeKey(z.name) === key) return { tz: z.tz, label: z.label }; + } + // Allow a couple shorthand friendly variants people type + if (key === "eastern" || key === "east") return { tz: "America/New_York", label: "ET" }; + if (key === "central" || key === "midwest") return { tz: "America/Chicago", label: "CT" }; + if (key === "mountain") return { tz: "America/Denver", label: "MT" }; + if (key === "pacific" || key === "west") return { tz: "America/Los_Angeles", label: "PT" }; + + // Abbreviations: "ET", "PST", etc. + const alias = ALIAS_TO_IANA[key.replace(/\./g, "")]; + if (alias) return { tz: alias.tz, label: alias.label }; + + // Power user path: accept IANA directly if (isValidIanaZone(v)) return { tz: v }; return null; @@ -227,6 +278,21 @@ async function getTzOrNull( return getUserTimezone({ userId, guildId, scope }); } +function shortHint(): string { + return [ + "Examples:", + '`/timezone set zone:"US Eastern"`', + "`/timezone set zone:ET`", + "`/timezone set zone:America/New_York`", + "", + "Common zones: US Eastern, US Central, US Mountain, US Pacific, UTC, UK, Central Europe, India, Japan, Australia East", + ].join("\n"); +} + +/* -------------------------------------------------------------------------- */ +/* Handlers */ +/* -------------------------------------------------------------------------- */ + async function handleSet(interaction: ChatInputCommandInteraction): Promise { const raw = interaction.options.getString("zone", true); const guildFlag = interaction.options.getBoolean("guild") ?? false; @@ -235,7 +301,7 @@ async function handleSet(interaction: ChatInputCommandInteraction): Promise { @@ -313,7 +377,7 @@ async function handleCompare(interaction: ChatInputCommandInteraction): Promise< if (!a) { await interaction.editReply( - "You have no timezone saved. Set it with `/timezone set zone:America/New_York` first.", + ["You have no timezone saved.", "Run `/timezone set` first.", "", shortHint()].join("\n"), ); return; } @@ -338,8 +402,8 @@ async function handleCompare(interaction: ChatInputCommandInteraction): Promise< [ "Timezone compare:", "", - `${interaction.user.username}: ${aNow} (${a.timezone}) • ${aOff}`, - `${target.username}: ${bNow} (${b.timezone}) • ${bOff}`, + `${interaction.user.username}: ${aNow} (${a.timezone}) | ${aOff}`, + `${target.username}: ${bNow} (${b.timezone}) | ${bOff}`, "", `${target.username} is ${rel}.`, ].join("\n"), @@ -350,6 +414,7 @@ function parseTimeString(raw: string): { hours: number; minutes: number } | null const s = raw.trim().toLowerCase(); if (!s) return null; + // Match "19:30" or "7:30pm" or "7pm" const m = s.match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i); if (!m) return null; @@ -409,7 +474,7 @@ async function handleConvert(interaction: ChatInputCommandInteraction): Promise< const toNorm = normalizeZoneInput(rawTo); if (!toNorm) { await interaction.editReply( - "I could not understand the **to** timezone. Use an IANA zone like `America/Los_Angeles` or `PST`.", + ["I could not understand the **to** timezone.", "", shortHint()].join("\n"), ); return; } @@ -421,7 +486,7 @@ async function handleConvert(interaction: ChatInputCommandInteraction): Promise< const fromNorm = normalizeZoneInput(rawFrom); if (!fromNorm) { await interaction.editReply( - "I could not understand the **from** timezone. Use an IANA zone like `America/New_York` or `EST`.", + ["I could not understand the **from** timezone.", "", shortHint()].join("\n"), ); return; } @@ -461,10 +526,10 @@ async function handleConvert(interaction: ChatInputCommandInteraction): Promise< [ "Time conversion:", "", - `From: ${fromTime} (${fromTz}${fromLabel ? `, ${fromLabel}` : ""}) • ${fromOff}`, - `To: ${toTime} (${toNorm.tz}${toNorm.label ? `, ${toNorm.label}` : ""}) • ${toOff}`, + `From: ${fromTime} (${fromTz}${fromLabel ? `, ${fromLabel}` : ""}) | ${fromOff}`, + `To: ${toTime} (${toNorm.tz}${toNorm.label ? `, ${toNorm.label}` : ""}) | ${toOff}`, "", `That is ${rel}.`, ].join("\n"), ); -} +} \ No newline at end of file From 429aaeab2b0e3f2c4c4f8bfeeb4bba34974a498c Mon Sep 17 00:00:00 2001 From: NickTheDevOpsGuy Date: Fri, 16 Jan 2026 20:19:26 -0500 Subject: [PATCH 2/2] style: auto-format with Prettier [skip-precheck] --- data/timezones.json | 2 +- src/commands/timezone/timezone.ts | 28 ++++++++++++++++++---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/data/timezones.json b/data/timezones.json index 182afb71..3ced8d7b 100644 --- a/data/timezones.json +++ b/data/timezones.json @@ -12,4 +12,4 @@ "updatedAt": "2026-01-17T01:18:58.565Z" } } -} \ No newline at end of file +} diff --git a/src/commands/timezone/timezone.ts b/src/commands/timezone/timezone.ts index 6dcc2cc1..5ea9cfe4 100644 --- a/src/commands/timezone/timezone.ts +++ b/src/commands/timezone/timezone.ts @@ -16,7 +16,9 @@ export const data = new SlashCommandBuilder() .addSubcommand((s) => s .setName("set") - .setDescription("Save your timezone (IANA like America/New_York or short name like ET)") + .setDescription( + "Save your timezone (IANA like America/New_York or short name like ET)", + ) .addStringOption((o) => o .setName("zone") @@ -87,7 +89,9 @@ export const data = new SlashCommandBuilder() .addStringOption((o) => o .setName("to") - .setDescription('Target zone (examples: "US Pacific", "PT", "America/Los_Angeles")') + .setDescription( + 'Target zone (examples: "US Pacific", "PT", "America/Los_Angeles")', + ) .setRequired(true), ) .addStringOption((o) => @@ -201,9 +205,11 @@ function normalizeZoneInput(raw: string): { tz: string; label?: string } | null // Allow a couple shorthand friendly variants people type if (key === "eastern" || key === "east") return { tz: "America/New_York", label: "ET" }; - if (key === "central" || key === "midwest") return { tz: "America/Chicago", label: "CT" }; + if (key === "central" || key === "midwest") + return { tz: "America/Chicago", label: "CT" }; if (key === "mountain") return { tz: "America/Denver", label: "MT" }; - if (key === "pacific" || key === "west") return { tz: "America/Los_Angeles", label: "PT" }; + if (key === "pacific" || key === "west") + return { tz: "America/Los_Angeles", label: "PT" }; // Abbreviations: "ET", "PST", etc. const alias = ALIAS_TO_IANA[key.replace(/\./g, "")]; @@ -336,9 +342,7 @@ async function handleShow(interaction: ChatInputCommandInteraction): Promise { @@ -377,7 +383,9 @@ async function handleCompare(interaction: ChatInputCommandInteraction): Promise< if (!a) { await interaction.editReply( - ["You have no timezone saved.", "Run `/timezone set` first.", "", shortHint()].join("\n"), + ["You have no timezone saved.", "Run `/timezone set` first.", "", shortHint()].join( + "\n", + ), ); return; } @@ -532,4 +540,4 @@ async function handleConvert(interaction: ChatInputCommandInteraction): Promise< `That is ${rel}.`, ].join("\n"), ); -} \ No newline at end of file +}