Skip to content

Commit

Permalink
Merge pull request #2609 from KaelenProctor/feature/tag-filter-partia…
Browse files Browse the repository at this point in the history
…l-match

Change tag filter to be partial matching instead of exact
  • Loading branch information
dekkerglen authored Feb 13, 2025
2 parents 207447a + be00653 commit 1e9b741
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 3 deletions.
3 changes: 3 additions & 0 deletions src/client/filtering/FuncOperations.js
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ const canCastWith = (mana, cost) => {
cost = [...cost]
.filter((symbol) => symbol[0] === 'x' || symbol[0] === 'y' || symbol[0] === 'z')
.map((symbol) => {
// eslint-disable-next-line no-console -- Debugging
console.debug(symbol);
if (symbol.length === 1) {
const intValue = parseInt(symbol[0], 10);
Expand Down Expand Up @@ -210,6 +211,7 @@ const canCastWith = (mana, cost) => {
intValue = 1;
}
}
// eslint-disable-next-line no-console -- Debugging
console.debug(intValue);
if (Number.isInteger(intValue)) {
const remove = [];
Expand Down Expand Up @@ -281,6 +283,7 @@ export const setCountOperation = (op, value) => {
export const setElementOperation = (op, value) => {
switch (op.toString()) {
case ':':
return (fieldValue) => fieldValue?.some((elem) => elem?.toLowerCase().includes(value));
case '=':
return (fieldValue) => fieldValue?.some((elem) => elem?.toLowerCase() === value);
case '<>':
Expand Down
14 changes: 12 additions & 2 deletions src/client/pages/FiltersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ const FiltersPage: React.FC<FiltersPageProps> = ({ loginCallback }) => (
rows={[
{
query: <code>tag:Signed</code>,
description: 'All cards in a cube who have a tag named Signed, case insensitive.',
description: 'All cards in a cube who have a tag which contains Signed, case insensitive.',
},
{
query: <code>tags=0</code>,
Expand Down Expand Up @@ -492,7 +492,8 @@ const FiltersPage: React.FC<FiltersPageProps> = ({ loginCallback }) => (
<Accordion title="Legality">
<p>
You can use <code>leg:</code>, <code>legal:</code>, or <code>legality:</code> to filter cards by
legality.
legality. Also <code>banned:</code>, <code>ban:</code>, or <code>restricted:</code> to check inversely.
The format name can also be double-quoted.
</p>
<Text semibold>Examples:</Text>
<Table
Expand All @@ -505,6 +506,15 @@ const FiltersPage: React.FC<FiltersPageProps> = ({ loginCallback }) => (
query: <code>-leg:Standard</code>,
description: 'All cards that are not legal in Standard.',
},
{
query: <code>banned:Modern</code>,
description: 'All cards that are banned in Modern.',
},
{
query: <code>restricted:"Vintage"</code>,
description:
'All cards that are restricted in Vintage (the only format with restrictions currently).',
},
]}
/>
</Accordion>
Expand Down
63 changes: 63 additions & 0 deletions tests/cards/filterOperations.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { setElementOperation } from '../../src/client/filtering/FuncOperations';

describe('setElementOperation', () => {
it('Invalid operator', async () => {
const result = () => setElementOperation('+=', 'Tag');
expect(result).toThrow(Error);
expect(result).toThrow('Unrecognized operator');
});

it('Equality operator matching', async () => {
//Predicate values are lowercased by the nearley grammar
const filterer = setElementOperation('=', 'fetch');

//The filter function for tags lowercases the inputs
const tags = ['brazen', 'Fetch', 'Kindred'];
expect(filterer(tags)).toBeTruthy();
});

it('Equality operator NOT matching', async () => {
//Predicate values are lowercased by the nearley grammar
const filterer = setElementOperation('=', 'travel');

//The filter function for tags lowercases the inputs
const tags = ['brazen', 'Fetch', 'Kindred'];
expect(filterer(tags)).toBeFalsy();
});

it('Contains operator matching', async () => {
//Predicate values are lowercased by the nearley grammar
const filterer = setElementOperation(':', 'fetch');

//The filter function for tags lowercases the inputs
const tags = ['brazen', 'Fetch Land', 'Kindred'];
expect(filterer(tags)).toBeTruthy();
});

it('Contains operator NOT matching', async () => {
//Predicate values are lowercased by the nearley grammar
const filterer = setElementOperation(':', 'rap');

//The filter function for tags lowercases the inputs
const tags = ['brazen', 'Fetch Land', 'Kindred'];
expect(filterer(tags)).toBeFalsy();
});

it.each(['!=', '<>'])('Inequality operator matching (%s)', async (op) => {
//Predicate values are lowercased by the nearley grammar
const filterer = setElementOperation(op, 'fetch');

//The filter function for tags lowercases the inputs
const tags = ['brazen', 'Fetch', 'Kindred'];
expect(filterer(tags)).toBeFalsy();
});

it.each(['!=', '<>'])('Inequality operator NOT matching (%s)', async (op) => {
//Predicate values are lowercased by the nearley grammar
const filterer = setElementOperation(op, 'transit');

//The filter function for tags lowercases the inputs
const tags = ['brazen', 'Fetch', 'Kindred'];
expect(filterer(tags)).toBeTruthy();
});
});
69 changes: 68 additions & 1 deletion tests/cards/filtering.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FilterResult, makeFilter } from '../../src/client/filtering/FilterCards';

describe('Filter syntax', () => {
describe('Name filter syntax', () => {
const assertValidNameFilter = (result: FilterResult) => {
expect(result.err).toBeFalsy();
expect(result.filter).toBeInstanceOf(Function);
Expand Down Expand Up @@ -129,3 +129,70 @@ describe('Filter syntax', () => {
expect(result.err).toBeTruthy();
});
});

describe('Tag filter syntax', () => {
const assertValidTagFilter = (result: FilterResult) => {
expect(result.err).toBeFalsy();
expect(result.filter).toBeInstanceOf(Function);
expect(result.filter?.fieldsUsed).toEqual(['tags']);
};

it('Tag contents filter operations', async () => {
assertValidTagFilter(makeFilter('tag:fetch'));
assertValidTagFilter(makeFilter('tags:fetch'));
});

it('Tag count filter operations', async () => {
//All filters based on the number of tags
assertValidTagFilter(makeFilter('tag=3'));
assertValidTagFilter(makeFilter('tag!=3'));
assertValidTagFilter(makeFilter('tag<>3'));
assertValidTagFilter(makeFilter('tag>0'));
assertValidTagFilter(makeFilter('tag>=0'));
assertValidTagFilter(makeFilter('tag<2'));
assertValidTagFilter(makeFilter('tag<=1'));
});
});

const formatFilters = ['legality', 'legal', 'leg', 'banned', 'ban', 'restricted'];

describe.each(formatFilters)('Format filter (%s)', (filterName) => {
const assertLegalityFilter = (result: FilterResult) => {
expect(result.err).toBeFalsy();
expect(result.filter).toBeInstanceOf(Function);
//All format type filters use the card's legality information
expect(result.filter?.fieldsUsed).toEqual(['legality']);
};

const availableFormats = [
'Standard',
'Pioneer',
'Modern',
'Legacy',
'Vintage',
'Brawl',
'Historic',
'Pauper',
'Penny',
'Commander',
];

it.each(availableFormats)(`${filterName} filtering (%s)`, async (formatName) => {
assertLegalityFilter(makeFilter(`${filterName}:${formatName}`));
assertLegalityFilter(makeFilter(`${filterName}:"${formatName}"`));
});

it(`${filterName} filtering other operators`, async () => {
assertLegalityFilter(makeFilter(`${filterName}=Legacy`));
assertLegalityFilter(makeFilter(`${filterName}<>Standard`));
assertLegalityFilter(makeFilter(`${filterName}!="Modern"`));
assertLegalityFilter(makeFilter(`${filterName}!="Commander"`));
});

it(`${filterName} filtering unknown formats`, async () => {
expect(makeFilter(`${filterName}:TinyLeaders`).err).toBeTruthy();
expect(makeFilter(`${filterName}:Lega`).err).toBeTruthy();
expect(makeFilter(`${filterName}:EDH`).err).toBeTruthy();
expect(makeFilter(`${filterName}:"Oathbreaker"`).err).toBeTruthy();
});
});

0 comments on commit 1e9b741

Please sign in to comment.