Display correct Local Time with DST Change

K, been trying to get this figured out for a few hours. Time to ask for help.

Times being displayed in the app are not correct with the change in daylight savings time.

Dates are UTC(GMT) being stored and queried from Airtable.

Looks like the dates being served to the app are OK.
Server module returns a string from the API and converts to datetime object
** The third being after the DST Change

2021-10-28 14:47:48.347000+00:00
<anvil.tz.tzoffset (0 hours)>
2021-11-05 14:47:00+00:00
<anvil.tz.tzoffset (0 hours)>
2021-11-09 15:47:00+00:00
<anvil.tz.tzoffset (0 hours)>

Here is my local format function (In a client module)

def dtm_local_pretty(dtm):
  print(dtm)
  print(dtm.tzinfo)
  dtm_lp = dtm.astimezone(tz.tzlocal()).strftime("%b %d %Y %I:%M %p")
  print(f'THE dtm_local is: {dtm_lp}')
  return dtm_lp

Here is the result:

THE dtm_local is: Oct 28 2021 10:47 AM
THE dtm_local is: Nov 05 2021 10:47 AM
THE dtm_local is: Nov 09 2021 11:47 AM

The offset after 11/7 should be 5 hours and display 10:47 AM

How do I make this DST aware?

Thanks for your help,

There’s a good write up of this problem at Python: Timezone and Daylight savings | by Bao Nguyen | Medium

The clocks change here in the UK this weekend. Here’s a demo using the pytz module on the server to show two dates either side of the change:

https://anvil.works/build#clone:TGQUI6QTT466P6M2=BAEM53SVC4LM2WKRQYHZ6GHY

Server module:

import anvil.server
import datetime as dt
import pytz


@anvil.server.callable
def get_dates(timezone):
    tz = pytz.timezone(timezone)
    noon_today = dt.datetime(2021, 10, 28, 12) # DST still in place
    noon_next_monday = dt.datetime(2021, 11, 1, 12) # After the clocks go back - same as UTC
    dates = [tz.localize(date) for date in (noon_today, noon_next_monday)]
    print(f"server side: {dates}")
    return dates

Client module:

import anvil.server
import datetime as dt

dates = anvil.server.call('get_dates', "Europe/London")
print(f"client side: {dates}")

Gives:

server side: [datetime.datetime(2021, 10, 28, 12, 0, tzinfo=<DstTzInfo 'Europe/London' BST+1:00:00 DST>), datetime.datetime(2021, 11, 1, 12, 0, tzinfo=<DstTzInfo 'Europe/London' GMT0:00:00 STD>)]
client side: [datetime.datetime(2021, 10, 28, 12, 0, tzinfo=<anvil.tz.tzoffset (1 hours)>), datetime.datetime(2021, 11, 1, 12, 0, tzinfo=<anvil.tz.tzoffset (0 hours)>)]
3 Likes

Owen, thank you very helpful.
I had looked at a number of posts using pytz but was stuck on trying to do in the client.

I have half implemented and seems to be working with a hard coded time zone.

Now just need to dynamically pass the timezone to the server.
anvil.tz.tzlocal() seems to give me the offset and not the time zone
<anvil.tz.tzlocal (-4 hour offset)>

Regardless: I have the default time zone of each user in a database so I can just use that.

1 Like

Are you saying one should explicitly define the time zone with a string rather than using tz.tzlocal()?

If this is the case, how do I get an app to work properly across timezones?

That depends on what you’re trying to do. tzlocal will give you the localised time using the current offset of the browser - but it doesn’t know anything about dst.

If you need the localised time using the offset that would have applied at that time, you need to use the timezone (rather than just an offset) and a dst aware library like pytz.

Yes, good explanation, my use case is that I am displaying a list of future scheduled events across the DST change.

This point should be made clear in a frequently reference doc like: Anvil Docs | Dealing with timezones

As a Python novice I read “Aware” and assume that would include DST. Would have saved me good deal of tinkering to just know I need to take this back to the server and use pytz.

In the end my final solution displays “Local” time as the default timezone for the user. Not a dynamic time based on their browser. So it is kind of a half solution but good enough for now. I am fortunate enough to have the timezone of each user in a database because we collect in on-boarding.

If anyone know how to pull the time zone from the browser (some custom javascript?), it would be good to know.

1 Like

This Javascript reportedly pulls the browser time zone:

Intl.DateTimeFormat().resolvedOptions().timeZone

Edit: with the Javascript bridge, this becomes:

    from anvil.js.window import Intl
    
    time_zone = Intl.DateTimeFormat().resolvedOptions().timeZone
    self.label_1.text = time_zone

Ok, so here is why this is hard information to find. It isn’t standard practice, so you wont find it suggested by almost any documentation. Why this is, is that this information is not really “ground truth” in programming, it is the local computers set computer time, which can be changed by any user at any time.
Programmers don’t like this, because it means the user can make themselves any time they like (1000’s of years in the future for example) which could create use cases that most people just don’t want to deal with.

That being said:

import time

    if time.localtime().tm_isdst:
      print("DST")
    else:
      print("NOT DST")
    

