👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

color count queries

+136 -19
+6 -6
.claude/SEARCH.md
··· 176 176 177 177 This matches Scryfall's behavior: `id:rg` finds cards playable in a Gruul commander deck. 178 178 179 - ### Identity Count Queries 179 + ### Color Count Queries 180 180 181 - The `identity` field also supports numeric comparisons on the *number* of colors: 182 - - `id>1` - Cards with more than 1 identity color (multicolor+) 183 - - `id=2` - Cards with exactly 2 identity colors 184 - - `id<=1` - Mono-color or colorless cards 185 - - `id=0` - Colorless cards only 181 + Both `color` and `identity` fields support numeric comparisons on the *number* of colors: 182 + - `c>1` or `id>1` - Cards with more than 1 color (multicolor+) 183 + - `c=2` or `id=2` - Cards with exactly 2 colors 184 + - `c<=1` or `id<=1` - Mono-color or colorless cards 185 + - `c=0` or `id=0` - Colorless cards only 186 186 187 187 This is useful for finding mono-color commanders, multicolor cards, etc. 188 188
+14
src/lib/search/__tests__/describe.test.ts
··· 114 114 }); 115 115 116 116 it.each([ 117 + ["c=0", "cards with exactly 0 colors"], 118 + ["c=1", "cards with exactly 1 color"], 119 + ["c=2", "cards with exactly 2 colors"], 120 + ["c>1", "cards with more than 1 color"], 121 + ["c>0", "cards with more than 0 colors"], 122 + ["c<3", "cards with fewer than 3 colors"], 123 + ["c>=2", "cards with 2 or more colors"], 124 + ["c<=1", "cards with 1 or fewer colors"], 125 + ["c!=1", "cards without exactly 1 color"], 126 + ])("color count: `%s` → %s", (query, expected) => { 127 + expect(desc(query)).toBe(expected); 128 + }); 129 + 130 + it.each([ 117 131 [ 118 132 "fire id>=g", 119 133 'name includes "fire" AND color identity includes at least {G}',
+88
src/lib/search/__tests__/integration.test.ts
··· 401 401 ); 402 402 }); 403 403 404 + describe("color count matching", () => { 405 + const mockColorless = { colors: [] as string[] } as Card; 406 + const mockMono = { colors: ["R"] } as Card; 407 + const mockTwoColor = { colors: ["U", "R"] } as Card; 408 + const mockThreeColor = { colors: ["W", "U", "B"] } as Card; 409 + const mockFiveColor = { colors: ["W", "U", "B", "R", "G"] } as Card; 410 + 411 + it.each([ 412 + ["c=0", mockColorless, true], 413 + ["c=0", mockMono, false], 414 + ["c=1", mockMono, true], 415 + ["c=1", mockTwoColor, false], 416 + ["c=2", mockTwoColor, true], 417 + ["c=3", mockThreeColor, true], 418 + ["c=5", mockFiveColor, true], 419 + ])("%s matches card with %d colors: %s", (query, card, expected) => { 420 + const result = search(query); 421 + expect(result.ok).toBe(true); 422 + if (result.ok) { 423 + expect(result.value.match(card)).toBe(expected); 424 + } 425 + }); 426 + 427 + it.each([ 428 + ["c>0", mockColorless, false], 429 + ["c>0", mockMono, true], 430 + ["c>1", mockMono, false], 431 + ["c>1", mockTwoColor, true], 432 + ["c>2", mockThreeColor, true], 433 + ])("%s (more than N colors) matches correctly", (query, card, expected) => { 434 + const result = search(query); 435 + expect(result.ok).toBe(true); 436 + if (result.ok) { 437 + expect(result.value.match(card)).toBe(expected); 438 + } 439 + }); 440 + 441 + it.each([ 442 + ["c<1", mockColorless, true], 443 + ["c<1", mockMono, false], 444 + ["c<2", mockMono, true], 445 + ["c<2", mockTwoColor, false], 446 + ["c<3", mockTwoColor, true], 447 + ])( 448 + "%s (fewer than N colors) matches correctly", 449 + (query, card, expected) => { 450 + const result = search(query); 451 + expect(result.ok).toBe(true); 452 + if (result.ok) { 453 + expect(result.value.match(card)).toBe(expected); 454 + } 455 + }, 456 + ); 457 + 458 + it.each([ 459 + ["c>=1", mockColorless, false], 460 + ["c>=1", mockMono, true], 461 + ["c>=2", mockMono, false], 462 + ["c>=2", mockTwoColor, true], 463 + ["c<=2", mockThreeColor, false], 464 + ["c<=3", mockThreeColor, true], 465 + ])( 466 + "%s (N or more/fewer colors) matches correctly", 467 + (query, card, expected) => { 468 + const result = search(query); 469 + expect(result.ok).toBe(true); 470 + if (result.ok) { 471 + expect(result.value.match(card)).toBe(expected); 472 + } 473 + }, 474 + ); 475 + 476 + it.each([ 477 + ["c!=1", mockMono, false], 478 + ["c!=1", mockTwoColor, true], 479 + ["c!=2", mockTwoColor, false], 480 + ])( 481 + "%s (not exactly N colors) matches correctly", 482 + (query, card, expected) => { 483 + const result = search(query); 484 + expect(result.ok).toBe(true); 485 + if (result.ok) { 486 + expect(result.value.match(card)).toBe(expected); 487 + } 488 + }, 489 + ); 490 + }); 491 + 404 492 describe("mana value matching", () => { 405 493 it.each([ 406 494 ["cmc=1", "Lightning Bolt", true],
+12 -8
src/lib/search/describe.ts
··· 212 212 function describeField(node: FieldNode): string { 213 213 const fieldLabel = FIELD_LABELS[node.field]; 214 214 215 - // Special handling for identity count queries (id>1, id=2, etc.) 216 - if (node.field === "identity" && node.value.kind === "number") { 215 + // Special handling for color/identity count queries (c>1, id>1, c=2, id=2, etc.) 216 + if ( 217 + (node.field === "color" || node.field === "identity") && 218 + node.value.kind === "number" 219 + ) { 217 220 const n = node.value.value; 221 + const label = node.field === "identity" ? "identity " : ""; 218 222 // Grammatically: "1 color" but "0/2/3 colors", "fewer than 3 colors", "2 or more colors" 219 223 const colorWordExact = n === 1 ? "color" : "colors"; 220 224 switch (node.operator) { 221 225 case ":": 222 226 case "=": 223 - return `cards with exactly ${n} identity ${colorWordExact}`; 227 + return `cards with exactly ${n} ${label}${colorWordExact}`; 224 228 case "!=": 225 - return `cards without exactly ${n} identity ${colorWordExact}`; 229 + return `cards without exactly ${n} ${label}${colorWordExact}`; 226 230 case "<": 227 - return `cards with fewer than ${n} identity colors`; 231 + return `cards with fewer than ${n} ${label}colors`; 228 232 case ">": 229 - return `cards with more than ${n} identity ${colorWordExact}`; 233 + return `cards with more than ${n} ${label}${colorWordExact}`; 230 234 case "<=": 231 - return `cards with ${n} or fewer identity colors`; 235 + return `cards with ${n} or fewer ${label}colors`; 232 236 case ">=": 233 - return `cards with ${n} or more identity colors`; 237 + return `cards with ${n} or more ${label}colors`; 234 238 } 235 239 } 236 240
+12 -1
src/lib/search/fields.ts
··· 88 88 return ok(compileOracleText(operator, value)); 89 89 90 90 // Color fields 91 - case "color": 91 + case "color": { 92 + // Numeric comparison: c>1 means "more than 1 color" 93 + if (value.kind === "number") { 94 + return ok( 95 + createOrderedMatcher( 96 + (card) => card.colors?.length ?? 0, 97 + value.value, 98 + operator, 99 + ), 100 + ); 101 + } 92 102 return ok(compileColorField((c) => c.colors, operator, value)); 103 + } 93 104 94 105 case "identity": { 95 106 // Numeric comparison: id>1 means "more than 1 color in identity"
+4 -4
src/lib/search/parser.ts
··· 355 355 if (this.match("WORD")) { 356 356 const value = this.previous().value; 357 357 358 - // For color fields, parse as colors - but identity can also take numeric values 359 - // for counting colors (id>1 = "more than 1 color in identity") 358 + // For color fields, parse as colors - but color/identity can also take numeric values 359 + // for counting colors (c>1 or id>1 = "more than 1 color") 360 360 if (field === "color" || field === "identity") { 361 - // Check if value is purely numeric (for identity count queries like id>1) 362 - if (field === "identity" && /^\d+$/.test(value)) { 361 + // Check if value is purely numeric (for count queries like c>1, id>1) 362 + if (/^\d+$/.test(value)) { 363 363 const num = parseInt(value, 10); 364 364 return ok({ kind: "number", value: num }); 365 365 }