Determine Time Offset given Olson TZID and local DateTime? - asp.net

I need to determine Time Offset given Olson TZID of an event and a DateTime of an event.
I suppose I can do it with a help of Noda Time, but I'm new here and need help - an example of actual API call sequence to perform this task.
A few more details of what we're using and doing.
We're running ASP.NET + SQL Server based Web site. Users of our site enter and save events which are happening at various locations. For each event Lat/Long and DateTime with time offset (of that event) are among required fields.
There are various scenarios of data entry, sometimes Lat/Long is entered first, sometimes DateTime is. System allows to determine Lat/Long from an address or a map point. We want Time Offset to be consistent with Lat/Long in most of the cases because we normally expect DateTime being entered as local to that event.
We use SQL Server DateTimeOffset field to store data.
We don't want to depend on third-party web services to determine an offset, but prefer our system to do that.
I'm free to download and use any necessary tools or data. I already downloaded shapefiles from http://efele.net/maps/tz/ and used Shape2SQL http://goo.gl/u7AUy to convert them to SQL Server table with TZIDs and GEOMs.
I know how to get TZID from Lat/Long by querying that table.
What I need, again, is to determine Time Offset from TZID and DateTime.

Thank you for Jon Skeet and Matt Johnson for providing an answer on Noda Time Google Group http://goo.gl/I9unm0. Jon explained how to get Microsoft BCL DateTimeOffset value given Olson TZID and NodaTime LocalDateTime values. Matt pointed to an example of determining if DST is active given ZonedDateTime: What is the System.TimeZoneInfo.IsDaylightSavingTime equivalent in NodaTime?
I'm going to write a blog post summarizing my experience of getting Olson TZID from Lat/Long, parsing DateTime string into LocalDateTime and determining DateTimeOffset. I'll show there how GetTmzIdByLocation(latitude, longitude) and GetRoughDateTimeOffsetByLocation(latitude, longitude) methods work and why I needed both (first method doesn't work for locations on ocean). Once I write this post I'll add a comment here.
Note, that parsing DateTime string in a code below is not optimal yet; as Matt explained in a Google Group post (link above) it's better to use Noda Time tools than BCL. See a related question at http://goo.gl/ZRZ7XP
My current code:
public object GetDateTimeOffset(string latitude, string longitude, string dateTime)
{
var tzFound = false;
var isDST = false;
var tmpDateTime = new DateTimeOffset(DateTime.Now).DateTime;
if (!String.IsNullOrEmpty(dateTime))
{
try
{
// Note: Looks stupid? I need to throw away TimeZone Offset specified in dateTime string (if any).
// Funny thing is that calling DateTime.Parse(dateTime) would automatically modify DateTime for its value in a system timezone.
tmpDateTime = DateTimeOffset.Parse(dateTime).DateTime;
}
catch (Exception) { }
}
try
{
var tmzID = GetTmzIdByLocation(latitude, longitude);
DateTimeOffset result;
if (String.IsNullOrEmpty(tmzID) || tmzID.ToLower() == "uninhabited") // TimeZone is unknown, it's probably an ocean, so we would just return time offest based on Lat/Long.
{
var offset = GetRoughDateTimeOffsetByLocation(latitude, longitude);
result = new DateTimeOffset(tmpDateTime, TimeSpan.FromMinutes(offset * 60)); // This only works correctly if tmpDateTime.Kind = Unspecified, see http://goo.gl/at3Vba
} // A known TimeZone is found, we can adjust for DST using Noda Time calls below.
else
{
tzFound = true;
// This was provided by Jon Skeet
var localDateTime = LocalDateTime.FromDateTime(tmpDateTime); // See Noda Time docs at http://goo.gl/XseiSa
var dateTimeZone = DateTimeZoneProviders.Tzdb[tmzID];
var zonedDateTime = localDateTime.InZoneLeniently(dateTimeZone); // See Noda Time docs at http://goo.gl/AqE8Qo
result = zonedDateTime.ToDateTimeOffset(); // BCL DateTimeOffset
isDST = zonedDateTime.IsDaylightSavingsTime();
}
return new { result = result.ToString(IncidentDateFormat), tzFound, isDST };
}
catch (Exception ex)
{
IMAPLog.LogEvent(System.Reflection.MethodBase.GetCurrentMethod().Name, "", ex);
throw new CustomHttpException("Unable to get timezone offset.");
}
}
An extension method (provided by Matt Johnson) for determining if DST is active What is the System.TimeZoneInfo.IsDaylightSavingTime equivalent in NodaTime?
public static class NodaTimeUtil
{
// An extension method by Matt Johnson - on Stack Overflow at http://goo.gl/ymy7Wb
public static bool IsDaylightSavingsTime(this ZonedDateTime zonedDateTime)
{
var instant = zonedDateTime.ToInstant();
var zoneInterval = zonedDateTime.Zone.GetZoneInterval(instant);
return zoneInterval.Savings != Offset.Zero;
}
}

Related

Was there a change in timezone for ToLocalTime

Currently I'm using ToLocalTime to convert the UTC date received from my backend. Is there a way to setup my ASP.NET Core 3.0 web site to use a specific timezone when calling ToLocalTime (instead of depending on the host region) -- or should I implement my own method/extension to converting to the time I need?
Official documentation of DateTime.ToLocalTime states:
Note that the exact output depends on the current culture and the local time zone of the system on which it is run.
You can use the TimeZoneInfo static method ConvertTimeFromUtc to convert an instance of DateTime (Note: if it's Kind property is Local you'll get an exception!) from Utc to whatever local time you want (Another note: the Kind property of the result is either Utc or Unspecified - depending on the target TimeZoneInfo).
Code example (copied from documentation page):
DateTime timeUtc = DateTime.UtcNow;
try
{
TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
DateTime cstTime = TimeZoneInfo.ConvertTimeFromUtc(timeUtc, cstZone);
Console.WriteLine("The date and time are {0} {1}.",
cstTime,
cstZone.IsDaylightSavingTime(cstTime) ?
cstZone.DaylightName : cstZone.StandardName);
}
catch (TimeZoneNotFoundException)
{
Console.WriteLine("The registry does not define the Central Standard Time zone.");
}
catch (InvalidTimeZoneException)
{
Console.WriteLine("Registry data on the Central Standard Time zone has been corrupted.");
}

Deacticate User based on last login date

Scenario: Deactivate the user whose login date is less than 42 from today. I have an user whose last login date is 1/22/2020(US Date format)/22/1/2020 5:12 pm. Here I wrote a batch apex for deactivating. My code has executed successfully and my batch status is completed but the user record is not deactivating.
Here is the code:
global class User_Deactivation implements Database.Batchable<SObject>
{
dateTime dt = date.today()-42;
public String query = 'SELECT Name, LastLoginDate, Id From User WHERE IsActive = true AND LastLoginDate=:dt ';
global Database.querylocator start(Database.BatchableContext bc)
{
return Database.getQueryLocator(query);
}
global void execute(Database.BatchableContext bc,List<User> scope)
{
List<User> userList = new List<User>();
for(User s:scope)
{
User u =(user)s;
userList.add(u);
}
if(userList.size() > 0)
{
for(User usr : userList)
{
usr.isActive = false;
}
}
update userList;
}
global void finish(Database.BatchableContext bc)
{
AsyncApexJob a = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed, TotalJobItems, CreatedBy.Email
FROM AsyncApexJob
WHERE Id = :BC.getJobId()];
Messaging.SingleEmailMessage mail = new Messaging.SingleEmailMessage();
String[] toAddresses = new String[] {a.CreatedBy.Email};
mail.setToAddresses(toAddresses);
mail.setSubject('Apex Job Status: ' + a.Status);
mail.setPlainTextBody('The batch Apex job processed ' + a.TotalJobItems + ' batches with '+ a.NumberOfErrors + ' failures.');
Messaging.sendEmail(new Messaging.SingleEmailMessage[] { mail });
}
}
please help me out on this
Multiple things you can improve here, where do I begin...
Initialisation(?) piece
dateTime dt = date.today()-42;
String query = 'SELECT Name, LastLoginDate, Id From User WHERE IsActive = true AND LastLoginDate=:dt';
Do you need Date or DateTime match? The way you wrote it it'll match only people who logged in exactly at midnight. System.debug(dt); would say 2020-01-23T00:00:00.000Z. It shouldn't be an equals sign, should be "less than" or "less or equal".
Or even better - you can make it bit more clear what you want to do, bit more "semantic" so the poor guy who's going to maintain it can understand it without extra comments. This reads more natural and uses the SOQL date literals, special "constants" to simplify your logic: SELECT Id, LastLoginDate FROM User WHERE isActive = true AND LastLoginDate != LAST_N_DAYS:42
What is this section of code anyway. It's not really static variables, it's not a constructor... I think it'll behave as a constructor. Be very, very careful with constructors for batches. The state of the class at the end of the constructor gets saved (serialised) and restored every time the class is scheduled to run. It's tempting to put some initialisation code into constructor, maybe read some custom settings, precalculate stuff... But then you'll be in for nasty surprise when admin adds new custom setting and the batch doesn't pick it up. In your case it's even worse, I'd suspect it'll serialise the dt and your today() will be frozen in time, not what you expected. To be safe move all initialisation logic to start()
And I'd even say whoever gave you the requirement didn't think it through. When you make new user they get a link they need to click in next 72h. If they didn't do it (maybe it was sent late Friday and they want to login on Monday) - this thing will dutifully kill their access at Friday night without giving them any chance to login. You need some "grace period". Maybe something like WHERE isActive = true AND (LastLoginDate < :x OR (LastLoginDate = null AND CreatedDate < :x))
start()
Queries in strings work and that's how a lot of batch documentation is written but they are poor practice. Where possible use a compiled query, in brackets. You get minimal improvement in execution (precompiled), you get compile-time warnings when you mess up (better than a runtime error which you might not notice if you don't monitor jobs). And most importantly - if somebody wants to delete a field - SF will detect a dependency and stop him/her. Use return Database.getQueryLocator([SELECT ...]); wherever you can.
execute()
Your scope already is a list of users, why do you do extra casts to User? Why do you add them to a helper list? Why 2 loops?
for(User u : scope){
u.isActive = false;
}
update users;
and you're done?
P.S. Why "global" all over the place?

