Ultra-fast timezone translation library for Arduino. Converts UTC timestamps to local time and vice-versa, handling arbitrary DST rules for both northern and southern hemispheres.
Designed to run on resource-constrained microcontrollers — from 8-bit AVR (Arduino Uno/Nano) to 32-bit ESP8266 and ESP32.
- 64-bit millisecond precision — all outputs are
uint64_tmilliseconds since the Unix epoch (1970-01-01 00:00:00 UTC). - 32-bit second input with automatic rollover handling — 32-bit Unix timestamps are accepted and extended to 64-bit via the Jan-1-2020 rollover heuristic (see 32-bit Rollover).
- Arbitrary DST rules — supports nth-weekday-of-month and last-weekday-of-month switch rules, covering both northern and southern hemisphere timezones.
- O(1) cached lookups — each instance caches the UTC boundaries of the
current offset period. Repeated conversions within the same DST/standard
season resolve with just two
uint64_tcomparisons. - Pure 32-bit arithmetic where possible — year-from-days binary search, date-to-days, and weekday calculations all use 32-bit math for AVR friendliness.
- Multiple independent instances — each
TimezoneTranslatorobject carries its own timezone definition and cache. Use one per timezone. - Broken-down time —
toTimeStruct()decomposes a millisecond timestamp into year/month/day/hour/minute/second/ms/weekday fields. - No dynamic allocation — zero
malloc/new; everything lives on the stack or in the object.
- Open Sketch → Include Library → Manage Libraries…
- Search for TimezoneTranslator.
- Click Install.
- Download or clone this repository.
- Copy the
TimezoneTranslatorfolder into your Arduinolibrariesdirectory (e.g.~/Arduino/libraries/TimezoneTranslator). - Restart the Arduino IDE.
Add to platformio.ini:
lib_deps =
costinbobes/TimezoneTranslator#include <TimezoneTranslator.h>
// US Eastern: UTC-5 standard, UTC-4 DST
// DST: 2nd Sunday of March at 02:00 → 1st Sunday of November at 02:00
TimezoneDefinition tzEST = { 3, 2, 11, 1, 0, 2, 2, -300, -240 };
TimezoneTranslator tz;
void setup() {
Serial.begin(115200);
tz.setLocalTimezone(tzEST);
// Build a UTC timestamp: 2026-07-15 12:00:00 UTC
uint64_t utcMs = TimezoneTranslator::dateToMs(2026, 7, 15, 12, 0, 0);
// Convert to local time (EDT in July → UTC-4)
uint64_t localMs = tz.utcToLocal(utcMs);
// Decompose into fields
TimeStruct ts;
TimezoneTranslator::toTimeStruct(&ts, localMs);
// Prints: 2026-07-15 08:00:00.000
Serial.print(ts.year); Serial.print('-');
Serial.print(ts.month); Serial.print('-');
Serial.print(ts.day); Serial.print(' ');
Serial.print(ts.hour); Serial.print(':');
Serial.print(ts.minute);Serial.print(':');
Serial.println(ts.second);
}
void loop() {}Defines a timezone's UTC offset and DST transition rules.
| Field | Type | Description |
|---|---|---|
dst_start_month |
uint8_t |
Month DST begins (1-12). Set to 0 for no DST. |
dst_start_week |
int8_t |
>0: nth occurrence of weekday; ≤0: last in month. |
dst_end_month |
uint8_t |
Month DST ends (1-12). |
dst_end_week |
int8_t |
>0: nth occurrence of weekday; ≤0: last in month. |
dst_weekday |
uint8_t |
Day of week for DST switch: 0=Sun, 1=Mon … 6=Sat. |
dst_start_hour |
uint8_t |
Local standard-time hour when DST begins (0-23). |
dst_end_hour |
uint8_t |
Local DST-time hour when DST ends (0-23). |
offset_min |
int16_t |
UTC offset in minutes when DST is not active. |
offset_dst_min |
int16_t |
UTC offset in minutes when DST is active. |
Broken-down time with millisecond precision (analogous to struct tm).
| Field | Type | Description |
|---|---|---|
year |
uint16_t |
Calendar year (1970+). |
month |
uint8_t |
Month, 1-12. |
day |
uint8_t |
Day of month, 1-31. |
hour |
uint8_t |
Hour, 0-23. |
minute |
uint8_t |
Minute, 0-59. |
second |
uint8_t |
Second, 0-59. |
ms |
uint16_t |
Millisecond, 0-999. |
weekday |
uint8_t |
Day of week: 0=Sunday … 6=Saturday. |
Internal cache structure. Users do not need to interact with this directly.
TimezoneTranslator();Creates an instance defaulting to UTC (offset 0, no DST).
bool setLocalTimezone(const TimezoneDefinition& tz);Sets the default timezone. Returns false if the definition is invalid
(e.g. dst_start_month > 12, or start month set without end month).
Invalidates the internal cache.
uint64_t utcToLocal(uint64_t utcMs, const TimezoneDefinition& tz);
uint64_t utcToLocal(uint64_t utcMs); // uses default tzConverts a UTC millisecond timestamp to local milliseconds.
The explicit-tz overload uses a temporary cache (always cold). The no-tz overload uses the instance cache, which stays warm across calls in the same DST/standard period.
uint64_t localToUtc(uint64_t localMs, const TimezoneDefinition& tz, bool prefer_dst = true);
uint64_t localToUtc(uint64_t localMs, bool prefer_dst = true); // uses default tzConverts a local millisecond timestamp to UTC milliseconds.
When clocks are set back, local times in the overlap window appear twice — once during DST and once during standard time. For example, US Eastern 01:30 on a fall-back day maps to either 05:30 UTC (EDT, the earlier instant) or 06:30 UTC (EST, the later instant).
Use the optional prefer_dst parameter to choose which UTC instant is returned:
prefer_dst |
Interpretation | UTC instant |
|---|---|---|
true (default) |
DST offset applied | earlier UTC (first clock occurrence) |
false |
Standard offset applied | later UTC (second clock occurrence) |
uint64_t ambiguous = TimezoneTranslator::dateToMs(2026, 11, 1, 1, 30, 0);
// DST interpretation (default) — 05:30 UTC
uint64_t utcDst = tz.localToUtc(ambiguous, TZ_EST); // prefer_dst = true
// Standard interpretation — 06:30 UTC
uint64_t utcStd = tz.localToUtc(ambiguous, TZ_EST, false); // prefer_dst = falseOutside the overlap window prefer_dst has no effect.
uint64_t utcToLocal(uint32_t utcSec, const TimezoneDefinition& tz);
uint64_t utcToLocal(uint32_t utcSec);
uint64_t localToUtc(uint32_t localSec, const TimezoneDefinition& tz, bool prefer_dst = true);
uint64_t localToUtc(uint32_t localSec, bool prefer_dst = true);Accept a uint32_t seconds timestamp, apply the Jan-1-2020 rollover
heuristic internally, then perform the conversion. Output is always
64-bit milliseconds. See 32-bit Rollover.
static void toTimeStruct(TimeStruct* dest, uint64_t utcMs);
static uint64_t dateToMs(uint16_t year, uint8_t month, uint8_t day,
uint8_t hour, uint8_t minute, uint8_t second);A uint32_t Unix timestamp counts seconds since 1970-01-01. It overflows
on 2038-01-19 03:14:07 UTC, after which the counter wraps back to 0.
Many RTC chips (DS1307, DS3231, PCF8563) and NTP libraries still expose time
as a 32-bit unsigned value, so post-2038 values will appear as small numbers
in the range 0 … ~1.58 billion.
The rollover heuristic uses January 1, 2020 (Unix time 1,577,836,800) as a dividing line:
| 32-bit value | Interpretation |
|---|---|
| ≥ 1,577,836,800 (2020-01-01) | Treated as-is — a normal timestamp in 2020 … 2038. |
| < 1,577,836,800 | Assumed rolled over — 2³² seconds are added before converting to milliseconds, placing the result in 2038 … 2106. |
This allows 32-bit sources to remain useful until approximately 2106 without any firmware change.
- It is far enough in the past (relative to 2026) that no legitimate "current" timestamp will fall below it.
- It leaves the full 2020 … 2038 range addressable without rollover adjustment.
- The cutoff date is a round, memorable constant.
The 32-bit overloads exist for convenience when interfacing with hardware or libraries that only provide seconds. All output from this library is 64-bit milliseconds. Whenever you have a choice, work in 64-bit throughout:
- No rollover ambiguity.
- Millisecond precision preserved.
- Valid until approximately the year 586,512.
If your RTC only provides seconds (e.g. DS3231), convert to 64-bit ms as
early as possible using dateToMs() and stay in 64-bit from that point on.
DST transition rules are defined using three fields per transition:
- month — which month the switch occurs (1-12).
- week — which occurrence of the weekday:
1= first,2= second,3= third,4= fourth,5= fifth.0or negative = last occurrence in the month.
- weekday — which day of the week (0=Sunday … 6=Saturday).
The hour field specifies the local wall-clock hour at which the switch happens. For DST start this is in standard time; for DST end this is in DST time.
DST start month < DST end month. The period between the two transitions is the DST (summer) period.
DST start month > DST end month (e.g. October → April). The DST period wraps across the year boundary. The library handles this automatically.
// UTC — no DST
TimezoneDefinition TZ_UTC = { 0, 0, 0, 0, 0, 0, 0, 0, 0 };
// US Eastern (America/New_York) — UTC-5 / UTC-4
TimezoneDefinition TZ_EST = { 3, 2, 11, 1, 0, 2, 2, -300, -240 };
// US Central (America/Chicago) — UTC-6 / UTC-5
TimezoneDefinition TZ_CST = { 3, 2, 11, 1, 0, 2, 2, -360, -300 };
// US Mountain (America/Denver) — UTC-7 / UTC-6
TimezoneDefinition TZ_MST = { 3, 2, 11, 1, 0, 2, 2, -420, -360 };
// US Pacific (America/Los_Angeles) — UTC-8 / UTC-7
TimezoneDefinition TZ_PST = { 3, 2, 11, 1, 0, 2, 2, -480, -420 };
// Central European (Europe/Berlin) — UTC+1 / UTC+2
TimezoneDefinition TZ_CET = { 3,-1, 10,-1, 0, 2, 3, 60, 120 };
// Eastern European (Europe/Bucharest) — UTC+2 / UTC+3
TimezoneDefinition TZ_EET = { 3,-1, 10,-1, 0, 3, 4, 120, 180 };
// India Standard Time (Asia/Kolkata) — UTC+5:30, no DST
TimezoneDefinition TZ_IST = { 0, 0, 0, 0, 0, 0, 0, 330, 330 };
// Japan Standard Time (Asia/Tokyo) — UTC+9, no DST
TimezoneDefinition TZ_JST = { 0, 0, 0, 0, 0, 0, 0, 540, 540 };
// Australia Eastern (Australia/Sydney) — UTC+10 / UTC+11
TimezoneDefinition TZ_AEST = { 10, 1, 4, 1, 0, 2, 3, 600, 660 };
// New Zealand (Pacific/Auckland) — UTC+12 / UTC+13
TimezoneDefinition TZ_NZST = { 9,-1, 4, 1, 0, 2, 3, 720, 780 };The normalize32() heuristic treats any 32-bit value below
UNIX_OFFSET_2020 (January 1, 2020) as a post-2038 rolled-over timestamp and
adds 2³² seconds to it. Passing genuine pre-2020 historical timestamps as
32-bit values will silently produce wrong results.
Recommendation: use the 64-bit uint64_t overloads whenever possible.
If you must use a 32-bit source, convert it to 64-bit milliseconds with
dateToMs() before calling the library.
During the spring-forward gap (e.g. 02:00–03:00 US Eastern), those local
times never actually exist. Passing a local time in this gap to localToUtc()
returns a UTC value as though the time is in DST; the result may not round-trip
correctly through utcToLocal().
examples/Benchmark/Benchmark.ino — Three-part example:
- Part 1 — API usage demonstration with readable output.
- Part 2 — Raw performance measurements (cold/warm cache, far-future years,
batch throughput, 32-bit vs 64-bit overhead,
toTimeStruct/dateToMsspeed). - Part 3 — Unit tests covering: predefined timezone constants, DST boundary
transitions (exact ms before/after switch), fall-back overlap with both
prefer_dstsettings, 2020 rollover heuristic edge cases, leap-year edge cases (Feb 29 on leap year; year 2100 which is not a leap year), anddateToMs↔toTimeStructround-tripping. Prints PASS/FAIL for each case and a summary at the end.
examples/DS3231_RTC_Example/DS3231_RTC_Example.ino — Real-world usage with
an I²C DS3231 RTC:
- Define local timezone (America/New_York).
- Set a local date/time, convert to UTC, write to the RTC.
- Read UTC seconds back from the RTC.
- Convert to local time, display as
YYYY-MM-DD HH:MM:SS.
Requires the RTClib library by Adafruit (install via Library Manager).
Measured on an ESP8266 (Generic ESP8266 Module, 80 MHz):
| Operation | Time |
|---|---|
utcToLocal — no DST |
~1 µs |
utcToLocal — cache hit |
~2 µs |
utcToLocal — cache miss (cold) |
~100 µs |
localToUtc — cache hit |
~3 µs |
toTimeStruct |
~30 µs |
dateToMs |
~15 µs |
Run the Benchmark example on your target hardware for exact numbers.
- Object size: ~24 bytes per
TimezoneTranslatorinstance (9-byteTimezoneDefinition+ 18-byteDstCache+ padding). - Code size: ~2-3 KB Flash (platform-dependent).
- Stack: Conversions use a small fixed amount of stack; no heap allocation.
Each TimezoneTranslator instance is independent. If you share one instance
across threads (e.g. ESP32 dual-core), protect it with a mutex. Alternatively,
create one instance per core — each will maintain its own cache.
All static utility methods (dateToMs, toTimeStruct, etc.)
are stateless and thread-safe.
Copyright (C) 2010-2026 Costin Bobes
MIT License — see LICENSE.txt for full text.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the conditions stated in the license file.