fix: resolve relative dates in view filters

This commit is contained in:
lucaronin
2026-05-16 18:34:41 +02:00
parent 678cc8f7ba
commit d81c696742
2 changed files with 156 additions and 5 deletions

View File

@@ -0,0 +1,95 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { VaultEntry, ViewDefinition } from '../types'
import { evaluateView } from './viewFilters'
const BASE_ENTRY: VaultEntry = {
path: '/vault/test.md',
filename: 'test.md',
title: 'Test',
isA: null,
aliases: [],
belongsTo: [],
relatedTo: [],
status: null,
archived: false,
modifiedAt: 1,
createdAt: 1,
fileSize: 100,
snippet: '',
wordCount: 0,
relationships: {},
icon: null,
color: null,
order: null,
sidebarLabel: null,
template: null,
sort: null,
view: null,
visible: null,
favorite: false,
favoriteIndex: null,
outgoingLinks: [],
properties: {},
listPropertiesDisplay: [],
}
function makeEntry(title: string, date: string): VaultEntry {
return {
...BASE_ENTRY,
path: `/vault/${title.toLowerCase()}.md`,
filename: `${title.toLowerCase()}.md`,
title,
properties: { date },
}
}
function dateEqualsView(value: string): ViewDefinition {
return {
name: 'Relative date',
icon: null,
color: null,
sort: null,
filters: { all: [{ field: 'date', op: 'equals', value }] },
}
}
describe('relative date view filters', () => {
afterEach(() => {
vi.useRealTimers()
})
it('matches today against date frontmatter at query time', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-07T12:00:00Z'))
const result = evaluateView(dateEqualsView('today'), [
makeEntry('Yesterday', '2026-04-06'),
makeEntry('Today', '2026-04-07'),
makeEntry('Tomorrow', '2026-04-08'),
])
expect(result.map((entry) => entry.title)).toEqual(['Today'])
})
it('matches future relative expressions against date frontmatter', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-07T12:00:00Z'))
const result = evaluateView(dateEqualsView('in 1 week'), [
makeEntry('Today', '2026-04-07'),
makeEntry('Next Week', '2026-04-14'),
makeEntry('Later', '2026-04-15'),
])
expect(result.map((entry) => entry.title)).toEqual(['Next Week'])
})
it('keeps unsupported calendar tokens from matching arbitrary dates', () => {
vi.useFakeTimers()
vi.setSystemTime(new Date('2026-04-07T12:00:00Z'))
expect(evaluateView(dateEqualsView('this month'), [
makeEntry('April Entry', '2026-04-15'),
])).toEqual([])
})
})

View File

@@ -174,6 +174,66 @@ function evaluateDateCondition(cond: FilterCondition, scalar: string | number |
return cond.op === 'before' ? tsMs < target : tsMs > target
}
type DateTimestamps = {
left: number
right: number
}
function isSameLocalDay(timestamps: DateTimestamps): boolean {
const leftTimestamp = timestamps.left
const rightTimestamp = timestamps.right
const leftDate = new Date(leftTimestamp)
const rightDate = new Date(rightTimestamp)
return leftDate.getFullYear() === rightDate.getFullYear()
&& leftDate.getMonth() === rightDate.getMonth()
&& leftDate.getDate() === rightDate.getDate()
}
type DateEqualityCondition = {
op: FilterCondition['op']
scalar: FieldScalar
condVal: string
}
function evaluateDateEqualityCondition(condition: DateEqualityCondition): boolean | null {
const { op, scalar, condVal } = condition
if (op !== 'equals' && op !== 'not_equals') return null
const tsMs = fieldTimestamp(scalar)
const target = toDateFilterTimestamp(condVal)
if (tsMs == null || target == null) return null
const matched = isSameLocalDay({ left: tsMs, right: target })
return op === 'equals' ? matched : !matched
}
type ScalarDateCondition = {
cond: FilterCondition
scalar: FieldScalar
condVal: string
}
function evaluateScalarDateCondition(condition: ScalarDateCondition): boolean | null {
const { cond, scalar, condVal } = condition
if (cond.op === 'before' || cond.op === 'after') {
return evaluateDateCondition(cond, scalar, condVal)
}
return evaluateDateEqualityCondition({ op: cond.op, scalar, condVal })
}
type ScalarCondition = ScalarDateCondition & {
regex: RegExp | null
}
function evaluateScalarCondition(condition: ScalarCondition): boolean {
const dateResult = evaluateScalarDateCondition(condition)
if (dateResult !== null) return dateResult
const { cond, scalar, condVal, regex } = condition
return evaluateTextCondition(cond, toString(scalar), condVal, regex)
}
function evaluateCondition(cond: FilterCondition, entry: VaultEntry): boolean {
const resolved = resolveField(entry, cond.field)
const emptyResult = evaluateEmptyCondition(cond.op, resolved)
@@ -187,9 +247,5 @@ function evaluateCondition(cond: FilterCondition, entry: VaultEntry): boolean {
return evaluateArrayCondition(cond, resolved, condVal, regex)
}
if (cond.op === 'before' || cond.op === 'after') {
return evaluateDateCondition(cond, resolved.value, condVal)
}
return evaluateTextCondition(cond, toString(resolved.value), condVal, regex)
return evaluateScalarCondition({ cond, scalar: resolved.value, condVal, regex })
}