I have an app that allows users to schedule tasks. Users can set a schedule (schedule, an rrule string) as well as a timezone (scheduleTimeZone, a string e.g. Asia/Dubai).
I am trying to write a function (getNextRunAt) that gets the next occurrence of the task at a UTC date and store this in my Postgres DB as a timestamptz field.
I'm struggling to account for DST, lots of the results are an hour or even a day off.
Here's the function (in TypeScript):
import RRule from 'rrule';
import moment from 'moment-timezone';
const getNextRunAt = ({
schedule,
scheduleTimeZone,
}: {
schedule?: string | null;
scheduleTimeZone?: string | null;
}): Date | undefined => {
if (!schedule) {
return undefined;
}
const options = RRule.parseString(schedule);
if (scheduleTimeZone) {
options.tzid = scheduleTimeZone;
}
const dtstart = moment.utc().toDate();
const rule = new RRule({ ...options, dtstart, count: 1 });
const dates = rule.all();
let date = dates[0];
if (scheduleTimeZone && moment(date).tz(scheduleTimeZone).isDST()) {
date = moment(date).subtract(1, 'hour').toDate();
}
return date;
};
export default getNextRunAt;
It works for some dates/times/timezones:
Date.now = jest.fn(() => new Date('2021-03-02 10:24:27.000000Z').getTime());
const nextDate = getNextRunAt({
schedule: 'RRULE:INTERVAL=1;BYDAY=MO,TU,WE,TH,SA,FR,SU;BYMINUTE=0;BYHOUR=9;BYSECOND=0;FREQ=DAILY',
scheduleTimeZone: 'America/Los_Angeles',
});
expect(nextDate).toEqual(new Date('2021-03-02 17:00:00.000000Z'));
// WORKS
But not for others:
Date.now = jest.fn(() => new Date('2021-03-02 10:24:27.000000Z').getTime());
const nextDate = getNextRunAt({
schedule: 'RRULE:INTERVAL=1;BYDAY=MO,TU,WE,TH,SA,FR,SU;BYMINUTE=0;BYHOUR=9;BYSECOND=0;FREQ=DAILY',
scheduleTimeZone: 'America/Los_Angeles',
});
expect(nextDate).toEqual(new Date('2021-03-02 17:00:00.000000Z'));
// DOES NOT WORK
Expected: 2021-03-02T17:00:00.000Z
Received: 2021-03-03T17:00:00.000Z
I think that the last case is wrong with a day. But for others solution might look like:
const getNextRunAt = ({ schedule, scheduleTimeZone }: { schedule?: string | null; scheduleTimeZone?: string | null }): Date | undefined => {
if (!schedule) {
return undefined;
}
const options = RRule.parseString(schedule);
const dtstart = moment();
const rule = new RRule({
...options,
dtstart: dtstart.toDate(),
count: 1,
});
const dates = rule.all();
const date = dates[0];
let mDate = moment.tz(date, scheduleTimeZone);
const offset = Math.abs(mDate.utcOffset()) > 16 ? mDate.utcOffset() / 60 : mDate.utcOffset();
if (!mDate.isDST()) {
mDate = mDate.add(1, 'hours');
}
mDate.add(-offset, 'hour');
return mDate.toDate();};
Related
I've in my react frontend multiple dates in an Array with this format 'MM/YYYY'
Now I want to get my history from MongoDB that's createdAt the time range of one month.
How can i pass my data in this axios get request?
My Frontend
let date = '11/2022'
const getHistory = async () => {
let monthYearStart = dayjs(date, 'MM/YYYY').format('YYYY.MM.01');
let monthYearEnd = dayjs(date, 'MM/YYYY').format('YYYY.MM.32');
const res = await axios.get('/api/monthlyhistory');
setPdfHistory(res.data);
};
getHistory().then(() => {});
My Backend
try {
const history = await History.find({
status: true,
createdAt: {
$gte: dayjs(new Date(monthYearStart, 'YYYY.MM.DD')),
$lt: dayjs(new Date(monthYearEnd, 'YYYY.MM.DD')),
},
});
res.json(history);
} catch (err) {
return res.status(500).json({ msg: err.message });
}
One option would be to pass the dates as query parameters. I would recommend using ISO 8601 format to remove ambiguity use the native Date constructor
Client-side
// Note these are local dates
const monthYearStart = new Date(2022, 10); // month is a zero-based index
const monthYearEnd = new Date(monthYearStart);
monthYearEnd.setMonth(monthYearEnd.getMonth() + 1);
monthYearEnd.setDate(monthYearEnd.getDate() - 1);
const res = await axios.get("/api/monthlyhistory", {
params: {
monthYearStart: monthYearStart.toISOString(),
monthYearEnd: monthYearEnd.toISOString(),
},
});
Server-side
const { monthYearStart, monthYearEnd } = req.query;
const history = await History.find({
status: true,
createdAt: {
$gte: new Date(monthYearStart),
$lt: new Date(monthYearEnd),
},
});
I want to select the data before or after a certain date in sqlite using Typeorm where date is in milliseconds.
Here is my Query Builder:
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder('p')
.where('createdAt < :cursor', {
cursor: "1624809058000"
})
.getMany()
We have to pass the date in this format: "yyyy-mm-dd HH:MM:ss.l" Example: "2000-01-01 00:00:00.000"
So, we have to convert the date in this format.
First solution
const chars = { T: ' ', Z: '' }
const dateInMilliseconds = "1624809058000"
const qb = getConnection()
.getRepository(Post)
.createQueryBuilder('p')
.where('createdAt < :cursor', {
cursor: new Date(parseInt(dateInMilliseconds))
.toISOString()
.replace(/[TZ]/g, ch => chars[ch])
})
.getMany()
Second solution
import { LessThan, MoreThan } from 'typeorm'
import { format } from 'date-fns'
// TypeORM Query Operators
const MoreThanDate = (date: Date) => MoreThan(format(date, 'yyyy-mm-dd HH:MM:SS'))
const LessThanDate = (date: Date) => LessThan(format(date, 'yyyy-mm-dd HH:MM:SS'))
const dateInMilliseconds = "1624809058000"
const posts = await Post.find({
createdAt: MoreThanDate(new Date(dateInMilliseconds))
})
Third solution
import { Between } from 'typeorm';
import { addYears, subYears } from 'date-fns';
// TypeORM Query Operators
const MoreThanDate = (date: Date) => Between(date, addYears(date, 100));
const LessThanDate = (date: Date) => Between(subYears(date, 100), date);
const dateInMilliseconds = "1624809058000"
const posts = await Post.find({
where: {
createdAt: MoreThanDate(new Date(dateInMillisecons)),
},
});
I have function called setMockDate that looks like:
/**
* #param {Date} expected
* #returns {Function} Call to remove Date mocking
*/
const setMockDate = (expected: Date): AnyObject => {
const RealDate = Date
function MockDate(mockOverride?: Date | number) {
return new RealDate(mockOverride || expected)
}
MockDate.UTC = RealDate.UTC
MockDate.parse = RealDate.parse
MockDate.now = () => expected.getTime()
MockDate.prototype = RealDate.prototype
// eslint-disable-next-line #typescript-eslint/no-explicit-any
global.Date = MockDate as any
return () => {
global.Date = RealDate
}
}
export default setMockDate
and used like:
test('Should change date', () => {
const mockDate = new Date('Feb 22, 2021 11:59:00')
setMockDate(mockDate)
expect(new Date()).toEqual(mockDate)
})
I wanted to change the MockDate function in setMockDate() to use an arrow function like:
const MockDate = (mockOverride?: Date | number) => {
return new RealDate(mockOverride || expected)
}
However I get TypeError:
TypeError: Date is not a constructor
Why I am getting this error when changing to use an arrow function?
MockDate is supposed to mimic new Date() which is a constructor and arrow function cannot be used as a constructor
More information here:
https://dmitripavlutin.com/when-not-to-use-arrow-functions-in-javascript/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions
I updated my helper function to:
import { AnyObject } from 'typings/custom'
/**
* #param {Date} expected
* #returns {Function} Call to remove Date mocking
*/
const setMockDate = (expected: Date): AnyObject => {
const RealDate = Date
function MockDate(mockOverride?: Date | number) {
return new RealDate(mockOverride || expected)
}
MockDate.now = () => expected.getTime()
MockDate.prototype = RealDate.prototype
// eslint-disable-next-line #typescript-eslint/no-explicit-any
global.Date = MockDate as any
return () => {
global.Date = RealDate
}
}
export default setMockDate
Using this helper looks like:
/**
* Testing a component that changes text based on time of day
* ie: good morning, good afternoon & good evening
*/
describe('Greeting', () => {
it('Should update message after time', () => {
jest.useFakeTimers()
setMockDate(new Date('Feb 22, 2021 11:59:00'))
const wrapper = mount(<Greeting />)
const greetingText = wrapper.text()
setMockDate(new Date('Feb 22, 2021 12:00:00'))
jest.advanceTimersByTime(60000)
expect(wrapper.text()).not.toBe(greetingText)
})
})
I am saving state in my React app via localStorage
const [items, setItem] = useState(() => {
let itemsString = window.localStorage.getItem('items');
if (itemsString) {
try {
return JSON.parse(itemsString);
} catch (e) {
console.error(e);
return [];
}
} else {
return [];
}
})
when I JSON.parse(itemsString) the date in the state has been converted to UTC (because strings/localstorage)
How do I JSON.parse() my state and reinitialize the date string to an object?
e.g. instead of returning 2019-07-19T00:28:03.058Z return Thu Jul 18 2019 20:28:03 GMT-0400 (Eastern Daylight Time) instead
Solution I came up with the help of Aaron's suggestion below.
Store old state. Map over state array, creating new empty object and storing each property in it, store date, convert date to string and pass that into new Date object to instantiate the value back to a date object on page refresh.
const [items, setItem] = useState(() => {
let itemsString = window.localStorage.getItem('items');
if (itemsString) {
try {
const oldState = JSON.parse(itemsString);
const newState = oldState.map(item => {
const date = item.date;
const container = {}
container.id = item.id
container.item = item.item
container.date = new Date(date.toString())
container.cost = item.cost
container.type = item.type
return container;
})
return newState;
} catch (e) {
console.error(e);
return [];
}
} else {
return [];
}
})
After you parse itemString just set the date key to a new Date object
const object = JSON.parse(itemString);
const newState = {...object, date: new Date(object.date)}
I have this MapStateToProps:
const mapStateToProps = (state) => {
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const ESFReport = getESFReport(state);
const resultStatusReport = getResultStatusReport(state);
return {
companInfo: getCompanyInfo(state),
companyId: getCompanyId(state),
...ESFReport,
...resultStatusReport,
year,
month,
};
};
And both ...ESFReport, and ...resultStatusReport, have the same property: report but I need somehow to change the name because in the same component I use const { report } = this.props two times but for different props.
How can I do this? (it used to work when I have only ...ESFReport, but when I added ...resultStatusReport, it broke).
Thanks in advance.
If you don't need anything other than report from the ESFReport and resultStatusReport objects you could do:
const mapStateToProps = (state) => {
const date = new Date();
const year = date.getFullYear();
const month = date.getMonth() + 1;
const { report: ESFReport } = getESFReport(state);
const { report: resultStatusReport } = getResultStatusReport(state);
return {
companInfo: getCompanyInfo(state),
companyId: getCompanyId(state),
ESFReport,
resultStatusReport,
year,
month,
};
};
It just renames the report property on each to ESFReport and resultStatusReport. You would then have this.props.ESFReport which would actually be ESFReport.report and this.props.resultStatusReport which would be resultStatusReport.report.