From 779144a4d55a6a7507d4751ea554e51856c79cab Mon Sep 17 00:00:00 2001 From: Xevion Date: Thu, 29 Jan 2026 03:09:43 -0600 Subject: [PATCH] feat: implement smart name abbreviation for instructor display --- web/src/lib/components/CourseTable.svelte | 33 ++++++++++++------- web/src/lib/course.test.ts | 26 ++++++++++++--- web/src/lib/course.ts | 40 ++++++++++++++++++++--- 3 files changed, 80 insertions(+), 19 deletions(-) diff --git a/web/src/lib/components/CourseTable.svelte b/web/src/lib/components/CourseTable.svelte index 5386167..a7e954f 100644 --- a/web/src/lib/components/CourseTable.svelte +++ b/web/src/lib/components/CourseTable.svelte @@ -551,20 +551,31 @@ const table = createSvelteTable({ {@const primary = getPrimaryInstructor( course.instructors, )} + {@const display = primaryInstructorDisplay(course)} + {@const commaIdx = display.indexOf(", ")} - + {#if display === "Staff"} {primaryInstructorDisplay( - course, - )}Staff - + {:else} + + {#if commaIdx !== -1} + {display.slice(0, commaIdx)}, + {display.slice(commaIdx + 1)} + {:else} + {display} + {/if} + + {/if} {#if primaryRating(course)} {@const r = primaryRating(course)!} diff --git a/web/src/lib/course.test.ts b/web/src/lib/course.test.ts index cb8cba0..6aaac34 100644 --- a/web/src/lib/course.test.ts +++ b/web/src/lib/course.test.ts @@ -101,11 +101,29 @@ describe("formatMeetingTime", () => { }); describe("abbreviateInstructor", () => { - it("abbreviates standard name", () => - expect(abbreviateInstructor("Heaps, John")).toBe("Heaps, J.")); + it("returns short names unabbreviated", () => + expect(abbreviateInstructor("Li, Bo")).toBe("Li, Bo")); + it("returns names within budget unabbreviated", () => + expect(abbreviateInstructor("Heaps, John")).toBe("Heaps, John")); it("handles no comma", () => expect(abbreviateInstructor("Staff")).toBe("Staff")); - it("handles multiple first names", () => - expect(abbreviateInstructor("Smith, Mary Jane")).toBe("Smith, M.")); + + // Progressive abbreviation with multiple given names + it("abbreviates trailing given names first", () => + expect(abbreviateInstructor("Ramirez, Maria Elena")).toBe("Ramirez, Maria E.")); + it("abbreviates all given names when needed", () => + expect(abbreviateInstructor("Ramirez, Maria Elena", 16)).toBe("Ramirez, M. E.")); + it("falls back to first initial only", () => + expect(abbreviateInstructor("Ramirez, Maria Elena", 12)).toBe("Ramirez, M.")); + + // Single given name that exceeds budget + it("abbreviates single given name when over budget", () => + expect(abbreviateInstructor("Bartholomew, Christopher", 18)).toBe("Bartholomew, C.")); + + // Respects custom maxLen + it("keeps full name when within custom budget", () => + expect(abbreviateInstructor("Ramirez, Maria Elena", 30)).toBe("Ramirez, Maria Elena")); + it("always abbreviates when budget is tiny", () => + expect(abbreviateInstructor("Heaps, John", 5)).toBe("Heaps, J.")); }); describe("getPrimaryInstructor", () => { diff --git a/web/src/lib/course.ts b/web/src/lib/course.ts index 82498a9..a402e3c 100644 --- a/web/src/lib/course.ts +++ b/web/src/lib/course.ts @@ -54,13 +54,45 @@ export function formatMeetingTime(mt: DbMeetingTime): string { return `${days} ${begin}–${end}`; } -/** Abbreviate instructor name: "Heaps, John" → "Heaps, J." */ -export function abbreviateInstructor(name: string): string { +/** + * Progressively abbreviate an instructor name to fit within a character budget. + * + * Tries each level until the result fits `maxLen`: + * 1. Full name: "Ramirez, Maria Elena" + * 2. Abbreviate trailing given names: "Ramirez, Maria E." + * 3. Abbreviate all given names: "Ramirez, M. E." + * 4. First initial only: "Ramirez, M." + * + * Names without a comma (e.g. "Staff") are returned as-is. + */ +export function abbreviateInstructor(name: string, maxLen: number = 18): string { + if (name.length <= maxLen) return name; + const commaIdx = name.indexOf(", "); if (commaIdx === -1) return name; + const last = name.slice(0, commaIdx); - const first = name.slice(commaIdx + 2); - return `${last}, ${first.charAt(0)}.`; + const parts = name.slice(commaIdx + 2).split(" "); + + // Level 2: abbreviate trailing given names, keep first given name intact + // "Maria Elena" → "Maria E." + if (parts.length > 1) { + const abbreviated = [parts[0], ...parts.slice(1).map((p) => `${p[0]}.`)].join(" "); + const result = `${last}, ${abbreviated}`; + if (result.length <= maxLen) return result; + } + + // Level 3: abbreviate all given names + // "Maria Elena" → "M. E." + if (parts.length > 1) { + const allInitials = parts.map((p) => `${p[0]}.`).join(" "); + const result = `${last}, ${allInitials}`; + if (result.length <= maxLen) return result; + } + + // Level 4: first initial only + // "Maria Elena" → "M." or "John" → "J." + return `${last}, ${parts[0][0]}.`; } /** Get primary instructor from a course, or first instructor */