Repository: streamlit/streamlit
Pull request: Fix timezone handling with slider
## 📚 Context

If a slider is used with a `time`/`datetime` value, it will be converted to a UTC timestamp in microseconds. The frontend itself is not aware of the actual timezone used in the backend. Thereby, it currently does some timezone conversations that results in diverging information between the value shown on the frontend and the actual value in the backend.   

- What kind of change does this PR introduce?

  - [x] Bugfix
  - [ ] Feature
  - [ ] Refactoring
  - [ ] Other, please describe:

## 🧠 Description of Changes

This PR removes any timezone conversation on the backend (it just replaces the `tzinfo` with UTC), and uses the UTC timezone in the frontend (with `moments`) instead of the browser timezone. Thereby, the datetime instances are always shown with their exact date and time. For example: `2020-01-10 23:30:00 EST` is rendered as `2020-01-10 23:30:00`; `2020-01-10 23:30:00 PT` is rendered as `2020-01-10 23:30:00`; `2020-01-10 23:30:00 UTC` is rendered as `2020-01-10 23:30:00`; ...
 
  - [x] This is a visible (user-facing) change

### Example:

```python
import streamlit as st
from datetime import time

period = st.slider(
    "Period:",
    min_value=time(0, 0),
    max_value=time(23, 45),
    value=(time(10, 00), time(23, 45)),
)
st.write(period)
```

**Revised:**

![image](https://user-images.githubusercontent.com/2852129/152066518-039035ad-0ac6-4604-a0b6-6fac56b83466.png)

**Current:**

![image](https://user-images.githubusercontent.com/2852129/152066438-d6c2ea5b-006f-4b36-b553-95d6cb9ab48c.png)

## 🧪 Testing Done

- [ ] Screenshots included
- [x] Added/Updated unit tests
- [ ] Added/Updated e2e tests

## 🌐 References

_Does this depend on other work, documents, or tickets?_

- **Issue**: Closes #3402

---

**Contribution License Agreement**

By submitting this pull request you agree that all contributions to this project are made under the Apache 2.0 license.


Problem statement:
Datetime slider showing incorrect values on the browser
### Summary

When I run my streamlit app at my localhost and the cloud, I see different st.slider time range values. To be precise the range values printed on the browser seem to be shifted. I suspect this shift is due to some time zone change.

### Code snippet:

```
from datetime import time
import streamlist as st

period = st.sidebar.slider("Period:", min_value=time(0,0), max_value=time(23, 45), value=(time(10, 00), time(23, 45)))
```

The code above shows a time period of 00:00 - 23:45 at my localhost, and 19:00 - 18:45 on the cloud with the same time length.

### Debug info

- Streamlit version: 0.82.0

### Suggestion

I think st.slider should have a timezone keyword and in default should return the datetime information in users timezone rather than the server's timezone.

Hints from issue discussion:
Hey @tdincer ! Thanks for the report! We have had a couple people mention our support for timezones is lacking. I have grouped this in with other issues. Most similar is #1346. We're going to get this in front of our product team to see how we can best integrate this with our future plans.

Commit message: Update micros values in tests

Patch to review:
diff --git a/frontend/src/components/widgets/Slider/Slider.tsx b/frontend/src/components/widgets/Slider/Slider.tsx
index 89e4007b0e88..bfe0d223c44a 100644
--- a/frontend/src/components/widgets/Slider/Slider.tsx
+++ b/frontend/src/components/widgets/Slider/Slider.tsx
@@ -185,7 +185,10 @@ class Slider extends React.PureComponent<Props, State> {
     const { format, options } = this.props.element
     if (this.isDateTimeType()) {
       // Python datetime uses microseconds, but JS & Moment uses milliseconds
-      return moment(value / 1000).format(format)
+      // The timestamp is always set to the UTC timezone, even so, the actual timezone
+      // for this timestamp in the backend could be different.
+      // However, the frontend component does not need to know about the actual timezone.
+      return moment.utc(value / 1000).format(format)
     }
 
     if (options.length > 0) {
diff --git a/lib/streamlit/elements/slider.py b/lib/streamlit/elements/slider.py
index 344f0d39fec3..9f4afe0ee14c 100644
--- a/lib/streamlit/elements/slider.py
+++ b/lib/streamlit/elements/slider.py
@@ -405,8 +405,11 @@ def _delta_to_micros(delta):
             UTC_EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
 
             def _datetime_to_micros(dt):
-                # If dt is naive, Python converts from local time
-                utc_dt = dt.astimezone(timezone.utc)
+                # The frontend is not aware of timezones and only expects a UTC-based timestamp (in microseconds).
+                # Since we want to show the date/time exactly as it is in the given datetime object,
+                # we just set the tzinfo to UTC and do not do any timezone conversions.
+                # Only the backend knows about original timezone and will replace the UTC timestamp in the deserialization.
+                utc_dt = dt.replace(tzinfo=timezone.utc)
                 return _delta_to_micros(utc_dt - UTC_EPOCH)
 
             # Restore times/datetimes to original timezone (dates are always naive)
@@ -418,8 +421,9 @@ def _datetime_to_micros(dt):
 
             def _micros_to_datetime(micros):
                 utc_dt = UTC_EPOCH + timedelta(microseconds=micros)
-                # Convert from utc back to original time (local time if naive)
-                return utc_dt.astimezone(orig_tz).replace(tzinfo=orig_tz)
+                # Add the original timezone. No conversation is required here,
+                # since in the serialization, we also just replace the timestamp with UTC.
+                return utc_dt.replace(tzinfo=orig_tz)
 
             value = list(map(_datetime_to_micros, value))
             min_value = _datetime_to_micros(min_value)
diff --git a/lib/tests/streamlit/slider_test.py b/lib/tests/streamlit/slider_test.py
index 4218128da36f..282d4732c126 100644
--- a/lib/tests/streamlit/slider_test.py
+++ b/lib/tests/streamlit/slider_test.py
@@ -55,10 +55,10 @@ def test_just_disabled(self):
     AWARE_TIME = time(12, 00, tzinfo=PST)
     AWARE_TIME_END = time(21, 00, tzinfo=PST)
     # datetimes are serialized in proto as micros since epoch
-    AWARE_DT_MICROS = 1577865600000000
-    AWARE_DT_END_MICROS = 1578211200000000
-    AWARE_TIME_MICROS = 946756800000000
-    AWARE_TIME_END_MICROS = 946789200000000
+    AWARE_DT_MICROS = 1577836800000000
+    AWARE_DT_END_MICROS = 1578182400000000
+    AWARE_TIME_MICROS = 946728000000000
+    AWARE_TIME_END_MICROS = 946760400000000
 
     @parameterized.expand(
         [


Code review comments:
diff_hunk: @@ -418,8 +421,9 @@ def _datetime_to_micros(dt):
 
             def _micros_to_datetime(micros):
                 utc_dt = UTC_EPOCH + timedelta(microseconds=micros)
-                # Convert from utc back to original time (local time if naive)
-                return utc_dt.astimezone(orig_tz).replace(tzinfo=orig_tz)
+                # Add the original timezone. No conversation is required here,
path: lib/streamlit/elements/slider.py
text: @user1:
nit: conversation -> conversion