diff options
Diffstat (limited to 'packages/shared')
| -rw-r--r-- | packages/shared/searchQueryParser.test.ts | 26 | ||||
| -rw-r--r-- | packages/shared/searchQueryParser.ts | 20 | ||||
| -rw-r--r-- | packages/shared/types/search.ts | 11 | ||||
| -rw-r--r-- | packages/shared/utils/relativeDateUtils.ts | 42 |
4 files changed, 98 insertions, 1 deletions
diff --git a/packages/shared/searchQueryParser.test.ts b/packages/shared/searchQueryParser.test.ts index ff69756c..7a86ecb5 100644 --- a/packages/shared/searchQueryParser.test.ts +++ b/packages/shared/searchQueryParser.test.ts @@ -319,6 +319,32 @@ describe("Search Query Parser", () => { }, }); }); + test("age queries", () => { + expect(parseSearchQuery("age:<3d")).toEqual({ + result: "full", + text: "", + matcher: { + type: "age", + relativeDate: { + direction: "newer", + amount: 3, + unit: "day", + }, + }, + }); + expect(parseSearchQuery("age:>2y")).toEqual({ + result: "full", + text: "", + matcher: { + type: "age", + relativeDate: { + direction: "older", + amount: 2, + unit: "year", + }, + }, + }); + }); test("complex queries", () => { expect(parseSearchQuery("is:fav -is:archived")).toEqual({ diff --git a/packages/shared/searchQueryParser.ts b/packages/shared/searchQueryParser.ts index d4e2bf2b..80f033b0 100644 --- a/packages/shared/searchQueryParser.ts +++ b/packages/shared/searchQueryParser.ts @@ -18,6 +18,7 @@ import { z } from "zod"; import { BookmarkTypes } from "./types/bookmarks"; import { Matcher } from "./types/search"; +import { parseRelativeDate } from "./utils/relativeDateUtils"; enum TokenType { And = "AND", @@ -40,7 +41,7 @@ const lexerRules: [RegExp, TokenType][] = [ [/^\s+or/i, TokenType.Or], [/^#/, TokenType.Hash], - [/^(is|url|list|after|before|feed):/, TokenType.Qualifier], + [/^(is|url|list|after|before|age|feed):/, TokenType.Qualifier], [/^"([^"]+)"/, TokenType.StringLiteral], @@ -247,6 +248,23 @@ MATCHER.setPattern( matcher: undefined, }; } + case "age:": + try { + const { direction, amount, unit } = parseRelativeDate(ident); + return { + text: "", + matcher: { + type: "age", + relativeDate: { direction, amount, unit }, + }, + }; + } catch (e) { + return { + // If parsing the relative time fails, emit it as pure text + text: (minus?.text ?? "") + qualifier.text + ident, + matcher: undefined, + }; + } default: // If the token is not known, emit it as pure text return { diff --git a/packages/shared/types/search.ts b/packages/shared/types/search.ts index 533eea25..4c64c0f5 100644 --- a/packages/shared/types/search.ts +++ b/packages/shared/types/search.ts @@ -48,6 +48,15 @@ const zDateBeforeMatcher = z.object({ inverse: z.boolean(), }); +const zAgeMatcher = z.object({ + type: z.literal("age"), + relativeDate: z.object({ + direction: z.enum(["newer", "older"]), + amount: z.number(), + unit: z.enum(["day", "week", "month", "year"]), + }), +}); + const zIsTaggedMatcher = z.object({ type: z.literal("tagged"), tagged: z.boolean(), @@ -76,6 +85,7 @@ const zNonRecursiveMatcher = z.union([ zFavouritedMatcher, zDateAfterMatcher, zDateBeforeMatcher, + zAgeMatcher, zIsTaggedMatcher, zIsInListMatcher, zTypeMatcher, @@ -97,6 +107,7 @@ export const zMatcherSchema: z.ZodType<Matcher> = z.lazy(() => { zFavouritedMatcher, zDateAfterMatcher, zDateBeforeMatcher, + zAgeMatcher, zIsTaggedMatcher, zIsInListMatcher, zTypeMatcher, diff --git a/packages/shared/utils/relativeDateUtils.ts b/packages/shared/utils/relativeDateUtils.ts new file mode 100644 index 00000000..437c3ea8 --- /dev/null +++ b/packages/shared/utils/relativeDateUtils.ts @@ -0,0 +1,42 @@ +interface RelativeDate { + direction: "newer" | "older"; + amount: number; + unit: "day" | "week" | "month" | "year"; +} + +const parseRelativeDate = (date: string): RelativeDate => { + const match = date.match(/^([<>])(\d+)([dwmy])$/); + if (!match) { + throw new Error(`Invalid relative date format: ${date}`); + } + const direction = match[1] === "<" ? "newer" : "older"; + const amount = parseInt(match[2], 10); + const unit = { + d: "day", + w: "week", + m: "month", + y: "year", + }[match[3]] as "day" | "week" | "month" | "year"; + return { direction, amount, unit }; +}; + +const toAbsoluteDate = (relativeDate: RelativeDate): Date => { + const date = new Date(); + switch (relativeDate.unit) { + case "day": + date.setDate(date.getDate() - relativeDate.amount); + break; + case "week": + date.setDate(date.getDate() - relativeDate.amount * 7); + break; + case "month": + date.setMonth(date.getMonth() - relativeDate.amount); + break; + case "year": + date.setFullYear(date.getFullYear() - relativeDate.amount); + break; + } + return date; +}; + +export { parseRelativeDate, toAbsoluteDate }; |