Problems with time

I coded this snippet to log the users IP and time on my website. It works but something is wrong with the time:
public static void UserLogin(string iPaddress, string uname)
{
DateTime dt = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now);
string cet= dt.AddHours(1).ToString("F", new CultureInfo("en-US"));
.....
}
The website is on a server somewhere in UK and to adjust the login time to CET without going too sophisticated, I simply toughth adding the hours difference using (AddHours) but for some reason, and I do not understand why, whatsoever number I put in there "AddHours(1)" will never get added and moreover, right now that is 13:55 at my location in Italy, the time recorder by the method is 1:55 am that is 12 hours behind even if there is no hours added "AddHours(0)".
Some help to understand what is going on in this method will be appreciated. Thanks.
You can convert time between time zones in more controlled way, for example:
DateTime nowutc = DateTime.UtcNow;
var cet = TimeZoneInfo.FindSystemTimeZoneById("Central European Standard Time");
var nowcet = TimeZoneInfo.ConvertTimeFromUtc(nowutc, cet);

ASP.net any way to cache things like this?

I have a function called on every single page:
/// <summary>
/// Gets the date of the latest blog entry
/// </summary>
public static DateTime GetNewestBlogDate()
{
DateTime ReturnDate = DateTime.Now.AddDays(30);
using (var db = new DataClassesDataContext())
{
var q = (from d in db.tblBlogEntries orderby d.date descending select new {d.date}).FirstOrDefault();
if (q != null)
ReturnDate = q.date;
}
return ReturnDate;
}
It works like this website, it gets the latest blog entry date and if it's greater than the users cookie value it displays a new icon next to the blog link.
It seems rather wasteful to keep calling this function per page request, called 1:1 on the number of page requests you have. Say you have 30,000 page views per day, that's 1,250 database queries per hour.
Is there any way I can cache this results, and have it expire say every hour?
I'm aware it's a bit of a micro optimisation, but given 10 or so similar functions per page it might add up to something worthwhile. You could denormalise it into a single table and return them all in one go, but I'd rather cache if possible as it's easier to manage.
Since it's not based on the user (the cookie is, but the query doesn't seem to be) - you can just use the standard ASP.NET Cache.
Just insert the result with an expiration of 1 hour. If you like, you can even use the callback to automatically refresh the cache.
Assuming you've stored it into MS-SQL, you could even use a SqlCacheDependency to invalidate when new data is inserted. Or, if your inserting code is well-factored, you could manually invalidate the cache then.
Just use the ASP.NET Cache object with an absolute expiration of 1 hour. Here's an example of how you might implement this:
public static DateTime GetNewestBlogDate()
{
HttpContext context = HttpContext.Current;
DateTime returnDate = DateTime.Now.AddDays(30)
string key = "SomeUniqueKey"; // You can use something like "[UserName]_NewestBlogDate"
object cacheObj = context.Cache[key];
if (cacheObj == null)
{
using (var db = new DataClassesDataContext())
{
var q = (from d in db.tblBlogEntries orderby d.date descending select new { d.date }).FirstOrDefault();
if (q != null)
{
returnDate = q.date;
context.Cache.Insert(key, returnDate, null, DateTime.Now.AddHours(1), Cache.NoSlidingExpiration);
}
}
}
else
{
returnDate = (DateTime)cacheObj;
}
return returnDate;
}
You haven't indicated what is done with the returned value. If the returned value is displayed the same way on each page, why not just place the code along with the markup to display the result in a user control (ASCX) file? You can then cache the control.
Make it a webmethod with a CacheDuration?
[WebMethod(CacheDuration=60)]
public static DateTime GetNewestBlogDate()

