Bash and Linux

Dates and Timezones

mediumSystem monitoring

Problem statement

Convert Unix timestamps from a log into readable UTC and India times, turn a date back into a timestamp, work out the minutes between two events, and build a dated file name. Logs, APIs and databases store time in many forms, and during an incident you must line up events from servers in different time zones.

events.log (Unix timestamps: seconds since 1970-01-01 UTC)

TEXT
1790850600 deploy started
1790851500 errors began
1790853300 rollback done
  1. Print each event's time in UTC and in India time (IST).
  2. Turn 2026-10-01 10:30:00 UTC into a timestamp.
  3. Print how many minutes passed from the first event to the last.
  4. Print the first event in ISO 8601 format, and a backup file name that contains its date.

Expected output:

TEXT
== each event in UTC and India time ==
2026-10-01 10:30:00 UTC (16:00:00 IST) deploy started
2026-10-01 10:45:00 UTC (16:15:00 IST) errors began
2026-10-01 11:15:00 UTC (16:45:00 IST) rollback done
== a date back to a timestamp ==
1790850600
== minutes from first to last event ==
45 minutes
== ISO 8601 and a dated file name ==
2026-10-01T10:30:00+00:00
backup-2026-10-01.tar.gz

Hints

Hint 1: date -u -d @1790850600 '+%F %T %Z' prints a timestamp in UTC. Put TZ=Asia/Kolkata in front of date to see the same moment in India time.

Approach

Optimal: date -d with TZ and formats

Covers: Unix time (epoch), date -d @N, date -u, TZ=Zone, format codes %F %T %Z %s, date -Iseconds, arithmetic $(( )), while read.

A timestamp is just a count of seconds. Unix time counts the seconds since 1970-01-01 00:00:00 UTC. 1790850600 is one exact moment, the same everywhere on Earth. Only the way you show it changes with the time zone:

%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart TB E(["1790850600
one moment"]):::purple E --> U["UTC
2026-10-01 10:30"]:::blue E --> I["India, UTC+5:30
16:00 IST"]:::green E --> N["New York, UTC-4
06:30 EDT"]:::yellow classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px

That is why servers should store and log time in UTC or as timestamps: no confusion, no daylight saving jumps. Convert to a local zone only when showing it to people.

date in both directions.

Command Does
date -u -d @1790850600 timestamp to a date, shown in UTC
TZ=Asia/Kolkata date -d @1790850600 the same moment in India time
date -u -d '2026-10-01 10:30:00' +%s a date (read as UTC) to a timestamp
date +%s the current timestamp
date -u -Iseconds -d @N ISO 8601, like 2026-10-01T10:30:00+00:00

-d (date) tells date which moment to use instead of now, and the @ means "this is a timestamp". -u means UTC. TZ=Asia/Kolkata sets the time zone for that one command; zone names come from /usr/share/zoneinfo.

Format codes. Anything after + is a pattern:

Code Prints Example
%F date 2026-10-01
%T time 10:30:00
%Z zone name UTC, IST
%s timestamp 1790850600
%H%M hour and minute 1030

%F is the best format for file names, because YYYY-MM-DD sorts in time order.

Walking through the code. The # Setup: lines only create the sample log, so skip past them.

  1. while read -r ts what reads each line into the timestamp and the rest of the text, then prints the event in both zones.
  2. date -u -d ... +%s prints the timestamp for 10:30 UTC, which is the first event.
  3. head and tail take the first and last timestamps, and $(( (last - first) / 60 )) gives 45 minutes.
  4. -Iseconds prints ISO 8601, and %F builds the file name.
%%{init: {"flowchart": {"padding": 18, "nodeSpacing": 30, "rankSpacing": 40, "htmlLabels": true}, "themeVariables": {"fontSize": "18px"}}}%% flowchart LR subgraph T["timeline in UTC"] direction TB T1["10:30 deploy started"]:::blue ~~~ T2["10:45 errors began"]:::red ~~~ T3["11:15 rollback done"]:::green end T --> D["45 minutes from start to end"]:::yellow classDef blue fill:#dbeafe,stroke:#2563eb,color:#1e3a8a,stroke-width:2px classDef yellow fill:#fef3c7,stroke:#d97706,color:#78350f,stroke-width:2px classDef green fill:#d1fae5,stroke:#059669,color:#064e3b,stroke-width:2px classDef red fill:#fee2e2,stroke:#dc2626,color:#7f1d1d,stroke-width:2px classDef purple fill:#ede9fe,stroke:#7c3aed,color:#4c1d95,stroke-width:2px classDef gray fill:#f3f4f6,stroke:#6b7280,color:#111827,stroke-width:2px linkStyle default stroke:#94a3b8,stroke-width:2px style T fill:transparent,stroke:#6b7280,stroke-width:2px

Edge cases. date -d is GNU only; on macOS use date -r 1790850600 or install gdate. Without -u, a date string is read in the machine's own zone, which is a classic source of off-by-hours bugs. Timestamps in milliseconds (13 digits, common in JavaScript and Java logs) must be divided by 1000 first.

# Setup: create the sample log in a fresh temporary folder
cd "$(mktemp -d)"
cat > events.log << 'LOG'
1790850600 deploy started
1790851500 errors began
1790853300 rollback done
LOG

echo "== each event in UTC and India time =="
while read -r ts what; do
  utc=$(date -u -d "@$ts" '+%F %T %Z')
  ist=$(TZ=Asia/Kolkata date -d "@$ts" '+%T %Z')
  echo "$utc  ($ist)  $what"
done < events.log

echo "== a date back to a timestamp =="
date -u -d '2026-10-01 10:30:00' +%s

echo "== minutes from first to last event =="
first=$(head -n 1 events.log | cut -d' ' -f1)
last=$(tail -n 1 events.log | cut -d' ' -f1)
echo "$(( (last - first) / 60 )) minutes"

echo "== ISO 8601 and a dated file name =="
date -u -Iseconds -d "@$first"
echo "backup-$(date -u -d "@$first" +%F).tar.gz"

Interview follow-ups

  • Print how long ago each event happened, in minutes, compared with now.

    Get the current timestamp once with now=$(date +%s), then for each line compute $(( (now - ts) / 60 )). Doing the maths on timestamps avoids any time zone or daylight saving mistakes. For a readable form, split the seconds into hours and minutes with / 3600 and % 3600 / 60. The output changes every run, which is why this page uses fixed timestamps instead.

Frequently asked questions

Usually because one side used local time and the other UTC, or because of daylight saving: zones like Europe/London shift by an hour twice a year, so the same local time can happen twice in autumn. India does not use daylight saving, so IST is always UTC + 5:30. Set servers to UTC (timedatectl set-timezone UTC) and keep local zones for display only, and the problem disappears.

For milliseconds, drop the last three digits first: date -u -d "@$((ms / 1000))". For ISO strings, GNU date reads them directly: date -u -d '2026-10-01T10:30:00Z' +%s. A trailing Z means UTC, and an offset like +05:30 is understood too. For many lines at once, awk or a small script is faster than calling date per line.

timedatectl shows the current zone, whether the clock is synced, and the UTC and local times together. timedatectl list-timezones lists valid names, and sudo timedatectl set-timezone UTC changes it. On minimal systems without systemd, the zone is the link /etc/localtime. The TZ variable overrides it for one command or one shell, as this page does.