feat: implement smart name abbreviation for instructor display

This commit is contained in:
2026-01-29 03:09:43 -06:00
parent 0da2e810fe
commit 779144a4d5
3 changed files with 80 additions and 19 deletions
+16 -5
View File
@@ -551,7 +551,15 @@ const table = createSvelteTable({
{@const primary = getPrimaryInstructor(
course.instructors,
)}
{@const display = primaryInstructorDisplay(course)}
{@const commaIdx = display.indexOf(", ")}
<td class="py-2 px-2 whitespace-nowrap">
{#if display === "Staff"}
<span
class="text-xs text-muted-foreground/60 uppercase"
>Staff</span
>
{:else}
<SimpleTooltip
text={primary?.displayName ??
"Staff"}
@@ -559,12 +567,15 @@ const table = createSvelteTable({
side="bottom"
passthrough
>
<span
>{primaryInstructorDisplay(
course,
)}</span
>
{#if commaIdx !== -1}
<span>{display.slice(0, commaIdx)},
<span class="text-muted-foreground">{display.slice(commaIdx + 1)}</span
></span>
{:else}
<span>{display}</span>
{/if}
</SimpleTooltip>
{/if}
{#if primaryRating(course)}
{@const r =
primaryRating(course)!}
+22 -4
View File
@@ -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", () => {
+36 -4
View File
@@ -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 */