Skip to content

Commit 95eff96

Browse files
cubehouseclaude
andcommitted
fix(destinations): find() prioritizes exact match over substring
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7e36a9d commit 95eff96

2 files changed

Lines changed: 46 additions & 5 deletions

File tree

src/ergonomic/destinations.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,32 @@ export class DestinationsApi {
3333
}
3434

3535
/**
36-
* Find the first destination whose slug or name loosely matches `query`.
36+
* Find the first destination whose slug or name matches `query`.
3737
*
3838
* Matching is case-insensitive and ignores non-alphanumeric characters on
39-
* both sides, then checks substring containment. For example, the query
40-
* `"waltdisneyworld"` matches a destination with slug
41-
* `"walt-disney-world"`. Returns `undefined` when nothing matches.
39+
* both sides. An exact normalized match on either `slug` or `name` wins
40+
* over a substring match — so a query of `"walt-disney-world"` returns the
41+
* destination with that exact slug even when another entry's slug/name
42+
* contains it as a substring (e.g. `"walt-disney-world-extra"`). If no
43+
* exact match is found, the first substring containment match is
44+
* returned. Returns `undefined` when nothing matches.
4245
*/
4346
async find(query: string): Promise<Destination | undefined> {
4447
const res = await this.list();
4548
const needle = normalize(query);
46-
return (res.destinations as Destination[]).find((d) => matches(d, needle));
49+
const destinations = (res.destinations ?? []) as Destination[];
50+
51+
// Pass 1: exact normalized match on slug or name.
52+
for (const d of destinations) {
53+
const slug = normalize(d.slug ?? '');
54+
const name = normalize(d.name ?? '');
55+
if (slug === needle || name === needle) return d;
56+
}
57+
58+
// Pass 2: substring containment.
59+
for (const d of destinations) {
60+
if (matches(d, needle)) return d;
61+
}
62+
return undefined;
4763
}
4864
}

test/unit/destinations.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,4 +63,29 @@ describe('destinations helper', () => {
6363
const d = await tp.destinations.find('waltdisneyworld');
6464
expect(d?.id).toBe('wdw');
6565
});
66+
67+
it('find prefers an exact normalized match over a substring match', async () => {
68+
// Ordering matters: the "extra" entry is listed FIRST so that a pure
69+
// substring scan would return it; the exact-match pass must still pick
70+
// the canonical `walt-disney-world` entry.
71+
const ambiguous = {
72+
destinations: [
73+
{
74+
id: 'wdw-extra',
75+
name: 'Walt Disney World Extra',
76+
slug: 'walt-disney-world-extra',
77+
parks: [],
78+
},
79+
{
80+
id: 'wdw',
81+
name: 'Walt Disney World Resort',
82+
slug: 'walt-disney-world',
83+
parks: [],
84+
},
85+
],
86+
};
87+
const tp = new ThemeParks({ fetch: mockFetch(ambiguous), cache: false });
88+
const d = await tp.destinations.find('walt-disney-world');
89+
expect(d?.id).toBe('wdw');
90+
});
6691
});

0 commit comments

Comments
 (0)