I need to make illusion of working in the selected by the user timezone. The problem that server and client code are stick to javascript date. So to achieve the requirement I have made mannualy mapping from utc to the date on the client side:
dateToServer(date) {
const momentDate = moment(date);
let serverDate = null;
if (momentDate.isValid() && date) {
const browserUtcOffset = momentDate.utcOffset();
serverDate = momentDate
.utc()
.subtract(this.clientTimeZoneOffset, 'minutes')
.add(browserUtcOffset, 'minutes')
.toDate();
}
return serverDate;
}
dateToClient(date) {
const momentDate = moment(date);
let uiDate = null;
if (momentDate.isValid() && date) {
const browserUtcOffset = momentDate.utcOffset();
uiDate = momentDate
.utc()
.subtract(browserUtcOffset, 'minutes')
.add(this.clientTimeZoneOffset, 'minutes')
.toDate();
}
return uiDate;
}
I'm adding/subtracting the browserUtcOffset, because it is adding/subtracting automatically by browser when the date is go between server and client.
It was working well, but this solution is missing handling of the DST. I'd like to check is DST active for the date and then add DST offset if need.
Here C# code, that can do this:
string timeZone = "Central Standard Time";
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
DateTime date = new DateTime(2011, 3, 14);
Console.WriteLine(timeZoneInfo.BaseUtcOffset.TotalMinutes); // -360
Console.WriteLine(timeZoneInfo.GetUtcOffset(date).TotalMinutes); // -300
Console.WriteLine(timeZoneInfo.IsDaylightSavingTime(date)); // true
Console.WriteLine(timeZoneInfo.DaylightName);
Console.WriteLine(timeZoneInfo.SupportsDaylightSavingTime);
I have found the isDST
in the momentjs, and when I have my windows local timezone to CST and check moment([2011, 2, 14]).isDST();
in browser console I see the true. How you can see the isDST
is depends on the browser local time.
Next step try to use moment-timezone to do smth like I have done in C#. Unfortunately I don't understand how to achieve this. The first problem that as start point I have: UTC time
, Base offset(-360 in C# sample)
, timezone name: Central Standard Time
, but timezones in the moment-timezone are different.
moment.tz.names().map(name => moment.tz.zone(name)).filter(zone => zone.abbrs.find(abbr => abbr === 'CST') != null)
this code returns 68 timezones.
Why there each of them have many abbrs? What mean untils? All I want to check if UTC time in the selected timezone, which is "Central Standard Time", the daylight saving day active. See C# sample one more time :)
string timeZone = "Central Standard Time";
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
DateTime date = new DateTime(2011, 3, 14);
Console.WriteLine(timeZoneInfo.BaseUtcOffset.TotalMinutes); //-360
Console.WriteLine(timeZoneInfo.GetUtcOffset(date).TotalMinutes); //-300
Console.WriteLine(timeZoneInfo.IsDaylightSavingTime(date)); //true, I have checked is daylightsavingtime for the date
Console.WriteLine(timeZoneInfo.DaylightName);
Console.WriteLine(timeZoneInfo.SupportsDaylightSavingTime);
The application has been written on the angularjs 1.6.3.
Check please the answer of Matt Johnson for this question. It was very usefull for me.
Vocabulary
Problem
The main problem f.e. when a man logged in on the PC which is located in the timezone +3, but setting for his account is -6. The angularjs and kendo is using on the project and the kendo time is working only with native js date. And native js date is always in the browser timezone, for this example +3. But I should setup the stuff like it in -6. F.e. example user has selected time 07:00, the UTC will be 04:00 (07:00 - 3), but as for his account the timezone is -6, the utc should be 13:00 (07:00 + 6). And this conversations are applied automatically when we converting native js date(UI date) to UTC and backward. So decision to count the offset on the server and get rid off browser timezone offset: utcTime = UItime + browserOffset - clientTimezoneBaseOffset - daylightSavingTimeOffset
.
However there is problem when need to get UI time back, f.e. today 06.06.2014 and the DST is true for this date, but when we are getting 03.03.2014 date from server we don't know if the DST was active for 03.03.2014.
Answer
In project on the server the .net, so in database the window's timezone IDs are stored. I have done it the next way: count on server DST for date ranges in the range from minimum date on the server and save on the client in the local storage. I like that in this case
the server is one source of truth, so the calculations can be performed on any client without manipulating the timezones. And no need to convert between IANA, Windows and rails timezones. But also there is a problem: need to precalculate DST in range from DateTime.MinValue
to DateTime.MaxValue
, currently to speed up I'm calculating DST for range from 01.01.2000
to DateTime.Now
- it is enough when converting value from database to UI, because in database the min date is on the 2008 year, but is not enough for html input, because user can select value greater than DateTime.Now
and lower then 01.01.2000
. To fix it I'm, planning with use of TimeZoneConverter send to client the IANATimezoneId
, and for cases when provided date(UI or server) is not in the range [01.01.2000, DateTime.Now]
belong on the moment.utc(date).tz(IANATimezoneId).isDST()
.
This is new code on the server side
private class DaylightSavingTimeDescriptor
{
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public bool IsDaylightSavingTime { get; set; }
}
private string GetDaylightSavingTimeDescriptorsJson(string timeZone)
{
string daylightSaveingTimeDescriptorsJson = String.Empty;
if(timeZone != null)
{
List<DaylightSavingTimeDescriptor> dstList = new List<DaylightSavingTimeDescriptor>();
TimeZoneInfo timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(timeZone);
DateTime startDate = new DateTime(2000, 1, 1);
DateTime dateIterator = startDate;
bool isDST = timeZoneInfo.IsDaylightSavingTime(startDate);
while (dateIterator < DateTime.Now)
{
bool currDST = timeZoneInfo.IsDaylightSavingTime(dateIterator);
if (isDST != currDST)
{
dstList.Add(new DaylightSavingTimeDescriptor()
{
EndTime = dateIterator.AddDays(-1),
IsDaylightSavingTime = isDST,
StartTime = startDate
});
startDate = dateIterator;
isDST = currDST;
}
dateIterator = dateIterator.AddDays(1);
}
daylightSaveingTimeDescriptorsJson = Newtonsoft.Json.JsonConvert.SerializeObject(dstList);
}
return daylightSaveingTimeDescriptorsJson;
}
And this is modified dateToServer, dateToClient on client side
export default class DateService{
constructor (authService) {
const authData = authService.getAuthData();
this.clientTimeZoneOffset = Number(authData.timeZoneOffset);
this.daylightSavingTimeRanges = authData.daylightSavingTimeRanges ? JSON.parse(authData.daylightSavingTimeRanges) : [];
}
getDaylightSavingTimeMinutesOffset(utcDate) {
const dstRange = this.daylightSavingTimeRanges.find(range => {
const momentStart = moment(range.startTime).utc();
const momentEnd = moment(range.endTime).utc();
const momentDate = moment(utcDate).utc();
return momentStart.isBefore(momentDate) && momentEnd.isAfter(momentDate);
});
const isDaylightSavingTime = dstRange ? dstRange.isDaylightSavingTime : false;
return isDaylightSavingTime ? '60' : 0;
}
dateToClient(date) {
const momentDate = moment(date);
let uiDate = null;
if (momentDate.isValid() && date) {
const browserUtcOffset = momentDate.utcOffset();
uiDate = momentDate
.utc()
.subtract(browserUtcOffset, 'minutes')
.add(this.clientTimeZoneOffset, 'minutes')
.add(this.getDaylightSavingTimeMinutesOffset(momentDate.utc()), 'minutes')
.toDate();
}
return uiDate;
}
dateToServer(date) {
const momentDate = moment(date);
let serverDate = null;
if (momentDate.isValid() && date) {
const browserUtcOffset = momentDate.utcOffset();
serverDate = momentDate
.utc()
.subtract(this.clientTimeZoneOffset, 'minutes')
.add(browserUtcOffset, 'minutes')
.subtract(this.getDaylightSavingTimeMinutesOffset(momentDate.utc()), 'minutes')
.toDate();
}
return serverDate;
}
}
PS
I want to get rid off offsets and use moment-timezone on the client and timezone converter on the server, but it will be later if customer ask, because I have never tried it before and I'm not sure that it will be working well, and current solution is working. Also the offsets anyway will be used for dateinputs components (angularjs), because they are using kendo-dateinput which ng-model is JS Date in browser local time, but I'm providing another timezone so need to transformations inside component.
PPS Best solution from my point of view, if timezone converter and moment-timezone will be working as expected
Anyway the whole app is operating the JS Date, so when user in Moscow with America in profile selects the datetime in kendo-input, or see setup kendo-scheduler, or display the date in the kendo-table, I need to manipulate with offsets. But instead of passing directly the client offset, I'm planning to pass IANA timezone id from the server with help of timezone converter and get the offset I need directly from moment-timezone.