Correctly treating DST shift with momentJS - javascript

I've came across something that I thought it was right, but now taking a closer look something is clearly wrong.
I'm on a project of a pill reminder app where someone can set notifications to remind him/her of taking pills in the correct time. There're medicines which a person can take for the rest of his life. And in that case I don't set alerts for years, I set for 3 months max and, when he takes one and mark it as done, I set another alert for 3 months later starting on that date/time.
This app will be released only in Brazil and we have Daylight Saving Time here. When it shifts to DST time the clocks must be adjusted to -1 hour after midnight, when going off DST it gains 1 hour.
For this project I'm using Firebase, Ionic 2, the LocalNotification Plugin and Moment JS.
I have to make a story of the user because other user can see if he's taking it correctly, so I use Moment JS to manipulate the datetime and save the notification and create a node for that user in firebase with UNIX time.
LET'S FINALLY GO TO THE PROBLEM.
When saving a date I check if this date is DST, if it is I add +3 hours to it, if it's not I add +2. I need to add this because when I wrap the isoString in the Moment() function it gives me -3 hours or -2 hours (Haven't searched for this, but I think Moment uses USA time).
This works fine if I'm saving dates inside DST times if I'm in DST time, if in some case I'm not on DST and save a notification for a DST time day it saves with +2 hours.
AN EXAMPLE
The DST time will shift to in DST on October 15. If I need to save 30 notifications, one per day everyday as 12AM, starting at October 1 up to October 30. From day 1 to day 15 the dates will be right, from day 16 to 30 they'll be with +2 hours.
Here's the basic code I use:
// THIS'LL SET MY DATEPICKER TO THE DATE/HOUR I'M IN.
minDate: any = Moment().isDST ? Moment().subtract(3, 'h').toDate().toISOString() : Moment().subtract(2, 'h').toDate().toISOString();
// THIS'LL CONVERT THE SELECTED DATE TO A UNIX TIME IN WICH I'LL USE TO SAVE THE NOTIFICATION AND THE MEDICATION DATA ON FIREBASE
unixConverted = Moment(this.minDate).isDST ? Moment(this.minDate).add(3, 'h').unix() : Moment(this.minDate).add(2, 'h').unix();
What is strange is that using Moment().unix() alone it give me the right time I'm in, if I use Moment(this.minDate).unix() it gives me -2 or -3 hours of the hour I selected.
So if it's in DST (in which I've set my clock to -1 hour) I add 3, if not I add 2.
So how is the proper way to manipulate this DST shift?
Is there a better way to do this than using DST?
Am I right using this logic or is this much more complex than what I think?

Ok so i've found a better way without using .isDST() method.
Simple use Moment().add(Moment().utcOffset(), 'm'), this'll get the current moment time and add the offset in minutes.
The .add() and .subract() methods makes Moment return the UTC time.
The utcOffset() returns a positive or negative number of minutes (representing hours) from UTC, like -60 or 180. So i'll get the correct respecting the time shift.
Worked like a charm for me.

Related

Node JS Date Object behaves strangely around daylight savings time

So I am working on a rather larger project and I ran into a bug and I wanted to see what I could be doing wrong. I loop though a large amount of data and round off some dates to clean my output into 15 minute buckets.
CODE:
//how I've been doing things
let expectedBehvaior = new Date("2020-11-01T05:14:00.000Z");
console.log(expectedBehvaior.toISOString());
expectedBehvaior.setMinutes(0);
console.log(expectedBehvaior.toISOString());
//using set minutes creates an issue for only hours 6 UTC on Nov 1st 2020
let crazyDate = new Date("2020-11-01T06:14:00.000Z");
console.log(crazyDate.toISOString());
crazyDate.setMinutes(0);
console.log(crazyDate.toISOString());
//how i fixed it
let fixedDate = new Date("2020-11-01T06:14:00.000Z");
console.log(fixedDate.toISOString());
fixedDate.setTime(fixedDate.getTime() - 60000 * fixedDate.getMinutes());
console.log(fixedDate.toISOString());
OUTPUT:
2020-11-01T05:14:00.000Z
2020-11-01T05:00:00.000Z
2020-11-01T06:14:00.000Z
2020-11-01T05:00:00.000Z
2020-11-01T06:14:00.000Z
2020-11-01T06:00:00.000Z
This seems related to daylight savings time in the US as the same thing happens on November 3rd 2019. Not sure how Date.setMinutes works in the background and was hoping someone could explain the behavior.
Thank you.
EDIT:
I am expecting the hour to remain the same after I call crazyDate.setMinutes(0). Instead it sets it back to 0500 UTC as opposed to 0600 UTC.

