(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
pull/115/head
Alex Hall 3 years ago
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…
Cancel
Save