mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) ValueParser for Date columns
Summary: Adds parseDateStrict function based on parseDate, uses it in DateParser subclass of ValueParser. Test Plan: Tweaked parseDate test to check parseDateStrict. Extended test in CopyPaste to test parsing dates as well as numbers. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3088
This commit is contained in:
parent
65e743931b
commit
e58df5df5b
@ -3,8 +3,8 @@ import * as gristTypes from 'app/common/gristTypes';
|
||||
import * as gutil from 'app/common/gutil';
|
||||
import { getCurrency, NumberFormatOptions } from 'app/common/NumberFormat';
|
||||
import NumberParse from 'app/common/NumberParse';
|
||||
import { parseDate } from 'app/common/parseDate';
|
||||
import { DateTimeFormatOptions, FormatOptions } from 'app/common/ValueFormatter';
|
||||
import { parseDateStrict } from 'app/common/parseDate';
|
||||
import { DateFormatOptions, DateTimeFormatOptions, FormatOptions } from 'app/common/ValueFormatter';
|
||||
|
||||
|
||||
export class ValueParser {
|
||||
@ -40,7 +40,7 @@ export class NumericParser extends ValueParser {
|
||||
|
||||
class DateParser extends ValueParser {
|
||||
public parse(value: string): any {
|
||||
return parseDate(value, this.widgetOpts);
|
||||
return parseDateStrict(value, (this.widgetOpts as DateFormatOptions).dateFormat!);
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,7 +60,6 @@ const parsers: { [type: string]: typeof ValueParser } = {
|
||||
};
|
||||
|
||||
// TODO these are not ready yet
|
||||
delete parsers.Date;
|
||||
delete parsers.DateTime;
|
||||
|
||||
export function createParser(
|
||||
|
@ -30,11 +30,19 @@ const PARSER_FORMATS: string[] = [
|
||||
'MMMM D',
|
||||
'MMMM Do YYYY',
|
||||
'MMMM Do',
|
||||
'D MMMM YYYY',
|
||||
'D MMMM',
|
||||
'Do MMMM YYYY',
|
||||
'Do MMMM',
|
||||
'MMMM',
|
||||
'MMM D YYYY',
|
||||
'MMM D',
|
||||
'MMM Do YYYY',
|
||||
'MMM Do',
|
||||
'D MMM YYYY',
|
||||
'D MMM',
|
||||
'Do MMM YYYY',
|
||||
'Do MMM',
|
||||
'MMM',
|
||||
'YYYY M D',
|
||||
'YYYY M',
|
||||
@ -45,11 +53,16 @@ const PARSER_FORMATS: string[] = [
|
||||
'D'
|
||||
];
|
||||
|
||||
const UNAMBIGUOUS_FORMATS = PARSER_FORMATS.filter(f => f.includes("MMM"));
|
||||
|
||||
// The TZ portion is based on moment's RFC2822 regex, supporting US time zones, and UT. See
|
||||
// https://momentjs.com/docs/#/parsing/string/
|
||||
const TIME_REGEX = /^(?:(\d\d?)(?::(\d\d?)(?::(\d\d?))?)?|(\d\d?)(\d\d))\s*([ap]m?)?$/i;
|
||||
const TZ_REGEX = /\s*(UTC?|GMT|[ECMP][SD]T|Z)|(?:([+-]\d\d?)(?::?(\d\d))?)$/i;
|
||||
|
||||
// Not picky about separators, so replace them in the date and format strings to be spaces.
|
||||
const SEPARATORS = /[\W_]+/g;
|
||||
|
||||
interface ParseOptions {
|
||||
time?: string;
|
||||
dateFormat?: string;
|
||||
@ -74,22 +87,12 @@ export function parseDate(date: string, options: ParseOptions = {}): number | nu
|
||||
if (!date) {
|
||||
return null;
|
||||
}
|
||||
// Not picky about separators, so replace them in the date and format strings to be spaces.
|
||||
const separators = /[\W_]+/g;
|
||||
const dateFormats = PARSER_FORMATS.slice();
|
||||
// If a preferred parse format is given, set that to be the first parser used.
|
||||
if (options.dateFormat) {
|
||||
// Momentjs has an undesirable feature in strict mode where MM and DD
|
||||
// matches require two digit numbers. Change MM, DD to M, D.
|
||||
let format = options.dateFormat.replace(/MM+/g, m => (m === 'MM' ? 'M' : m))
|
||||
.replace(/DD+/g, m => (m === 'DD' ? 'D' : m))
|
||||
.replace(separators, ' ');
|
||||
format = _getPartialFormat(date, format);
|
||||
// Consider some alternatives to the preferred format.
|
||||
const variations = _buildVariations(format);
|
||||
dateFormats.unshift(...variations);
|
||||
dateFormats.unshift(..._buildVariations(options.dateFormat, date));
|
||||
}
|
||||
const cleanDate = date.replace(separators, ' ');
|
||||
const cleanDate = date.replace(SEPARATORS, ' ');
|
||||
let datetime = cleanDate.trim();
|
||||
let timeformat = '';
|
||||
if (options.time) {
|
||||
@ -107,6 +110,38 @@ export function parseDate(date: string, options: ParseOptions = {}): number | nu
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to parseDate, with these differences:
|
||||
* - Only for a date (no time part)
|
||||
* - Only falls back to UNAMBIGUOUS_FORMATS, not the full PARSER_FORMATS
|
||||
* - Optionally adds all dates which match some format to `results`, otherwise returns first match.
|
||||
* This is safer so it can be used for parsing when pasting a large number of dates
|
||||
* and won't silently swap around day and month.
|
||||
*/
|
||||
export function parseDateStrict(date: string, dateFormat: string | null, results?: Set<number>): number | undefined {
|
||||
if (!date) {
|
||||
return;
|
||||
}
|
||||
const dateFormats = [];
|
||||
if (dateFormat) {
|
||||
dateFormats.push(..._buildVariations(dateFormat, date));
|
||||
}
|
||||
dateFormats.push(...UNAMBIGUOUS_FORMATS);
|
||||
const cleanDate = date.replace(SEPARATORS, ' ').trim();
|
||||
for (const format of dateFormats) {
|
||||
const m = moment.tz(cleanDate, format, true, 'UTC');
|
||||
if (m.isValid()) {
|
||||
const value = m.valueOf() / 1000;
|
||||
if (results) {
|
||||
results.add(value);
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Helper function to get the partial format string based on the input. Momentjs has a feature
|
||||
// which allows defaulting to the current year, month and/or day if not accounted for in the
|
||||
// parser. We remove any parts of the parser not given in the input to take advantage of this
|
||||
@ -137,7 +172,26 @@ function _getPartialFormat(input: string, format: string): string {
|
||||
// Moment non-strict mode is considered bad, as it's far too lax. But moment's strict mode is too
|
||||
// strict. We want to allow YY|YYYY for either year specifier, as well as M for MMM or MMMM month
|
||||
// specifiers. It's silly that we need to create multiple format variations to support this.
|
||||
function _buildVariations(format: string) {
|
||||
function _buildVariations(dateFormat: string, date: string) {
|
||||
// Momentjs has an undesirable feature in strict mode where MM and DD
|
||||
// matches require two digit numbers. Change MM, DD to M, D.
|
||||
let format = dateFormat.replace(/MM+/g, m => (m === 'MM' ? 'M' : m))
|
||||
.replace(/DD+/g, m => (m === 'DD' ? 'D' : m))
|
||||
.replace(SEPARATORS, ' ')
|
||||
.trim();
|
||||
|
||||
// Allow the input date to end with a 4-digit year even if the format doesn't mention the year
|
||||
if (
|
||||
format.includes("M") &&
|
||||
format.includes("D") &&
|
||||
!format.includes("Y")
|
||||
) {
|
||||
format += " YYYY";
|
||||
}
|
||||
|
||||
format = _getPartialFormat(date, format);
|
||||
|
||||
// Consider some alternatives to the preferred format.
|
||||
const variations = new Set<string>([format]);
|
||||
const otherYear = format.replace(/Y{2,4}/, (m) => (m === 'YY' ? 'YYYY' : (m === 'YYYY' ? 'YY' : m)));
|
||||
variations.add(otherYear);
|
||||
|
@ -1695,6 +1695,22 @@ export async function openDocumentSettings() {
|
||||
await driver.findWait('.test-modal-title', 5000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns date format for date and datetime editor
|
||||
*/
|
||||
export async function getDateFormat(): Promise<string> {
|
||||
return driver.find('[data-test-id=Widget_dateFormat] .test-select-row').getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes date format for date and datetime editor
|
||||
*/
|
||||
export async function setDateFormat(format: string) {
|
||||
await driver.find('[data-test-id=Widget_dateFormat]').click();
|
||||
await driver.findContent('.test-select-menu .test-select-row', format).click();
|
||||
await waitForServer();
|
||||
}
|
||||
|
||||
} // end of namespace gristUtils
|
||||
|
||||
stackWrapOwnMethods(gristUtils);
|
||||
|
Loading…
Reference in New Issue
Block a user