How do I control the TimeZone of events returned by the .NET Google Calendar API?

I have the following code for use in my asp.net website:
CalendarService service = new CalendarService("mycalendar");
EventQuery query = new EventQuery();
query.Uri = new Uri(group.GroupEventsURL);
query.SingleEvents = true;
query.SortOrder = CalendarSortOrder.ascending;
query.ExtraParameters = "orderby=starttime";
query.NumberToRetrieve = 50;
query.TimeZone = "America/Chicago";
EventFeed feed = service.Query(query);
Which produces the following URL:
http://www.google.com/calendar/feeds/TRIMMEDgroup.calendar.google.com/private-TRIMMED/full?max-results=50&orderby=starttime&ctz=America%2FChicago&sortorder=ascending&singleevents=true
According to the documentation (emphasis mine), I expect the Times in each EventEntry to be in the Central time zone:
ctz: The current timezone. If not specified, times are returned in the calendar time zone.
Times in the resulting feed will be represented in this timezone.
Replace all spaces with underscores (e.g. "ctz=America/Los_Angeles").
But my server is hosted in Arizona, so (for now) all of the dates on the calendar are two hours earlier than they should be. Am I doing something wrong? How do I get the dates in the feed to be in the Central time zone even though the server is in Arizona?
I do not plan on moving my hosting any time soon, but since Arizona does not participate in Daylight Savings Time, I cannot simply add two hours to every date.
Don't fight it. The Google.GData.Calendar library sets each time to the system's local time. Here's what I could dig up about it.
You'll need to convert all those times from every EventEntry
DateTime offsetStartTime = GetTimeZoneOffset(entry.Times[0].StartTime, "Mountain Standard Time");
public static DateTime GetTimeZoneOffset(DateTime dt, string win32Id)
{
var timeUtc = dt.ToUniversalTime();
TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById(win32Id);
DateTime cstTime = TimeZoneInfo.ConvertTimeFromUtc(timeUtc, cstZone);
return cstTime;
}
I am still open to other ideas, but here is what I got to work. I created my own CalendarService class, which converts the dates from:
2010-10-13T18:30:00.000-05:00
to:
2010-10-13 18:30:00
These dates are then converted to the same (and correct) DateTime in any time zone.
internal class CalendarService2 : CalendarService
{
public CalendarService2(string applicationName) : base(applicationName) { }
public new EventFeed Query(EventQuery feedQuery)
{
EventFeed feed = new EventFeed(feedQuery.Uri, this);
using (Stream input = base.Query(feedQuery.Uri, DateTime.MinValue))
{
XmlDocument doc = new XmlDocument();
doc.Load(input);
XmlNodeList nodes = doc.SelectNodes("(//#startTime|//#endTime)[contains(.,'.000-')]");
foreach (XmlNode node in nodes)
{
node.Value = node.Value.Remove(node.Value.Length - 10).Replace('T', ' ');
}
using (MemoryStream output = new MemoryStream())
{
doc.Save(output);
output.Position = 0;
feed.NewAtomEntry += new FeedParserEventHandler(this.OnParsedNewEntry);
feed.NewExtensionElement += new ExtensionElementEventHandler(this.OnNewExtensionElement);
feed.Parse(output, AlternativeFormat.Atom);
}
}
return feed;
}
}

Resources