@tiledesk/tiledesk-server
Version:
The Tiledesk server module
173 lines (145 loc) • 6.3 kB
JavaScript
const moment = require('moment-timezone');
class DatesUtil {
/**
* Helper function to parse a date with optional time
* Supports formats: DD/MM/YYYY, DD/MM/YYYY HH:mm, DD/MM/YYYY HH:mm:ss, DD/MM/YYYY, HH:mm:ss
*
* @param {string} dateString - Date string with or without time
* @param {string} timezone - Timezone
* @returns {moment.Moment} Parsed moment object
*/
parseDate(dateString, timezone) {
if (!dateString || typeof dateString !== 'string') {
throw new Error(`The date must be a string: ${dateString}`);
}
if (!timezone || typeof timezone !== 'string') {
throw new Error(`The timezone must be a string: ${timezone}`);
}
// Validate timezone: check if it's a valid IANA timezone
if (!moment.tz.zone(timezone)) {
throw new Error(`Invalid timezone: ${timezone}. Please use a valid IANA timezone (e.g., "Europe/Rome", "America/New_York")`);
}
const trimmedDate = dateString.trim();
// List of possible formats to try
const formats = [
'DD/MM/YYYY HH:mm:ss',
'DD/MM/YYYY, HH:mm:ss',
'DD/MM/YYYY HH:mm',
'DD/MM/YYYY, HH:mm',
'DD/MM/YYYY',
'YYYY/MM/DD HH:mm:ss',
'YYYY/MM/DD, HH:mm:ss',
'YYYY/MM/DD HH:mm',
'YYYY/MM/DD, HH:mm',
'YYYY/MM/DD'
];
// Try every format until one valid is found
for (const format of formats) {
// Parse the date in the specified format (without timezone)
//const parsed = moment(trimmedDate, format, true); // strict mode
const parsed = moment.tz(trimmedDate, format, timezone);
if (parsed && parsed.isValid && parsed.isValid()) {
// Apply the timezone to the parsed date
const dateWithTimezone = parsed.tz(timezone);
// Verify that the timezone was applied correctly
if (!dateWithTimezone || !dateWithTimezone.isValid || !dateWithTimezone.isValid()) {
throw new Error(`Unable to apply timezone ${timezone} to date: ${dateString}`);
}
return dateWithTimezone;
}
}
throw new Error(`Unable to parse the date: ${dateString} with timezone: ${timezone}`);
}
/**
* Checks if a date string contains time information
*
* @param {string} dateString - Date string
* @returns {boolean} True if contains time information
*/
hasTimeComponent(dateString) {
// Checks if contains time information (HH:mm or HH:mm:ss)
return /[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?/.test(dateString);
}
/**
* Converts local dates to UTC for database queries
*
* @param {string|null|undefined} startDate - Start date in the format DD/MM/YYYY or DD/MM/YYYY HH:mm:ss (e.g. "20/01/2026" or "20/01/2026 14:30:00"). Optional.
* @param {string|null|undefined} endDate - End date in the format DD/MM/YYYY or DD/MM/YYYY HH:mm:ss (e.g. "20/01/2026" or "20/01/2026 23:59:59"). Optional.
* @param {string} timezone - User timezone (e.g. "Europe/Rome", "America/New_York", "Asia/Tokyo")
* @returns {Object} Object with startDateUTC and/or endDateUTC in ISO string format for MongoDB
*/
convertLocalDatesToUTC(startDate, endDate, timezone) {
if (!timezone || typeof timezone !== 'string') {
throw new Error(`The timezone must be a string: ${timezone}`);
}
// Validate timezone: check if it's a valid IANA timezone
if (!moment.tz.zone(timezone)) {
throw new Error(`Invalid timezone: ${timezone}. Please use a valid IANA timezone (e.g., "Europe/Rome", "America/New_York")`);
}
const result = {
startDateUTC: null,
endDateUTC: null,
startDate: null,
endDate: null
};
// Parse startDate if provided
if (startDate) {
const startMoment = this.parseDate(startDate, timezone);
// If startDate does not contain time, use the start of the day
// Otherwise use the specified time
const startParsed = this.hasTimeComponent(startDate)
? startMoment
: startMoment.startOf('day');
result.startDateUTC = startParsed.utc().toISOString();
result.startDate = startParsed.utc().toDate();
}
// Parse endDate if provided
if (endDate) {
const endMoment = this.parseDate(endDate, timezone);
// If endDate does not contain time, use the start of the next day
// Otherwise use the specified time
const endParsed = this.hasTimeComponent(endDate)
? endMoment
: endMoment.clone().add(1, 'day').startOf('day');
result.endDateUTC = endParsed.utc().toISOString();
result.endDate = endParsed.utc().toDate();
}
// At least one date must be provided
if (!startDate && !endDate) {
throw new Error('At least one of startDate or endDate must be provided');
}
return result;
}
/**
* Creates a MongoDB query object with the converted dates in UTC
*
* @param {string|null|undefined} startDate - Start date in the format DD/MM/YYYY or DD/MM/YYYY HH:mm:ss. Optional (from this date onwards).
* @param {string|null|undefined} endDate - End date in the format DD/MM/YYYY or DD/MM/YYYY HH:mm:ss. Optional (until this date).
* @param {string} timezone - User timezone
* @param {string} fieldName - Field name in the database (default: "createdAt")
* @returns {Object} MongoDB query object with $gte (if startDate provided), $lt/$lte (if endDate provided), or both
*/
createDateRangeQuery(startDate, endDate, timezone, fieldName = 'createdAt') {
const { startDate: start, endDate: end } = this.convertLocalDatesToUTC(startDate, endDate, timezone);
const query = {};
const dateQuery = {};
// Add start date condition (from this date onwards)
if (start) {
dateQuery.$gte = start;
}
// Add end date condition (until this date)
if (end) {
// If endDate contains time, use $lte to include up to that time
// Otherwise use $lt to exclude the start of the next day
const endOperator = endDate && this.hasTimeComponent(endDate) ? '$lte' : '$lt';
dateQuery[endOperator] = end;
}
// Only add the date query if at least one condition was set
if (Object.keys(dateQuery).length > 0) {
query[fieldName] = dateQuery;
}
return query;
}
}
const datesUtil = new DatesUtil();
module.exports = datesUtil;