Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
liamwhite committed Mar 13, 2024
1 parent 886539c commit bce552a
Show file tree
Hide file tree
Showing 24 changed files with 1,094 additions and 901 deletions.
877 changes: 0 additions & 877 deletions assets/js/match_query.js

This file was deleted.

15 changes: 15 additions & 0 deletions assets/js/match_query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { defaultMatcher } from './query/factory';
import { generateLexArray } from './query/lex';
import { parseTokens } from './query/parse';
import { parseTerm } from './query/term';

function parseWithDefaultMatcher(term: string, fuzz: number) {
return parseTerm(term, fuzz, defaultMatcher);
}

function parseSearch(query: string) {
const tokens = generateLexArray(query, parseWithDefaultMatcher);
return parseTokens(tokens);
}

export default parseSearch;
106 changes: 106 additions & 0 deletions assets/js/query/__tests__/date.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { makeDateMatcher } from '../date';

function daysAgo(days: number) {
return new Date(Date.now() - days * 86400000).toISOString();
}

describe('Date parsing', () => {
it('should match relative dates (upper bound)', () => {
const matcher = makeDateMatcher('3 days ago', 'lte');

expect(matcher(daysAgo(4), 'created_at', 0)).toBe(true);
expect(matcher(daysAgo(2), 'created_at', 0)).toBe(false);
});

it('should match relative dates (lower bound)', () => {
const matcher = makeDateMatcher('3 days ago', 'gte');

expect(matcher(daysAgo(4), 'created_at', 0)).toBe(false);
expect(matcher(daysAgo(2), 'created_at', 0)).toBe(true);
});

it('should match absolute date ranges', () => {
const ltMatcher = makeDateMatcher('2025', 'lt');
const gtMatcher = makeDateMatcher('2023', 'gt');

expect(ltMatcher(new Date(Date.UTC(2025, 5, 21)).toISOString(), 'created_at', 0)).toBe(false);
expect(ltMatcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true);
expect(ltMatcher(new Date(Date.UTC(2023, 5, 21)).toISOString(), 'created_at', 0)).toBe(true);

expect(gtMatcher(new Date(Date.UTC(2025, 5, 21)).toISOString(), 'created_at', 0)).toBe(true);
expect(gtMatcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true);
expect(gtMatcher(new Date(Date.UTC(2023, 5, 21)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through years', () => {
const matcher = makeDateMatcher('2024', 'eq');

expect(matcher(new Date(Date.UTC(2025, 5, 21)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2023, 5, 21)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through months', () => {
const matcher = makeDateMatcher('2024-06', 'eq');

expect(matcher(new Date(Date.UTC(2024, 6, 21)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2024, 4, 21)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through days', () => {
const matcher = makeDateMatcher('2024-06-21', 'eq');

expect(matcher(new Date(Date.UTC(2024, 5, 22)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2024, 5, 20)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through hours', () => {
const matcher = makeDateMatcher('2024-06-21T06', 'eq');

expect(matcher(new Date(Date.UTC(2024, 5, 21, 7)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 5)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through minutes', () => {
const matcher = makeDateMatcher('2024-06-21T06:21', 'eq');

expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 22)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 20)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through seconds', () => {
const matcher = makeDateMatcher('2024-06-21T06:21:30Z', 'eq');

expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21, 31)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21, 30)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 6, 21, 29)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through seconds with positive timezone offset', () => {
const matcher = makeDateMatcher('2024-06-21T06:21:30+01:30', 'eq');

expect(matcher(new Date(Date.UTC(2024, 5, 21, 4, 51, 31)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 4, 51, 30)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 4, 51, 29)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should match absolute dates through seconds with negative timezone offset', () => {
const matcher = makeDateMatcher('2024-06-21T06:21:30-01:30', 'eq');

expect(matcher(new Date(Date.UTC(2024, 5, 21, 7, 51, 31)).toISOString(), 'created_at', 0)).toBe(false);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 7, 51, 30)).toISOString(), 'created_at', 0)).toBe(true);
expect(matcher(new Date(Date.UTC(2024, 5, 21, 7, 51, 29)).toISOString(), 'created_at', 0)).toBe(false);
});

it('should not match malformed absolute date expressions', () => {
expect(() => makeDateMatcher('2024-06-21T06:21:30+01:3020', 'eq')).toThrow('Cannot parse date string: 2024-06-21T06:21:30+01:3020');
});

it('should not match malformed relative date expressions', () => {
expect(() => makeDateMatcher('3 test failures ago', 'eq')).toThrow('Cannot parse date string: 3 test failures ago');
});
});
36 changes: 36 additions & 0 deletions assets/js/query/__tests__/literal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { makeLiteralMatcher } from '../literal';

describe('Literal field parsing', () => {
it('should handle exact matching in arrayed fields', () => {
const matcher = makeLiteralMatcher('safe', 0, false);
expect(matcher('safe, solo', 'tags', 0)).toBe(true);
expect(matcher('solo', 'tags', 0)).toBe(false);
});

it('should handle exact matching in non-arrayed fields', () => {
const matcher = makeLiteralMatcher('safe', 0, false);
expect(matcher('safe, solo', 'description', 0)).toBe(false);
expect(matcher('safe', 'description', 0)).toBe(true);
expect(matcher('solo', 'description', 0)).toBe(false);
});

it('should handle fuzzy matching based on normalized edit distance', () => {
const matcher = makeLiteralMatcher('fluttersho', 0.8, false);
expect(matcher('fluttershy', 'tags', 0)).toBe(true);
expect(matcher('rarity', 'tags', 0)).toBe(false);
});

it('should handle fuzzy matching based on raw edit distance', () => {
const matcher = makeLiteralMatcher('fluttersho', 1, false);
expect(matcher('fluttershy', 'tags', 0)).toBe(true);
expect(matcher('rarity', 'tags', 0)).toBe(false);
});

it('should handle wildcard matching', () => {
const matcher = makeLiteralMatcher('fl?tter*', 0, true);
expect(matcher('fluttershy', 'tags', 0)).toBe(true);
expect(matcher('flitter', 'tags', 0)).toBe(true);
expect(matcher('rainbow dash', 'tags', 0)).toBe(false);
expect(matcher('gentle flutter', 'tags', 0)).toBe(false);
});
});
53 changes: 53 additions & 0 deletions assets/js/query/__tests__/number.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { makeNumberMatcher } from '../number';

describe('Number parsing', () => {
it('should match numbers directly', () => {
const intMatch = makeNumberMatcher(2067, 0, 'eq');

expect(intMatch('2066', 'value', 0)).toBe(false);
expect(intMatch('2067', 'value', 0)).toBe(true);
expect(intMatch('2068', 'value', 0)).toBe(false);
expect(intMatch('20677', 'value', 0)).toBe(false);
});

it('should match number ranges', () => {
const ltMatch = makeNumberMatcher(2067, 0, 'lt');
const lteMatch = makeNumberMatcher(2067, 0, 'lte');
const gtMatch = makeNumberMatcher(2067, 0, 'gt');
const gteMatch = makeNumberMatcher(2067, 0, 'gte');

expect(ltMatch('2066', 'value', 0)).toBe(true);
expect(ltMatch('2067', 'value', 0)).toBe(false);
expect(ltMatch('2068', 'value', 0)).toBe(false);
expect(lteMatch('2066', 'value', 0)).toBe(true);
expect(lteMatch('2067', 'value', 0)).toBe(true);
expect(lteMatch('2068', 'value', 0)).toBe(false);
expect(gtMatch('2066', 'value', 0)).toBe(false);
expect(gtMatch('2067', 'value', 0)).toBe(false);
expect(gtMatch('2068', 'value', 0)).toBe(true);
expect(gteMatch('2066', 'value', 0)).toBe(false);
expect(gteMatch('2067', 'value', 0)).toBe(true);
expect(gteMatch('2068', 'value', 0)).toBe(true);
});

it('should not match unparsed values', () => {
const matcher = makeNumberMatcher(2067, 0, 'eq');

expect(matcher('NaN', 'value', 0)).toBe(false);
expect(matcher('test', 'value', 0)).toBe(false);
});

it('should interpret fuzz as an inclusive range around the value', () => {
const matcher = makeNumberMatcher(2067, 3, 'eq');

expect(matcher('2063', 'value', 0)).toBe(false);
expect(matcher('2064', 'value', 0)).toBe(true);
expect(matcher('2065', 'value', 0)).toBe(true);
expect(matcher('2066', 'value', 0)).toBe(true);
expect(matcher('2067', 'value', 0)).toBe(true);
expect(matcher('2068', 'value', 0)).toBe(true);
expect(matcher('2069', 'value', 0)).toBe(true);
expect(matcher('2070', 'value', 0)).toBe(true);
expect(matcher('2071', 'value', 0)).toBe(false);
});
});
50 changes: 50 additions & 0 deletions assets/js/query/__tests__/user.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { makeUserMatcher } from '../user';

describe('User field parsing', () => {
beforeEach(() => {
/* eslint-disable camelcase */
window.booru.interactions = [
{image_id: 0, user_id: 0, interaction_type: 'faved', value: null},
{image_id: 0, user_id: 0, interaction_type: 'voted', value: 'up'},
{image_id: 1, user_id: 0, interaction_type: 'voted', value: 'down'},
{image_id: 2, user_id: 0, interaction_type: 'hidden', value: null},
];
/* eslint-enable camelcase */
});

it('should parse my:faves', () => {
const matcher = makeUserMatcher('faves');

expect(matcher('', 'my', 0)).toBe(true);
expect(matcher('', 'my', 1)).toBe(false);
expect(matcher('', 'my', 2)).toBe(false);
});

it('should parse my:upvotes', () => {
const matcher = makeUserMatcher('upvotes');

expect(matcher('', 'my', 0)).toBe(true);
expect(matcher('', 'my', 1)).toBe(false);
expect(matcher('', 'my', 2)).toBe(false);
});

it('should parse my:downvotes', () => {
const matcher = makeUserMatcher('downvotes');

expect(matcher('', 'my', 0)).toBe(false);
expect(matcher('', 'my', 1)).toBe(true);
expect(matcher('', 'my', 2)).toBe(false);
});

it('should not parse other my: fields', () => {
const hiddenMatcher = makeUserMatcher('hidden');
const watchedMatcher = makeUserMatcher('watched');

expect(hiddenMatcher('', 'my', 0)).toBe(false);
expect(hiddenMatcher('', 'my', 1)).toBe(false);
expect(hiddenMatcher('', 'my', 2)).toBe(false);
expect(watchedMatcher('', 'my', 0)).toBe(false);
expect(watchedMatcher('', 'my', 1)).toBe(false);
expect(watchedMatcher('', 'my', 2)).toBe(false);
});
});
35 changes: 35 additions & 0 deletions assets/js/query/boolean.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AstMatcher } from './types';

export function matchAny(...matchers: AstMatcher[]): AstMatcher {
return (e: HTMLElement) => {
for (const matcher of matchers) {
if (matcher(e)) {
return true;
}
}
return false;
};
}

export function matchAll(...matchers: AstMatcher[]): AstMatcher {
return (e: HTMLElement) => {
for (const matcher of matchers) {
if (!matcher(e)) {
return false;
}
}
return true;
};
}

export function matchNot(matcher: AstMatcher): AstMatcher {
return (e: HTMLElement) => {
return !matcher(e);
};
}

export function matchNone(): AstMatcher {
return () => {
return false;
};
}
Loading

0 comments on commit bce552a

Please sign in to comment.