mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) Parsing pasted datetimes
Summary: Add function parseDateTime which parses a string containing both date and time componenents, intended for parsing pasted strings. Add DateTimeParser subclass of ValueParser. Test Plan: Extended parseDate.ts and CopyPaste.ts tests. Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D3152
This commit is contained in:
parent
e997d091b3
commit
064455b2f7
@ -5,7 +5,7 @@ import * as gutil from 'app/common/gutil';
|
|||||||
import {safeJsonParse} from 'app/common/gutil';
|
import {safeJsonParse} from 'app/common/gutil';
|
||||||
import {getCurrency, NumberFormatOptions} from 'app/common/NumberFormat';
|
import {getCurrency, NumberFormatOptions} from 'app/common/NumberFormat';
|
||||||
import NumberParse from 'app/common/NumberParse';
|
import NumberParse from 'app/common/NumberParse';
|
||||||
import {parseDateStrict} from 'app/common/parseDate';
|
import {parseDateStrict, parseDateTime} from 'app/common/parseDate';
|
||||||
import {DateFormatOptions, DateTimeFormatOptions, formatDecoded, FormatOptions} from 'app/common/ValueFormatter';
|
import {DateFormatOptions, DateTimeFormatOptions, formatDecoded, FormatOptions} from 'app/common/ValueFormatter';
|
||||||
import flatMap = require('lodash/flatMap');
|
import flatMap = require('lodash/flatMap');
|
||||||
|
|
||||||
@ -47,12 +47,16 @@ class DateParser extends ValueParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DateTimeParser extends DateParser {
|
class DateTimeParser extends ValueParser {
|
||||||
constructor(type: string, widgetOpts: DateTimeFormatOptions, docSettings: DocumentSettings) {
|
constructor(type: string, widgetOpts: DateTimeFormatOptions, docSettings: DocumentSettings) {
|
||||||
super(type, widgetOpts, docSettings);
|
super(type, widgetOpts, docSettings);
|
||||||
const timezone = gutil.removePrefix(type, "DateTime:") || '';
|
const timezone = gutil.removePrefix(type, "DateTime:") || '';
|
||||||
this.widgetOpts = {...widgetOpts, timezone};
|
this.widgetOpts = {...widgetOpts, timezone};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public parse(value: string): any {
|
||||||
|
return parseDateTime(value, this.widgetOpts);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -99,8 +103,6 @@ const parsers: { [type: string]: typeof ValueParser } = {
|
|||||||
ChoiceList: ChoiceListParser,
|
ChoiceList: ChoiceListParser,
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO these are not ready yet
|
|
||||||
delete parsers.DateTime;
|
|
||||||
|
|
||||||
export function createParser(
|
export function createParser(
|
||||||
type: string, widgetOpts: FormatOptions, docSettings: DocumentSettings
|
type: string, widgetOpts: FormatOptions, docSettings: DocumentSettings
|
||||||
|
@ -55,9 +55,12 @@ const PARSER_FORMATS: string[] = [
|
|||||||
'D'
|
'D'
|
||||||
];
|
];
|
||||||
|
|
||||||
const UNAMBIGUOUS_FORMATS = PARSER_FORMATS.filter(f => f.includes("MMM"));
|
const UNAMBIGUOUS_FORMATS = [
|
||||||
|
'YYYY M D',
|
||||||
|
...PARSER_FORMATS.filter(f => f.includes("MMM")),
|
||||||
|
];
|
||||||
|
|
||||||
const TIME_REGEX = /^(?:(\d\d?)(?::(\d\d?)(?::(\d\d?))?)?|(\d\d?)(\d\d))\s*([ap]m?)?$/i;
|
const TIME_REGEX = /(?:^|\s+|T)(?:(\d\d?)(?::(\d\d?)(?::(\d\d?))?)?|(\d\d?)(\d\d))\s*([ap]m?)?$/i;
|
||||||
// [^a-zA-Z] because no letters are allowed directly before the abbreviation
|
// [^a-zA-Z] because no letters are allowed directly before the abbreviation
|
||||||
const UTC_REGEX = /[^a-zA-Z](UTC?|GMT|Z)$/i;
|
const UTC_REGEX = /[^a-zA-Z](UTC?|GMT|Z)$/i;
|
||||||
const NUMERIC_TZ_REGEX = /([+-]\d\d?)(?::?(\d\d))?$/i;
|
const NUMERIC_TZ_REGEX = /([+-]\d\d?)(?::?(\d\d))?$/i;
|
||||||
@ -109,12 +112,15 @@ export function parseDate(date: string, options: ParseOptions = {}): number | nu
|
|||||||
const cleanDate = date.replace(SEPARATORS, ' ');
|
const cleanDate = date.replace(SEPARATORS, ' ');
|
||||||
let datetime = cleanDate.trim();
|
let datetime = cleanDate.trim();
|
||||||
let timeformat = '';
|
let timeformat = '';
|
||||||
if (options.time) {
|
let time = options.time;
|
||||||
const standardTime = standardizeTime(options.time, options.timezone!);
|
if (time) {
|
||||||
if (!standardTime) {
|
const parsedTimeZone = parseTimeZone(time, options.timezone!);
|
||||||
|
const parsedTime = standardizeTime(parsedTimeZone.remaining);
|
||||||
|
if (!parsedTime || parsedTime.remaining) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const {time, tzOffset} = standardTime;
|
time = parsedTime.time;
|
||||||
|
const {tzOffset} = parsedTimeZone;
|
||||||
datetime += ' ' + time + tzOffset;
|
datetime += ' ' + time + tzOffset;
|
||||||
timeformat = ' HH:mm:ss' + (tzOffset ? 'Z' : '');
|
timeformat = ' HH:mm:ss' + (tzOffset ? 'Z' : '');
|
||||||
}
|
}
|
||||||
@ -136,7 +142,9 @@ export function parseDate(date: string, options: ParseOptions = {}): number | nu
|
|||||||
* This is safer so it can be used for parsing when pasting a large number of dates
|
* 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.
|
* and won't silently swap around day and month.
|
||||||
*/
|
*/
|
||||||
export function parseDateStrict(date: string, dateFormat: string | null, results?: Set<number>): number | undefined {
|
export function parseDateStrict(
|
||||||
|
date: string, dateFormat: string | null, results?: Set<number>, timezone: string = 'UTC'
|
||||||
|
): number | undefined {
|
||||||
if (!date) {
|
if (!date) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -147,7 +155,7 @@ export function parseDateStrict(date: string, dateFormat: string | null, results
|
|||||||
dateFormats.push(...UNAMBIGUOUS_FORMATS);
|
dateFormats.push(...UNAMBIGUOUS_FORMATS);
|
||||||
const cleanDate = date.replace(SEPARATORS, ' ').trim();
|
const cleanDate = date.replace(SEPARATORS, ' ').trim();
|
||||||
for (const format of dateFormats) {
|
for (const format of dateFormats) {
|
||||||
const m = moment.tz(cleanDate, format, true, 'UTC');
|
const m = moment.tz(cleanDate, format, true, timezone);
|
||||||
if (m.isValid()) {
|
if (m.isValid()) {
|
||||||
const value = m.valueOf() / 1000;
|
const value = m.valueOf() / 1000;
|
||||||
if (results) {
|
if (results) {
|
||||||
@ -159,6 +167,45 @@ export function parseDateStrict(date: string, dateFormat: string | null, results
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parseDateTime(dateTime: string, options: ParseOptions): number | undefined {
|
||||||
|
dateTime = dateTime.trim();
|
||||||
|
if (!dateTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateFormat = options.dateFormat || null;
|
||||||
|
const timezone = options.timezone || "UTC";
|
||||||
|
|
||||||
|
const dateOnly = parseDateStrict(dateTime, dateFormat, undefined, timezone);
|
||||||
|
if (dateOnly) {
|
||||||
|
return dateOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedTimeZone = parseTimeZone(dateTime, timezone);
|
||||||
|
let tzOffset = '';
|
||||||
|
if (parsedTimeZone) {
|
||||||
|
tzOffset = parsedTimeZone.tzOffset;
|
||||||
|
dateTime = parsedTimeZone.remaining;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedTime = standardizeTime(dateTime);
|
||||||
|
if (!parsedTime) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateTime = parsedTime.remaining;
|
||||||
|
const date = parseDateStrict(dateTime, dateFormat);
|
||||||
|
|
||||||
|
if (!date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dateString = moment.unix(date).format("YYYY-MM-DD");
|
||||||
|
dateTime = dateString + ' ' + parsedTime.time + tzOffset;
|
||||||
|
const fullFormat = "YYYY-MM-DD HH:mm:ss" + (tzOffset ? 'Z' : '');
|
||||||
|
return moment.tz(dateTime, fullFormat, true, timezone).valueOf() / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Helper function to get the partial format string based on the input. Momentjs has a feature
|
// 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
|
// which allows defaulting to the current year, month and/or day if not accounted for in the
|
||||||
@ -228,26 +275,23 @@ function calculateOffset(tzMatch: string[]): string {
|
|||||||
return sign + hhOffset.slice(1).padStart(2, '0') + ':' + (mmOffset || '0').padStart(2, '0');
|
return sign + hhOffset.slice(1).padStart(2, '0') + ':' + (mmOffset || '0').padStart(2, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses time of the form, roughly, HH[:MM[:SS]][am|pm] [TZ]. Returns the time in the
|
function parseTimeZone(str: string, timezone: string): { remaining: string, tzOffset: string } {
|
||||||
// standardized HH:mm:ss format, and an offset string that's empty or is of the form [+-]HH:mm.
|
str = str.trim();
|
||||||
// This turns out easier than coaxing moment to parse time sensibly and flexibly.
|
|
||||||
function standardizeTime(timeString: string, timezone: string): { time: string, tzOffset: string } | undefined {
|
|
||||||
let cleanTime = timeString.trim();
|
|
||||||
|
|
||||||
let tzMatch = UTC_REGEX.exec(cleanTime);
|
let tzMatch = UTC_REGEX.exec(str);
|
||||||
let matchStart = 0;
|
let matchStart = 0;
|
||||||
let tzOffset = '';
|
let tzOffset = '';
|
||||||
if (tzMatch) {
|
if (tzMatch) {
|
||||||
tzOffset = '+00:00';
|
tzOffset = '+00:00';
|
||||||
matchStart = tzMatch.index + 1; // skip [^a-zA-Z] at regex start
|
matchStart = tzMatch.index + 1; // skip [^a-zA-Z] at regex start
|
||||||
} else {
|
} else {
|
||||||
tzMatch = NUMERIC_TZ_REGEX.exec(cleanTime);
|
tzMatch = NUMERIC_TZ_REGEX.exec(str);
|
||||||
if (tzMatch) {
|
if (tzMatch) {
|
||||||
tzOffset = calculateOffset(tzMatch);
|
tzOffset = calculateOffset(tzMatch);
|
||||||
matchStart = tzMatch.index;
|
matchStart = tzMatch.index;
|
||||||
} else if (timezone) {
|
} else if (timezone) {
|
||||||
// Abbreviations are simply stripped and ignored, so tzOffset is not set in this case
|
// Abbreviations are simply stripped and ignored, so tzOffset is not set in this case
|
||||||
tzMatch = tzAbbreviations(timezone).exec(cleanTime);
|
tzMatch = tzAbbreviations(timezone).exec(str);
|
||||||
if (tzMatch) {
|
if (tzMatch) {
|
||||||
matchStart = tzMatch.index + 1; // skip [^a-zA-Z] at regex start
|
matchStart = tzMatch.index + 1; // skip [^a-zA-Z] at regex start
|
||||||
}
|
}
|
||||||
@ -255,11 +299,20 @@ function standardizeTime(timeString: string, timezone: string): { time: string,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (tzMatch) {
|
if (tzMatch) {
|
||||||
cleanTime = cleanTime.slice(0, matchStart).trim();
|
str = str.slice(0, matchStart).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const match = TIME_REGEX.exec(cleanTime);
|
return {remaining: str, tzOffset};
|
||||||
if (match) {
|
}
|
||||||
|
|
||||||
|
// Parses time of the form, roughly, HH[:MM[:SS]][am|pm]. Returns the time in the
|
||||||
|
// standardized HH:mm:ss format.
|
||||||
|
// This turns out easier than coaxing moment to parse time sensibly and flexibly.
|
||||||
|
function standardizeTime(timeString: string): { remaining: string, time: string } | undefined {
|
||||||
|
const match = TIME_REGEX.exec(timeString);
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let hours = parseInt(match[1] || match[4], 10);
|
let hours = parseInt(match[1] || match[4], 10);
|
||||||
const mm = (match[2] || match[5] || '0').padStart(2, '0');
|
const mm = (match[2] || match[5] || '0').padStart(2, '0');
|
||||||
const ss = (match[3] || '0').padStart(2, '0');
|
const ss = (match[3] || '0').padStart(2, '0');
|
||||||
@ -270,6 +323,5 @@ function standardizeTime(timeString: string, timezone: string): { time: string,
|
|||||||
hours = 0;
|
hours = 0;
|
||||||
}
|
}
|
||||||
const hh = String(hours).padStart(2, '0');
|
const hh = String(hours).padStart(2, '0');
|
||||||
return {time: `${hh}:${mm}:${ss}`, tzOffset};
|
return {remaining: timeString.slice(0, match.index).trim(), time: `${hh}:${mm}:${ss}`};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1714,6 +1714,22 @@ export async function setDateFormat(format: string) {
|
|||||||
await waitForServer();
|
await waitForServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns time format for datetime editor
|
||||||
|
*/
|
||||||
|
export async function getTimeFormat(): Promise<string> {
|
||||||
|
return driver.find('[data-test-id=Widget_timeFormat] .test-select-row').getText();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes time format for datetime editor
|
||||||
|
*/
|
||||||
|
export async function setTimeFormat(format: string) {
|
||||||
|
await driver.find('[data-test-id=Widget_timeFormat]').click();
|
||||||
|
await driver.findContent('.test-select-menu .test-select-row', format).click();
|
||||||
|
await waitForServer();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns "Show column" setting value of a reference column.
|
* Returns "Show column" setting value of a reference column.
|
||||||
*/
|
*/
|
||||||
|
Loading…
Reference in New Issue
Block a user