Date ranges with DST in javascript - 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.

Related

Monday and Sunday of previous week in Unix timestamp AND in New Zealand timezone

I was wondering what would be the best way to tackle getting the unix timestamps of Monday and Sunday in New Zealand timezone while the system clock (AWS Lambda) is in a different timezone.
I've tried the below and it seems to work well in my local computer however obviously when executed on AWS, it'll be a different timezone.
Can someone please suggest the best way to deal with timezones so the code can run on whatever location?
var monday = moment().day(-13).startOf('day').toDate().getTime() // Monday last week
var sunday = moment().day(-7).startOf('day').toDate().getTime() // Sunday last week
Unix Timestamp always give time in UTC regardless of your position. To get the local time of your position please apply the timezone offset to the unix timestamp. https://en.wikipedia.org/wiki/Unix_time
A few things:
You can use Moment-Timezone to work with IANA time zone identifiers in Moment.
New Zealand has two different IANA time zone identifiers:
Pacific/Auckland covers most of New Zealand, which uses UTC+12:00 during standard time, and UTC+13:00 during daylight saving time.
Pacific/Chatham covers the Chatham Islands which is also part of New Zealand. However, Chatham uses UTC+12:45 during standard time, and UTC+13:45 during daylight saving time.
You do not need to convert to a Date object. Moment can give you a Unix timestamp directly, either as seconds with .unix(), or as milliseconds with .valueOf(). Both imply a conversion to UTC, as Unix timestamps are inherently UTC based.
One cannot get a Unix timestamp for an entire day, but rather only for a point in time. The timestamp you are probably looking for would be at the start of the local day, which is always midnight (00:00) in New Zealand (but not necessarily in other time zones on DST transition days, depending on the time of the transition).
When use Moment's day function, day(-13) doesn't mean "Monday last week". It means "two Mondays ago." If you meant "one Monday ago", that would be day(-6). Likewise day(-7) means "one Sunday ago. In both cases, it doesn't count the current day. For example, since today is Sunday 2019-09-01, day(-7) refers to Sunday 2019-08-25.
Putting this all together:
var oneMondayAgoInAuckland = moment.tz('Pacific/Auckland').day(-6).startOf('day').valueOf();
var oneSundayAgoInAuckland = moment.tz('Pacific/Auckland').day(-7).startOf('day').valueOf();
At the moment, these return 1566734400000 and 1566648000000 respectively.

Set date() to midnight in users timezone with moment.js

I use moment.js to display a UTC date in the users local timezone:
var date = new Date(Date.UTC(2016,03,30,0,0,0));
var now = new Date();
var diff = (date.getTime()/1000) - (now.getTime()/1000);
var textnode = document.createTextNode(moment(date).format('dddd, DD.MM.YYYY') + ' a las ' + moment(date).format('HH:mm A'));
document.getElementsByClassName("date")[0].appendChild(textnode.cloneNode(true));
I later use the diff variable to show a countdown timer.
I would like to show a different countdown timer to everyone in their local time zone. (Using the difference till its midnight in their time zone, not in UTC)
But I am struggeling to get it work. Instead of using var date = new Date(Date.UTC(2016,03,30,0,0,0)); I probably need to use some function of moment.js that gives me till midnight in the users time zone.
The best example would be new years eve. If I use UTC everyone would have the same counter (9 hours left) but in different parts of the world this wouldn't make sense. For someone in australia it should be 2 hours left, and for someone in the US 14 hours.
I'm not sure that I fully understand your question, but I'll give you some general advice and tips.
When using moment.js, there is very little need to ever use the Date object. Only use it for interacting with other APIs that expect a Date object.
To get a moment in UTC, just use moment.utc(...), passing the appropriate arguments, such as moment.utc([2016,3,30]) or moment.utc('2016-04-30') for midnight April 30th UTC.
If you want to convert that back to the user's local time, use the .local() function. For example, moment.utc('2016-04-30').local() will create a moment with the equivalent local time to the UTC time provided.
If you want a moment in the user's local time, then that would be moment(...), such as moment([2016,3,30]) or moment('2016-04-30') for midnight April 30th local time.
You can difference two moments using the diff function, which can give the answer in specific units, such as m1.diff(m2, 'seconds') where m1 and m2 are moment objects.
You don't need to call format twice. Just encapsulate any text you want outputed with square brackets. .format('dddd, DD.MM.YYYY [a las] HH:mm A')
You might look into moment's locale support. If I'm not mistaken, "a las" indicates Spanish, however it's not always "a las", but sometimes "a la", if the hour is 1. Also, moment only uses those words in its .calendar() function, such as when producing a phrase like "mañana a las 13:17". A regular date formatted with .format('LLLL') in the Spanish locale would be something like: "sábado, 19 de marzo de 2016 13:17". So, you might want to verify that "a las" is exactly what you want in every case.
The title to this question was how to set a date to midnight. For that, I recommend using moment's startOf function. m.startOf('day') will give set the moment m to the start of the day, which is usually midnight. Keep in mind that not every local day actually starts at midnight in every time zone. Due to anomalies like daylight saving time, some days might start at 1:00. For example, this occurs in Brazil on October 16th this year.
Also, if you created the moment in UTC mode, you may wish to convert it back to local mode first before setting it to the start of the day. If you don't want to change the original moment object, be sure to clone it first.
Putting this all together:
var m1 = moment.utc([2016,3,30]);
var m2 = m1.clone().local().startOf('day');
var now = moment();
var diff = m1.diff(now, 'seconds');

