From d2d11e0208e8a20145910bbdd02e7b273fb0aa13 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Fri, 10 Nov 2023 14:30:13 +0100 Subject: [PATCH] fix(Date & Time Node): Add fromFormat option to solve ambiguous date strings (#7675) Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/spreadsheet-date-issue/31551 --------- Co-authored-by: Michael Kret --- .../nodes/DateTime/V2/DateTimeV2.node.ts | 24 +++++++++++-------- .../DateTime/V2/FormatDateDescription.ts | 10 ++++++++ .../nodes/DateTime/V2/GenericFunctions.ts | 16 +++++++++---- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts b/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts index 1d0af3da18..ae3f11a526 100644 --- a/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts +++ b/packages/nodes-base/nodes/DateTime/V2/DateTimeV2.node.ts @@ -130,7 +130,7 @@ export class DateTimeV2 implements INodeType { const duration = this.getNodeParameter('duration', i) as number; const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; - const dateToAdd = parseDate.call(this, addToDate, workflowTimezone); + const dateToAdd = parseDate.call(this, addToDate, { timezone: workflowTimezone }); const returnedDate = dateToAdd.plus({ [timeUnit]: duration }); item.json[outputFieldName] = returnedDate.toString(); @@ -141,7 +141,7 @@ export class DateTimeV2 implements INodeType { const duration = this.getNodeParameter('duration', i) as number; const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; - const dateToAdd = parseDate.call(this, subtractFromDate, workflowTimezone); + const dateToAdd = parseDate.call(this, subtractFromDate, { timezone: workflowTimezone }); const returnedDate = dateToAdd.minus({ [timeUnit]: duration }); item.json[outputFieldName] = returnedDate.toString(); @@ -150,14 +150,18 @@ export class DateTimeV2 implements INodeType { const date = this.getNodeParameter('date', i) as string; const format = this.getNodeParameter('format', i) as string; const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; - const { timezone } = this.getNodeParameter('options', i) as { timezone: boolean }; + const { timezone, fromFormat } = this.getNodeParameter('options', i) as { + timezone: boolean; + fromFormat: string; + }; if (date === null || date === undefined) { item.json[outputFieldName] = date; } else { - const dateLuxon = timezone - ? parseDate.call(this, date, workflowTimezone) - : parseDate.call(this, date); + const dateLuxon = parseDate.call(this, date, { + timezone: timezone ? workflowTimezone : undefined, + fromFormat, + }); if (format === 'custom') { const customFormat = this.getNodeParameter('customFormat', i) as string; item.json[outputFieldName] = dateLuxon.toFormat(customFormat); @@ -171,7 +175,7 @@ export class DateTimeV2 implements INodeType { const mode = this.getNodeParameter('mode', i) as string; const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; - const dateLuxon = parseDate.call(this, date, workflowTimezone); + const dateLuxon = parseDate.call(this, date, { timezone: workflowTimezone }); if (mode === 'roundDown') { const toNearest = this.getNodeParameter('toNearest', i) as string; @@ -193,8 +197,8 @@ export class DateTimeV2 implements INodeType { isoString: boolean; }; - const luxonStartDate = parseDate.call(this, startDate, workflowTimezone); - const luxonEndDate = parseDate.call(this, endDate, workflowTimezone); + const luxonStartDate = parseDate.call(this, startDate, { timezone: workflowTimezone }); + const luxonEndDate = parseDate.call(this, endDate, { timezone: workflowTimezone }); const duration = luxonEndDate.diff(luxonStartDate, unit); if (isoString) { item.json[outputFieldName] = duration.toString(); @@ -207,7 +211,7 @@ export class DateTimeV2 implements INodeType { const outputFieldName = this.getNodeParameter('outputFieldName', i) as string; const part = this.getNodeParameter('part', i) as keyof DateTime | 'week'; - const parsedDate = parseDate.call(this, date, workflowTimezone); + const parsedDate = parseDate.call(this, date, { timezone: workflowTimezone }); const selectedPart = part === 'week' ? parsedDate.weekNumber : parsedDate.get(part); item.json[outputFieldName] = selectedPart; returnData.push(item); diff --git a/packages/nodes-base/nodes/DateTime/V2/FormatDateDescription.ts b/packages/nodes-base/nodes/DateTime/V2/FormatDateDescription.ts index 40dbc9090b..18c041b279 100644 --- a/packages/nodes-base/nodes/DateTime/V2/FormatDateDescription.ts +++ b/packages/nodes-base/nodes/DateTime/V2/FormatDateDescription.ts @@ -119,6 +119,16 @@ export const FormatDateDescription: INodeProperties[] = [ default: {}, options: [ includeInputFields, + { + displayName: 'From Date Format', + name: 'fromFormat', + type: 'string', + default: 'e.g yyyyMMdd', + hint: 'Tokens are case sensitive', + // eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id + description: + 'Format in which the input \'Date\' is, it\'s helpful when the format is not recognized automatically. Use those tokens to define the format.', + }, { displayName: 'Use Workflow Timezone', name: 'timezone', diff --git a/packages/nodes-base/nodes/DateTime/V2/GenericFunctions.ts b/packages/nodes-base/nodes/DateTime/V2/GenericFunctions.ts index 153d875d3b..7f0dbe8f27 100644 --- a/packages/nodes-base/nodes/DateTime/V2/GenericFunctions.ts +++ b/packages/nodes-base/nodes/DateTime/V2/GenericFunctions.ts @@ -6,15 +6,18 @@ import { NodeOperationError } from 'n8n-workflow'; export function parseDate( this: IExecuteFunctions, date: string | number | DateTime, - timezone?: string, + options: Partial<{ + timezone: string; + fromFormat: string; + }> = {}, ) { let parsedDate; if (date instanceof DateTime) { parsedDate = date; } else { - // Check if the input is a number - if (!Number.isNaN(Number(date))) { + // Check if the input is a number, don't convert to number if fromFormat is set + if (!Number.isNaN(Number(date)) && !options.fromFormat) { //input is a number, convert to number in case it is a string formatted number date = Number(date); // check if the number is a timestamp in float format and convert to integer @@ -23,6 +26,7 @@ export function parseDate( } } + let timezone = options.timezone; if (Number.isInteger(date)) { const timestampLengthInMilliseconds1990 = 12; // check if the number is a timestamp in seconds or milliseconds and create a moment object accordingly @@ -37,7 +41,11 @@ export function parseDate( timezone = `Etc/GMT-${offset * 1}`; } - parsedDate = DateTime.fromISO(moment(date).toISOString()); + if (options.fromFormat) { + parsedDate = DateTime.fromFormat(date as string, options.fromFormat); + } else { + parsedDate = DateTime.fromISO(moment(date).toISOString()); + } } parsedDate = parsedDate.setZone(timezone || 'Etc/UTC');