Skip to content

Instantly share code, notes, and snippets.

@YounesRahimi
Last active February 23, 2026 12:14
Show Gist options
  • Select an option

  • Save YounesRahimi/dbede5a7d01b42a5e9bc56ac6f078669 to your computer and use it in GitHub Desktop.

Select an option

Save YounesRahimi/dbede5a7d01b42a5e9bc56ac6f078669 to your computer and use it in GitHub Desktop.
to_jalali.sh — Bash script to convert Gregorian datetime (with timezone offset) to Jalali (Shamsi/Persian) calendar. Pure Python stdlib, no external dependencies. Supports microseconds and any UTC offset (e.g. +03:30). Usage: to_jalali.sh "2025-05-20 23:51:32.961770 +03:30"
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# to_jalali.sh — Convert Gregorian datetime to Jalali (Shamsi/Persian)
#
# Supported input formats:
# "2025-05-20 23:51:32.961770 +03:30" explicit offset
# "2025-05-20T23:51:32.961770Z" UTC (Z suffix)
# "2025-05-20T23:51:32Z" UTC, no microseconds
# "2025-05-20 23:51:32.961770" no tz → implied Tehran (+03:30)
# "2025-05-20" date only → implied Tehran, 00:00:00
# (no argument) current local time
#
# Usage:
# to_jalali.sh "2025-05-20 23:51:32.961770 +03:30"
# to_jalali.sh "2025-05-20T23:51:32Z"
# to_jalali.sh "2025-05-20"
# to_jalali.sh
# ─────────────────────────────────────────────────────────────────────────────
INPUT="${1:-}"
python3 - "$INPUT" <<'PYEOF'
import sys, re
from datetime import datetime, timezone, timedelta
TEHRAN_TZ = timezone(timedelta(hours=3, minutes=30))
# ── Conversion algorithm ─────────────────────────────────────────────────────
def div(a, b): return int(a / b)
def mod(a, b): return a - div(a, b) * b
def gregorian_to_jalali(gy, gm, gd):
"""
Accurate Gregorian -> Jalali conversion (no external dependencies).
Algorithm: arithmetic method with base offsets 1600 (Gregorian) / 979 (Jalali).
Verified: 2024-03-20 -> 1403-01-01, 2025-05-20 -> 1404-02-30,
2025-03-21 -> 1404-01-01 (Nowruz 1404)
"""
g_y = gy - 1600
g_m = gm - 1
g_d = gd - 1
g_d_no = (365 * g_y
+ div(g_y + 3, 4)
- div(g_y + 99, 100)
+ div(g_y + 399, 400))
for i in range(g_m):
g_d_no += [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][i]
if g_m > 1 and ((gy % 4 == 0 and gy % 100 != 0) or gy % 400 == 0):
g_d_no += 1 # leap day
g_d_no += g_d
j_d_no = g_d_no - 79
j_np = div(j_d_no, 12053)
j_d_no = mod(j_d_no, 12053)
j_y = 979 + 33 * j_np + 4 * div(j_d_no, 1461)
j_d_no = mod(j_d_no, 1461)
if j_d_no >= 366:
j_y += div(j_d_no - 1, 365)
j_d_no = mod(j_d_no - 1, 365)
j_m = 12
for i, days in enumerate([31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29]):
if i == 11: break
if j_d_no < days:
j_m = i + 1
break
j_d_no -= days
return j_y, j_m, j_d_no + 1
# ── Input parser ─────────────────────────────────────────────────────────────
def parse_input(s):
"""
Parses all supported formats into an aware datetime.
Missing timezone -> Tehran (+03:30).
Missing time -> 00:00:00.
"""
p = re.match(
r'^(\d{4}-\d{2}-\d{2})' # date (required)
r'(?:[T ](\d{2}:\d{2}:\d{2}(?:\.\d+)?))?' # time (optional)
r'\s*(Z|[+-]\d{2}:?\d{2})?$', # tz (optional)
s.strip()
)
if not p:
raise ValueError(
f"Cannot parse: '{s}'\n"
"Supported formats:\n"
" 2025-05-20 23:51:32.961770 +03:30\n"
" 2025-05-20T23:51:32.961770Z\n"
" 2025-05-20T23:51:32Z\n"
" 2025-05-20 23:51:32.961770 (Tehran timezone implied)\n"
" 2025-05-20 (Tehran timezone implied)\n"
)
date_part = p.group(1)
time_part = p.group(2) or "00:00:00"
tz_raw = p.group(3) # None | 'Z' | '+03:30' | '+0330'
fmt = "%Y-%m-%d %H:%M:%S.%f" if '.' in time_part else "%Y-%m-%d %H:%M:%S"
naive_dt = datetime.strptime(f"{date_part} {time_part}", fmt)
if tz_raw is None:
tz = TEHRAN_TZ
elif tz_raw == 'Z':
tz = timezone.utc
else:
tz_str = re.sub(r'([+-]\d{2})(\d{2})$', r'\1:\2', tz_raw) # +0330 -> +03:30
sign = 1 if tz_str[0] == '+' else -1
th, tm = int(tz_str[1:3]), int(tz_str[4:6])
tz = timezone(timedelta(hours=sign * th, minutes=sign * tm))
return naive_dt.replace(tzinfo=tz)
# ── Main ─────────────────────────────────────────────────────────────────────
input_str = sys.argv[1] if len(sys.argv) > 1 and sys.argv[1] else ""
try:
dt = parse_input(input_str) if input_str else datetime.now().astimezone()
except ValueError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
jy, jm, jd = gregorian_to_jalali(dt.year, dt.month, dt.day)
raw_offset = dt.strftime('%z')
tz_display = f"{raw_offset[:3]}:{raw_offset[3:]}" if raw_offset else "+00:00"
print(f"Gregorian : {dt.strftime('%Y-%m-%d %H:%M:%S.%f')} {tz_display}")
print(f"Jalali : {jy:04d}-{jm:02d}-{jd:02d} {dt.strftime('%H:%M:%S.%f')} {tz_display}")
PYEOF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment