diff --git a/app/features/feed/core/FeedItems.server.ts b/app/features/feed/core/FeedItems.server.ts index acd5ccc4f..233d356ce 100644 --- a/app/features/feed/core/FeedItems.server.ts +++ b/app/features/feed/core/FeedItems.server.ts @@ -33,6 +33,7 @@ import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.s import * as SplatoonRotationRepository from "~/features/splatoon-rotations/SplatoonRotationRepository.server"; import { getViewerTimezone } from "~/features/timezone/timezone-context.server"; import { eventLeaderboards } from "~/features/tournament-organization/core/leaderboards.server"; +import * as Series from "~/features/tournament-organization/core/Series"; import type { BuildAbilitiesTuple, MainWeaponId, @@ -3467,13 +3468,12 @@ async function seriesWinOf(result: { }) { if (result.organizationId === null) return null; - const series = ( - await FeedRepository.findSeriesByOrganizationId(result.organizationId) - ).find((s) => - s.substringMatches.some((sub) => - result.name.toLowerCase().includes(sub.toLowerCase()), + const series = Series.findByEventName({ + series: await FeedRepository.findSeriesByOrganizationId( + result.organizationId, ), - ); + eventName: result.name, + }); if (!series) return null; return { diff --git a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts index c13a4a5a2..cf8ed33c5 100644 --- a/app/features/tournament-organization/TournamentOrganizationRepository.server.ts +++ b/app/features/tournament-organization/TournamentOrganizationRepository.server.ts @@ -23,6 +23,7 @@ import { } from "~/utils/kysely.server"; import { toDBBoolean } from "~/utils/sql"; import { mySlugify } from "~/utils/urls"; +import * as Series from "./core/Series"; import { TOURNAMENT_SERIES_EVENTS_PER_PAGE } from "./tournament-organization-constants"; interface CreateArgs { @@ -544,11 +545,7 @@ export async function findAllSeriesWinsByUserId({ eb.and([ eb("CalendarEvent.organizationId", "=", organizationId), eb("CalendarEvent.hidden", "=", 0), - eb.or( - substringMatches.map((match) => - eb("CalendarEvent.name", "like", `%${match}%`), - ), - ), + calendarEventNameMatchesSeries(substringMatches)(eb), ]); const tournamentWins = await db @@ -772,12 +769,9 @@ export function update({ for (const s of insertedSeries) { const matchingTiers = finalizedTournaments - .filter((t) => { - const eventNameLower = t.name.toLowerCase(); - return s.substringMatches.some((match) => - eventNameLower.includes(match.toLowerCase()), - ); - }) + .filter((t) => + Series.matchesEventName({ series: s, eventName: t.name }), + ) .filter((t) => t.tier !== null) .map((t) => t.tier); @@ -932,12 +926,7 @@ export async function updateSeriesTierHistory({ .where("organizationId", "=", organizationId) .execute(); - const eventNameLower = eventName.toLowerCase(); - const matchingSeries = series.find((s) => - s.substringMatches.some((match) => - eventNameLower.includes(match.toLowerCase()), - ), - ); + const matchingSeries = Series.findByEventName({ series, eventName }); if (!matchingSeries) return; diff --git a/app/features/tournament-organization/core/Series.test.ts b/app/features/tournament-organization/core/Series.test.ts new file mode 100644 index 000000000..dd8738d6f --- /dev/null +++ b/app/features/tournament-organization/core/Series.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, test } from "vitest"; +import * as Series from "./Series"; + +describe("Series.findByEventName", () => { + const series = [ + { id: 1, substringMatches: ["low ink"] }, + { id: 2, substringMatches: ["Swim Or Sink", "sos"] }, + ]; + + test.each([ + { why: "lower-cased match", eventName: "Low Ink November", expected: 1 }, + { why: "mixed-case match", eventName: "swim or sink 42", expected: 2 }, + { why: "second substring", eventName: "SOS #12", expected: 2 }, + { why: "no match", eventName: "In The Zone 40", expected: undefined }, + ])("$why", ({ eventName, expected }) => { + expect(Series.findByEventName({ series, eventName })?.id).toBe(expected); + }); +}); diff --git a/app/features/tournament-organization/core/Series.ts b/app/features/tournament-organization/core/Series.ts index 0d1a43cbb..9d07827c8 100644 --- a/app/features/tournament-organization/core/Series.ts +++ b/app/features/tournament-organization/core/Series.ts @@ -6,11 +6,22 @@ export function findByEventName({ series: T[]; eventName: string; }) { - const eventNameLower = eventName.toLowerCase(); - return series.find((oneSeries) => - oneSeries.substringMatches.some((substringMatch) => - eventNameLower.includes(substringMatch.toLowerCase()), - ), + matchesEventName({ series: oneSeries, eventName }), + ); +} + +/** Whether one of the series' substrings appears in the event's name. */ +export function matchesEventName({ + series, + eventName, +}: { + series: { substringMatches: string[] }; + eventName: string; +}) { + const eventNameLower = eventName.toLowerCase(); + + return series.substringMatches.some((substringMatch) => + eventNameLower.includes(substringMatch.toLowerCase()), ); } diff --git a/app/features/tournament-organization/core/SeriesTeamCount.server.ts b/app/features/tournament-organization/core/SeriesTeamCount.server.ts index 4dea460b6..0a862fd8f 100644 --- a/app/features/tournament-organization/core/SeriesTeamCount.server.ts +++ b/app/features/tournament-organization/core/SeriesTeamCount.server.ts @@ -4,6 +4,7 @@ import * as R from "remeda"; import { cache, ttl } from "~/utils/cache.server"; import { dateToDatabaseTimestamp } from "~/utils/dates"; import * as TournamentOrganizationRepository from "../TournamentOrganizationRepository.server"; +import * as Series from "./Series"; const CACHE_KEY = "series-team-counts"; /** How old an edition can be and still say something about the next one. */ @@ -33,12 +34,10 @@ export async function lookup() { const series = seriesByOrganizationId.get(tournament.organizationId); if (!series) return tournament.teamCount; - const nameLower = tournament.name.toLowerCase(); - const match = series.find((candidate) => - candidate.substringMatches.some((substring) => - nameLower.includes(substring.toLowerCase()), - ), - ); + const match = Series.findByEventName({ + series, + eventName: tournament.name, + }); if (!match) return tournament.teamCount; return Math.max( @@ -71,9 +70,7 @@ async function seriesTeamCounts() { .filter( (tournament) => tournament.organizationId === row.organizationId && - row.substringMatches.some((substring) => - tournament.name.toLowerCase().includes(substring.toLowerCase()), - ), + Series.matchesEventName({ series: row, eventName: tournament.name }), ) .slice(-EDITIONS_CONSIDERED) .map((tournament) => tournament.teamCount); diff --git a/app/features/tournament-organization/loaders/org.$slug.server.ts b/app/features/tournament-organization/loaders/org.$slug.server.ts index ad0fd1e77..12804081c 100644 --- a/app/features/tournament-organization/loaders/org.$slug.server.ts +++ b/app/features/tournament-organization/loaders/org.$slug.server.ts @@ -4,6 +4,7 @@ import { calculateTentativeTier } from "~/features/tournament/core/tiering"; import * as TrophyRepository from "~/features/trophies/TrophyRepository.server"; import type { SerializeFrom } from "~/utils/remix"; import { eventLeaderboards } from "../core/leaderboards.server"; +import * as Series from "../core/Series"; import * as TournamentOrganizationRepository from "../TournamentOrganizationRepository.server"; import { TOURNAMENT_SERIES_LEADERBOARD_SIZE } from "../tournament-organization-constants"; import { tournamentOrganizationSearchParams } from "../tournament-organization-search-params"; @@ -27,9 +28,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const seriesId = _seriesId ?? - organization.series.find((s) => - s.substringMatches.some((match) => source?.toLowerCase().includes(match)), - )?.id; + (source + ? Series.findByEventName({ + series: organization.series, + eventName: source, + })?.id + : undefined); const seriesInfo = async () => { const series = seriesId