Skip to content

Instantly share code, notes, and snippets.

@alex-d-boyd
Created September 20, 2020 02:13
Show Gist options
  • Save alex-d-boyd/25bd947131e5a44aafb99595149c7912 to your computer and use it in GitHub Desktop.
Save alex-d-boyd/25bd947131e5a44aafb99595149c7912 to your computer and use it in GitHub Desktop.
Simple script to convert between time zones.
#! /usr/bin/env python3
"""Simple timezone converter"""
import argparse
import datetime
import re
import sys
TZS = [
# UTC
UTC := datetime.timezone.utc,
# Japan
JST := datetime.timezone(datetime.timedelta(hours=9), name='JST'),
# USA
PST := datetime.timezone(datetime.timedelta(hours=-8), name='PST'),
PDT := datetime.timezone(datetime.timedelta(hours=-7), name='PDT'),
MST := datetime.timezone(datetime.timedelta(hours=-7), name='MST'),
MDT := datetime.timezone(datetime.timedelta(hours=-6), name='MDT'),
CST := datetime.timezone(datetime.timedelta(hours=-6), name='CST'),
CDT := datetime.timezone(datetime.timedelta(hours=-5), name='CDT'),
EST := datetime.timezone(datetime.timedelta(hours=-5), name='EST'),
EDT := datetime.timezone(datetime.timedelta(hours=-4), name='EDT'),
# Australia
AWST := datetime.timezone(datetime.timedelta(hours=8), name='AWST'),
AWDT := datetime.timezone(datetime.timedelta(hours=9), name='AWDT'),
ACST := datetime.timezone(datetime.timedelta(hours=9, minutes=30), name='ACST'),
ACDT := datetime.timezone(datetime.timedelta(hours=10, minutes=30), name='ACDT'),
AEST := datetime.timezone(datetime.timedelta(hours=10), name='AEST'),
AEDT := datetime.timezone(datetime.timedelta(hours=11), name='AEDT'),
]
TIMEZONES = {tz.tzname(datetime.datetime.now()): tz for tz in TZS}
TIME_FORMAT = '%Y-%m-%d %H:%M:%S %Z (UTC%z)'
class ParseError(ValueError):
"""Errors in parsing inputs into valid values"""
def __init__(self, item, message):
self.item = item
self.message = message
def _parse_args():
def upper(st):
return st.upper()
parser = argparse.ArgumentParser(prefix_chars=r'/-@',
formatter_class=argparse.RawDescriptionHelpFormatter,
description='Convert times between timezones',
epilog="""The following characters can be used as separators:
for date: ./-<space>
for time: .:<space>
If <space> is used, the date or time must be quoted.
In addition to ±HHMM, certain timezone names my be used for timezone.""")
parser.add_argument('date', help='date (YYYYMMDD)')
parser.add_argument('time', help='time (HHMMSS)')
parser.add_argument('timezone', help='timezone (±HHMM / name)', type=upper)
parser.add_argument('-t', '--target-timezone', metavar='±HHMM', dest='tgt',
help='timezone to convert to (default JST)', default=JST, type=upper)
parser.add_argument('-f', '--format', default = TIME_FORMAT,
help='format for output, as strftime format string')
args=parser.parse_args()
return args
def _parse_date(date):
pattern = """
(?P<year>\d{4})
(?P<sep>[ ./-])?
(?P<month>\d?\d)
(?P=sep)?
(?P<day>\d?\d)
"""
if parsed_date := re.match(pattern, date, re.VERBOSE):
return list(map(int,parsed_date.group('year', 'month', 'day')))
else:
raise ParseError('date', f"couldn't parse {date} into a date")
def _parse_time(time):
pattern = """
^(?P<hour>\d?\d)
(?P<sep>[ .:-])?
(?P<minute>\d{2})
((?P=sep)?
(?P<second>\d{2}))?$
"""
if parsed_time := re.match(pattern, time, re.VERBOSE):
return list(map(int, filter(None,
parsed_time.group('hour', 'minute', 'second'))))
else:
raise ParseError('time', f"couldn't parse {time} into a time")
def _parse_tz(tz):
pattern = """
(?P<sign>[+-])
(?P<hour>\d?\d)
(?P<sep>[ .:-])?
(?P<minute>\d{2})
"""
if parsed_tz := re.match(pattern, tz, re.VERBOSE):
return parsed_tz.group('sign', 'hour', 'minute')
else:
raise ParseError('timezone', f"couldn't parse {tz} into a timezone")
def _make_tz(tz):
if isinstance(tz, datetime.tzinfo):
return tz
timezone = TIMEZONES.get(tz)
if timezone is None:
tz_parts = _parse_tz(tz)
hours, minutes = map(int, tz_parts[1:])
if tz_parts[0] == '-':
hours = -hours
timezone = datetime.timezone(
datetime.timedelta(hours=hours, minutes=minutes), name='')
return timezone
def _parse_datetime(date, time, tz):
parts = _parse_date(date)
parts.extend(_parse_time(time))
timezone = _make_tz(tz)
return datetime.datetime(*parts, tzinfo=timezone)
if __name__ == '__main__':
args = _parse_args()
try:
dt = _parse_datetime(args.date, args.time, args.timezone)
except ParseError as err:
sys.exit(f'ERROR: Invalid {err.item}')
tgt = _make_tz(args.tgt)
print(dt.astimezone(tgt).strftime(args.format))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment