fix: resolve relative dates in view filters
This commit is contained in:
95
src/utils/viewFilters.relativeDates.test.ts
Normal file
95
src/utils/viewFilters.relativeDates.test.ts
Normal 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([])
|
||||
})
|
||||
})
|
||||
@@ -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 })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user