MomentJS, how to ignore user's local timezone, and use

Here's the scenario:
1- User opens the website, and enters 07:00 am using a dropdown field, which will give me this :
// 1578600000000 => Save to DB
// Fri Jan 10 2020 07:00:00 GMT+1100 (Australian Eastern Daylight Time)
The user himself is in Sydney, which means his local clock is on GMT+1100
However, he wants to represent this time as Asia/Tehran time, because that's where he's going to be tomorrow. So essentially, he wants me to completely ignore his local time and see him as if he's in Asia/Tehran. So when he's in Tehran tomorrow, he can see his calendar has 07:00am.
On the other hand, are the people all over the world who will see his available time, let's say from Australia/Perth.
I thought something like the below work, as per momentJS documentation, but it doesn't.
First, convert the timezone to Asia/Tehran, which is the user's desired place:
const desiredTimeZone = 'Asia/Tehran';
let OriginalDesired = moment.tz(1578600000000,desiredTimeZone);
Then, when representing it to the people in Australia/Perth make sure it's in their timezone
const PerthTimeZone = 'Australia/Perth`; // this is dynamic, can be anything
OriginalDesired.clone().tz(PerthTimeZone);
I naively thought this should work. But I noticed the original timestamp 1578600000000 is a UTC timestamp, meaning it's not really 07:00am, it's actually 20:00pm, because Javascript
has subtracted 11 hours, which is the very original user's local timezone's offset, from the user entry.
I managed to work around it by adding and subtracting the offset in a dramatic way, but it only works in one scenario.
const originalTime = 1578600000000;
const LocalAdjustment = moment(originalTime).tz("Australia/Sydney").utcOffset() * 60000;
const DesiredAdjustment = moment(originalTime).tz("Asia/Tehran").utcOffset() * 60000;
const newUTC = originalTime + LocalAdjustment - DesiredAdjustment;
And when representing this to the user's in Tehran
moment(newUTC).tz("Asia/Tehran").format('hh:mma'); // 07:00am.
I know this is probably stupid and obviously only works in one scenario, but is this the right path that I'm going? or is there an easier way?
By the way, all the calculations are on my server, which is 'UTC'.
You said:
1- User opens the website, and enters 07:00 am using a dropdown field, which will give me this : // 1578600000000 ...
You've already lost. If the local time zone is not relevant, then don't write code that assumes that it is.
In other words, you probably have something along the lines of:
moment("2020-01-10 07:00")
Instead you should have something like:
moment.tz("2020-01-10 07:00", "Asia/Tehran")
Or rather, you should simply send "2020-01-10 07:00" and "Asia/Tehran" to your database, then later retrieve them and pass them to moment-timezone when you need to know what moment that represents.
As to your other approach, it's generally not a good idea to add or subtract time zone offsets from timestamps. Unix timestamps are inherently UTC based. Adding or subtracting will produce a different moment in time, not adjust for time zone.
Consider that there's also a slim (but not impossible) chance that the offsets returned by your code are incorrect, as they would have to be shifted before being looked up. In other words, Tehran was at UTC+3:30 on the date in question, so the timestamp passed would have to be adjusted by 3 hours 3 minutes before being passed to the moment constructor. This leads to circular logic, and is difficult to resolve. It will show up for timestamps near transitions (either for DST, or for changes to standard time for a particular time zone).

Date ranges with DST in javascript

I'm looking for best practices regarding dates - where it's the date itself that's important rather than a particular time on that day.
An excellent start was this question:
Daylight saving time and time zone best practices
I'd like some guidance applying this for my situation. I have medications starting on a particular date, and ending on another. I then need to query medications which are active in a given date range.
I've tried setting start and end dates as midnight local time, then storing in UTC on the database. I could add timezone entries too.
I'm using moment.js on both client and server, and can use moment timezone if needed.
I'm wondering how to deal with the effect of DST on my times - which makes an hour difference in my locally-midnight UTC times between DST and non DST periods.
The problem I have is for example when some medications have end dates set during a DST period, and some which were set in a non-DST period. Then, their UTC times differ by an hour. When a query is made for a particular date range starting at local midnight, it's not accurate as there are two different representations of midnight. The query itself may treat midnight as one of two different times, depending on when in the year the query is made.
The end result is that a medication may appear to end a day later than it should, or start a day earlier.
A simple but wonky workaround would be to consistently set the start date as 1am in standard (non DST) time, and end dates as 11:59pm standard (non DST) time, and query at midnight.
Or, should I check the start and end dates of each query, and work out what the UTC offset would be for each date?
But I'd much prefer to know what best practice is in this situation. Thanks.
Both the JavaScript Date object and the moment object in moment.js are for representing a specific instant in time. In other words, a date and a time. They internally track time by counting the number of milliseconds that have elapsed since the Unix Epoch (Midnight, Jan 1st 1970 UTC) - ignoring leap seconds.
That means, fundamentally, they are not the best way to work with whole calendar dates. When you have only a date, and you use a date+time value to track it, then you are arbitrarily assigning a time of day to represent the entire day. Usually, this is midnight - but as you pointed out, that leads to problems with daylight saving time.
Consider that in some parts of the world (such as Brazil) the transition occurs right at midnight - that is, in the spring, the clocks jump from 11:59:59 to 01:00:00. If you specify midnight on that date, the browser will either jump forward or jump backward (depending on which browser you are using)!
And if you convert a local date-at-midnight to a different time zone (such as UTC), you could change the date itself! If you must use a date+time to store a date-only value, use noon instead of midnight. This will mitigate most (but not all) of the adjustment issues.
The better idea is to treat whole dates as whole dates. Don't assign them a time, and don't try to adjust them to UTC. Don't use a Date or a moment. Instead, store them either as an ISO-8601 formatted string like "2014-11-25", or if you need to do math on them, consider storing them as an integer number of whole days since some starting value. For example, using the same Jan 1st 1970 epoch date, we can represent November 11th 2014 as 16399 with the following JavaScript:
function dateToValue(year, month, day) {
return Date.UTC(year, month-1, day) / 86400000;
}
function valueToDate(value) {
var d = new Date(86400000 * value);
return { year : d.getUTCFullYear(),
month : d.getUTCMonth() + 1,
day : d.getUTCDate()
};
}
There are a few other things to keep in mind when working with whole dates:
When working with ranges of whole dates, humans tend to use fully-inclusive intervals. For example, Jan 1st to Jan 2nd would be two days. This is different from date+time (and time-only) ranges, in which humans tend to use half-open intervals. For example, 1:00 to 2:00 would be one hour.
Due to time zones, everyone's concept of "today" is different around the globe. We usually define "today" by our own local time zone. So normally:
var d = new Date();
var today = { year : d.getFullYear(),
month : d.getMonth() + 1,
day : d.getDate()
};
You usually don't want to shift this to UTC or another time zone, unless your business operates globally under that time zone. This is rare, but it does occur. (Example, StackOverflow uses UTC days for its calculations of badges and other achievements.)
I hope this gets you started. You asked a fairly broad question, so I tried to answer in way that would address the primary concerns. If you have something more specific, please update your question and I'll try to respond.
If you would like even more information on this subject, I encourage you to watch my Pluralsight course, Date and Time Fundamentals.

Script for business hours javascript

I need script that will display "Open" & "Close" function on my site.
Script should display "OPEN" every day from Monday to Friday from 08:00am to 19:30pm
and for Saturday should display "OPEN" from 08:00am to 15:00pm (else display CLOSED)
Sunday is CLOSED all day long.
I try to manage this script but I was not able to achieve it:
var Digital=new Date()
var hours=Digital.getHours()
if (hours>=08:00&&hours<=19:30)
document.write('Open')
else if (hours>=19:31&&hours<=07:59)
document.write('Close')
but i need addition for the days, this is just for time.
The hours variable will be an integer number, you need to compare it to a number, like this:
if (hours >= 8 && hours <= 19)
document.write('Open')
else if (hours >= 19 && hours <= 7)
document.write('Close')
When rewrite those methods, you will need to get and compare the minutes from the Digital variable too.
You need to check the current date using if statements before checking the time. Your formatting was slightly off, as digital.getHours() returns a whole number rather than those formatted strings.
I also added a setInterval to update the status every minute, in case the page is left open for prolonged time.
Here is the fiddle: http://jsfiddle.net/u6bwJ/1/
EDIT: Fixed a few bugs (namely typos). I also see you need localization for this. I made some changes to the top of the code which adjust for timezone, so it's always displaying information based on local time. There is one caveat though, and that is that it is currently hardcoded to include daylight savings. This means it will be inaccurate once DST switches.
Line 10:
utc1Time=new Date(localTime.getTime() + (localTime.getTimezoneOffset() + 120) * 60000);
That + 120 is adding 2 hours after converting client time to UTC time, which makes it UTC+1 and then adds the DST offset. You will need to add some way to check if DST is in effect, something along the lines of
utc1Time.toString().match(/daylight/i)
but I will leave that to you, as this is probably enough of a framework for you to build upon.
Hope this helped :D

Monotonic/Vixie-style DST interpretation in JavaScript

I'm looking for an JavaScript algorithm to convert local calendar date-times into UTC milliseconds since Unix epoch (or to Date objects representing the same). A typical, and often useful, way to do this for some relative calendar date YYYY-MM-DD hh:mm:ss.zzz is:
# Close, but not quite monotonic
(new Date(YYYY, MM-1, DD, hh, mm, ss, zzz)).getTime()
JavaScript implementations have their ways of dealing with DST (read: daylight saving time, summer time, your own locale's equivalent as necessary), and unfortunately they don't work for the application at hand, which instead calls for local time that is monotonic (but may be distorted if necessary).
The conversion must be monotonic in the sense that for two relative calendar dates (i.e. timezone-ignorant, DST-ignorant) A and B, the conversion, a function C, is such that
If A is strictly earlier than B then C(A) ≤ C(B)
If A is simultaneous to B then C(A) = C(B)
If A is strictly later than B then C(A) ≥ C(B)
(This doesn't refer to monotonicity in the sense that successive calls to a time-getting function are strictly non-decreasing—this application has no concept of the current time and doesn't need anything like that.)
I've started working on an implementation of my own, but it's threatening to be complicated, and I think perhaps someone else has better ideas.
The questions are:
Has this already been implemented?
If not, is there a saner way to implement this than what I've outlined below?
Do the discontinuity-finding heuristics work for all known DSTs worldwide?
JavaScript Date's behavior
The following represent the corner cases for DST in the US in 2012. The values are experimental results from Firefox. Here, let the function C represent the result of creating a Date object using the given local date and time, then using the getTime() method to retrieve the UTC milliseconds.
Skipped hour: On 2012-03-11, the start date of DST, the hour starting 02:00 is skipped in local time: The minute following 01:59 is 03:00. In Firefox, a later input time may result in an earlier resolved time; e.g. 01:45 < 02:15, but C(01:45) > C(02:15), so the scale is not monotonic.
Doubled hour: On 2012-11-04, the end date of DST, the hour starting 01:00 occurs twice in local time: The minute following 01:59 daylight time is 01:00 standard time, then the minute following 01:59 standard time is 02:00 standard time. In Firefox, C(01:30) corresponds to the later repetition, 01:30 standard time.
This does not break monotonicity, as long as the resolving behavior is guaranteed to favor the later time. (I don't have documentation of this guarantee, but perhaps it follows from some language in ECMA-262.)
Required/preferred behavior
Here, let the function C represent a conversion with the desired behavior.
Skipped hour: The date for a skipped minute should be resolved to the first following unskipped minute. For example, on the DST start date, 02:15, 02:30, and 02:45 would resolve to the next unskipped minute, 03:00; in other words, C(02:15) = C(02:30) = C(02:45) = C(03:00).
Unskipped times would remain untransformed: Compare 01:45 < 02:15 < 02:45 < 03:15 to C(01:45) < C(02:15) = C(02:45) < C(03:15).
Doubled hour: The date for a minute occurring multiple times should be resolved to the first occurrence only; e.g. 01:30 on the end date of DST would be resolved to 01:30 daylight time rather than standard time, since that is the earlier of the two.
Same as before, except that the earlier time is guaranteed.
These rules are loosely based on those of Vixie cron. However, this application doesn't deal with a concept of current time and thus doesn't have the state it would need to watch the clock for time changes. It would need some other way to determine if and when times will be skipped or doubled.
Incidentally, as an additional requirement, the implementation must not assume that it is running in a US locale; it needs to work internationally and, wherever possible, use detection over configuration.
Implementation thoughts
One thing I thought might work for detecting whether a date falls into a discontinuity would be to test the width of the span of the local dates ±1 calendar day from the date. If the difference between the UTC times of the two dates is less than or greater than 48 hours, it would imply that some time had been skipped or doubled, respectively.
If skipped, we might further determine whether the given time itself is skipped if, after converting to UTC and back, the hh:mm:ss.zzz reads differently. If so, the time is resolved to the first minute after the discontinuity.
If doubled, we might determine the range of all times in the later repetition. If the given time falls within the later repetition, it is reverted to the earlier; otherwise, it is left alone.
Both of these could require the exact location of the discontinuities, which could for example be accomplished with a binary search bounded by the ±1 dates.
This heuristic could fail for multiple reasons; though I'm of the impression that they are unlikely, summer time rules are strange and inconsistent worldwide:
If it's possible for more than one discontinuity in either direction to occur within the same 3 calendar days. (In the US, there are two per year, months apart. I doubt anyplace adjusts any amount greater than, say, four hours.)
If the discontinuities are complementary, they may not be detected in the first place.
In any case, a simple search would make the assumption that there is only one discontinuity within the range.
If it's possible for a single discontinuity to account for a duration of (nearly) 3 calendar days. (In the US, each discontinuity accounts for one hour. I'm fairly certain that summer time adjustment is never on the order of days anywhere.)
An implementation consistent with Required/preferred behavior above, at least for the current USA rules, is now available. Overall, the mechanism isn't terribly inefficient. In the worst case (a skipped minute), it's O(lg n) where n is the number of minutes of change between summer and winter (60 where I live), and in all other cases, it's O(1).
The input is a "face" (a calendar date and time ignorant of DST).
The output is a Date with its local face set based on the input face.
If the input face represents exactly one local date, the output Date is the same as if the stats of the face were passed to the Date constructor.
If the input face represents zero local dates (due to DST skipping forward), the output Date reflects the first minute following the skip.
If the input face represents two local dates (due to DST repeating), the output Date reflects the earlier of the two.
Notes on the implementation:
The .getTimezoneOffset() method is used to determine whether two dates are on opposite sides of a discontinuity.
The offset before any discontinuity a face might be near is found by retrieving the offset of a Date 24 local hours prior to that face.
The face is converted to a Date by passing its stats to the Date constructor to be interpreted locally.
If the local face of the converted Date is not the same as the input face, this face is not directly representable in local time; it has been skipped.
The converted Date is treated as invalid.
The timezone offset following the discontinuity is determined by retrieving the offset of a Date 24 local face hours after the input face.
The difference between the offsets before and after the discontinuity is found. From that amount of time before the input face to that amount of time after (which should by definition both be representable as local dates), a binary search is used to locate the first minute after the discontinuity. A Date representing this minute is the output.
If the local face of the converted Date is the same as the input face,
If the converted Date has the pre-discontinuity offset, it is correct.
This includes any face not near a DST change; if there is no discontinuity, then all times for that day share the same offset.
Even if the face is in a doubled time, the early interpretation is the correct one. (Whether this can happen may be implementation-dependent.)
If the converted Date occurs after the discontinuity,
It is correct if it is at a time later than the discontinuity by at least the offset difference.
If it is in the range of time from the discontinuity through one offset difference afterward, it is in the later interpretation of a doubled time. The correct date is found by subtracting the offset difference, yielding the earlier interpretation.
A Date is determined to be in this range if a Date that is earlier by one offset difference has the early offset. Since the converted Date has the late offset, it is determined that the discontinuity happened more recently than that.

Categories

Resources