.tm_isdst just resovles to a 1 or a zero (…so falsey).

time.localtime().tm_zone will give you a zero padded string for your UTC timezone ( so “-05” for UTC -5:00 where I am.

time.strftime("%z", time.localtime())

…is going to give you an offset string with the adjusted for local client browsers computer time with daylight savings time. (so -0400 for me as of October 29th 2021, since daylight savings time is active where I am)
The nice thing about this is, if you tell a modern OS that you live in a place that does not have Daylight Savings time, time.localtime() should adjust for this.

As an example, Arizona in the United States does not observe but states it borders does, making it possible for your time to change by 1hr by traveling to Nevada, but only for about 1/2 the year :crazy_face:

You can use this offset string to “re-create” a timezone aware datetime object by plugging the string back into an unaware datetime object.

This was a pain to research, My use case was that I had to interpret some live data that had timestamps from an old system that does not have offsets recorded, so my local system adds the local timezone (same location as the old system) before writing to an anvil data-table datetime column so I can compare datetimes using anvil. This is only accurate because it is live information, (actual people workers doing things) and the information is never generated at night during the dst shift.

2 Likes

All, thank you again for your help and insights, thought I would share my final solution.
(still need to revisit for some exception handling)

@jshaffstall the JS works for me, I added . . .

    from anvil.js.window import Intl
    
    time_zone = Intl.DateTimeFormat().resolvedOptions().timeZone
   print(f'JS TIMEZONE IS: {time_zone}')

. . . and in the console!

JS TIMEZONE IS: America/New_York

Which I can then pass to my function as the override.

Background on the function: I am consuming some data from multiple data sources, all return data as a list of dicts with all dates as string values in UTC. Wanted a single function that I could pass the response from any of these sources and provide dates in any flavor for other purposes.

Tried to be a little verbose in the doc string so other newbies like me might be able repurpose.

def dates_process(lst_of_dic, lst_date_keys, str_strip, str_pretty="%b %d %Y %I:%M %p", timezone_override=None):
    """
    Create date objects and pretty strings for specified keys in passed dic.

    For each dict in list and each date_key in dict.  Convert the field to datetime object with tz=utc.
    Then convert the utc to specified local timezone(ins) and America/New_York(est)
    Then create pretty strings of all three.
    The original data is returned with six additional key/values appended to each dict for each field.

    Passed data needs a key "timezone" with string recognized by pytz or must be passed the override parameter.
        https://gist.github.com/heyalexej/8bf688fd67d7199be4a1682b3eec7568
        i.e. "America/New_York

    Creates new key/values for:
        [orig]_utc_dtm as aware datetime object in utc
        [orig]_ins_dtm as aware datetime object in instructors default timezone
        [orig]_est_dtm as aware datetime object in America/New_York timezone
        [orig]_utc_str as string in utc
        [orig]_ins_str as string in instructors default timezone
        [orig]_est_str as string in America/New_York timezone


    :param lst_of_dic: List of dicts as data rows
    :type lst_of_dic: list

    :param lst_date_keys: List of date dict keys to process exp. ['start_time', 'created_at']
    :type lst_date_keys: list

    :param str_strip: String format of the date string to convert to datetime
    For each data source the original date string format of the time may be slightly different
    and str_strip will depend on source data.
    exp. "%Y-%m-%dT%H:%M:%S%fZ"
    :type str_strip: str

    :param str_pretty: String format for pretty presentation of date.  A default is defined  for consistent
    presentation across the app but can be overridden
    exp. "%b %d %Y %I:%M %p"
    :type str_pretty: str

    :param timezone_override:
    :type timezone_override: str

    :return: lst of dicts appended with new dtm fields
    """
    # lst_date_keys = ['start_time', 'created_at']
    tz_utc = pytz.timezone("UTC")
    tz_est = pytz.timezone("America/New_York")

    for m in lst_of_dic:
        # Use either passed time zone override or read from 'timezone' key in dic
        if timezone_override is not None:
            tz_inst = timezone_override
        else:
            tz_inst = pytz.timezone(m['timezone'])

        # for each date key passed, apply date functions derriving new key names from original
        for f in lst_date_keys:
            try:
                # Create date objects
                m[f'{f}_utc_dtm'] = dt.strptime(m[f], str_strip).replace(tzinfo=tz_utc)
                m[f'{f}_ins_dtm'] = m[f'{f}_utc_dtm'].astimezone(tz_inst)
                m[f'{f}_est_dtm'] = m[f'{f}_utc_dtm'].astimezone(tz_est)
                # Create pretty strings
                m[f'{f}_utc_str'] = m[f'{f}_utc_dtm'].strftime(str_pretty)
                m[f'{f}_ins_str'] = m[f'{f}_ins_dtm'].strftime(str_pretty)
                m[f'{f}_est_str'] = m[f'{f}_est_dtm'].strftime(str_pretty)

            except:
                continue

    return lst_of_dic
2 Likes

A simpler version of the server code part, starting from the time_zone gotten from the JS code and a Python datetime object dt:

import pytz
tz_user = pytz.timezone(time_zone)
dt_user = dt.astimezone(tz_user)