gristlabs_grist-core/app/client/ui/RelativeDatesOptions.ts
Cyprien P 18d016c745 (core) Fix date filter for DateTime columns.
Summary:
Date filter was not taking timezone correclty into account, which was
causing to wrong-inclusion and wrong-exclusion of dates near the
bounds.

Diff fixes that, it also bring little refactoring that hopefully clarifies things a little.

Test Plan: Includes brand new test for `app/common/ColumnFilterFunc`.

Reviewers: jarek

Reviewed By: jarek

Differential Revision: https://phab.getgrist.com/D3763
2023-01-18 10:18:15 +01:00

159 lines
4.8 KiB
TypeScript

import {
CURRENT_DATE,
diffUnit,
formatRelBounds,
IPeriod,
IRelativeDateSpec,
isEquivalentRelativeDate,
relativeDateToUnixTimestamp
} from "app/common/RelativeDates";
import { IRangeBoundType, isRelativeBound } from "app/common/FilterState";
import getCurrentTime from "app/common/getCurrentTime";
import moment from "moment-timezone";
export const DEPS = {getCurrentTime};
export interface IRelativeDateOption {
label: string;
value: number|IRelativeDateSpec;
}
const DEFAULT_OPTION_LIST: IRelativeDateSpec[] = [
CURRENT_DATE, [{
quantity: -3,
unit: 'day',
}], [{
quantity: -7,
unit: 'day',
}], [{
quantity: -30,
unit: 'day',
}], [{
quantity: 0,
unit: 'year',
}], [{
quantity: 3,
unit: 'day',
}], [{
quantity: 7,
unit: 'day',
}], [{
quantity: 30,
unit: 'day',
}], [{
quantity: 0,
unit: 'year',
endOf: true,
}]];
export function relativeDatesOptions(value: IRangeBoundType, valueFormatter: (val: any) => string
): Array<{label: string, spec: IRangeBoundType}> {
return relativeDateOptionsSpec(value)
.map((spec) => ({spec, label: formatBoundOption(spec, valueFormatter)}));
}
// Returns a list of different relative date spec that all match passed in date value. If value is
// undefined it returns a default list of spec meant to showcase user the different flavors of
// relative date.
function relativeDateOptionsSpec(value: IRangeBoundType): Array<IRangeBoundType> {
if (value === undefined) {
return DEFAULT_OPTION_LIST;
} else if (isRelativeBound(value)) {
value = relativeDateToUnixTimestamp(value);
}
const date = moment.utc(value * 1000);
const res: IRangeBoundType[] = [value];
let relDate = getMatchingDoubleRelativeDate(value, {unit: 'day'});
if (Math.abs(relDate[0].quantity) <= 90) {
res.push(relDate);
}
relDate = getMatchingDoubleRelativeDate(value, {unit: 'week'});
if (Math.abs(relDate[0].quantity) <= 4) {
res.push(relDate);
}
// any day of the month (with longer limit for 1st day of the month)
relDate = getMatchingDoubleRelativeDate(value, {unit: 'month'});
if (Math.abs(relDate[0].quantity) <= (date.date() === 1 ? 12 : 3)) {
res.push(relDate);
}
// If date is 1st of Jan show 1st day of year options
if (date.date() === 1 && date.month() === 0) {
res.push(getMatchingDoubleRelativeDate(value, {unit: 'year'}));
}
// 31st of Dec
if (date.date() === 31 && date.month() === 11) {
res.push(getMatchingDoubleRelativeDate(value, {unit: 'year', endOf: true}));
}
// Last day of any month
if (date.clone().endOf('month').date() === date.date()) {
relDate = getMatchingDoubleRelativeDate(value, {unit: 'month', endOf: true});
if (Math.abs(relDate[0].quantity) < 12) {
res.push(relDate);
}
}
return res;
}
function now(): moment.Moment {
const m = DEPS.getCurrentTime();
return moment.utc([m.year(), m.month(), m.date()]);
}
// Returns a relative date spec as a sequence of one or two IPeriod that allows to match dateValue
// starting from the current date. The first period has .unit, .startOf and .endOf set according to
// passed in option.
export function getMatchingDoubleRelativeDate(
dateValue: number,
option: {unit: 'day'|'week'|'month'|'year', endOf?: boolean}
): IPeriod[] {
const {unit} = option;
const date = moment.utc(dateValue * 1000);
const dateNow = now();
const quantity = diffUnit(date, dateNow.clone(), unit);
const m = dateNow.clone().add(quantity, unit);
if (option.endOf) { m.endOf(unit); m.startOf('day'); }
else { m.startOf(unit); }
const dayQuantity = diffUnit(date, m, 'day');
const res = [{quantity, ...option}];
// Only add a 2nd period when it is not moot.
if (dayQuantity) { res.push({quantity: dayQuantity, unit: 'day'}); }
return res;
}
export function formatBoundOption(bound: IRangeBoundType, valueFormatter: (val: any) => string): string {
return isRelativeBound(bound) ? formatRelBounds(bound) : valueFormatter(bound);
}
// Update relativeDate to match the new date picked by user.
export function updateRelativeDate(relativeDate: IRelativeDateSpec, date: number): IRelativeDateSpec|number {
const periods = Array.isArray(relativeDate) ? relativeDate : [relativeDate];
if ([1, 2].includes(periods.length)) {
const {unit, endOf} = periods[0];
const relDate = getMatchingDoubleRelativeDate(date, {unit, endOf});
// Returns the relative date only if it is one of the suggested relative dates, otherwise
// returns the absolute date.
const options = relativeDateOptionsSpec(date);
if (options.find(opt => isRelativeBound(opt) && isEquivalentRelativeDate(opt, relDate))) {
return relDate;
}
return date;
}
throw new Error(
`Relative date spec does only support 1 or 2 periods, got ${periods.length}!`
);
}