Skip to content

[🐛 BUG] date and date_range issue when browser timezone is < +00 #2879

Description

@robinschucker

🔍 What went wrong?

tgb.date and tgb.date_range used with with_time=False loose one day every time you select a new date and your browser time has a timezone with negative offset (e.g. "america/los_angeles" which is UTC-0700).

For e.g. (using US date format mm/dd/yyyy)
Date: select "06/06/2025" > date display will show "06/05/2025"
Date Range:

  • initialized at 12/13/2024 - 12/05/2025
  • select end = "12/10/2025" > displayed range will be 12/12/2024 - 12/09/2025
  • select end = "12/12/2025" > displayed range will be 12/11/2024 - 12/11/2025

I was able to reproduce the issue with any timezone with negative offset ("america/los_angeles" which is UTC-0700, "america/new_york" which is UTC-0400, "america/sao_paulo" which is UTC-0300) when it works just fine with offset is positive (e.g. "europe/berlin" which is UTC+0200)

✅ Expected Behavior

Date: select "06/06/2025" > date display will show "06/06/2025"
Date Range:

  • initialized at 12/13/2024 - 12/05/2025
  • select end = "12/10/2025" > displayed range will be 12/13/2024 - 12/10/2025
  • select end = "12/12/2025" > displayed range will be 12/13/2024 - 12/12/2025

🔄 Steps to Reproduce

I was able to reproduce the issue with any timezone with negative offset ("america/los_angeles" which is UTC-0700, "america/new_york" which is UTC-0400, "america/sao_paulo" which is UTC-0300) when it works just fine with offset is positive (e.g. "europe/berlin" which is UTC+0200)

"""Minimal Taipy app to exercise date_range via GUI builder."""

from __future__ import annotations

import datetime

import taipy as tp
import taipy.gui.builder as tgb
from taipy.gui import State
from taipy.gui.extension import ElementLibrary

# Date-only range (default: with_time=False)
dates = [
    datetime.date(2025, 1, 1),
    datetime.date(2025, 12, 31),
]

# Single date picker (date only, no time)
selected_date = datetime.date(2025, 6, 15)


def _format_date(label: str, value: datetime.date | None) -> str:
    if value is None:
        return f"**{label}:** (not set)"
    return f"**{label}:** `{value}`"


def _format_range(label: str, value: list) -> str:
    if not value or len(value) < 2:
        return f"**{label}:** (incomplete range)"
    start, end = value[0], value[1]
    return f"**{label}:** `{start}` → `{end}`"


def _build_selection_summary(
    dates_value: list,
    selected_date_value: datetime.date | None,
) -> str:
    return "\n\n".join(
        [
            _format_range("Date range", dates_value),
            _format_date("Single date", selected_date_value),
        ]
    )


selection_summary = _build_selection_summary(dates, selected_date)


def on_dates_change(state: State, var_name: str, var_value: list) -> None:
    state.selection_summary = _build_selection_summary(state.dates, state.selected_date)


def on_selected_date_change(state: State, var_name: str, var_value: datetime.date) -> None:
    state.selection_summary = _build_selection_summary(state.dates, state.selected_date)


class _BrowserClockLibrary(ElementLibrary):
    """Loads browser_clock.js in the app shell (inline page scripts do not run)."""

    def get_name(self) -> str:
        return "browser_clock"

    def get_elements(self) -> dict:
        return {}

    def get_scripts(self) -> list[str]:
        return ["browser_clock.js"]


with tgb.Page() as page:
    tgb.text("# Date range test", mode="md")

    with tgb.part():
        tgb.text("### Browser time", mode="md")
        with tgb.html("div", id="browser-clock", style="white-space: pre-line"):
            tgb.html(None, "Loading…")

    with tgb.part():
        tgb.text("### Date range (no time)", mode="md")
        tgb.date_range(
            "{dates}",
            label_start="Start date",
            label_end="End date",
            on_change=on_dates_change,
        )

    with tgb.part():
        tgb.text("### Single date (no time)", mode="md")
        tgb.date(
            "{selected_date}",
            label="Pick a date",
            with_time=False,
            on_change=on_selected_date_change,
        )

    with tgb.part():
        tgb.text("### Current selection", mode="md")
        tgb.text("{selection_summary}", mode="md")


if __name__ == "__main__":
    gui = tp.Gui(page, libraries=[_BrowserClockLibrary()])
    gui.run(title="Date range test", use_reloader=True, port="auto")

with browser_clock.js:

(function () {
  const CLOCK_SELECTOR = "#browser-clock";

  function formatOffset(date) {
    const mins = -date.getTimezoneOffset();
    const sign = mins >= 0 ? "+" : "-";
    const abs = Math.abs(mins);
    const h = String(Math.floor(abs / 60)).padStart(2, "0");
    const m = String(abs % 60).padStart(2, "0");
    return "UTC" + sign + h + ":" + m;
  }

  function tick(el) {
    const now = new Date();
    const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
    const local = now.toLocaleString(undefined, {
      dateStyle: "full",
      timeStyle: "long",
    });
    el.textContent =
      "Local: " +
      local +
      "\nTimezone: " +
      tz +
      "\nOffset: " +
      formatOffset(now) +
      "\nISO (UTC): " +
      now.toISOString();
  }

  let started = false;

  function startClock() {
    if (started) {
      return true;
    }
    const el = document.querySelector(CLOCK_SELECTOR);
    if (!el) {
      return false;
    }
    started = true;
    tick(el);
    window.setInterval(function () {
      tick(el);
    }, 1000);
    return true;
  }

  function init() {
    if (startClock()) {
      return;
    }
    const root = document.getElementById("root");
    if (!root) {
      return;
    }
    const observer = new MutationObserver(function () {
      if (startClock()) {
        observer.disconnect();
      }
    });
    observer.observe(root, { childList: true, subtree: true });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init);
  } else {
    init();
  }
})();

The follow this to change your browser timezone in Chrome

💡 Possible Solution (Optional)

I believe the issue is how internally taipy is going from displayed date string to datetime representation to displayed date string.
select "06/06/2025" > becomes datetime.datetime(2025, 6, 6, 0, 0, tzinfo=datetime.timezone.utc) > which converted to browser time (UTC-7) will be datetime.datetime(2025, 6, 5, 17, 0, 0, tzinfo=ZoneInfo("America/Los_Angeles")) and thus "06/05/2025" is displayed.

Possibly related existing issue and question

🖼️ Screenshots (Optional)

Gif:
https://drive.google.com/file/d/13UXycsWza6a1TLUtmFQI_XsyHqp-3RJF/view?usp=sharing

💻 Runtime Environment

macOS 26.4 + Chrome Version 146.0.7680.178 (Official Build) (arm64)

🌐 Browser (if applicable)

Chrome

🖥️ Operating System

Mac

📦 Taipy Version

taipy==4.1.1 taipy-common==4.1.1 taipy-core==4.1.1 taipy-gui==4.1.2 taipy-rest==4.1.1 taipy-templates==4.1.1

📋 Additional Context (Optional)

📜 Code of Conduct

  • I have checked the existing issues to avoid duplicates.
  • I am willing to work on this issue (optional)

✅ Acceptance Criteria

  • A reproducible unit test is added.
  • Code coverage is at least 90%.
  • The bug reporter validated the fix.
  • Relevant documentation updates or an issue created in

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions