Skip to content

Converta um timestamp entre fusos horários snippet

Converter um instante entre zonas é a tarefa de datas mais mal compreendida: um fuso horário é uma REGRA (com transições de DST), não um offset fixo, por isso 'somar 6 horas' está errado duas vezes por ano na maioria das zonas.

Converter um instante entre zonas é a tarefa de datas mais mal compreendida: um fuso horário é uma REGRA (com transições de DST), não um offset fixo, por isso 'somar 6 horas' está errado duas vezes por ano na maioria das zonas. O modelo correto em todo o lado: um instante (UTC) mais um nome de zona IANA ('Europe/Paris') — a biblioteca procura o offset que estava em efeito NAQUELE instante. Horas de parede sem zona (datetimes naive) não podem ser convertidas, apenas reinterpretadas; mantenha os instantes em UTC nas fronteiras e formate por zona apenas para humanos.

Receita executável · 13 linguagensAbrir a ferramenta timestamp →
Dates & Timetimezonedstutcdateconversioniana

Every language

13 linguagens, copy-ready. One at a time with syntax highlighting, or all inline.

SQLSQLrunnable
-- Naive UTC timestamp → Paris wall clock (the conversion idiom):
SELECT TIMESTAMP '2026-07-21 12:00:00'
       AT TIME ZONE 'UTC'           -- label the instant: this is 12:00Z
       AT TIME ZONE 'Europe/Paris'  -- render its Paris wall clock
       AS paris_wall;               -- 2026-07-21 14:00:00

-- now() is already timestamptz — only the trailing cast belongs:
SELECT now() AT TIME ZONE 'Europe/Paris';

The two casts are not symmetric: on a naive timestamp AT TIME ZONE labels the instant; on a timestamptz it renders the wall clock — same operator, opposite direction. now() AT TIME ZONE 'UTC' AT TIME ZONE 'Europe/Paris' is therefore the classic bug (it re-labels now's UTC digits as Paris); run both forms in the playground and compare.

Run in the SQL playground →
JSJavaScript
// No first-class API yet — the stdlib trick is Intl with the zone built in:
function inZone(instant, timeZone) {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hourCycle: 'h23',
  }).formatToParts(instant);
  const get = (type) => parts.find((p) => p.type === type).value;
  return `${get('year')}-${get('month')}-${get('day')} ` +
         `${get('hour')}:${get('minute')}:${get('second')}`;
}

inZone(new Date('2026-07-21T12:00:00Z'), 'Europe/Paris');
// '2026-07-21 14:00:00' — the offset in effect AT that instant (+02:00, DST)

No first-class timezone conversion in the language yet — formatToParts with a timeZone is the acknowledged hack, and Temporal (Stage 3) will replace it. The formatter looks up the offset for that exact instant, DST transitions included; hourCycle: 'h23' because hour12: false can emit '24' at midnight.

TSTypeScript
export function inZone(instant: Date, timeZone: string): string {
  const parts = new Intl.DateTimeFormat('en-CA', {
    timeZone,
    year: 'numeric', month: '2-digit', day: '2-digit',
    hour: '2-digit', minute: '2-digit', second: '2-digit',
    hourCycle: 'h23',
  }).formatToParts(instant);
  const get = (type: string) =>
    parts.find((p) => p.type === type)!.value;
  return `${get('year')}-${get('month')}-${get('day')} ` +
         `${get('hour')}:${get('minute')}:${get('second')}`;
}

The same stdlib trick, typed and wrapped — the non-null assertion is safe because formatToParts always emits every component you requested. Temporal will replace this with a first-class TimeZone type; until it ships, this is the honest signature: instant in, wall-clock string out.

GoGo
import "time"

func inZone(t time.Time, zone string) (time.Time, error) {
	loc, err := time.LoadLocation(zone) // IANA name, e.g. "Europe/Paris"
	if err != nil {
		return time.Time{}, err
	}
	return t.In(loc), nil // same instant, that zone's wall clock
}

utc, _ := time.Parse(time.RFC3339, "2026-07-21T12:00:00Z")
paris, _ := inZone(utc, "Europe/Paris")
paris.Format("2006-01-02 15:04:05") // 2026-07-21 14:00:00

t.In(loc) re-labels the instant; t.Add(2 * time.Hour) is the wrong tool — it moves the instant itself and is off by the offset twice a year. LoadLocation reads the system tz database: in a bare container, import _ "time/tzdata" to embed one.

RsRust
use chrono::{TimeZone, Utc};
use chrono_tz::{Europe::Paris, Tz};

let utc = Utc.with_ymd_and_hms(2026, 7, 21, 12, 0, 0).unwrap();
utc.with_timezone(&Paris); // 2026-07-21 14:00:00 CEST

// zone from a string (e.g. a user preference):
let tz: Tz = "Europe/Paris".parse().unwrap();
utc.with_timezone(&tz); // same instant, DST-correct

chrono alone has offsets but NOT the IANA database — chrono-tz embeds it at compile time, so no system tz files are read at runtime. with_timezone keeps the instant and recomputes the wall clock; the result's offset reflects the rules in effect at that instant.

PHPPHP
$utc = new DateTime('2026-07-21T12:00:00', new DateTimeZone('UTC'));
$paris = (clone $utc)->setTimezone(new DateTimeZone('Europe/Paris'));

$paris->format('Y-m-d H:i:s T'); // '2026-07-21 14:00:00 CEST'

setTimezone converts the instant; DateTime::modify('+2 hours') is NOT a conversion — it shifts the wall clock onto a different instant. DateTime is mutable, hence the clone; and construct with the source zone, or the server's date-default-timezone is silently assumed.

PyPython
from datetime import datetime, timezone
from zoneinfo import ZoneInfo

utc = datetime(2026, 7, 21, 12, 0, tzinfo=timezone.utc)
paris = utc.astimezone(ZoneInfo('Europe/Paris'))

paris.isoformat()  # '2026-07-21T14:00:00+02:00'

zoneinfo is stdlib since Python 3.9 — no pytz needed (its localize() is a known footgun). astimezone() on a NAIVE datetime doesn't raise; it assumes your local time and quietly converts the wrong instant — attach tzinfo first.

C++C++
#include <chrono>
#include <format>

std::chrono::zoned_time paris{"Europe/Paris", std::chrono::system_clock::now()};

std::format("{:%Y-%m-%d %H:%M:%S %Z}", paris); // 2026-07-21 14:00:00 CEST

zoned_time applies the offset in effect AT that instant, DST transitions included — never add hours by hand. It reads the OS tz database (IANA): locate_zone throws std::runtime_error for an unknown zone name, so guard user-supplied input. C++20 time zones need GCC 13+ or MSVC 19.29+.

C#C#
using System;
using System.Globalization;

DateTimeOffset utc = DateTimeOffset.Parse(
    "2026-07-21T12:00:00Z", CultureInfo.InvariantCulture);

TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris");
DateTimeOffset paris = TimeZoneInfo.ConvertTime(utc, zone);

paris.ToString("yyyy-MM-dd HH:mm:ss zzz"); // 2026-07-21 14:00:00 +02:00

On .NET 8+ / Linux FindSystemTimeZoneById accepts IANA names — Windows IDs are converted internally via ICU, so either spelling works cross-platform. ConvertTime applies the offset in effect at that instant; AddHours(2) does not.

JvJava
import java.time.ZoneId;
import java.time.ZonedDateTime;

ZonedDateTime utc = ZonedDateTime.parse("2026-07-21T12:00:00Z");
ZonedDateTime paris = utc.withZoneSameInstant(ZoneId.of("Europe/Paris"));

paris.toString(); // 2026-07-21T14:00+02:00[Europe/Paris]

withZoneSameInstant converts; withZoneSameLocal is the trap with the friendly name — it keeps the wall clock and silently changes the instant. ZoneId.of takes IANA names, and toString carries offset AND zone, which is the shape to log.

SwSwift
import Foundation

let utc = ISO8601DateFormatter().date(from: "2026-07-21T12:00:00Z")!

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
formatter.timeZone = TimeZone(identifier: "Europe/Paris") // the converter

formatter.string(from: utc) // "2026-07-21 14:00:00"

Foundation has no convert-by-name one-liner: Date is instants (UTC) and the FORMATTER does the zone math — setting timeZone is where the conversion happens. DateFormatter is expensive to create; hoist one per zone in hot paths.

KtKotlin
import java.time.ZoneId
import java.time.ZonedDateTime

val utc = ZonedDateTime.parse("2026-07-21T12:00:00Z")
val paris = utc.withZoneSameInstant(ZoneId.of("Europe/Paris"))

println(paris) // 2026-07-21T14:00+02:00[Europe/Paris]

Same JVM java.time APIs in a cleaner chain. ZoneId is the modern type — the legacy TimeZone's 3-letter IDs ('CET', 'EST') are ambiguous and resolved differently per platform; always use the IANA name.

RbRuby
require 'time'

utc = Time.utc(2026, 7, 21, 12, 0, 0)
paris = utc.getlocal('Europe/Paris')

paris.strftime('%Y-%m-%d %H:%M:%S %z') # '2026-07-21 14:00:00 +0200'

require 'time' is what lets getlocal take an IANA zone name — without it only a numeric offset works. Rails adds utc.in_time_zone('Europe/Paris'); zone-less getlocal/getutc fall back to ENV['TZ'], so pass the zone explicitly.

Keep going

Try the interactive timestamp tool →