Extracting utcOffset from an ISO String with Moment.js

Using moment.js, I'm attempting to extract the offset from an ISO date string so I can use the offset later when formatting an epoch timestamp to ensure the conversion of the timestamp is in the same timezone.
Even though the offset in the string is -0400, the result is always 0;
var currentTime = "2015-03-18T16:10:00.001-0400";
var utcOffset = moment(currentTime).utcOffset(); // 0
I've attempted to use parseZone() as well without success. Is there a way to extract -0400 from the string so I can use it while formatting another time?
Thanks for the help!
KC
The correct way to extract the offset is indeed with parseZone
var currentTime = "2015-03-18T16:10:00.001-0400";
var utcOffset = moment.parseZone(currentTime).utcOffset();
This should result in -240, which means 240 minutes behind UTC, which is the same as the -0400 in the input string. If you wanted the string form, instead of utcOffset() you could use .format('Z') for "-04:00" or .format('ZZ') for "-0400".
The form you gave in the question just uses the computer's local time zone. So it is currently UTC+00:00 in your time zone (or wherever the code is running), that would explain why you would get a zero. You have to use parseZone to retain the offset of the input string.
Also - your use case is a bit worrying. Remember, an offset is not the same thing as a time zone. A time zone can change its offset at different points in time. Many time zones do this to accommodate daylight saving time. If you pick an offset off of one timestamp and apply it to another, you don't have any guarantees that the offset is correct for the new timestamp.
As an example, consider the US Eastern time zone, which just changed from UTC-05:00 to UTC-04:00 when daylight saving time took effect on March 8th, 2015. If you took a value like the one you provided, and applied it to a date of March 1st, you would be placing it into the Atlantic time zone instead of the Eastern time zone.

Set JavaScript countdown to end at midnight PST?

I am trying to get a countdown to end Monday # midnight PST. I thought I had it working a week ago but apparently not.
I am using date.js
var monday = Date.today().next().monday();
var UTCmonday = new Date(monday.getTime() + monday.getTimezoneOffset() * 60000);
var PSTmonday = new Date(UTCmonday.setHours(UTCmonday.getHours() + 9));
$('#defaultCountdown').countdown({until: UTCmonday});
I think the problem is in determining UTC time? Am I right? How do I work this out?
Assuming you Pacific Standard Time, then you need to remember that PST === UTC-8
Thus, your third line would be
var PSTmonday = new Date(UTCmonday.setHours(UTCmonday.getHours() - 8));
The problem with this is that this will fail if the UTC is any earlier than 8am, since you can't pass a negative number into setHours.
Since you're using Datejs, why not use its full capabilities for changing the timezone?
http://code.google.com/p/datejs/wiki/APIDocumentation#setTimezone
Getting the time strictly in PST doesn't make much sense, as for almost half of the year PST isn't observed in the Pacific time zone. PST (UTC-8) is observed in the winter, and PDT (UTC-7) is observed in the summer. You can't represent Pacific Time as just a fixed offset, and unless it happens to be your own local time zone, you can't easily determine the transition between them without a time zone database. See the timezone tag wiki.
Also, date.js has been abandoned. I can't recommend any solution that continues its use. Support for the setTimezone method that Dancrumb suggested is culture specific, and it still doesn't take daylight saving time into consideration.
Instead, I recommend trying moment.js. You can use the moment-timezone add-on to work with the America/Los_Angeles zone - which is a good exemplar of US Pacific time. Make sure your moment-timezone-data.js file includes at least this zone. Then you can do the following:
var m = moment().tz('America/Los_Angeles').day(7).startOf('day');
var s = m.toISOString(); // if you need a string output representing UTC
var dt = m.toDate(); // if you need an actual JavaScript Date object
Let's break that down a bit:
moment() gets you the current time
tz('America/Los_Angeles') adjusts the time to the timezone you are interested in.
day(7) advances the time to the next Monday.
startOf('day') snaps the time back to midnight.

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